from __future__ import annotations

import importlib
import unittest
from datetime import UTC
from pathlib import Path
from types import SimpleNamespace
from typing import cast
from unittest.mock import patch

from app.batch_processing.infrastructure.mongo_repositories import MongoBatchCaseRepository
from app.config import config
from app.llm.schemas import DocumentoQuirurgicoStructured, HistoriaClinicaStructured
from app.services.clinical_document_service import ClinicalDocumentRequest, ClinicalDocumentService
from app.services.clinical_structured_extraction import ClinicalStructuredExtractionService
from app.services.deterministic_signals import extract_deterministic_signals
from app.services.document_identity_extraction import extract_document_identity
from app.services.historia_summary import (
    extract_historia_edad_text,
    extract_historia_fecha_ingreso_text,
    extract_historia_fecha_nacimiento_text,
    extract_historia_prestador_text,
    extract_historia_sexo_text,
)


FIXTURES_DIR = Path(__file__).parent / "fixtures"


class DeterministicSignalExtractorTest(unittest.TestCase):
    def test_extracts_buga_redacted_header_metadata(self) -> None:
        text = (FIXTURES_DIR / "historia_buga_redacted_header.txt").read_text(encoding="utf-8")

        identity = extract_document_identity(text, strategy="historia_clinica")

        self.assertEqual(identity.case_number, "1390913")
        self.assertEqual(identity.patient_name, "")
        self.assertEqual(identity.patient_id, "")
        self.assertEqual(set(identity.redacted_identity_fields), {"patient_id", "patient_name"})
        self.assertEqual(extract_historia_prestador_text(text), "FUNDACION SAN JOSE DE BUGA")
        self.assertEqual(extract_historia_edad_text(text), "76 AÑOS")
        self.assertEqual(extract_historia_sexo_text(text), "MASCULINO")
        self.assertEqual(extract_historia_fecha_nacimiento_text(text), "08/04/1949")
        self.assertEqual(extract_historia_fecha_ingreso_text(text), "27/10/2025 11:06")

    def test_preserves_valle_salud_header_metadata(self) -> None:
        text = (FIXTURES_DIR / "historia_valle_salud_header.txt").read_text(encoding="utf-8")

        identity = extract_document_identity(text, strategy="historia_clinica")

        self.assertEqual(identity.case_number, "178474")
        self.assertEqual(identity.patient_name, "JULIO CESAR ACOSTA GUEVARA")
        self.assertEqual(identity.patient_id, "93387170")
        self.assertEqual(extract_historia_prestador_text(text), "Inversiones Médicas Valle Salud S.A.S")
        self.assertEqual(extract_historia_edad_text(text), "51 AÑOS")
        self.assertEqual(extract_historia_sexo_text(text), "MASCULINO")
        self.assertEqual(extract_historia_fecha_nacimiento_text(text), "29/05/1972")
        self.assertEqual(extract_historia_fecha_ingreso_text(text), "07/05/2024 09:38")

    def test_normalizes_buga_metadata_and_renders_redaction_without_persisting_placeholder(self) -> None:
        text = (FIXTURES_DIR / "historia_buga_redacted_header.txt").read_text(encoding="utf-8")
        payload = {
            "tipo_documento": "historia_clinica",
            "descripcion": text,
            "case_number": "1390913",
            "patient_id": "",
            "nombre_paciente": "",
            "analysis_structured": {
                "dt": "historia_clinica",
                "pn": "XXXXXXXXXXXXXXXXXXX",
                "ps": "CC - XXXXXXXXXXXXXXXXXXX",
                "nc": "1390913",
                "ip": "1390913",
                "rs": "Paciente hospitalizado para manejo clínico y seguimiento especializado.",
            },
        }

        ClinicalDocumentService._normalize_historia_structured_payload(  # noqa: SLF001
            object.__new__(ClinicalDocumentService),
            payload,
        )

        structured = payload["analysis_structured"]
        self.assertEqual(structured["ps"], "FUNDACION SAN JOSE DE BUGA")
        self.assertEqual(structured["nc"], "1390913")
        self.assertEqual(structured["ed"], "76 AÑOS")
        self.assertEqual(structured["sx"], "MASCULINO")
        self.assertNotIn("pn", structured)
        self.assertNotIn("ip", structured)
        self.assertEqual(set(payload["redacted_identity_fields"]), {"patient_id", "patient_name"})
        analysis_html = payload["analisis_html"]
        self.assertIsInstance(analysis_html, str)
        assert isinstance(analysis_html, str)
        self.assertEqual(analysis_html.count("Dato censurado por protección"), 2)
        self.assertNotIn("XXXXXXXXXXXXXXXXXXX", analysis_html)
        self.assertNotIn("Dato censurado por protección", str(structured))

    def test_manual_identity_overrides_redacted_source_without_losing_traceability(self) -> None:
        text = (FIXTURES_DIR / "historia_buga_redacted_header.txt").read_text(encoding="utf-8")
        payload = {
            "tipo_documento": "historia_clinica",
            "descripcion": text,
            "case_number": "1390913",
            "patient_id": "123456789",
            "nombre_paciente": "PACIENTE IDENTIFICADO MANUALMENTE",
            "redacted_identity_fields": ["patient_id", "patient_name"],
            "analysis_structured": {
                "dt": "historia_clinica",
                "rs": "Paciente hospitalizado para manejo clínico y seguimiento especializado.",
            },
        }

        ClinicalDocumentService._normalize_historia_structured_payload(  # noqa: SLF001
            object.__new__(ClinicalDocumentService),
            payload,
        )

        structured = payload["analysis_structured"]
        self.assertEqual(structured["pn"], "PACIENTE IDENTIFICADO MANUALMENTE")
        self.assertEqual(structured["ip"], "123456789")
        self.assertEqual(set(payload["redacted_identity_fields"]), {"patient_id", "patient_name"})
        self.assertNotIn("Dato censurado por protección", payload["analisis_html"])

    def test_extracts_metadata_inline_codes_dates_and_candidates(self) -> None:
        text = """
        Nombre del paciente: ANA MARIA LOPEZ
        CC 12345678
        Caso No: CASE-77
        Fecha de ingreso: 2026-05-20

        K35.8 - Apendicitis aguda

        Procedimientos realizados
        123456 Apendicectomia laparoscopica

        Medicamentos administrados
        Dipirona - 1 g IV
        Acetaminofen - 500 mg VO
        """

        snapshot = extract_deterministic_signals(text, "historia_clinica")

        self.assertEqual(snapshot.patient_name, "ANA MARIA LOPEZ")
        self.assertEqual(snapshot.patient_id, "12345678")
        self.assertEqual(snapshot.case_number, "CASE-77")
        self.assertIn("2026-05-20", snapshot.dates)
        self.assertTrue(snapshot.has_inline_cie10)
        self.assertTrue(snapshot.has_inline_cups)
        self.assertEqual(snapshot.inline_cie10[0]["codigo"], "K35.8")
        self.assertEqual(snapshot.inline_cups[0]["codigo_cups"], "123456")
        self.assertIn("123456 Apendicectomia laparoscopica", snapshot.procedures)
        self.assertIn("Dipirona - 1 g IV", snapshot.medications)
        self.assertTrue(snapshot.has_structured_enough_metadata)

    def test_marks_historia_summary_as_skippable_after_deterministic_compaction(self) -> None:
        repeated_line = "Evolucion estable sin cambios clinicos relevantes."
        text = "\n".join([repeated_line] * 40)

        snapshot = extract_deterministic_signals(
            text,
            "historia_clinica",
            summary_threshold=200,
            max_chars=120,
        )

        self.assertTrue(snapshot.should_skip_historia_summary)
        self.assertEqual(snapshot.compacted_text, repeated_line)

    def test_historia_patient_id_does_not_fall_back_to_case_number(self) -> None:
        text = """
        Caso: 203922
        NO. ADMISION: 344006
        CC - 1088025550Identificación
        LUISA FERNANDA IBARRA CANONombre del Paciente
        203922No. de Caso:
        """

        snapshot = extract_deterministic_signals(text, "historia_clinica")

        self.assertEqual(snapshot.case_number, "203922")
        self.assertEqual(snapshot.patient_id, "1088025550")
        self.assertEqual(snapshot.patient_name, "LUISA FERNANDA IBARRA CANO")

    def test_historia_patient_name_rejects_narrative_candidate(self) -> None:
        text = """
        Caso: 203922
        Paciente:
        TRAIDO POR PERSONAL PARAMEDICO
        """

        snapshot = extract_deterministic_signals(text, "historia_clinica")

        self.assertEqual(snapshot.patient_name, "")

    def test_document_identity_strategy_extracts_prefactura_context(self) -> None:
        text = """
        Extracto de Cuenta
        PreFactura de Servicios
        Caso No. CM - 203922
        Paciente :
        Fecha Ingreso : 25/05/2026
        LUISA FERNANDA IBARRA CANOConvenio: Señores :
        """

        identity = extract_document_identity(text, strategy="prefactura")

        self.assertEqual(identity.strategy, "prefactura")
        self.assertEqual(identity.case_number, "203922")
        self.assertEqual(identity.patient_name, "LUISA FERNANDA IBARRA CANO")
        self.assertIn("prefactura_patient_name", identity.evidence)

    def test_document_identity_strategy_extracts_historia_context(self) -> None:
        text = """
        Caso: 203922
        NO. ADMISION: 344006
        CC - 1088025550Identificación
        LUISA FERNANDA IBARRA CANONombre del Paciente
        """

        identity = extract_document_identity(text, strategy="historia_clinica")

        self.assertEqual(identity.strategy, "historia_clinica")
        self.assertEqual(identity.case_number, "203922")
        self.assertEqual(identity.patient_id, "1088025550")
        self.assertEqual(identity.patient_name, "LUISA FERNANDA IBARRA CANO")

    def test_historia_identity_reads_admission_and_marks_masked_fields(self) -> None:
        text = """
        No. Historia: CCXXXXXXXXXXXXXXXX - Admision: 1390913 - Paciente: XXXXXXXXXXXXXXXXXXXX1 de 31
        No. H. C. CCXXXXXXXXXX - 1390913 Fecha de Ingreso 27/10/2025 11:06
        PACIENTE XXXXXXXXXXXXXXXXXXXX DOC. ID. CC - XXXXXXXXXXXXXXXX
        En caso DRENAJE oscuro.
        """

        identity = extract_document_identity(text, strategy="historia_clinica")

        self.assertEqual(identity.case_number, "1390913")
        self.assertEqual(identity.patient_id, "")
        self.assertEqual(identity.patient_name, "")
        self.assertEqual(
            set(identity.redacted_identity_fields),
            {"patient_id", "patient_name"},
        )
        self.assertIn("identificacion_censurada", identity.evidence)
        self.assertIn("nombre_paciente_censurado", identity.evidence)

    def test_historia_explicit_case_has_priority_over_admission(self) -> None:
        identity = extract_document_identity(
            "Caso: 178474\nNO. ADMISION: 298229\nEn este caso DRENAJE",
            strategy="historia_clinica",
        )

        self.assertEqual(identity.case_number, "178474")

    def test_historia_rejects_alphabetic_case_candidate(self) -> None:
        identity = extract_document_identity(
            "En este caso OSCURO se indica DRENAJE.",
            strategy="historia_clinica",
        )

        self.assertEqual(identity.case_number, "")


