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
