from __future__ import annotations

import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any, cast
from unittest.mock import patch

import pytest
from PIL import Image

from app.case_epicrisis.application.use_cases import (
    GetCaseEpicrisisContextUseCase,
    UpdatePdfPreparationMetadataUseCase,
)
from app.case_epicrisis.application.utils import (
    build_pdf_preparation_procedimientos,
    coerce_pdf_draft,
)


ROOT = Path(__file__).resolve().parents[1]


def test_manual_entries_keep_stable_keys_and_legacy_arrays_bidirectionally() -> None:
    draft = coerce_pdf_draft(
        {
            "manual_diagnosticos": ["S92.0 - Fractura"],
            "manual_medicamento_entries": [
                {"key": "manual:med:1", "value": "Dipirona | IV | 1 g | 2 | administrado"}
            ],
        }
    )

    assert draft["manual_diagnostico_entries"][0]["key"].startswith("manual:diagnostico:")
    assert draft["manual_diagnostico_entries"][0]["value"] == draft["manual_diagnosticos"][0]
    assert draft["manual_medicamentos"] == ["Dipirona | IV | 1 g | 2 | administrado"]
    assert draft["manual_medicamento_entries"][0]["key"] == "manual:med:1"


def test_preparation_projection_applies_manual_group_only_to_visual_order() -> None:
    selectable = [
        {"key": "official", "codigo_cups": "100001", "descripcion": "Oficial", "clasificacion": "quirurgico"},
        {"key": "manual", "codigo_cups": "100002", "descripcion": "Manual", "clasificacion": "quirurgico"},
        {"key": "plain", "codigo_cups": "100003", "descripcion": "Sin grupo", "clasificacion": "quirurgico"},
    ]
    enriched = [
        {**selectable[0], "grupo_quirurgico_soat": 10, "costo_facturado": {"valor_total": "100"}},
        {**selectable[1], "grupo_quirurgico_soat": None, "costo_facturado": {"valor_total": "900"}},
        {**selectable[2], "grupo_quirurgico_soat": None, "costo_facturado": {"valor_total": "1000"}},
    ]
    context = {
        "pdf_preparation_user_metadata": {
            "procedure_soat_groups": [
                {
                    "procedure_key": "manual",
                    "group": 20,
                    "actor": "auditor",
                    "updated_at": "2026-08-17T12:00:00Z",
                    "origin": "manual_visual",
                },
                {
                    "procedure_key": "official",
                    "group": 23,
                    "actor": "auditor",
                    "updated_at": "2026-08-17T12:00:00Z",
                    "origin": "manual_visual",
                },
            ]
        }
    }

    with (
        patch(
            "app.case_epicrisis.application.utils.build_pdf_selectable_procedimientos",
            return_value=selectable,
        ),
        patch("app.case_epicrisis.application.utils.extract_invoice_cost_lines", return_value=[]),
        patch("app.case_epicrisis.application.utils.attach_and_sort_costs", return_value=enriched),
    ):
        projection = build_pdf_preparation_procedimientos(context)

    assert [item["key"] for item in projection] == ["manual", "official", "plain"]
    assert projection[0]["grupo_soat_origen"] == "manual_visual"
    assert projection[1]["grupo_soat_preparacion"] == 10
    assert projection[1]["grupo_soat_origen"] == "oficial"
    assert all("costo_facturado" not in item and "grupo_quirurgico_soat" not in item for item in projection)