class ClinicalDocumentServiceDeterministicGatingTest(unittest.TestCase):
    class FakeInsertResult:
        inserted_id = "analysis-1"

    class FakeCollection:
        def __init__(self) -> None:
            self.inserted: list[dict] = []

        def insert_one(self, payload):
            self.inserted.append(dict(payload))
            return ClinicalDocumentServiceDeterministicGatingTest.FakeInsertResult()

        def find_one(self, *args, **kwargs):
            return None

    class FakeLegacyStorageCollection:
        def find_one(self, *args, **kwargs):
            return None

    class TrackingCie10Retriever:
        def __init__(self) -> None:
            self.batch_called = False
            self.single_called = False
            self.db = object()
            self.single_calls: list[tuple[str, int]] = []

        def asignar_codigos_batch(self, html):
            self.batch_called = True
            raise AssertionError("No debió usar asignar_codigos_batch")

        def asignar_codigo_cie10(self, diagnostico, temperature=0.0, k=30):
            self.single_called = True
            self.single_calls.append((diagnostico, k))
            return "Código: A00 - Descripción: Diagnóstico de prueba"

    class TrackingCupsRetriever:
        def __init__(self) -> None:
            self.called = False
            self.calls: list[list[str]] = []

        def asignar_codigos(self, procedimientos):
            self.called = True
            self.calls.append(list(procedimientos))
            return SimpleNamespace(
                to_dict=lambda orient="records": [
                    {"codigo_cups": "123456", "procedimiento": procedimientos[0], "score": 0.9}
                ]
            )

    def _build_service(self, *, cie10_retriever, cups_retriever) -> ClinicalDocumentService:
        service = ClinicalDocumentService(
            mongo_storage=SimpleNamespace(
                guardar_analisis=lambda **kwargs: "legacy-1",
                collection=self.FakeLegacyStorageCollection(),
            ),
            mongo_analyses=SimpleNamespace(collection=self.FakeCollection()),
            client_groq=None,
            client_gemini=None,
            cie10_retriever=cie10_retriever,
            cups_retriever=cups_retriever,
            colombia_tz=UTC,
            llm_router=SimpleNamespace(),
        )
        service.batch_case_repository = cast(
            MongoBatchCaseRepository,
            SimpleNamespace(get_user_case=lambda username, case_key: None),
        )
        return service

    def test_historia_uses_inline_codes_without_invoking_retrievers(self) -> None:
        cie10_retriever = self.TrackingCie10Retriever()
        cups_retriever = self.TrackingCupsRetriever()
        service = self._build_service(cie10_retriever=cie10_retriever, cups_retriever=cups_retriever)

        structured_model = HistoriaClinicaStructured.model_validate(
            {
                "pn": "ANA MARIA LOPEZ",
                "rs": "Resumen breve",
                "dx": [],
                "pr": [{"c": "123456", "d": "Apendicectomia laparoscopica"}],
                "md": [],
            }
        )
        historia_request = SimpleNamespace(
            extraer_historia_estructurada=lambda raw_text, llm_router=None: SimpleNamespace(
                analysis_structured=structured_model.model_dump(
                    by_alias=True,
                    exclude_none=True,
                    exclude_defaults=True,
                ),
                analysis_model=structured_model,
                rendered_html="<p>historia</p>",
            )
        )

        with patch(
            "app.services.clinical_document_service._load_historia_clinica_request_class",
            return_value=historia_request,
        ):
            result = service.process_and_persist(
                ClinicalDocumentRequest(
                    raw_text=(
                        "Nombre del paciente: ANA MARIA LOPEZ\n"
                        "K35.8 - Apendicitis aguda\n"
                        "Procedimientos realizados\n"
                        "123456 Apendicectomia laparoscopica\n"
                    ),
                    detected_type="historia_clinica",
                    username="tester",
                    original_name="historia.pdf",
                )
            )

        self.assertFalse(cie10_retriever.batch_called)
        self.assertFalse(cie10_retriever.single_called)
        self.assertFalse(cups_retriever.called)
        self.assertEqual(result["codigos_cie10"][0]["codigo"], "K35.8")
        self.assertEqual(result["codigos_cups"][0]["codigo_cups"], "123456")

    def test_quirurgico_uses_inline_cups_without_invoking_retriever(self) -> None:
        cups_retriever = self.TrackingCupsRetriever()
        service = self._build_service(
            cie10_retriever=self.TrackingCie10Retriever(),
            cups_retriever=cups_retriever,
        )

        structured_model = DocumentoQuirurgicoStructured.model_validate(
            {
                "pn": "PACIENTE QX",
                "pp": "Colecistectomia",
                "hl": "Hallazgos",
                "dp": "Descripcion",
                "dx": [],
                "pr": [
                    {
                        "c": "765432",
                        "d": "Colecistectomia laparoscopica",
                    }
                ],
            }
        )
        service.structured_extraction_service = cast(
            ClinicalStructuredExtractionService,
            SimpleNamespace(
                extract=lambda **kwargs: SimpleNamespace(
                    analysis_structured=structured_model.model_dump(
                        by_alias=True,
                        exclude_none=True,
                        exclude_defaults=True,
                    ),
                    analysis_model=structured_model,
                    rendered_html="<p>quirurgico</p>",
                )
            ),
        )

        result = service.process_and_persist(
            ClinicalDocumentRequest(
                raw_text="Procedimientos realizados\n765432 Colecistectomia laparoscopica",
                detected_type="quirurgico",
                username="tester",
                original_name="qx.pdf",
            )
        )

        self.assertFalse(cups_retriever.called)
        self.assertEqual(result["codigos_cups"][0]["codigo_cups"], "765432")

    def test_patient_name_resolution_prefers_deterministic_text_before_html(self) -> None:
        service = self._build_service(
            cie10_retriever=self.TrackingCie10Retriever(),
            cups_retriever=self.TrackingCupsRetriever(),
        )
        raw_text = "Nombre del paciente: JUAN PEREZ GOMEZ"

        resolved = service._resolve_patient_name_from_sources(
            provided_patient_name="",
            case_key="",
            username="tester",
            analisis_html="<p><b>Nombre del paciente</b></p><p>Paciente HTML</p>",
            analysis_structured={},
            raw_text=raw_text,
            deterministic_signals=extract_deterministic_signals(raw_text, "laboratorio"),
        )

        self.assertEqual(resolved, "JUAN PEREZ GOMEZ")

    def test_assign_cie10_list_uses_configured_k_and_deduplicates_inputs(self) -> None:
        cie10_retriever = self.TrackingCie10Retriever()
        service = self._build_service(
            cie10_retriever=cie10_retriever,
            cups_retriever=self.TrackingCupsRetriever(),
        )

        with patch.object(config, "CIE10_RESOLUTION_K", 2):
            result = service._assign_cie10_list(
                [
                    "Dolor abdominal agudo",
                    "Dolor abdominal agudo",
                    "Fiebre persistente",
                    "Nauseas",
                ]
            )

        self.assertEqual(len(result), 2)
        self.assertEqual(
            cie10_retriever.single_calls, [("Dolor abdominal agudo", 2), ("Fiebre persistente", 2)]
        )

    def test_assign_cups_list_deduplicates_and_limits_pending_procedures(self) -> None:
        cups_retriever = self.TrackingCupsRetriever()
        service = self._build_service(
            cie10_retriever=self.TrackingCie10Retriever(),
            cups_retriever=cups_retriever,
        )

        with patch.object(config, "CIE10_RESOLUTION_K", 2):
            result, error = service._assign_cups_list(
                [
                    "Apendicectomia laparoscopica",
                    "Apendicectomia laparoscopica",
                    "Colecistectomia",
                    "Lavado quirurgico",
                ]
            )

        self.assertIsNone(error)
        self.assertEqual(cups_retriever.calls, [["Apendicectomia laparoscopica", "Colecistectomia"]])
        self.assertEqual(len(result), 1)


