from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import patch

import pytest
from fastapi.templating import Jinja2Templates

from app.batch_processing.infrastructure.heuristic_classifier import HeuristicDocumentClassifier
from app.clinical_pipeline.domain.errors import HistoriaSummaryCoverageError
from app.clinical_pipeline.domain.models import HistoriaProcessingMetrics
from app.llm import DefaultModelSelectionPolicy, LLMErrorKind, LLMProviderError, LLMTask
from app.llm.models import LLMTextResult
from modules.data_collection.read_pdf import ReadPdf
from modules.processing.resumen_google import _resumir_historia_larga


class _Page:
    def __init__(self, text: str):
        self.text = text

    def extract_text(self, **_kwargs) -> str:
        return self.text


class _Pdf:
    def __init__(self, pages: list[_Page]):
        self.pages = pages

    def close(self) -> None:
        return None


class _SummaryRouter:
    def __init__(self, responses: list[str | Exception] | None = None):
        self.requests = []
        self.responses = list(responses or [])

    def generate_text(self, request):
        self.requests.append(request)
        if self.responses:
            content = self.responses.pop(0)
        elif request.task == LLMTask.HISTORIA_CHUNK_SUMMARY:
            content = f"resumen-bloque-{len(self.requests)}"
        else:
            content = "resumen-final"
        if isinstance(content, Exception):
            raise content
        model = (
            "gemini-2.5-flash-lite" if request.task == LLMTask.HISTORIA_CHUNK_SUMMARY else "gemini-2.5-flash"
        )
        return LLMTextResult(content=content, provider="gemini", model=model)


def test_pdf_extraction_uses_page_fallback_and_reports_coverage() -> None:
    plumber = _Pdf([_Page("pagina uno"), _Page("")])
    pypdf = SimpleNamespace(pages=[_Page("alternativa uno"), _Page("pagina dos")])

    with (
        patch("modules.data_collection.read_pdf.pdfplumber.open", return_value=plumber),
        patch("modules.data_collection.read_pdf.PdfReader", return_value=pypdf),
    ):
        result = ReadPdf.read_with_metadata(b"pdf")

    assert result.text == "pagina uno\n\f\npagina dos"
    assert result.metadata.total_pages == 2
    assert result.metadata.pages_with_text == 2
    assert result.metadata.pypdf2_fallback_pages == [2]
    assert result.metadata.page_coverage_ratio == 1.0
    assert all("pagina uno" not in warning for warning in result.metadata.warnings)


def test_pdf_extraction_marks_pages_without_text_without_logging_content(caplog) -> None:
    plumber = _Pdf([_Page("")])
    pypdf = SimpleNamespace(pages=[_Page("")])

    with (
        patch("modules.data_collection.read_pdf.pdfplumber.open", return_value=plumber),
        patch("modules.data_collection.read_pdf.PdfReader", return_value=pypdf),
    ):
        result = ReadPdf.read_with_metadata(b"pdf")

    assert result.text == ""
    assert result.metadata.pages_without_text == [1]
    assert result.metadata.page_coverage_ratio == 0.0
    assert "contenido-clinico" not in caplog.text


@pytest.mark.parametrize(
    "filename",
    ["HC13820761.PDF", "HC13909131.PDF", "HISTORIA CLINICA PACIENTE.pdf"],
)
def test_classifier_keeps_complete_history_despite_surgical_density(filename: str) -> None:
    text = " ".join(
        [
            "Motivo de consulta",
            "Enfermedad actual",
            "Antecedentes",
            "Examen fisico",
            *("procedimiento quirurgico quirofano cirugia" for _ in range(20)),
        ]
    )

    decision = HeuristicDocumentClassifier().inspect(filename, text)

    assert decision.document_type == "historia_clinica"
    assert decision.confidence >= 0.9


def test_classifier_keeps_autonomous_surgical_report() -> None:
    decision = HeuristicDocumentClassifier().inspect(
        "reporte_operatorio.pdf",
        "Reporte quirurgico. Diagnostico preoperatorio. Hallazgos y procedimiento quirurgico.",
    )

    assert decision.document_type == "quirurgico"