def test_preparation_projection_preserves_pop_evidence_and_reconciliation() -> None:
    context = {
        "curation_schema_version": "v1",
        "procedimientos_curados": [
            {
                "item_id": "pop-procedure",
                "codigo_cups": "793101",
                "description": "Osteosíntesis de tibia",
                "classification": "quirurgico",
                "status": "aprobado",
                "states": ["realizado"],
                "reconciliation_status": "realizado_no_facturado",
                "reconciliation_reason": "El CUPS realizado no aparece facturado con el mismo código.",
                "warnings": ["postoperatorio_contextual", "posible_glosa_realizado_no_facturado"],
                "evidence": [
                    {
                        "document_type": "historia_clinica",
                        "filename": "historia-pop.pdf",
                        "page": 4,
                        "section": "evolucion",
                        "excerpt": "POP de Osteosíntesis de tibia",
                    }
                ],
            }
        ],
        "pdf_draft": {},
    }

    projection = build_pdf_preparation_procedimientos(context)

    assert projection[0]["estado_documental"] == ["realizado"]
    assert projection[0]["evidencias"][0]["excerpt"] == "POP de Osteosíntesis de tibia"
    assert projection[0]["estado_conciliacion"] == "realizado_no_facturado"
    assert "postoperatorio_contextual" in projection[0]["advertencias"]


class _CacheRepository:
    def __init__(self) -> None:
        self.saved: dict[str, Any] = {}

    def ensure_indexes(self) -> None:
        return None

    def get(self, username: str, case_key: str) -> dict[str, Any] | None:
        return None

    def upsert(
        self,
        *,
        username: str,
        case_key: str,
        context: dict[str, Any],
        regen_requested: bool,
    ) -> None:
        self.saved = {
            "username": username,
            "case_key": case_key,
            "context": context,
            "regen_requested": regen_requested,
        }


class _GetContext:
    def execute(self, username: str, case_key: str, *, regen: bool = False) -> dict[str, Any]:
        return {"pdf_preparation_user_metadata": {"procedure_soat_groups": []}}


def test_preparation_metadata_records_author_and_can_be_cleared() -> None:
    repository = _CacheRepository()
    get_context = cast(GetCaseEpicrisisContextUseCase, _GetContext())
    use_case = UpdatePdfPreparationMetadataUseCase(repository, get_context)
    projection = [{"key": "proc-1", "clasificacion": "quirurgico", "grupo_soat_origen": ""}]

    with (
        patch("app.case_epicrisis.application.use_cases.normalize_context_payload", side_effect=lambda value: value),
        patch(
            "app.case_epicrisis.application.use_cases.build_pdf_preparation_procedimientos",
            return_value=projection,
        ),
    ):
        assigned = use_case.execute("auditor.medico", "case-1", procedure_key="proc-1", group=18)
        cleared = use_case.execute("auditor.medico", "case-1", procedure_key="proc-1", group=None)

    assignment = assigned["pdf_preparation_user_metadata"]["procedure_soat_groups"][0]
    assert assignment["actor"] == "auditor.medico"
    assert assignment["group"] == 18
    assert assignment["origin"] == "manual_visual"
    assert cleared["pdf_preparation_user_metadata"]["procedure_soat_groups"] == []
    assert repository.saved["context"] == cleared


def test_preparation_template_exposes_tabs_sheet_and_inline_metadata() -> None:
    template = (ROOT / "web/includes/epicrisis/preparar_pdf.html").read_text(encoding="utf-8")
    header = template.split('<header class="pdf-workspace__header">', maxsplit=1)[1].split("</header>", maxsplit=1)[0]

    assert template.count('data-preparation-tab="') == 1  # generated by the four-item Jinja loop
    assert template.count('data-preparation-panel="') == 4
    assert "role=\"tablist\"" in template
    assert template.count('data-empty-for="') == 4
    assert '<dialog id="pdf_detail_sheet"' in template
    assert 'id="pdf_selection_summary"' in header
    assert 'id="pdf_autosave_status" role="status" aria-live="polite"' in header
    assert 'id="agente_qx_status"' in header
    assert 'class="pdf-selection-bar"' not in template
    assert 'id="generar_pdf_preparation_btn"' not in template


