from __future__ import annotations

import unittest
from types import SimpleNamespace
from unittest.mock import patch

from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient

from app.auth import get_current_user
from app.case_epicrisis.infrastructure.gateways import ServicesEpicrisisCodingGateway
from app.config import config
from app.llm import LLMOutputKind, LLMResolvedRoute
from app.llm.schemas import SoatGeneratedCodeStructured, SoatGlosaStructured, SoatValidationStructured
from app.routes import soat
from app.services.soat_processing import (
    _agente_codificar_desde_soat_item,
    _construir_soat_y_glosa,
    _resolve_soat_validation_model,
    _validar_codigo_soat_con_documento,
    generar_codigos_desde_soat_con_gemini,
)


class _FakeDoc:
    def __init__(self, page: int, content: str) -> None:
        self.metadata = {"page": page, "source": "soat.pdf"}
        self.page_content = content
        self.score = 0.9


class _FakeSoatRetriever:
    def __init__(self, docs: list[_FakeDoc] | None = None) -> None:
        self.docs = docs
        self.calls: list[tuple[str, int]] = []

    def buscar(self, query: str, k: int = 8):
        self.calls.append((query, k))
        if self.docs is not None:
            return list(self.docs)
        return [_FakeDoc(12, f"Evidencia {query}"), _FakeDoc(12, f"Evidencia {query}")]

    def asignar_codigos(self, procedimientos):
        return [{"codigo_soat": "S1", "descripcion": str(procedimientos[0] or "").strip()}]


class _FakeCupsDataFrame:
    def __init__(self, rows):
        self._rows = list(rows)

    def iterrows(self):
        yield from enumerate(self._rows)


class _FakeCupsRetriever:
    def __init__(self, rows=None) -> None:
        self.rows = rows or [
            {"procedimiento": "Lavado quirúrgico", "codigo_cups": "123456", "score": 0.95},
            {"procedimiento": "Curación avanzada", "codigo_cups": "654321", "score": 0.82},
        ]

    def asignar_codigos(self, procedimientos):
        return _FakeCupsDataFrame(self.rows)


class _FakeCie10Retriever:
    def __init__(self) -> None:
        self.calls: list[tuple[str, int | None]] = []

    def asignar_codigo_cie10(self, descripcion: str, k=None, **kwargs):
        self.calls.append((descripcion, k))
        return "Código: S82.2 - Descripción: Fractura de tibia\nCódigo: T14.9 - Descripción: Trauma no especificado"


class _FakeLLMRouter:
    def __init__(self) -> None:
        self.generate_structured_calls = 0
        self.generate_text_calls: list[str] = []

    def _resolve_route(self, task):
        return LLMResolvedRoute(
            task=task,
            output_kind=LLMOutputKind.STRUCTURED_OBJECT,
            provider="gemini",
            model="gemini-2.5-flash",
        )

    def generate_text(self, request):
        self.generate_text_calls.append(request.task.value)
        if request.task.value == "soat_chat":
            return SimpleNamespace(content="Respuesta SOAT controlada", provider="gemini", model="gemini-2.5-flash")
        if request.task.value == "soat_manual_code_lookup":
            return SimpleNamespace(
                content="S123 - Procedimiento SOAT (p.12)",
                provider="gemini",
                model="gemini-2.5-flash",
            )
        return SimpleNamespace(
            content="Salida textual controlada",
            provider="gemini",
            model="gemini-2.5-flash",
        )

    def generate_structured(self, request):
        self.generate_structured_calls += 1
        model = request.output_model
        if model is SoatGeneratedCodeStructured:
            payload = SoatGeneratedCodeStructured(
                codigo_soat="S1",
                descripcion="Lavado quirúrgico",
                cie10_principal={"c": "S82.2", "d": "Fractura de tibia", "j": "Diagnóstico principal"},
                cie10_secundarios=[{"c": "T14.9", "d": "Trauma no especificado"}],
                cups_principal={"c": "123456", "d": "Lavado quirúrgico", "j": "Procedimiento principal"},
                cups_alternativos=[{"c": "654321", "d": "Curación avanzada"}],
                analisis_clinico="Coherencia diagnóstica y procedimental adecuada.",
                justificacion="Corresponde al procedimiento descrito.",
                fuente="llm_router",
            )
            return SimpleNamespace(content=payload.model_dump(by_alias=True, exclude_none=True))
        if model is SoatValidationStructured:
            payload = SoatValidationStructured(
                estado_validacion="✅ JUSTIFICADO",
                verificacion_manual="Código presente en manual SOAT y consistente con la descripción.",
                coherencia_documental="Hallazgos, procedimiento y diagnósticos son coherentes entre sí.",
                riesgo_glosa="Riesgo bajo con soporte documental suficiente.",
                recomendaciones="Agregar cita de página del manual en el soporte final.",
                pertinencia_codigos="La combinación CUPS/CIE-10 respalda el cobro.",
                nota_aclaratoria="Se confirma procedimiento de lavado quirúrgico por trauma abierto.",
                resumen_validacion="El código está justificado por la evidencia clínica y el manual SOAT.",
                cie10_propuestos=[{"c": "S82.2", "d": "Fractura de tibia"}],
                cups_propuestos=[{"c": "123456", "d": "Lavado quirúrgico"}],
                evidencia_soat=[{"p": 12, "e": "Evidencia manual SOAT", "s": 0.9}],
                contexto_cups="CUPS 123456 lavado quirúrgico",
                contexto_cie10="CIE-10 S82.2 fractura de tibia",
            )
            return SimpleNamespace(content=payload.model_dump(by_alias=True, exclude_none=True))
        if model is SoatGlosaStructured:
            payload = SoatGlosaStructured(
                resumen="La documentación es consistente y el riesgo de glosa es bajo.",
                riesgos_altos=[],
                riesgos_medios=["Asegurar soporte de página del manual SOAT."],
                mitigaciones=["Adjuntar referencia normativa y nota operatoria."],
                nota_sugerida="Procedimiento justificado por hallazgos intraoperatorios.",
            )
            return SimpleNamespace(content=payload.model_dump(by_alias=True, exclude_none=True))
        raise AssertionError(f"Modelo no esperado: {model}")