def test_long_history_covers_first_middle_and_last_input_with_expected_tasks() -> None:
    text = "INICIO " + ("a" * 650) + " MITAD " + ("b" * 650) + " FINAL"
    router = _SummaryRouter()

    summary_result = _resumir_historia_larga(
        text,
        summary_chunk=500,
        chunk_overlap=80,
        max_retries=0,
        llm_router=router,
        return_metrics=True,
    )
    assert isinstance(summary_result, tuple)
    result, metrics = summary_result
    assert isinstance(metrics, HistoriaProcessingMetrics)

    chunk_prompts = [
        request.prompt for request in router.requests if request.task == LLMTask.HISTORIA_CHUNK_SUMMARY
    ]
    assert any("INICIO" in prompt for prompt in chunk_prompts)
    assert any("MITAD" in prompt for prompt in chunk_prompts)
    assert any("FINAL" in prompt for prompt in chunk_prompts)
    assert result == "resumen-final"
    assert metrics.coverage_ratio == 1.0
    assert metrics.complete is True
    assert metrics.chunk_model == "gemini-2.5-flash-lite"
    assert metrics.final_model == "gemini-2.5-flash"
    assert router.requests[-1].task == LLMTask.HISTORIA_FINAL_SUMMARY


def test_long_history_retries_a_recoverable_chunk() -> None:
    router = _SummaryRouter(responses=["", "recuperado", "consolidado"])
    delays: list[float] = []

    summary_result = _resumir_historia_larga(
        "texto " * 30,
        summary_chunk=500,
        max_retries=2,
        retry_delays=[2, 5],
        sleep_fn=delays.append,
        llm_router=router,
        return_metrics=True,
    )
    assert isinstance(summary_result, tuple)
    _result, metrics = summary_result
    assert isinstance(metrics, HistoriaProcessingMetrics)

    assert metrics.complete is True
    assert len(router.requests) == 3
    assert delays == [2]


def test_long_history_fails_after_two_retries_without_partial_result() -> None:
    router = _SummaryRouter(responses=["", "", ""])
    delays: list[float] = []

    with pytest.raises(HistoriaSummaryCoverageError) as exc_info:
        _resumir_historia_larga(
            "texto " * 30,
            summary_chunk=500,
            max_retries=2,
            retry_delays=[2, 5],
            sleep_fn=delays.append,
            llm_router=router,
            return_metrics=True,
        )

    assert exc_info.value.failed_chunks == [1]
    assert exc_info.value.retryable is True
    assert exc_info.value.source == "external_provider"
    assert exc_info.value.error_kind == "empty_response"
    assert len(router.requests) == 3
    assert delays == [2, 5]


def test_long_history_respects_provider_retry_after_when_it_is_longer() -> None:
    router = _SummaryRouter(
        responses=[
            LLMProviderError(
                "Gemini temporalmente no disponible",
                kind=LLMErrorKind.TRANSIENT,
                provider="gemini",
                model="gemini-2.5-flash-lite",
                retryable=True,
                details={"retry_after_seconds": 7},
            ),
            "recuperado",
            "consolidado",
        ]
    )
    delays: list[float] = []

    result = _resumir_historia_larga(
        "texto " * 30,
        summary_chunk=500,
        max_retries=2,
        retry_delays=[2, 5],
        sleep_fn=delays.append,
        llm_router=router,
    )

    assert result == "consolidado"
    assert delays == [7]


def test_long_history_does_not_retry_non_recoverable_provider_error() -> None:
    router = _SummaryRouter(
        responses=[
            LLMProviderError(
                "Gemini no configurado",
                kind=LLMErrorKind.INVALID_CONFIGURATION,
                provider="gemini",
                model="gemini-2.5-flash-lite",
            )
        ]
    )
    delays: list[float] = []

    with pytest.raises(HistoriaSummaryCoverageError) as exc_info:
        _resumir_historia_larga(
            "texto " * 30,
            summary_chunk=500,
            max_retries=2,
            retry_delays=[2, 5],
            sleep_fn=delays.append,
            llm_router=router,
        )

    assert exc_info.value.retryable is False
    assert exc_info.value.source == "internal"
    assert exc_info.value.error_kind == "invalid_configuration"
    assert len(router.requests) == 1
    assert delays == []


