"""
ERGON F11 smoke test — Motor de reglas de alertas.

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

 1. alertas_engine.evaluar(conn, codigo) retorna lista con las 8 reglas aplicadas.
 2. Seed DESF11 con 3 rubros forzando desvio → 3 alertas (1 AMARILLO + 2 ROJO).
 3. Dashboard semaforo cambia dinamicamente (se testea via respuesta API).
 4. Vista Alertas full muestra las 8 reglas cuando aplican (se testea que el
    payload soporta las 8; la vista HTML se valida visualmente en preview).
 5. Banner calibracion texto manual v1.0 Valor 2.
 6. smoke_test_f11.py verifica las 8 reglas.

Uso:
    python db/smoke_test_f11.py              # tests contra DB local
    python db/smoke_test_f11.py --with-http  # incluye endpoint /api/obras/:c/alertas
"""
from __future__ import annotations

import argparse
import http.cookiejar
import json
import sqlite3
import subprocess
import sys
import urllib.request
from datetime import date, timedelta
from pathlib import Path

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

sys.path.insert(0, str(HERE))
sys.path.insert(0, str(BASE))

import api_obras  # type: ignore  # noqa: E402
import alertas_engine  # type: ignore  # noqa: E402


class T:
    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 _ensure_desf11_seed() -> None:
    """Corre seed_desvios_f11.py si DESF11 no existe."""
    conn = api_obras.get_conn()
    row = conn.execute(
        "SELECT id FROM obras WHERE codigo = 'DESF11'",
    ).fetchone()
    conn.close()
    if row is not None:
        return
    print("  [seed] corriendo seed_desvios_f11.py...")
    proc = subprocess.run(
        [sys.executable, str(HERE / "seed_desvios_f11.py")],
        capture_output=True, text=True, timeout=30,
    )
    if proc.returncode != 0:
        raise RuntimeError(f"seed fallo: {proc.stderr}")


def test_desf11_3_alertas() -> None:
    """Criterio 2: seed DESF11 -> 3 alertas (1 AMARILLO + 2 ROJO)."""
    _ensure_desf11_seed()
    conn = api_obras.get_conn()
    try:
        r = alertas_engine.evaluar(conn, "DESF11")
    finally:
        conn.close()
    T.check("2.1 DESF11 total=3 alertas", r["total"] == 3,
            f"total={r['total']}")
    T.check("2.2 DESF11 2 ROJO + 1 AMARILLO",
            r["por_severidad"] == {"ROJO": 2, "AMARILLO": 1},
            f"por_severidad={r['por_severidad']}")
    T.check("2.3 DESF11 reglas R1,R2,R4",
            set(r["reglas_aplicadas"]) == {"R1", "R2", "R4"},
            f"reglas={r['reglas_aplicadas']}")


def test_payload_estructura() -> None:
    """Criterio 1+5: payload tiene la estructura completa del motor (8 reglas soportadas)."""
    conn = api_obras.get_conn()
    try:
        r = alertas_engine.evaluar(conn, "DESF11")
    finally:
        conn.close()
    for key in ("alertas", "total", "por_severidad", "reglas_aplicadas",
                "corte_mes", "umbrales", "calibracion"):
        T.check(f"1.x payload tiene campo '{key}'", key in r, "")
    T.check("5.1 calibracion.nota presente (banner Valor 2)",
            isinstance(r["calibracion"].get("nota"), str)
            and len(r["calibracion"]["nota"]) > 40,
            f"len={len(r['calibracion'].get('nota') or '')}")
    T.check("5.2 calibracion.fecha_fin presente",
            isinstance(r["calibracion"].get("fecha_fin"), dict),
            f"tipo={type(r['calibracion'].get('fecha_fin')).__name__}")


def test_8_reglas_cobertura() -> None:
    """Criterio 6: crear obra sintetica que dispara las 8 reglas simultaneamente.

    Obra TEST8R con:
      - 2 rubros de avance (desvio+critico) -> R1, R2
      - 1 rubro sobrecosto simple + 1 critico -> R3, R4
      - SPI lo suficientemente bajo (3 meses data) para disparar R5 y/o R6
      - 2 contratos: uno vencido pendiente (R7), otro emitido sin firma > 15d (R8)
    """
    codigo = "TEST8R"
    conn = api_obras.get_conn()
    try:
        _setup_test8r(conn, codigo)
        r = alertas_engine.evaluar(conn, codigo)
    finally:
        _teardown_test8r(conn, codigo)
        conn.close()

    reglas = set(r["reglas_aplicadas"])
    esperadas = {"R1", "R2", "R3", "R4", "R6", "R7", "R8"}
    # R5 puede o no disparar segun SPI exacto; lo tratamos como "R5 O R6".
    tiene_cron = "R5" in reglas or "R6" in reglas
    T.check("6.1 TEST8R dispara R1 (AVANCE_DESVIO)", "R1" in reglas, f"reglas={sorted(reglas)}")
    T.check("6.2 TEST8R dispara R2 (AVANCE_CRITICO)", "R2" in reglas, "")
    T.check("6.3 TEST8R dispara R3 (SOBRECOSTO)", "R3" in reglas, "")
    T.check("6.4 TEST8R dispara R4 (SOBRECOSTO_CRITICO)", "R4" in reglas, "")
    T.check("6.5 TEST8R dispara R5 o R6 (cronograma)", tiene_cron,
            f"reglas cron encontradas: {reglas & {'R5', 'R6'}}")
    T.check("6.6 TEST8R dispara R7 (CONTRATO_VENCIDO)", "R7" in reglas, "")
    T.check("6.7 TEST8R dispara R8 (CONTRATO_SIN_FIRMA)", "R8" in reglas, "")