def test_clinical_medication_summary_uses_badges_without_reconciliation_duplicate() -> None:
    template = (ROOT / "web/includes/epicrisis/resumen_clinico.html").read_text(encoding="utf-8")

    assert 'class="medication-statuses"' in template
    assert 'class="medication-pertinence medication-pertinence--{{ med.pertinencia.estado }}"' in template
    assert 'class="medication-matrix__reconciliation"' not in template


def test_preparation_tabs_hide_every_inactive_panel() -> None:
    stylesheet = (ROOT / "static/css/epicrisis.css").read_text(encoding="utf-8")
    hidden_rule = stylesheet.split(".pdf-tab-panel[hidden]", maxsplit=1)[1].split("}", maxsplit=1)[0]

    assert "display: none;" in hidden_rule


def test_search_normalization_is_accent_insensitive() -> None:
    if not shutil.which("node"):
        return
    script = """
globalThis.EpicrisisApp = {};
await import('./static/js/epicrisis/preparation.js');
if (globalThis.EpicrisisApp.preparation.normalizeSearch('Diagnóstico: TÓRAX') !== 'diagnostico: torax') {
  throw new Error('La normalización no elimina tildes');
}
"""
    result = subprocess.run(
        ["node", "--input-type=module", "-e", script],
        cwd=ROOT,
        capture_output=True,
        text=True,
        check=False,
    )
    assert result.returncode == 0, result.stderr


