"""
ERGON F10 smoke test — Export Excel FCAT1 desde DB.

Valida los 6 criterios de cierre §2.5 del prompt Sesion F:

 1. `python generate_dg_civil.py --obra TDG1` produce xlsx sin error.
 2. Comparacion celda-a-celda contra TDG1 original dentro de tolerancia.
 3. `--obra VITR1` produce xlsx con 4 rubros (parametricidad verificada).
 4. Boton UI "Exportar FCAT1" descarga archivo con nombre correcto.   [HTTP smoke]
 5. Grep TDG1|FCAT1|Obra Demo en xlsx VITR1 = 0 matches (no fuga demo->real).
 6. smoke_test verde.

Uso:
    python db/smoke_test_f10.py              # todos los tests offline
    python db/smoke_test_f10.py --with-http  # incluye test 4 (server debe correr)
"""
from __future__ import annotations

import argparse
import http.cookiejar
import io
import json
import os
import subprocess
import sys
import urllib.request
import urllib.parse
import zipfile
from pathlib import Path

HERE = Path(__file__).resolve().parent
BASE = HERE.parent
SERVER_URL = "http://localhost:8080"

# Monetario: +/- 1 (Gs). Porcentaje: +/- 0.01 pp (F10 §2.1 default).
TOL_MONETARIO = 1.0
TOL_PP = 0.01

TDG1_ORIGINAL = BASE / "TDG1_N2_Dashboard_DG.xlsx"


class T:
    """Utilidades minimas de assert con formato consistente."""
    total = 0
    passed = 0
    failed: list[str] = []

    @classmethod
    def check(cls, name: str, cond: bool, detail: str = "") -> bool:
        cls.total += 1
        if cond:
            cls.passed += 1
            print(f"  [OK]  {name}" + (f" — {detail}" if detail else ""))
            return True
        cls.failed.append(name)
        print(f"  [FAIL] {name}" + (f" — {detail}" if detail else ""))
        return False

    @classmethod
    def summary(cls) -> int:
        print()
        print(f"Resultados: {cls.passed}/{cls.total} OK")
        if cls.failed:
            print("Fallos:")
            for f in cls.failed:
                print(f"  - {f}")
            return 1
        return 0


def _run_generator(obra: str, cwd: Path) -> tuple[int, str, Path | None]:
    """Ejecuta `python generate_dg_civil.py --obra OBRA` y devuelve (rc, stdout, xlsx_path)."""
    proc = subprocess.run(
        [sys.executable, str(BASE / "generate_dg_civil.py"), "--obra", obra],
        cwd=str(cwd),
        capture_output=True,
        text=True,
        timeout=60,
    )
    stdout = (proc.stdout or "") + (proc.stderr or "")
    # Buscar la linea "Excel ERGON generado: <path>"
    xlsx_path = None
    for line in stdout.splitlines():
        if "Excel ERGON generado:" in line:
            p = line.split("generado:", 1)[1].strip()
            xlsx_path = Path(p)
            break
    return proc.returncode, stdout, xlsx_path


def test_tdg1_generacion() -> Path | None:
    """Criterio 1: --obra TDG1 produce xlsx sin error."""
    rc, out, path = _run_generator("TDG1", BASE)
    T.check("1.1 CLI --obra TDG1 retorna rc=0", rc == 0, f"rc={rc}")
    T.check("1.2 CLI TDG1 produce archivo xlsx", path is not None and path.exists(),
            str(path))
    if path and path.exists():
        T.check("1.3 xlsx TDG1 naming '{CODIGO}_Corte_YYYY-MM-DD.xlsx'",
                path.name.startswith("TDG1_Corte_") and path.name.endswith(".xlsx"),
                path.name)
    return path


def test_vitr1_generacion() -> Path | None:
    """Criterio 3: --obra VITR1 produce xlsx con 4 rubros (parametricidad)."""
    rc, out, path = _run_generator("VITR1", BASE)
    T.check("3.1 CLI --obra VITR1 retorna rc=0", rc == 0, f"rc={rc}")
    T.check("3.2 VITR1 xlsx generado", path is not None and path.exists(), str(path))
    if not (path and path.exists()):
        return path

    # Leer la hoja PARAMETROS y contar rubros.
    from openpyxl import load_workbook
    wb = load_workbook(path, data_only=False)
    try:
        rubros_count = _count_rubros_en_parametros(wb["PARAMETROS"])
        T.check("3.3 VITR1 tiene 4 rubros en PARAMETROS",
                rubros_count == 4, f"encontrados={rubros_count}")
        # Tambien AVANCE debe tener 4 filas de datos (primer bloque de rubros).
        avance_count = _count_rubros_en_avance(wb["AVANCE"])
        T.check("3.4 VITR1 AVANCE hoja con 4 filas de rubros",
                avance_count == 4, f"encontrados={avance_count}")
    finally:
        wb.close()
    return path