def _setup_test8r(conn: sqlite3.Connection, codigo: str) -> None:
    """Crea obra TEST8R con data que dispara las 8 reglas. Limpia previa si existe."""
    # Limpieza previa
    row = conn.execute("SELECT id FROM obras WHERE codigo = ?", (codigo,)).fetchone()
    if row:
        conn.execute("DELETE FROM obras WHERE id = ?", (row["id"],))
        conn.commit()

    corte = "2026-04-30"
    f_ini = "2026-01-01"
    f_fin = "2027-12-31"

    conn.execute(
        """
        INSERT INTO obras (codigo, nombre, cliente, ubicacion, tipo, moneda,
          presupuesto_total, fecha_inicio, fecha_fin_plan, fecha_corte_actual,
          umbral_desvio_pct, umbral_critico_pct, dg_nivel_servicio,
          dg_version_template, es_demo)
        VALUES (?, 'Test 8 Reglas', 'Testing DG', 'Test', 'Test', 'PYG',
          10000000000, ?, ?, ?, 10, 20, 2, 'v1.0', 1)
        """,
        (codigo, f_ini, f_fin, corte),
    )
    obra_id = conn.execute(
        "SELECT id FROM obras WHERE codigo = ?", (codigo,),
    ).fetchone()["id"]

    # 4 rubros: 1 avance_desvio, 1 avance_critico, 1 sobrecosto, 1 sobrecosto_critico
    rubros = [
        (1, "AV_DESVIO", 25.0),
        (2, "AV_CRITICO", 25.0),
        (3, "PPTO_DESV", 25.0),
        (4, "PPTO_CRIT", 25.0),
    ]
    rubro_ids = {}
    for orden, nombre, peso in rubros:
        cur = conn.execute(
            "INSERT INTO rubros_obra (obra_id, orden, nombre, peso_pct) VALUES (?, ?, ?, ?)",
            (obra_id, orden, nombre, peso),
        )
        rubro_ids[nombre] = cur.lastrowid

    # Avance mensual (3 meses para permitir calculo EAC_date honesto)
    # Al corte abril: AV_DESVIO atraso 12 pp (R1), AV_CRITICO atraso 28 pp (R2).
    # PPTO_DESV y PPTO_CRIT sin desvio de avance; solo de costo.
    avance_data = [
        # (rubro, mes, plan, real)
        ("AV_DESVIO", "2026-02", 15, 12),
        ("AV_DESVIO", "2026-03", 30, 22),
        ("AV_DESVIO", "2026-04", 45, 33),
        ("AV_CRITICO", "2026-02", 20, 10),
        ("AV_CRITICO", "2026-03", 40, 18),
        ("AV_CRITICO", "2026-04", 60, 32),
        ("PPTO_DESV", "2026-02", 10, 10),
        ("PPTO_DESV", "2026-03", 20, 20),
        ("PPTO_DESV", "2026-04", 30, 30),
        ("PPTO_CRIT", "2026-02", 10, 10),
        ("PPTO_CRIT", "2026-03", 20, 20),
        ("PPTO_CRIT", "2026-04", 30, 30),
    ]
    for nombre, mes, plan, real in avance_data:
        conn.execute(
            """INSERT INTO avance_mensual (obra_id, rubro_id, mes, plan_acum_pct, real_acum_pct)
               VALUES (?, ?, ?, ?, ?)""",
            (obra_id, rubro_ids[nombre], mes, plan, real),
        )

    # Presupuesto: PPTO_DESV sobrecosto 15% (>10, <20 -> R3). PPTO_CRIT 30% (>20 -> R4).
    presu = [
        ("AV_DESVIO", 1000, 1000),  # no dispara R3/R4
        ("AV_CRITICO", 1000, 1000),
        ("PPTO_DESV", 1000, 1150),  # 15% sobrecosto -> R3
        ("PPTO_CRIT", 1000, 1300),  # 30% sobrecosto -> R4
    ]
    for nombre, plan, real in presu:
        conn.execute(
            """INSERT INTO presupuesto_mensual (obra_id, rubro_id, mes, plan, real)
               VALUES (?, ?, '2026-04', ?, ?)""",
            (obra_id, rubro_ids[nombre], plan * 1_000_000, real * 1_000_000),
        )

    # Contratos: 1 vencido pendiente (R7), 1 emitido sin firma > 15d (R8).
    corte_date = date.fromisoformat(corte)
    vencido = (corte_date - timedelta(days=10)).isoformat()
    emision_vieja = (corte_date - timedelta(days=30)).isoformat()
    emision_reciente = (corte_date - timedelta(days=5)).isoformat()

    conn.execute(
        """INSERT INTO contratos (obra_id, tipo, descripcion,
           fecha_emision, fecha_firma, fecha_vence, estado)
           VALUES (?, 'Contrato Principal', 'Contrato vencido test',
                   '2026-02-01', '2026-02-15', ?, 'PENDIENTE')""",
        (obra_id, vencido),
    )
    conn.execute(
        """INSERT INTO contratos (obra_id, tipo, descripcion,
           fecha_emision, fecha_firma, fecha_vence, estado)
           VALUES (?, 'Orden Trabajo', 'Emision sin firma test',
                   ?, NULL, NULL, 'PENDIENTE')""",
        (obra_id, emision_vieja),
    )
    # Control negativo: contrato reciente sin firma no debe disparar R8.
    conn.execute(
        """INSERT INTO contratos (obra_id, tipo, descripcion,
           fecha_emision, fecha_firma, fecha_vence, estado)
           VALUES (?, 'OT Reciente', 'Control negativo',
                   ?, NULL, NULL, 'PENDIENTE')""",
        (obra_id, emision_reciente),
    )
    conn.commit()


