from __future__ import annotations

import asyncio
from copy import deepcopy
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace

import pytest
from fastapi import HTTPException

from app.case_epicrisis.application.curation import apply_curation_decision
from app.case_epicrisis.domain.curation import (
    CandidateOrigin,
    CaseCuration,
    CurationDecisionType,
    CurationStatus,
    ObjectiveProcedure,
    SoatCurationCandidate,
    SoatCurationValuation,
)
from app.core.heavy_resources import validate_faiss_directory, validate_loaded_faiss
from app.routes.soat import consultar_tarifa_por_cups
from app.scripts.cups_soat_crosswalk import import_crosswalk
from app.soat_crosswalk.application.resolution import (
    CupsCodeNotFoundError,
    CupsSoatResolver,
    InvalidCupsCodeError,
)
from app.soat_crosswalk.application.search import SearchCupsSoatCandidates
from app.soat_crosswalk.domain.models import (
    CrosswalkCatalog,
    CrosswalkRelationship,
    CrosswalkSource,
    CrosswalkStatus,
)
from app.soat_tariffs.application.valuation import valuate_factura_soat
from app.soat_tariffs.domain.models import (
    CatalogSource,
    SoatCatalog,
    SoatCatalogEntry,
    SoatTariffComponent,
)


class FakeCupsCatalog:
    version = "cups-2026-test"

    def __init__(self, items: dict[str, str] | None = None) -> None:
        self.items = items or {"793101": "Reducción abierta"}

    def load(self) -> None:
        return None

    def contains(self, code: str) -> bool:
        return code in self.items

    def description(self, code: str) -> str | None:
        return self.items.get(code)


class FakeTariffCatalog:
    def __init__(self, catalogs: dict[int, SoatCatalog]) -> None:
        self.catalogs = catalogs

    def load(self, year: int) -> SoatCatalog:
        if year not in self.catalogs:
            raise FileNotFoundError
        return self.catalogs[year]

    def find_exact(self, year: int, code: str) -> SoatCatalogEntry | None:
        return next((item for item in self.load(year).entries if item.code == code), None)

    def validate_all(self) -> dict[int, str]:
        return {year: "ready" for year in self.catalogs}


class FakeCrosswalk:
    def __init__(self, relationships: list[CrosswalkRelationship]) -> None:
        self.catalog = CrosswalkCatalog(
            version="crosswalk-test",
            effective_from="2026-01-01",
            normative_version="test",
            cups_catalog_version="cups-2026-test",
            soat_catalog_version="soat-2026-test",
            source=CrosswalkSource(
                filename="source.csv",
                sha256="a" * 64,
                reference="fuente validada",
            ),
            relationships=relationships,
        )

    def load(self) -> CrosswalkCatalog:
        return self.catalog

    def find_by_cups(self, cups_code: str) -> list[CrosswalkRelationship]:
        return [item for item in self.catalog.relationships if item.cups_code == cups_code]

    def validate(self) -> dict[str, int | str]:
        return {"status": "confirmed", "relationships": len(self.catalog.relationships)}


def _entry(code: str, group: int = 7) -> SoatCatalogEntry:
    source = CatalogSource(
        url="https://example.test/soat",
        sha256="b" * 64,
        page_table="tabla test",
    )
    return SoatCatalogEntry(
        code=code,
        description=f"Procedimiento SOAT {code}",
        surgical_group=group,
        base_coefficient=Decimal("5"),
        components=[
            SoatTariffComponent(name="cirujano", code="39005", coefficient=Decimal("1")),
            SoatTariffComponent(name="anestesia", code="39105", coefficient=Decimal("1")),
            SoatTariffComponent(name="ayudantia", code="39118", coefficient=Decimal("1")),
            SoatTariffComponent(name="sala", code="39209", coefficient=Decimal("1")),
            SoatTariffComponent(name="materiales", code="39303", coefficient=Decimal("1")),
        ],
        source=source,
    )