def _count_rubros_en_parametros(ws) -> int:
    """Cuenta los rubros listados bajo el header 'RUBROS DE OBRA' en PARAMETROS."""
    header_row = None
    for row in ws.iter_rows(min_row=1, max_row=200, max_col=3, values_only=False):
        for cell in row:
            if cell.value and str(cell.value).strip().upper().startswith("RUBROS DE OBRA"):
                header_row = cell.row
                break
        if header_row:
            break
    if not header_row:
        return -1
    # Las filas de rubros arrancan en header_row+1 y terminan en la primera fila con "TOTAL PESO".
    count = 0
    for r in range(header_row + 1, header_row + 200):
        b = ws.cell(row=r, column=2).value
        if not b:
            break
        if str(b).strip().upper() == "TOTAL PESO":
            break
        count += 1
    return count


def _count_rubros_en_avance(ws) -> int:
    """Cuenta filas con rubro en la hoja AVANCE (col B desde fila 5 hasta AVANCE PONDERADO)."""
    count = 0
    for r in range(5, 300):
        v = ws.cell(row=r, column=2).value
        if not v:
            break
        if str(v).strip().upper() == "AVANCE PONDERADO":
            break
        count += 1
    return count


def test_no_fuga_demo(xlsx_vitr1: Path | None) -> None:
    """Criterio 5: grep TDG1|FCAT1|Obra Demo|Grupo DG|Hoppe en VITR1.xlsx = 0."""
    if not xlsx_vitr1 or not xlsx_vitr1.exists():
        T.check("5. no-fuga demo->real en VITR1", False, "xlsx VITR1 no existe")
        return
    patterns = ["TDG1", "FCAT1", "Obra Demo", "Grupo DG Desarrollo", "Ing. Vicente Hoppe"]
    hits: list[tuple[str, str, int]] = []
    with zipfile.ZipFile(str(xlsx_vitr1)) as z:
        for name in z.namelist():
            if not name.endswith(".xml"):
                continue
            data = z.read(name).decode("utf-8", errors="replace")
            for pat in patterns:
                if pat in data:
                    hits.append((name, pat, data.count(pat)))
    if hits:
        detail = "; ".join(f"{h[0]}:{h[1]}x{h[2]}" for h in hits[:5])
        T.check("5. VITR1.xlsx no tiene referencias demo", False, detail)
    else:
        T.check("5. VITR1.xlsx no tiene referencias demo", True,
                "0 hits en " + ", ".join(patterns))