def test_long_history_propagates_final_consolidation_failure_metadata() -> None:
    failures = [
        LLMProviderError(
            "Cuota temporalmente agotada",
            kind=LLMErrorKind.RATE_LIMITED,
            provider="gemini",
            model="gemini-2.5-flash",
            retryable=True,
        )
        for _ in range(3)
    ]
    router = _SummaryRouter(responses=["resumen-bloque", *failures])
    delays: list[float] = []

    with pytest.raises(HistoriaSummaryCoverageError) as exc_info:
        _resumir_historia_larga(
            "texto " * 30,
            summary_chunk=500,
            max_retries=2,
            retry_delays=[2, 5],
            sleep_fn=delays.append,
            llm_router=router,
        )

    assert exc_info.value.stage == LLMTask.HISTORIA_FINAL_SUMMARY.value
    assert exc_info.value.failed_chunks == []
    assert exc_info.value.attempts == 3
    assert exc_info.value.provider == "gemini"
    assert exc_info.value.model == "gemini-2.5-flash"
    assert exc_info.value.error_kind == "rate_limited"
    assert exc_info.value.retryable is True
    assert delays == [2, 5]


def test_model_routes_use_lite_for_chunks_and_flash_for_high_risk_history() -> None:
    policy = DefaultModelSelectionPolicy()

    chunk_route = policy.resolve(LLMTask.HISTORIA_CHUNK_SUMMARY)
    final_route = policy.resolve(LLMTask.HISTORIA_FINAL_SUMMARY)
    extraction_route = policy.resolve(
        LLMTask.CLINICAL_DOCUMENT_EXTRACT,
        metadata={"document_type": "historia_clinica", "risk_level": "high"},
    )

    assert chunk_route.model == "gemini-2.5-flash-lite"
    assert final_route.model == "gemini-2.5-flash"
    assert extraction_route.model == "gemini-2.5-flash"


def test_epicrisis_renders_quality_panel_only_when_metadata_exists() -> None:
    template = Jinja2Templates(directory="web").env.get_template("epicrisis.html")
    historia = {
        "tipo_documento": "historia_clinica",
        "effective_document_type": "historia_clinica",
        "extraction_metadata": {"total_pages": 10, "pages_with_text": 10, "warnings": []},
        "historia_processing": {"coverage_ratio": 1.0, "chunks_succeeded": 2, "chunks_total": 2},
        "analysis_provider": "gemini",
        "analysis_model_name": "gemini-2.5-flash",
        "review_required": False,
        "review_messages": [],
    }

    rendered = template.render(nombre_paciente="Paciente", historia=historia)
    legacy_rendered = template.render(
        nombre_paciente="Paciente",
        historia={"tipo_documento": "historia_clinica"},
    )

    assert 'id="processing-quality-title"' in rendered
    assert "10 de 10" in rendered
    assert "gemini-2.5-flash" in rendered
    assert 'id="processing-quality-title"' not in legacy_rendered