class HistoriaPromptGatingTest(unittest.TestCase):
    def _load_resumen_google_module(self):
        with (
            patch.object(config, "GROQ_API_KEY", "test-key"),
            patch.object(
                config,
                "GEMINI_API_KEY",
                "test-key",
            ),
        ):
            module = importlib.import_module("modules.processing.resumen_google")
            return importlib.reload(module)

    def test_prepare_historia_prompt_text_summarizes_full_long_source_even_if_compaction_fits(self) -> None:
        module = self._load_resumen_google_module()
        text = "\n".join(["Paciente estable sin cambios relevantes."] * 50)

        with patch.object(module, "_resumir_historia_larga", return_value="Resumen estable") as summary_mock:
            prompt_text = module._prepare_historia_prompt_text(
                text,
                summary_threshold=200,
                summary_chunk=500,
                max_chars=120,
            )

        self.assertIn("[PACK HISTORIA CLINICA]", prompt_text)
        summary_mock.assert_called_once()
        self.assertIn("version=v5", prompt_text)
        self.assertIn("Resumen estable", prompt_text)

    def test_prepare_historia_prompt_text_uses_summary_when_compaction_is_still_large(self) -> None:
        module = self._load_resumen_google_module()
        text = "\n".join(f"Linea clinica unica {index} con detalles relevantes" for index in range(40))

        with patch.object(module, "_resumir_historia_larga", return_value="Resumen compacto") as summary_mock:
            prompt_text = module._prepare_historia_prompt_text(
                text,
                summary_threshold=200,
                summary_chunk=500,
                max_chars=500,
            )

        summary_mock.assert_called_once()
        self.assertIn("[RESUMEN PREVIO DEL DOCUMENTO COMPLETO]", prompt_text)
        self.assertIn("Resumen compacto", prompt_text)

    def test_prepare_historia_prompt_text_applies_explicit_compact_budget(self) -> None:
        module = self._load_resumen_google_module()
        text = "\n".join(
            [
                "Linea repetida de contexto que no debe duplicarse",
                "Linea repetida de contexto que no debe duplicarse",
                "Detalle clinico adicional que debe preservarse si cabe",
            ]
        )

        with (
            patch.object(module, "_resumir_historia_larga", side_effect=AssertionError("no-call")),
            patch.object(
                config,
                "HISTORIA_COMPACT_MAX_CHARS",
                70,
            ),
        ):
            prompt_text = module._prepare_historia_prompt_text(
                text,
                summary_threshold=1000,
                summary_chunk=500,
                max_chars=120,
            )

        self.assertLessEqual(len(prompt_text), 120)
        self.assertIn("version=v5", prompt_text)
        self.assertEqual(prompt_text.count("Linea repetida"), 1)

    def test_prepare_historia_prompt_text_prioritizes_summary_section_over_eps_header(self) -> None:
        module = self._load_resumen_google_module()
        text = "\n".join(
            [
                "EPS: SEGUROS COMERCIALES BOLIVAR S.A.",
                "Nombre del paciente: JUAN PEREZ",
                "Resumen",
                "Paciente con trauma toracico, dolor intenso y manejo hospitalario inicial.",
                "Diagnósticos",
                "S20.2 - Contusión de tórax",
            ]
        )

        with patch.object(module, "_resumir_historia_larga", side_effect=AssertionError("no-call")):
            prompt_text = module._prepare_historia_prompt_text(
                text,
                summary_threshold=10_000,
                summary_chunk=500,
                max_chars=600,
            )

        self.assertIn("[RESUMEN CLINICO]", prompt_text)
        self.assertIn(
            "Paciente con trauma toracico, dolor intenso y manejo hospitalario inicial.", prompt_text
        )
        self.assertIn("EPS: SEGUROS COMERCIALES BOLIVAR S.A.", prompt_text)
        self.assertLess(
            prompt_text.index("[RESUMEN CLINICO]"),
            prompt_text.index("EPS: SEGUROS COMERCIALES BOLIVAR S.A."),
        )

    def test_extraer_historia_estructurada_prefers_detailed_generated_summary(self) -> None:
        module = self._load_resumen_google_module()
        detailed_summary = (
            "Paciente femenina de 47 años que ingresa por accidente de tránsito con trauma en tobillo y pie "
            "izquierdos, edema, limitación funcional y herida en primer dedo del pie izquierdo. Niega otros "
            "traumas y síntomas respiratorios; se documentan antecedentes, estudios diagnósticos y manejo inicial."
        )
        structured_model = HistoriaClinicaStructured.model_validate(
            {
                "pn": "BLANCA LIJIA RENGIFO AGUIRRE",
                "rs": "PACIENTE QUIEN INGRESA EN CONTEXTO DE ACCIDENTE DE TRANSITO, TRAIDA POR PARAMEDICOS.",
                "dx": [],
                "pr": [],
                "md": [],
            }
        )
        fake_result = SimpleNamespace(
            analysis_model=structured_model,
            analysis_structured=structured_model.model_dump(
                by_alias=True, exclude_none=True, exclude_defaults=True
            ),
            rendered_html="<p>HTML previo</p>",
        )

        with (
            patch.object(config, "HISTORIA_SUMMARY_THRESHOLD", 100),
            patch.object(module, "_resumir_historia_larga", return_value=detailed_summary),
            patch.object(
                module,
                "ClinicalStructuredExtractionService",
                return_value=SimpleNamespace(extract=lambda **kwargs: fake_result),
            ),
        ):
            result = module.HistoriaClinicaRequest.extraer_historia_estructurada(
                "\n".join(
                    [
                        "Resumen",
                        "PACIENTE QUIEN INGRESA EN CONTEXTO DE ACCIDENTE DE TRANSITO, TRAIDA POR PARAMEDICOS.",
                        *[f"Linea clinica detallada {idx}" for idx in range(400)],
                    ]
                ),
                llm_router=SimpleNamespace(),
            )

        self.assertEqual(result.analysis_structured["rs"], detailed_summary)
        self.assertIn("Paciente femenina de 47 años", result.rendered_html)