class _FailingLLMRouter:
    def _resolve_route(self, task):
        return LLMResolvedRoute(
            task=task,
            output_kind=LLMOutputKind.STRUCTURED_OBJECT,
            provider="gemini",
            model="gemini-2.5-flash",
        )

    def generate_structured(self, request):
        raise RuntimeError("router unavailable")


class _MemoryCacheRepository:
    def __init__(self) -> None:
        self.cache_version = "v1"
        self.items: dict[tuple[str, str, str], dict] = {}

    def get(self, *, username: str, task: str, fingerprint: str):
        return self.items.get((username, task, fingerprint))

    def upsert(self, *, username: str, task: str, fingerprint: str, provider: str, model: str, payload, metadata=None):
        self.items[(username, task, fingerprint)] = {
            "usuario": username,
            "task": task,
            "fingerprint": fingerprint,
            "provider": provider,
            "model": model,
            "payload": payload,
            "metadata": dict(metadata or {}),
        }


class SoatProcessingStructuredTest(unittest.IsolatedAsyncioTestCase):
    async def test_validar_codigo_soat_returns_legacy_compatible_shape(self) -> None:
        result = await _validar_codigo_soat_con_documento(
            codigo_soat="S1",
            descripcion_soat="Lavado quirúrgico",
            diagnosticos_prequx="Fractura abierta de tibia",
            procedimientos="Lavado quirúrgico",
            hallazgos="Contaminación moderada",
            descripcion_proc="Se realiza lavado y desbridamiento",
            justificacion="Control de daño",
            diagnosticos_postqx="Fractura estabilizada",
            soat_retriever=_FakeSoatRetriever(),
            retriever=_FakeCie10Retriever(),
            cups_retriever=_FakeCupsRetriever(),
            client_groq=None,
            client_gemini=None,
            llm_router=_FakeLLMRouter(),
        )

        self.assertIn("JUSTIFICADO", result["validacion_resultado"])
        self.assertEqual(result["cie10_propuestos"][0]["codigo"], "S82.2")
        self.assertEqual(result["cups_propuestos"][0]["codigo_cups"], "123456")
        self.assertEqual(result["contexto_rag"]["total_evidencia_soat"], 1)

    async def test_soat_budgeting_limits_evidence_and_retrieval_sizes(self) -> None:
        soat_retriever = _FakeSoatRetriever(
            docs=[
                _FakeDoc(12, "A" * 1200),
                _FakeDoc(12, "A" * 1200),
                _FakeDoc(13, "B" * 1200),
                _FakeDoc(14, "C" * 1200),
            ]
        )
        cie10_retriever = _FakeCie10Retriever()
        cups_retriever = _FakeCupsRetriever(
            rows=[
                {"procedimiento": f"Procedimiento {i}", "codigo_cups": f"{123450 + i}", "score": 1 - (i * 0.01)}
                for i in range(12)
            ]
        )

        with patch.object(config, "SOAT_RAG_PRIMARY_K", 6), patch.object(config, "SOAT_RAG_CODE_K", 4), patch.object(
            config,
            "SOAT_RAG_DESC_K",
            4,
        ), patch.object(config, "SOAT_RAG_MAX_EVIDENCE", 2), patch.object(
            config,
            "SOAT_RAG_MAX_EXTRACT_CHARS",
            900,
        ), patch.object(config, "SOAT_PROMPT_MAX_CUPS", 3), patch.object(config, "SOAT_PROMPT_MAX_CIE10", 1):
            result = await _validar_codigo_soat_con_documento(
                codigo_soat="S1",
                descripcion_soat="Lavado quirúrgico",
                diagnosticos_prequx="Fractura abierta de tibia",
                procedimientos="Lavado quirúrgico",
                hallazgos="Contaminación moderada",
                descripcion_proc="Se realiza lavado y desbridamiento",
                justificacion="Control de daño",
                diagnosticos_postqx="Fractura estabilizada",
                soat_retriever=soat_retriever,
                retriever=cie10_retriever,
                cups_retriever=cups_retriever,
                client_groq=None,
                client_gemini=None,
                llm_router=_FailingLLMRouter(),
            )

        self.assertEqual(soat_retriever.calls[0][1], 6)
        self.assertEqual(soat_retriever.calls[1][1], 4)
        self.assertEqual(soat_retriever.calls[2][1], 4)
        self.assertEqual(len(result["evidencia_soat"]), 2)
        self.assertLessEqual(len(result["evidencia_soat"][0]["extracto"]), 900)
        self.assertEqual(len(result["cups_propuestos"]), 3)
        self.assertEqual(len(result["cie10_propuestos"]), 1)

    def test_generate_codes_from_soat_uses_structured_router_and_keeps_legacy_shape(self) -> None:
        with patch(
            "app.services.soat_processing._parsear_respuesta_gemini_codigos",
            side_effect=AssertionError("legacy parser should not run"),
        ):
            result = generar_codigos_desde_soat_con_gemini(
                [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}],
                client_gemini=None,
                llm_router=_FakeLLMRouter(),
            )

        self.assertEqual(result[0]["codigo_soat"], "S1")
        self.assertEqual(result[0]["cie10_principal"]["codigo"], "S82.2")
        self.assertEqual(result[0]["cups_principal"]["codigo"], "123456")
        self.assertEqual(result[0]["fuente"], "llm_router")

    def test_generate_codes_from_soat_returns_compatible_defaults_when_router_fails(self) -> None:
        with patch(
            "app.services.soat_processing._parsear_respuesta_gemini_codigos",
            side_effect=AssertionError("legacy parser should not run"),
        ):
            result = generar_codigos_desde_soat_con_gemini(
                [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}],
                client_gemini=None,
                llm_router=_FailingLLMRouter(),
            )

        self.assertEqual(result[0]["codigo_soat"], "S1")
        self.assertIsNone(result[0]["cie10_principal"])
        self.assertEqual(result[0]["fuente"], "fallback")
        self.assertIn("codificación estructurada", result[0]["error"])

    def test_generate_codes_from_soat_reuses_cached_structured_payload(self) -> None:
        router = _FakeLLMRouter()
        cache_repository = _MemoryCacheRepository()

        first = generar_codigos_desde_soat_con_gemini(
            [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}],
            client_gemini=None,
            llm_router=router,
            cache_repository=cache_repository,
            username="auditor",
        )
        self.assertEqual(router.generate_structured_calls, 1)

        second = generar_codigos_desde_soat_con_gemini(
            [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}],
            client_gemini=None,
            llm_router=router,
            cache_repository=cache_repository,
            username="auditor",
        )

        self.assertEqual(router.generate_structured_calls, 1)
        self.assertEqual(first, second)

    def test_generate_codes_from_soat_does_not_cache_fallback_errors(self) -> None:
        cache_repository = _MemoryCacheRepository()
        failing_router = _FailingLLMRouter()
        result = generar_codigos_desde_soat_con_gemini(
            [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}],
            client_gemini=None,
            llm_router=failing_router,
            cache_repository=cache_repository,
            username="auditor",
        )

        self.assertEqual(result[0]["fuente"], "fallback")
        self.assertEqual(cache_repository.items, {})

    def test_construir_soat_y_glosa_renders_structured_html(self) -> None:
        soat_resultados, glosa_html = _construir_soat_y_glosa(
            historia_html="<p>Historia</p>",
            qx_html="<p><b>Procedimientos realizados</b></p><ol><li>Lavado quirúrgico</li></ol>",
            soat_retriever=_FakeSoatRetriever(),
            client_groq=None,
            llm_router=_FakeLLMRouter(),
        )

        self.assertEqual(soat_resultados[0]["codigo_soat"], "S1")
        self.assertIn("Análisis de Riesgo de Glosa", glosa_html)
        self.assertIn("Mitigaciones Recomendadas", glosa_html)

    def test_construir_soat_y_glosa_uses_deterministic_fallback_without_legacy_llm(self) -> None:
        with patch(
            "app.services.soat_processing.analizar_glosa_y_generar_nota",
            side_effect=AssertionError("legacy glosa generator should not run"),
        ):
            _, glosa_html = _construir_soat_y_glosa(
                historia_html="<p>Historia</p>",
                qx_html="<p>Lavado quirúrgico</p>",
                soat_retriever=_FakeSoatRetriever(),
                client_groq=None,
                llm_router=_FailingLLMRouter(),
            )

        self.assertIn("Análisis de Riesgo de Glosa", glosa_html)
        self.assertIn("Pendiente de validación manual", glosa_html)

    async def test_agent_and_invoice_validation_share_structured_resolution_path(self) -> None:
        validation_model = _resolve_soat_validation_model(
            llm_router=_FakeLLMRouter(),
            metadata={"operation": "test", "codigo_soat": "S1"},
            codigo_soat="S1",
            descripcion_soat="Lavado quirúrgico",
            diagnosticos_prequx="",
            procedimientos="Lavado quirúrgico",
            hallazgos="",
            descripcion_proc="Lavado quirúrgico",
            justificacion="",
            diagnosticos_postqx="",
            evidence_bundle={
                "evidencia_soat": [{"page": 12, "extracto": "Evidencia"}],
                "propuestas_cie10": [{"codigo": "S82.2", "descripcion": "Fractura de tibia"}],
                "propuestas_cups": [{"codigo_cups": "123456", "procedimiento": "Lavado quirúrgico"}],
                "contexto_cie10": "CIE-10 S82.2",
                "contexto_cups": "CUPS 123456",
            },
        )

        with patch(
            "app.services.soat_processing._resolve_soat_validation_model",
            return_value=validation_model,
        ) as resolver:
            _agente_codificar_desde_soat_item(
                {"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"},
                _FakeSoatRetriever(),
                _FakeCie10Retriever(),
                _FakeCupsRetriever(),
                None,
                None,
                llm_router=_FakeLLMRouter(),
            )
            result = await _validar_codigo_soat_con_documento(
                codigo_soat="S1",
                descripcion_soat="Lavado quirúrgico",
                diagnosticos_prequx="Fractura abierta de tibia",
                procedimientos="Lavado quirúrgico",
                hallazgos="Contaminación moderada",
                descripcion_proc="Se realiza lavado y desbridamiento",
                justificacion="Control de daño",
                diagnosticos_postqx="Fractura estabilizada",
                soat_retriever=_FakeSoatRetriever(),
                retriever=_FakeCie10Retriever(),
                cups_retriever=_FakeCupsRetriever(),
                client_groq=None,
                client_gemini=None,
                llm_router=_FakeLLMRouter(),
            )

        self.assertEqual(resolver.call_count, 2)
        self.assertIn("validacion_resultado", result)

    async def test_invoice_validation_returns_defaults_when_router_fails_without_legacy_text_llm(self) -> None:
        with patch(
            "app.services.soat_processing._generar_analisis_rag",
            side_effect=AssertionError("legacy analysis should not run"),
        ), patch(
            "app.services.soat_processing._generar_nota_aclaratoria_soat",
            side_effect=AssertionError("legacy note generator should not run"),
        ):
            result = await _validar_codigo_soat_con_documento(
                codigo_soat="S1",
                descripcion_soat="Lavado quirúrgico",
                diagnosticos_prequx="Fractura abierta de tibia",
                procedimientos="Lavado quirúrgico",
                hallazgos="Contaminación moderada",
                descripcion_proc="Se realiza lavado y desbridamiento",
                justificacion="Control de daño",
                diagnosticos_postqx="Fractura estabilizada",
                soat_retriever=_FakeSoatRetriever(),
                retriever=_FakeCie10Retriever(),
                cups_retriever=_FakeCupsRetriever(),
                client_groq=None,
                client_gemini=None,
                llm_router=_FailingLLMRouter(),
            )

        self.assertIn("REQUIERE REVISIÓN", result["validacion_resultado"])
        self.assertGreaterEqual(len(result["cie10_propuestos"]), 1)
        self.assertGreaterEqual(len(result["cups_propuestos"]), 1)

    async def test_soat_routes_keep_http_contract_with_structured_backend(self) -> None:
        app = FastAPI()
        app.include_router(soat.router)

        async def override_current_user():
            return SimpleNamespace(username="auditor", role="auditor")

        app.dependency_overrides[get_current_user] = override_current_user
        app.state.services = SimpleNamespace(
            client_groq=None,
            client_gemini=None,
            llm_router=_FakeLLMRouter(),
            llm_task_cache_repository=None,
            soat_retriever=_FakeSoatRetriever(),
            cie10_retriever=_FakeCie10Retriever(),
            cups_retriever=_FakeCupsRetriever(),
            base_dir=None,
        )

        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://testserver") as client:
            codificar = await client.post(
                "/agente/codificar_desde_soat",
                json={"items": [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}]},
            )
            validar = await client.post(
                "/soat/validar_factura",
                json={
                    "documento_quirurgico": (
                        "DIAGNÓSTICOS PREQUIRÚRGICO\nFractura abierta\n\n"
                        "PROCEDIMIENTOS REALIZADOS\nLavado quirúrgico\n\n"
                        "HALLAZGOS QUIRÚRGICOS\nContaminación moderada\n\n"
                        "DESCRIPCIÓN DEL PROCEDIMIENTO\nLavado y desbridamiento"
                    ),
                    "codigos_soat_factura": [{"codigo": "S1", "descripcion": "Lavado quirúrgico"}],
                },
            )

        self.assertEqual(codificar.status_code, 200)
        self.assertIn("cups_seleccionados", codificar.json()["resultados"][0])
        self.assertEqual(validar.status_code, 200)
        self.assertIn("validaciones", validar.json())
        self.assertIn("resumen", validar.json())

    async def test_generate_codes_route_accepts_llm_router_without_gemini(self) -> None:
        app = FastAPI()
        app.include_router(soat.router)

        async def override_current_user():
            return SimpleNamespace(username="auditor", role="auditor")

        app.dependency_overrides[get_current_user] = override_current_user
        app.state.services = SimpleNamespace(
            client_groq=None,
            client_gemini=None,
            llm_router=_FakeLLMRouter(),
            llm_task_cache_repository=None,
            soat_retriever=_FakeSoatRetriever(),
            cie10_retriever=_FakeCie10Retriever(),
            cups_retriever=_FakeCupsRetriever(),
            base_dir=None,
        )

        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://testserver") as client:
            response = await client.post(
                "/soat/generar_codigos_gemini",
                json={"descripciones": [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}]},
            )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()["resultados"][0]["cups_principal"]["codigo"], "123456")

    async def test_soat_chat_route_uses_router_and_keeps_http_contract(self) -> None:
        app = FastAPI()
        app.include_router(soat.router)
        router = _FakeLLMRouter()

        async def override_current_user():
            return SimpleNamespace(username="auditor", role="auditor")

        app.dependency_overrides[get_current_user] = override_current_user
        app.state.services = SimpleNamespace(
            client_groq=None,
            client_gemini=None,
            llm_router=router,
            llm_task_cache_repository=None,
            soat_retriever=_FakeSoatRetriever(),
            cie10_retriever=_FakeCie10Retriever(),
            cups_retriever=_FakeCupsRetriever(),
            base_dir=None,
        )

        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://testserver") as client:
            response = await client.post("/soat/chat", json={"mensaje": "¿Cuál es el código?"})

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()["respuesta"], "Respuesta SOAT controlada")
        self.assertIn("fuentes", response.json())
        self.assertIn("soat_chat", router.generate_text_calls)


class ServicesEpicrisisCodingGatewayStructuredTest(unittest.TestCase):
    def test_gateway_uses_structured_router_for_generated_codes(self) -> None:
        gateway = ServicesEpicrisisCodingGateway(
            client_groq=None,
            client_gemini=None,
            soat_retriever=_FakeSoatRetriever(),
            cie10_retriever=_FakeCie10Retriever(),
            llm_router=_FakeLLMRouter(),
        )

        results = gateway.generate_codes_from_descriptions(
            [{"codigo_soat": "S1", "descripcion": "Lavado quirúrgico"}]
        )

        self.assertEqual(results[0]["cie10_principal"]["codigo"], "S82.2")
        self.assertEqual(results[0]["cups_principal"]["codigo"], "123456")