def test_comparacion_tdg1_vs_original(xlsx_tdg1_nuevo: Path | None) -> None:
    """Criterio 2: comparacion celda-a-celda TDG1 vs TDG1 original (tolerancia).

    Se compara campos escalares que el seed no modifico:
        PARAMETROS!C6 (codigo), C7 (cliente), C8 (ubicacion), C9 (tipo),
        C12 (inicio), C13 (fin), C14 (corte), C18 (presupuesto).

    Exclusiones deliberadas:
    - Nombre (C5): el seed actualizo "Obra Demo" -> "Obra Demo (DEMO)" para
      reforzar el chip [DEMO] visible (requisito F1 sprint 1). Delta esperado,
      no bug F10.
    - Pesos de rubros: el seed los normalizo a 100% (el DEMO_CONFIG legacy
      sumaba 101%). Delta esperado, no bug F10.
    """
    if not xlsx_tdg1_nuevo or not xlsx_tdg1_nuevo.exists():
        T.check("2. comparacion TDG1 vs original", False, "xlsx TDG1 no generado")
        return
    if not TDG1_ORIGINAL.exists():
        T.check("2. comparacion TDG1 vs original", False,
                f"original {TDG1_ORIGINAL.name} no existe para comparar")
        return

    from openpyxl import load_workbook
    wb_nuevo = load_workbook(str(xlsx_tdg1_nuevo), data_only=False)
    wb_orig = load_workbook(str(TDG1_ORIGINAL), data_only=False)
    try:
        p_new = wb_nuevo["PARAMETROS"]
        p_old = wb_orig["PARAMETROS"]

        # (celda, descripcion, es_numerico) — nombre excluido deliberadamente
        campos = [
            ("C6",  "codigo",       False),
            ("C7",  "cliente",      False),
            ("C8",  "ubicacion",    False),
            ("C9",  "tipo",         False),
            ("C12", "fecha_inicio", False),
            ("C13", "fecha_fin",    False),
            ("C14", "fecha_corte",  False),
            ("C18", "presupuesto",  True),
        ]
        # Nombre: log informativo, no assert
        nombre_new = str(p_new["C5"].value)
        nombre_old = str(p_old["C5"].value)
        if nombre_new != nombre_old:
            print(f"      [nota] PARAMETROS!C5 nombre: "
                  f"nuevo={nombre_new!r} original={nombre_old!r} "
                  f"(delta esperado por seed DEMO)")
        match = 0
        total = len(campos)
        for celda, desc, es_num in campos:
            v_new = p_new[celda].value
            v_old = p_old[celda].value
            ok = False
            if es_num:
                try:
                    ok = abs(float(v_new) - float(v_old)) <= TOL_MONETARIO
                except (TypeError, ValueError):
                    ok = False
            else:
                ok = str(v_new) == str(v_old)
            if ok:
                match += 1
            else:
                print(f"      [delta] PARAMETROS!{celda} {desc}: "
                      f"nuevo={v_new!r} original={v_old!r}")
        T.check(
            "2. PARAMETROS celdas escalares TDG1 nuevo == TDG1 original",
            match == total,
            f"{match}/{total} coincidencias (tolerancia "
            f"+/-{TOL_MONETARIO} Gs monetario, exacto no-numerico)",
        )
    finally:
        wb_nuevo.close()
        wb_orig.close()


def test_http_endpoint() -> None:
    """Criterio 4: endpoint /api/obras/:codigo/export/excel devuelve xlsx.

    Requiere server corriendo en localhost:8080 y usuario demo admin en la DB.
    """
    print()
    print("  [HTTP] Intentando login admin@demo.local ...")
    # cookie jar para session
    cj = http.cookiejar.CookieJar()
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))

    # 1. Login
    login_body = json.dumps({
        "email": "admin@demo.local",
        "password": "demo1234",
    }).encode("utf-8")
    login_req = urllib.request.Request(
        SERVER_URL + "/api/auth/login",
        data=login_body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with opener.open(login_req, timeout=10) as r:
            T.check("4.1 login admin OK", r.status == 200, f"status={r.status}")
    except Exception as e:
        T.check("4.1 login admin OK", False, f"error: {e}")
        return

    # 2. GET export
    export_url = SERVER_URL + "/api/obras/TDG1/export/excel"
    try:
        with opener.open(export_url, timeout=30) as r:
            status = r.status
            ctype = r.headers.get("Content-Type", "")
            cd = r.headers.get("Content-Disposition", "")
            data = r.read()
    except Exception as e:
        T.check("4.2 GET export excel", False, f"error: {e}")
        return
    T.check("4.2 GET /api/obras/TDG1/export/excel status=200",
            status == 200, f"status={status}")
    T.check("4.3 Content-Type xlsx",
            "spreadsheetml" in ctype, f"content-type={ctype}")
    T.check("4.4 Content-Disposition con filename correcto",
            'filename="TDG1_Corte_' in cd and '.xlsx' in cd,
            cd)
    # Sanity: los primeros 4 bytes deben ser "PK" (zip signature de xlsx).
    T.check("4.5 body empieza con PK (zip signature)",
            data[:2] == b"PK", f"prefix={data[:4]!r}")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--with-http", action="store_true",
                        help="incluir test 4 (requiere server corriendo)")
    args = parser.parse_args()

    print(f"ERGON F10 smoke test (base={BASE})")
    print()

    xlsx_tdg1 = test_tdg1_generacion()
    test_comparacion_tdg1_vs_original(xlsx_tdg1)
    xlsx_vitr1 = test_vitr1_generacion()
    test_no_fuga_demo(xlsx_vitr1)

    if args.with_http:
        test_http_endpoint()
    else:
        print("  [SKIP] 4.* HTTP endpoint (pasar --with-http con server activo)")

    sys.exit(T.summary())


if __name__ == "__main__":
    main()