def test_epicrisis_curation_renders_only_items_requiring_human_review() -> None:
    template = Jinja2Templates(directory="web").env.get_template("epicrisis.html")
    context = {
        "nombre_paciente": "Paciente",
        "case_key": "case-1",
        "curation_schema_version": "v1",
        "curation_version": "version-1",
        "curation_generation_blocked": False,
        "diagnosticos_curados": [
            {
                "item_id": "diagnosis-pending",
                "code": "S202",
                "description": "Diagnóstico pendiente",
                "status": "pendiente_revision",
                "evidence": [],
            },
            {
                "item_id": "diagnosis-auto",
                "code": "S499",
                "description": "Diagnóstico validado automáticamente",
                "status": "validado_automaticamente",
                "evidence": [],
            },
            {
                "item_id": "diagnosis-approved",
                "code": "S400",
                "description": "Diagnóstico aprobado",
                "status": "aprobado",
                "evidence": [],
            },
        ],
        "procedimientos_curados": [
            {
                "item_id": "procedure-conflict",
                "codigo_cups": "793203",
                "codigo_soat": "",
                "codigo_facturacion": "",
                "description": "Procedimiento en conflicto",
                "status": "conflicto",
                "states": ["documentado"],
                "evidence": [],
            },
            {
                "item_id": "procedure-corrected",
                "codigo_cups": "808112",
                "codigo_soat": "",
                "codigo_facturacion": "",
                "description": "Procedimiento corregido",
                "status": "corregido",
                "states": ["realizado"],
                "evidence": [],
            },
            {
                "item_id": "procedure-rejected",
                "codigo_cups": "836301",
                "codigo_soat": "",
                "codigo_facturacion": "",
                "description": "Procedimiento rechazado",
                "status": "rechazado",
                "states": ["facturado"],
                "evidence": [],
            },
        ],
        "conflictos_curacion": [],
        "catalogos_utilizados": [],
    }

    rendered = template.render(**context)

    assert "Diagnóstico pendiente" in rendered
    assert "Procedimiento en conflicto" in rendered
    assert "Diagnóstico validado automáticamente" not in rendered
    assert "Diagnóstico aprobado" not in rendered
    assert "Procedimiento corregido" not in rendered
    assert "Procedimiento rechazado" not in rendered
    assert "Diagnósticos <span>1</span>" in rendered
    assert "Procedimientos <span>1</span>" in rendered
    assert rendered.count('data-curation-action="approve"') == 2


def test_epicrisis_curation_renders_resolved_state_without_review_actions() -> None:
    template = Jinja2Templates(directory="web").env.get_template("epicrisis.html")
    rendered = template.render(
        nombre_paciente="Paciente",
        case_key="case-1",
        curation_schema_version="v1",
        curation_version="version-1",
        curation_generation_blocked=False,
        diagnosticos_curados=[
            {
                "item_id": "diagnosis-auto",
                "code": "S499",
                "description": "Diagnóstico validado automáticamente",
                "status": "validado_automaticamente",
                "evidence": [],
            }
        ],
        procedimientos_curados=[],
        conflictos_curacion=[],
        catalogos_utilizados=[],
    )

    assert "Conciliación al día." in rendered
    assert "No hay diagnósticos ni procedimientos pendientes de revisión." in rendered
    assert "Diagnóstico validado automáticamente" not in rendered
    assert 'data-curation-action="approve"' not in rendered


def test_epicrisis_curation_conflict_does_not_disable_pdf_generation() -> None:
    template = Jinja2Templates(directory="web").env.get_template("epicrisis.html")
    rendered = template.render(
        nombre_paciente="Paciente",
        case_key="case-1",
        shell_sidebar_enabled=True,
        curation_schema_version="v1",
        curation_version="version-1",
        curation_generation_blocked=True,
        curation_blocking_reason="Existe un conflicto diagnóstico principal.",
        diagnosticos_curados=[
            {
                "item_id": "diagnosis-conflict",
                "code": "S423",
                "description": "Diagnóstico en conflicto",
                "status": "conflicto",
                "evidence": [],
            }
        ],
        procedimientos_curados=[],
        conflictos_curacion=[],
        catalogos_utilizados=[],
    )

    assert 'id="generar_pdf_btn"' in rendered
    assert 'id="generar_pdf_btn"\n      onclick="generarPdfEpicrisis()">\n      Generar PDF' in rendered
    assert "Revisión pendiente" in rendered
    assert "Puedes generar el PDF con la selección actual sin resolver primero estas revisiones." in rendered
    assert "Salida bloqueada" not in rendered
    assert "curationGenerationBlocked" not in rendered