def _catalog(year: int, *codes: str) -> SoatCatalog:
    source = CatalogSource(
        url="https://example.test/soat",
        sha256="b" * 64,
        page_table="tabla test",
    )
    return SoatCatalog(
        year=year,
        unit="UVB",
        unit_value=Decimal("12110"),
        version=f"soat-{year}-test",
        generated_from=[source],
        entries=[_entry(code, 7 + index) for index, code in enumerate(codes)],
    )


def _relationship(soat_code: str, group: int = 7) -> CrosswalkRelationship:
    return CrosswalkRelationship(
        cups_code="793101",
        cups_description="Reducción abierta",
        soat_code=soat_code,
        soat_description=f"Procedimiento SOAT {soat_code}",
        surgical_group=group,
        reference="fuente validada; fila 2",
    )


def _resolver(*relationships: CrosswalkRelationship) -> CupsSoatResolver:
    tariffs = FakeTariffCatalog({2026: _catalog(2026, "13170", "18211")})
    return CupsSoatResolver(
        cups_catalog=FakeCupsCatalog(),
        crosswalk=FakeCrosswalk(list(relationships)),
        tariff_catalog=tariffs,
    )


def test_resolver_covers_unique_ambiguous_unmapped_conflict_and_year() -> None:
    unique = _resolver(_relationship("13170")).resolve("793101", 2026)
    assert unique.status == CrosswalkStatus.RESOLVED
    assert unique.selected_soat_code == "13170"

    ambiguous = _resolver(_relationship("13170"), _relationship("18211", 8)).resolve("793101", 2026)
    assert ambiguous.status == CrosswalkStatus.AMBIGUOUS
    assert [item.soat_code for item in ambiguous.candidates] == ["13170", "18211"]

    assert _resolver().resolve("793101", 2026).status == CrosswalkStatus.UNMAPPED
    assert _resolver(_relationship("13170")).resolve(
        "793101", 2026, billed_soat_code="18211"
    ).status == CrosswalkStatus.CONFLICT
    assert _resolver(_relationship("13170")).resolve(
        "793101", 2025
    ).status == CrosswalkStatus.UNSUPPORTED_YEAR

    with pytest.raises(InvalidCupsCodeError):
        _resolver().resolve("79310", 2026)
    with pytest.raises(CupsCodeNotFoundError):
        _resolver().resolve("999999", 2026)


def test_natural_language_search_only_resolves_structured_cups_candidates() -> None:
    retriever = SimpleNamespace(
        buscar=lambda query, k: [
            SimpleNamespace(metadata={"codigo": "793101", "nombre": query}),
            SimpleNamespace(metadata={"codigo": "NO-VALIDO"}),
            SimpleNamespace(metadata={"codigo": "793101"}),
        ]
    )
    results = SearchCupsSoatCandidates(
        retriever=retriever,
        resolver=_resolver(_relationship("13170")),
    ).execute("reducción abierta", limit=5)
    assert [(item.cups_code, item.selected_soat_code) for item in results] == [("793101", "13170")]


def test_invoice_crosswalk_assigns_unique_code_and_preserves_original_money() -> None:
    invoice = {
        "informacion_factura": {"fecha_emision": "2026-02-01"},
        "lineas_canonicas": [{"total": "$900.000", "valor_unitario": "$900.000"}],
        "servicios_procedimientos": {
            "procedimientos_quirurgicos": [
                {
                    "fecha_servicio": "2026-01-20",
                    "codigo_facturacion": "INTERNO",
                    "codigo_referencia": "793101",
                    "descripcion": "Reducción abierta",
                    "total": "$900.000",
                    "valor_unitario": "$900.000",
                }
            ]
        },
        "analisis_financiero": {"valor_total_factura": "$900.000"},
    }
    original = deepcopy(invoice)
    tariffs = FakeTariffCatalog({2026: _catalog(2026, "13170")})
    result = valuate_factura_soat(invoice, tariffs, _resolver(_relationship("13170")))
    procedure = result["servicios_procedimientos"]["procedimientos_quirurgicos"][0]
    assert procedure["codigo_soat"] == "13170"
    assert procedure["grupo_quirurgico_soat"] == 7
    assert procedure["valoracion_soat"]["fuente_asignacion"] == "crosswalk_cups_unico"
    assert result["lineas_canonicas"] == original["lineas_canonicas"]
    assert result["analisis_financiero"] == original["analisis_financiero"]