def test_procedure_classification_is_inline_editable_and_persisted_by_stable_key() -> None:
    if not shutil.which("node"):
        return
    script = r"""
import fs from "node:fs";
import vm from "node:vm";

const documentListeners = {};
const procedures = [{
  key: "procedure-1",
  codigo_cups: "890202",
  descripcion: "Consulta de primera vez",
  clasificacion: "sin_clasificar",
  clasificacion_automatica: "sin_clasificar",
}];

function createRow() {
  const row = {
    dataset: {},
    children: [],
    _html: "",
    setAttribute() {},
    set innerHTML(value) {
      this._html = value;
      const itemKey = /data-item-key="([^"]*)"/.exec(value)?.[1] || "";
      const procedureKey = /data-procedure-key="([^"]*)"/.exec(value)?.[1] || "";
      const automatic = /data-automatic-classification="([^"]*)"/.exec(value)?.[1] || "sin_clasificar";
      const checkbox = {
        checked: /\bchecked\b/.test(value),
        disabled: /\bdisabled\b/.test(value),
        dataset: { itemKey },
      };
      const selected = /<option value="([^"]*)" selected>/.exec(value)?.[1] || "sin_clasificar";
      const row = this;
      const select = {
        value: selected,
        dataset: { procedureKey, automaticClassification: automatic },
        closest(selector) { return selector.includes("pdf-list__row") ? row : this; },
      };
      this.checkbox = checkbox;
      this.select = select;
    },
    get innerHTML() { return this._html; },
    querySelector(selector) {
      if (selector.includes("checkbox")) return this.checkbox;
      if (selector.includes("pdf-procedure-classification")) return this.select;
      return null;
    },
  };
  return row;
}

const list = {
  children: [],
  _html: "",
  set innerHTML(value) { this._html = value; this.children = []; },
  get innerHTML() { return this._html; },
  appendChild(row) { this.children.push(row); },
  querySelectorAll(selector) {
    if (selector.includes("pdf-list__row")) return this.children;
    if (selector.includes("pdf-procedure-classification")) return this.children.map((row) => row.select);
    if (selector.includes("checkbox")) return this.children.map((row) => row.checkbox).filter((checkbox) => checkbox.checked);
    return [];
  },
};

globalThis.document = {
  addEventListener(type, callback) { documentListeners[type] = callback; },
  dispatchEvent() {},
  getElementById(id) { return id === "procedimientos_list" ? list : null; },
  querySelectorAll(selector) {
    if (selector.includes("#procedimientos_list")) {
      return list.querySelectorAll(selector.replace("#procedimientos_list ", ""));
    }
    return [];
  },
  createElement() { return createRow(); },
};

globalThis.CustomEvent = class CustomEvent { constructor(type, init) { this.type = type; this.detail = init?.detail; } };
globalThis.location = { search: "", pathname: "/epicrisis/caso/case-1" };
globalThis.EpicrisisApp = {
  state: {
    data: { caseKey: "case-1", pdfDraft: {} },
    pdfSelectionData: { procedimientos: procedures },
    syncGlobals() {},
  },
  preparation: {
    applyFilters(section) { if (section !== "procedimientos") throw new Error("Filtro incorrecto"); },
    updateCounts() {},
  },
  utils: {
    escapeHtml(value) { return String(value ?? ""); },
    stripHtml(value) { return String(value ?? ""); },
    deduplicarTextos(items) { return items; },
  },
  medicamentos: {
    formatearMedicamentoPresentacion(item) { return String(item?.medicamento || item || ""); },
    deduplicarMedicamentos(items) { return items; },
  },
  ayudasDiagnosticas: {
    normalizarAyudaDiagnostica(item) { return item; },
    deduplicarAyudasDiagnosticas(items) { return items; },
  },
  diagnosticos: {},
};

vm.runInThisContext(fs.readFileSync("static/js/epicrisis/selection-ui.js", "utf8"));
vm.runInThisContext(fs.readFileSync("static/js/epicrisis/draft.js", "utf8"));
EpicrisisApp.selection.renderSeccionSeleccion("procedimientos", procedures);

const row = list.children[0];
if (row._html.includes('class="procedure-classification"') || row._html.includes("<fieldset")) {
  throw new Error("La fila aún contiene el bloque residual de clasificación");
}
if (!row._html.includes("pdf-procedure-classification") || !row._html.includes("Quirúrgico") || !row._html.includes("No quirúrgico")) {
  throw new Error("La fila debe incluir el selector con ambas clasificaciones");
}

const select = row.select;
select.value = "quirurgico";
documentListeners.change({ target: select });
if (procedures[0].clasificacion !== "quirurgico" || row.dataset.classification !== "quirurgico") {
  throw new Error("La clasificación no se actualizó en memoria y en la fila");
}
if (row.checkbox.disabled || !row.checkbox.checked) {
  throw new Error("El procedimiento clasificado debe quedar incluido y habilitado");
}
const selected = EpicrisisApp.selection.obtenerElementosSeleccionados("procedimientos");
if (selected.length !== 1 || selected[0].clasificacion !== "quirurgico") {
  throw new Error("La selección serializada no conserva la clasificación");
}
const payload = EpicrisisApp.draft.construirPayloadDraftActual();
if (payload.pdf_draft.procedure_classification_overrides["procedure-1"] !== "quirurgico") {
  throw new Error("El payload no conserva el override por clave estable");
}
"""
    result = subprocess.run(
        ["node", "--input-type=module", "-e", script],
        cwd=ROOT,
        capture_output=True,
        text=True,
        check=False,
    )
    assert result.returncode == 0, result.stderr or result.stdout


def test_preparation_js_covers_keyboard_filters_visible_selection_and_focus_restore() -> None:
    source = (ROOT / "static/js/epicrisis/preparation.js").read_text(encoding="utf-8")
    tabs_source = (ROOT / "static/js/epicrisis/tabs.js").read_text(encoding="utf-8")

    assert "generar_pdf_preparation_btn" not in source
    assert 'tabDataKey: "preparationTab"' in source
    assert '"ArrowLeft"' in tabs_source and '"ArrowRight"' in tabs_source
    assert '"Home"' in tabs_source and '"End"' in tabs_source
    assert "normalizeSearch(`${row.dataset.searchText} ${row.textContent}`)" in source
    assert "visibleRows(section).forEach" in source
    assert "row.hidden = !(matchesText && matchesClassification && matchesGroup && matchesAid)" in source
    assert 'dialog.addEventListener("close", () => sheetContext?.opener?.focus())' in source
    assert "Fuente POP" in source
    assert "Extracto original" in source
    assert "estado_conciliacion" in source
    assert 'setAutosaveStatus("saving")' in (ROOT / "static/js/epicrisis/draft.js").read_text(
        encoding="utf-8"
    )