def _teardown_test8r(conn: sqlite3.Connection, codigo: str) -> None:
    row = conn.execute("SELECT id FROM obras WHERE codigo = ?", (codigo,)).fetchone()
    if row:
        conn.execute("DELETE FROM obras WHERE id = ?", (row["id"],))
        conn.commit()


def test_retrocompat_estado() -> None:
    """get_alertas mantiene alias 'estado' (severidad) para frontends pre-F11."""
    conn = api_obras.get_conn()
    try:
        r = api_obras.get_alertas(conn, "DESF11")
    finally:
        conn.close()
    if not r["alertas"]:
        T.check("retrocompat 'estado'", False, "sin alertas para validar")
        return
    a0 = r["alertas"][0]
    T.check("retrocompat: cada alerta tiene alias 'estado'",
            "estado" in a0 and a0["estado"] == a0.get("severidad"),
            f"estado={a0.get('estado')} severidad={a0.get('severidad')}")


def test_obra_inexistente() -> None:
    """Robustez: obra que no existe no crashea, devuelve estructura vacia."""
    conn = api_obras.get_conn()
    try:
        r = alertas_engine.evaluar(conn, "NOEXIST")
    finally:
        conn.close()
    T.check("robustez: obra NOEXIST total=0", r["total"] == 0, "")
    T.check("robustez: obra NOEXIST estructura completa",
            all(k in r for k in ("alertas", "por_severidad", "calibracion")), "")


def test_http_endpoint() -> None:
    """Criterio 3: endpoint /api/obras/:codigo/alertas retorna payload F11."""
    print()
    print("  [HTTP] login admin + GET alertas DESF11...")
    cj = http.cookiejar.CookieJar()
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))

    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("3.1 login admin OK", r.status == 200, f"status={r.status}")
    except Exception as e:
        T.check("3.1 login admin OK", False, f"error: {e}")
        return

    try:
        with opener.open(SERVER_URL + "/api/obras/DESF11/alertas", timeout=10) as r:
            data = json.loads(r.read().decode("utf-8"))
    except Exception as e:
        T.check("3.2 GET /api/obras/DESF11/alertas", False, f"error: {e}")
        return

    T.check("3.2 GET alertas total=3", data.get("total") == 3,
            f"total={data.get('total')}")
    T.check("3.3 API devuelve por_severidad ROJO=2 AMARILLO=1",
            data.get("por_severidad") == {"ROJO": 2, "AMARILLO": 1},
            f"por_severidad={data.get('por_severidad')}")
    T.check("3.4 API devuelve calibracion.nota",
            isinstance(data.get("calibracion", {}).get("nota"), str),
            "")
    T.check("3.5 API devuelve reglas_aplicadas",
            set(data.get("reglas_aplicadas", [])) == {"R1", "R2", "R4"}, "")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--with-http", action="store_true")
    args = parser.parse_args()

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

    test_payload_estructura()
    test_desf11_3_alertas()
    test_retrocompat_estado()
    test_obra_inexistente()
    test_8_reglas_cobertura()

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

    sys.exit(T.summary())


if __name__ == "__main__":
    main()