def test_invoice_explicit_soat_wins_and_marks_crosswalk_conflict() -> None:
    invoice = {
        "informacion_factura": {"fecha_emision": "2026-02-01"},
        "servicios_procedimientos": {
            "procedimientos_quirurgicos": [
                {
                    "fecha_servicio": "2026-01-20",
                    "codigo_facturacion": "18211",
                    "codigo_referencia": "793101",
                    "descripcion": "Reducción abierta",
                }
            ]
        },
    }
    tariffs = FakeTariffCatalog({2026: _catalog(2026, "13170", "18211")})
    result = valuate_factura_soat(invoice, tariffs, _resolver(_relationship("13170")))
    procedure = result["servicios_procedimientos"]["procedimientos_quirurgicos"][0]
    assert procedure["codigo_soat"] == "18211"
    assert procedure["valoracion_soat"]["estado"] == "conflicto"
    assert procedure["valoracion_soat"]["regla_aplicada"] == "soat_explicito_contradice_crosswalk"


def test_importer_normalizes_hydrates_and_rejects_duplicates(tmp_path: Path) -> None:
    source = tmp_path / "crosswalk.csv"
    source.write_text("cups,soat\n793101,1101\n", encoding="utf-8")
    tariffs = FakeTariffCatalog({2026: _catalog(2026, "01101")})
    output = tmp_path / "crosswalk.json"
    catalog = import_crosswalk(
        source,
        cups_column="cups",
        soat_column="soat",
        output_path=output,
        cups_catalog=FakeCupsCatalog(),
        tariff_catalog=tariffs,
    )
    assert catalog.relationships[0].soat_code == "01101"
    assert catalog.relationships[0].cups_description == "Reducción abierta"
    assert output.read_text(encoding="utf-8").endswith("\n")

    source.write_text("cups,soat\n793101,1101\n793101,01101\n", encoding="utf-8")
    with pytest.raises(ValueError, match="duplicado"):
        import_crosswalk(
            source,
            cups_column="cups",
            soat_column="soat",
            output_path=output,
            cups_catalog=FakeCupsCatalog(),
            tariff_catalog=tariffs,
        )


def test_importer_rejects_missing_corrupt_columns_unknown_codes_and_group(tmp_path: Path) -> None:
    tariffs = FakeTariffCatalog({2026: _catalog(2026, "01101")})
    common = {
        "cups_column": "cups",
        "soat_column": "soat",
        "output_path": tmp_path / "output.json",
        "cups_catalog": FakeCupsCatalog(),
        "tariff_catalog": tariffs,
    }
    with pytest.raises(FileNotFoundError):
        import_crosswalk(tmp_path / "missing.csv", **common)

    corrupt = tmp_path / "corrupt.xlsx"
    corrupt.write_bytes(b"not-an-xlsx")
    with pytest.raises(ValueError, match="leer"):
        import_crosswalk(corrupt, **common)

    source = tmp_path / "crosswalk.csv"
    source.write_text("wrong,soat\n793101,1101\n", encoding="utf-8")
    with pytest.raises(ValueError, match="Columna requerida"):
        import_crosswalk(source, **common)

    source.write_text("cups,soat\n999999,1101\n", encoding="utf-8")
    with pytest.raises(ValueError, match="CUPS desconocido"):
        import_crosswalk(source, **common)

    source.write_text("cups,soat\n793101,99999\n", encoding="utf-8")
    with pytest.raises(ValueError, match="SOAT desconocido"):
        import_crosswalk(source, **common)

    source.write_text("cups,soat,group\n793101,1101,8\n", encoding="utf-8")
    with pytest.raises(ValueError, match="contradice"):
        import_crosswalk(source, group_column="group", **common)