def test_preparation_visual_smoke_desktop_and_mobile_with_sheet() -> None:
    firefox = shutil.which("firefox")
    if not firefox:
        return
    globals_css = (ROOT / "static/css/globals.css").as_uri()
    epicrisis_css = (ROOT / "static/css/epicrisis.css").as_uri()
    html = f"""<!doctype html><html lang="es"><head><meta charset="utf-8">
<link rel="stylesheet" href="{globals_css}"><link rel="stylesheet" href="{epicrisis_css}"></head>
<body class="epicrisis-page"><main class="epicrisis-content"><section class="pdf-workspace">
<header class="pdf-workspace__header"><div><p class="pdf-workspace__eyebrow">Publicación clínica</p>
<h2>Preparar PDF</h2><p>Elementos publicables del caso.</p></div><div class="pdf-workspace__meta"><span>Analizando hallazgos quirúrgicos…</span><span class="pdf-workspace__selection-meta"><strong>3 elementos seleccionados</strong><span>Todos los cambios guardados</span></span></div></header>
<nav class="pdf-tabs"><button class="pdf-tabs__tab" aria-selected="true">Diagnósticos <span class="pdf-tabs__count">2/2</span></button><button class="pdf-tabs__tab">Procedimientos <span class="pdf-tabs__count">1/2</span></button></nav>
<div class="pdf-toolbar"><label class="pdf-search"><span>Buscar diagnóstico</span><input value=""></label><div class="pdf-toolbar__actions"><button class="ui-button">Seleccionar visibles</button></div></div>
<div class="pdf-list"><div class="pdf-list__head pdf-list__row--diagnosis"><span>Incluir</span><span>CIE-10</span><span>Diagnóstico</span><span>Descripción</span><span>Detalle</span></div>
<div class="pdf-list__row pdf-list__row--diagnosticos"><span><input type="checkbox" checked></span><strong>S92.0</strong><span>Fractura</span><span>Fractura del pie</span><button class="pdf-row-detail">Ver</button></div></div>
</section></main>
<dialog class="pdf-sheet" id="sheet"><form class="pdf-sheet__surface"><header><h3>Detalle de procedimiento</h3><button class="pdf-sheet__close">×</button></header><div class="pdf-sheet__body"><dl class="pdf-detail-list"><div><dt>Estado</dt><dd>Quirúrgico</dd></div><div><dt>Fuente</dt><dd>Historia clínica</dd></div></dl></div></form></dialog>
<script>if(location.hash==='#sheet')document.getElementById('sheet').showModal()</script></body></html>"""
    with tempfile.TemporaryDirectory() as temporary_directory:
        directory = Path(temporary_directory)
        fixture = directory / "preparation.html"
        fixture.write_text(html, encoding="utf-8")
        captures = [
            ("desktop.png", "1440,1000", fixture.as_uri()),
            ("mobile-sheet.png", "390,844", f"{fixture.as_uri()}#sheet"),
        ]
        for filename, viewport, url in captures:
            output = directory / filename
            result = subprocess.run(
                [firefox, "--headless", "--screenshot", str(output), "--window-size", viewport, url],
                capture_output=True,
                text=True,
                check=False,
                timeout=25,
            )
            if result.returncode != 0:
                pytest.skip(f"Firefox headless no está disponible en este sandbox: {result.returncode}")
            with Image.open(output) as capture:
                expected_size = tuple(int(value) for value in viewport.split(","))
                assert capture.size == expected_size
                colors = capture.convert("RGB").getcolors(maxcolors=expected_size[0] * expected_size[1])
                assert colors is not None and len(colors) > 20