def test_human_soat_selection_records_versions_source_and_recalculation() -> None:
    procedure = ObjectiveProcedure(
        item_id="px-review",
        description="Reducción abierta",
        codigo_cups="793101",
        origin=CandidateOrigin.EXPLICIT,
        status=CurationStatus.PENDING_REVIEW,
        soat_candidates=[
            SoatCurationCandidate(
                soat_code="13170",
                description="Procedimiento SOAT 13170",
                surgical_group=7,
                reference="fuente validada; fila 2",
            )
        ],
    )
    case = CaseCuration(version="v1", procedures=[procedure], pending_item_ids=[procedure.item_id])
    valuation = SoatCurationValuation(
        year=2026,
        soat_code="13170",
        surgical_group=7,
        base_tariff=1000,
        components={"cirujano": 1000},
        catalog_version="soat-2026-test",
        source_reference="fuente validada; fila 2",
    )
    updated = apply_curation_decision(
        case,
        item_id=procedure.item_id,
        decision=CurationDecisionType.CORRECT,
        actor="auditor",
        expected_version="v1",
        selected_soat_code="13170",
        reason="Soporte quirúrgico confirmado",
        crosswalk_version="crosswalk-test",
        tariff_catalog_version="soat-2026-test",
        source_reference="fuente validada; fila 2",
        selected_soat_valuation=valuation,
    )
    selected = updated.procedures[0]
    assert selected.codigo_soat == selected.selected_soat_code == "13170"
    assert selected.selected_soat_valuation == valuation
    assert selected.decisions[0].actor == "auditor"
    assert selected.decisions[0].crosswalk_version == "crosswalk-test"


def test_strict_faiss_validation_rejects_missing_empty_and_incompatible(tmp_path: Path) -> None:
    with pytest.raises(FileNotFoundError):
        validate_faiss_directory(tmp_path / "missing")
    index_dir = tmp_path / "index"
    index_dir.mkdir()
    (index_dir / "index.faiss").write_bytes(b"")
    (index_dir / "index.pkl").write_bytes(b"pickle")
    with pytest.raises(ValueError, match="vacío"):
        validate_faiss_directory(index_dir)

    store = SimpleNamespace(
        index=SimpleNamespace(ntotal=2, d=3),
        index_to_docstore_id={0: "a"},
        docstore=SimpleNamespace(_dict={"a": object(), "b": object()}),
        embedding_function=SimpleNamespace(embed_query=lambda _: [0.0, 0.0, 0.0]),
    )
    with pytest.raises(ValueError, match="Mapeo"):
        validate_loaded_faiss(store, name="test")

    store.index_to_docstore_id = {0: "a", 1: "b"}
    store.embedding_function = SimpleNamespace(embed_query=lambda _: [0.0, 0.0])
    with pytest.raises(ValueError, match="Dimensión incompatible"):
        validate_loaded_faiss(store, name="test")

    (index_dir / "index.faiss").write_bytes(b"faiss")
    (index_dir / "manifest.json").write_text(
        '{"artifacts":{"index.faiss":"bad","index.pkl":"bad"}}',
        encoding="utf-8",
    )
    with pytest.raises(ValueError, match="Hash inválido"):
        validate_faiss_directory(index_dir)


def test_tariff_route_contract_for_pending_source_and_invalid_requests() -> None:
    result = asyncio.run(consultar_tarifa_por_cups("793101", 2026, object()))
    assert result["status"] == "unmapped"
    assert "valuation" not in result

    with pytest.raises(HTTPException) as unsupported:
        asyncio.run(consultar_tarifa_por_cups("793101", 2025, object()))
    assert unsupported.value.status_code == 422

    with pytest.raises(HTTPException) as invalid:
        asyncio.run(consultar_tarifa_por_cups("79310", 2026, object()))
    assert invalid.value.status_code == 422

    with pytest.raises(HTTPException) as missing:
        asyncio.run(consultar_tarifa_por_cups("000000", 2026, object()))
    assert missing.value.status_code == 404
