"""
ERGON -- Smoke test end-to-end del pipeline parser de bitacora.

Flujo:
  1. Inserta un daily_log de prueba en obra TDG1 (usa texto multi-evento).
  2. Llama a trigger_parse -> escribe parse_borrador, parse_estado='proposed'.
  3. Llama a confirmar_acciones con las acciones parseadas tal cual.
  4. Verifica que hay filas nuevas en asistencia_diaria, materiales y
     presupuesto_mensual con los datos esperados.
  5. Verifica que daily_log quedo con parse_estado='confirmed' y
     acciones_derivadas poblado.
  6. Limpieza: rollback (borrar las filas insertadas + el log entry).

Uso:
    cd 02_HERRAMIENTAS/db
    python smoke_test_parser_pipeline.py
"""
from __future__ import annotations

import json
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from api_obras import get_conn  # type: ignore  # noqa: E402
from parse_dispatcher import (  # type: ignore  # noqa: E402
    trigger_parse, confirmar_acciones, rollback_acciones,
)


BITACORA_TEST = {
    "fecha": "2025-01-29",
    "autor": "smoke_test",
    "actividades": (
        "Jornada con 52 personas. Recibimos 200 bolsas de cemento. "
        "Pagamos 3 millones de guaranies a Carlos Benitez (Albanileria) "
        "por mano de obra de la semana."
    ),
    "observaciones": "Sin lluvia, temperatura 28 grados.",
}


def banner(s: str):
    print("\n" + "=" * 70)
    print(s)
    print("=" * 70)


def test_guards_y_validaciones(conn, codigo: str) -> None:
    """Verifica: guard re-parse 'confirmed' (409) + regex fecha/mes invalidos.
    Sin llamadas al parser (casos negativos pre-claude)."""
    banner("C. Guards + validaciones de formato")
    obra_id = conn.execute(
        "SELECT id FROM obras WHERE codigo=?", (codigo,)
    ).fetchone()["id"]

    fecha_test = "2025-01-31"
    conn.execute(
        "DELETE FROM asistencia_diaria WHERE obra_id=? AND fecha=?",
        (obra_id, fecha_test)
    )
    conn.execute(
        "DELETE FROM daily_log WHERE obra_id=? AND fecha=? AND autor='smoke_test_guards'",
        (obra_id, fecha_test)
    )
    conn.commit()

    # --- C1: guard re-parse confirmed -> 409 ---
    cur = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones, parse_estado) "
        "VALUES (?,?,?,?,?,?)",
        (obra_id, fecha_test, "smoke_test_guards", "Test", "", "proposed")
    )
    entry_id = cur.lastrowid
    conn.commit()
    acciones = [{"tool": "upsert_asistencia", "input": {
        "fecha": fecha_test, "total_personal": 10,
        "lluvia_mm": 0.0, "dia_perdido": False, "notas": "C"
    }}]
    r = confirmar_acciones(conn, codigo, entry_id, acciones, "smoke_test_guards")
    assert r["ok"], f"setup confirm fallo: {r}"
    ins_id = r["diff"][0]["resultado"]["id"]

    # Ahora esta 'confirmed'. trigger_parse debe devolver 409 sin llamar a Claude.
    r_guard = trigger_parse(conn, codigo, entry_id)
    assert not r_guard["ok"], f"guard fallo, deberia rechazar: {r_guard}"
    assert r_guard["status"] == 409, f"esperaba 409, got {r_guard['status']}"
    assert "confirmado" in r_guard["error"].lower(), f"mensaje inesperado: {r_guard}"
    # Estado no debe haber cambiado
    estado_post = conn.execute(
        "SELECT parse_estado FROM daily_log WHERE id=?", (entry_id,)
    ).fetchone()["parse_estado"]
    assert estado_post == "confirmed", f"estado degradado a {estado_post}"
    print(f"  C1: re-parse 'confirmed' -> 409 OK (estado intacto)")

    # --- C2: fecha invalida en _apply_asistencia ---
    cur2 = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones, parse_estado) "
        "VALUES (?,?,?,?,?,?)",
        (obra_id, fecha_test, "smoke_test_guards", "Test2", "", "proposed")
    )
    entry_id2 = cur2.lastrowid
    conn.commit()
    acciones_bad_fecha = [{"tool": "upsert_asistencia", "input": {
        "fecha": "29/01/2025",  # formato invalido
        "total_personal": 10,
    }}]
    r2 = confirmar_acciones(conn, codigo, entry_id2, acciones_bad_fecha, "smoke_test_guards")
    assert not r2["ok"], f"deberia fallar por formato fecha, got {r2}"
    assert r2["status"] == 400
    assert any("formato invalido" in e for e in r2["errors"]), \
        f"mensaje regex no encontrado: {r2}"
    print(f"  C2: fecha '29/01/2025' -> ValueError OK ({r2['errors'][0][:80]})")

    # --- C3: mes invalido en _apply_presupuesto_mensual ---
    cur3 = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones, parse_estado) "
        "VALUES (?,?,?,?,?,?)",
        (obra_id, fecha_test, "smoke_test_guards", "Test3", "", "proposed")
    )
    entry_id3 = cur3.lastrowid
    conn.commit()
    # Usar primer rubro real de la obra para no fallar en la resolucion nombre->id
    rubro_nombre = conn.execute(
        "SELECT nombre FROM rubros_obra WHERE obra_id=? ORDER BY orden LIMIT 1",
        (obra_id,)
    ).fetchone()["nombre"]
    acciones_bad_mes = [{"tool": "upsert_presupuesto_mensual", "input": {
        "rubro_nombre": rubro_nombre,
        "mes": "enero-2025",  # formato invalido
        "concepto": "real",
        "monto": 1000000,
    }}]
    r3 = confirmar_acciones(conn, codigo, entry_id3, acciones_bad_mes, "smoke_test_guards")
    assert not r3["ok"], f"deberia fallar por formato mes, got {r3}"
    assert r3["status"] == 400
    assert any("formato invalido" in e for e in r3["errors"]), \
        f"mensaje regex no encontrado: {r3}"
    print(f"  C3: mes 'enero-2025' -> ValueError OK ({r3['errors'][0][:80]})")

    # Cleanup
    conn.execute("DELETE FROM asistencia_diaria WHERE id=?", (ins_id,))
    conn.execute("DELETE FROM daily_log WHERE id IN (?,?,?)",
                 (entry_id, entry_id2, entry_id3))
    conn.commit()
    print("  cleanup OK")


def test_update_rollback(conn, codigo: str) -> None:
    """Verifica que rollback restaura valores previos tras un UPDATE.

    Flujo: confirm A (total=30) -> confirm B mismo dia (total=45, UPDATE) ->
    rollback B -> fila debe volver a total=30 (los valores de A).
    Sin llamadas al parser (acciones hardcodeadas) para no gastar tokens.
    """
    banner("B. Test UPDATE rollback con prev_values")
    obra_id = conn.execute(
        "SELECT id FROM obras WHERE codigo=?", (codigo,)
    ).fetchone()["id"]

    fecha_test = "2025-01-30"

    # Limpieza preventiva de corridas anteriores
    conn.execute(
        "DELETE FROM asistencia_diaria WHERE obra_id=? AND fecha=?",
        (obra_id, fecha_test)
    )
    conn.execute(
        "DELETE FROM daily_log WHERE obra_id=? AND fecha=? AND autor='smoke_test_upd'",
        (obra_id, fecha_test)
    )
    conn.commit()

    # Entry A: crea asistencia con total=30
    cur_a = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones, parse_estado) "
        "VALUES (?,?,?,?,?,?)",
        (obra_id, fecha_test, "smoke_test_upd", "Primer parte", "", "proposed")
    )
    entry_a = cur_a.lastrowid
    conn.commit()

    acciones_a = [{"tool": "upsert_asistencia", "input": {
        "fecha": fecha_test, "total_personal": 30,
        "lluvia_mm": 0.0, "dia_perdido": False, "notas": "A"
    }}]
    r_a = confirmar_acciones(conn, codigo, entry_a, acciones_a, "smoke_test_upd")
    assert r_a["ok"], f"confirm A fallo: {r_a}"
    ref_a = r_a["diff"][0]["resultado"]
    assert ref_a["op"] == "insert", f"A: esperaba insert, got {ref_a['op']}"
    fila_id = ref_a["id"]
    print(f"  A: INSERT asistencia_diaria#{fila_id} total=30")

    # Entry B: MISMA fecha -> UPDATE con total=45
    cur_b = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones, parse_estado) "
        "VALUES (?,?,?,?,?,?)",
        (obra_id, fecha_test, "smoke_test_upd", "Segundo parte", "", "proposed")
    )
    entry_b = cur_b.lastrowid
    conn.commit()

    acciones_b = [{"tool": "upsert_asistencia", "input": {
        "fecha": fecha_test, "total_personal": 45,
        "lluvia_mm": 2.5, "dia_perdido": False, "notas": "B"
    }}]
    r_b = confirmar_acciones(conn, codigo, entry_b, acciones_b, "smoke_test_upd")
    assert r_b["ok"], f"confirm B fallo: {r_b}"
    ref_b = r_b["diff"][0]["resultado"]
    assert ref_b["op"] == "update", f"B: esperaba update, got {ref_b['op']}"
    assert ref_b["id"] == fila_id
    assert "prev_values" in ref_b, f"B: prev_values ausente -> {ref_b}"
    assert ref_b["prev_values"]["total_personal"] == 30, \
        f"B: prev_values.total_personal != 30 -> {ref_b['prev_values']}"
    assert ref_b["prev_values"]["notas"] == "A"
    print(f"  B: UPDATE asistencia_diaria#{fila_id} total=45 (prev_values captura total=30)")

    fila_tras_b = conn.execute(
        "SELECT total_personal, lluvia_mm, notas FROM asistencia_diaria WHERE id=?",
        (fila_id,)
    ).fetchone()
    assert fila_tras_b["total_personal"] == 45
    assert fila_tras_b["lluvia_mm"] == 2.5
    assert fila_tras_b["notas"] == "B"

    # Rollback B -> debe restaurar total=30, lluvia=0, notas=A
    r_roll = rollback_acciones(conn, codigo, entry_b)
    assert r_roll["ok"], f"rollback B fallo: {r_roll}"
    assert r_roll["count_reverted"] == 1, f"esperaba 1 reverted, got {r_roll}"
    assert r_roll["count_skipped"] == 0, f"esperaba 0 skipped, got {r_roll}"
    assert r_roll["reverted"][0]["op"] == "update"
    print(f"  rollback B: {r_roll['count_reverted']} reverted (UPDATE restaurado)")

    fila_restaurada = conn.execute(
        "SELECT total_personal, lluvia_mm, dia_perdido, notas "
        "FROM asistencia_diaria WHERE id=?",
        (fila_id,)
    ).fetchone()
    assert fila_restaurada["total_personal"] == 30, \
        f"total no restaurado: {fila_restaurada['total_personal']}"
    assert fila_restaurada["lluvia_mm"] == 0.0
    assert fila_restaurada["notas"] == "A"
    print(f"  fila post-rollback: total={fila_restaurada['total_personal']}, "
          f"lluvia={fila_restaurada['lluvia_mm']}, notas={fila_restaurada['notas']}")

    # Guard: refuerzo de que entry confirmed sin prev_values (legacy) hace skip.
    # Simulamos manipulando acciones_derivadas: quitar prev_values del diff.
    import json as _json
    # Preparamos otro ciclo A'/B' para testear el skip legacy
    conn.execute(
        "DELETE FROM asistencia_diaria WHERE obra_id=? AND fecha=?",
        (obra_id, fecha_test)
    )
    conn.commit()

    cur_la = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones, parse_estado) "
        "VALUES (?,?,?,?,?,?)",
        (obra_id, fecha_test, "smoke_test_upd", "Legacy A", "", "proposed")
    )
    entry_la = cur_la.lastrowid
    conn.commit()
    confirmar_acciones(conn, codigo, entry_la, acciones_a, "smoke_test_upd")

    cur_lb = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones, parse_estado) "
        "VALUES (?,?,?,?,?,?)",
        (obra_id, fecha_test, "smoke_test_upd", "Legacy B", "", "proposed")
    )
    entry_lb = cur_lb.lastrowid
    conn.commit()
    r_lb = confirmar_acciones(conn, codigo, entry_lb, acciones_b, "smoke_test_upd")
    assert r_lb["ok"]

    # Simular legacy: strippear prev_values del diff ya guardado
    diff_legacy = _json.loads(conn.execute(
        "SELECT acciones_derivadas FROM daily_log WHERE id=?", (entry_lb,)
    ).fetchone()["acciones_derivadas"])
    for item in diff_legacy:
        item["resultado"].pop("prev_values", None)
    conn.execute(
        "UPDATE daily_log SET acciones_derivadas=? WHERE id=?",
        (_json.dumps(diff_legacy, ensure_ascii=False), entry_lb)
    )
    conn.commit()

    r_legacy_roll = rollback_acciones(conn, codigo, entry_lb)
    assert r_legacy_roll["ok"], f"legacy rollback fallo: {r_legacy_roll}"
    assert r_legacy_roll["count_reverted"] == 0, \
        f"legacy: no deberia revertir nada, got {r_legacy_roll}"
    assert r_legacy_roll["count_skipped"] == 1, \
        f"legacy: deberia skip 1, got {r_legacy_roll}"
    assert "sin snapshot" in r_legacy_roll["skipped"][0]["motivo"]
    print(f"  legacy (sin prev_values) -> skip OK: {r_legacy_roll['skipped'][0]['motivo']}")

    # Cleanup
    conn.execute("DELETE FROM asistencia_diaria WHERE id=?", (fila_id,))
    conn.execute("DELETE FROM asistencia_diaria WHERE obra_id=? AND fecha=?",
                 (obra_id, fecha_test))
    conn.execute("DELETE FROM daily_log WHERE id IN (?,?,?,?)",
                 (entry_a, entry_b, entry_la, entry_lb))
    conn.commit()
    print("  cleanup OK")


def main():
    if not os.environ.get("ANTHROPIC_API_KEY"):
        env_file = Path(__file__).resolve().parents[1] / ".env"
        if env_file.exists():
            for line in env_file.read_text(encoding="utf-8").splitlines():
                if line.startswith("ANTHROPIC_API_KEY="):
                    os.environ["ANTHROPIC_API_KEY"] = line.split("=", 1)[1].strip()
                    break
    if not os.environ.get("ANTHROPIC_API_KEY"):
        print("ERROR: ANTHROPIC_API_KEY no seteada")
        sys.exit(1)

    conn = get_conn()
    codigo = "TDG1"

    banner("1. Verificando obra TDG1")
    row = conn.execute("SELECT id FROM obras WHERE codigo=?", (codigo,)).fetchone()
    if not row:
        print("ERROR: obra TDG1 no existe en la DB")
        sys.exit(1)
    obra_id = row["id"]
    print(f"  obra_id = {obra_id}")

    rubros = conn.execute(
        "SELECT nombre FROM rubros_obra WHERE obra_id=? ORDER BY orden", (obra_id,)
    ).fetchall()
    print(f"  rubros_activos = {len(rubros)}")
    subs = conn.execute(
        "SELECT nombre FROM subcontratistas WHERE obra_id=? ORDER BY orden", (obra_id,)
    ).fetchall()
    print(f"  subcontratistas = {len(subs)}")

    banner("2. Insertando daily_log de prueba")
    cur = conn.execute(
        "INSERT INTO daily_log (obra_id, fecha, autor, actividades, observaciones) "
        "VALUES (?,?,?,?,?)",
        (obra_id, BITACORA_TEST["fecha"], BITACORA_TEST["autor"],
         BITACORA_TEST["actividades"], BITACORA_TEST["observaciones"])
    )
    conn.commit()
    entry_id = cur.lastrowid
    print(f"  entry_id = {entry_id}")
    print(f"  texto:   {BITACORA_TEST['actividades'][:80]}...")

    # Capturar estado de las tablas destino ANTES del confirm para luego diff
    def snap_count(tabla: str) -> int:
        return conn.execute(f"SELECT COUNT(*) FROM {tabla}").fetchone()[0]

    snap_before = {
        "asistencia_diaria": snap_count("asistencia_diaria"),
        "materiales":        snap_count("materiales"),
        "presupuesto_mensual": snap_count("presupuesto_mensual"),
    }
    print(f"  conteos pre-confirm: {snap_before}")

    try:
        banner("3. trigger_parse: Claude extrae acciones")
        r1 = trigger_parse(conn, codigo, entry_id)
        if not r1.get("ok"):
            print("ERROR parse:", r1.get("error"))
            sys.exit(1)
        borrador = r1["borrador"]
        acciones = borrador["acciones"]
        print(f"  acciones propuestas: {len(acciones)}")
        for a in acciones:
            print(f"    - {a['tool']}: {json.dumps(a['input'], ensure_ascii=False)[:120]}")
        print(f"  tokens input: {borrador['meta']['usage']['input_tokens']}, "
              f"output: {borrador['meta']['usage']['output_tokens']}")
        if borrador["meta"].get("resumen_modelo"):
            print(f"  resumen modelo: {borrador['meta']['resumen_modelo'][:150]}")

        # Verificar que quedo en parse_estado='proposed'
        post_parse = conn.execute(
            "SELECT parse_estado, parse_borrador IS NOT NULL AS tiene_borrador "
            "FROM daily_log WHERE id=?", (entry_id,)
        ).fetchone()
        assert post_parse["parse_estado"] == "proposed", \
            f"estado inesperado: {post_parse['parse_estado']}"
        assert post_parse["tiene_borrador"] == 1, "parse_borrador vacio"
        print(f"  daily_log.parse_estado = 'proposed' OK")

        banner("4. confirmar_acciones: aplicar a tablas destino")
        r2 = confirmar_acciones(conn, codigo, entry_id, acciones, usuario="smoke_test")
        if not r2.get("ok"):
            print("ERROR confirmar:", r2.get("errors"))
            sys.exit(1)
        print(f"  {r2['count']} acciones aplicadas")
        for d in r2["diff"]:
            res = d["resultado"]
            print(f"    - {d['tool']} -> {res['tabla']}#{res['id']} ({res['op']}) {res.get('dato', {})}")

        banner("5. Verificar cambios en tablas destino")
        snap_after = {
            "asistencia_diaria": snap_count("asistencia_diaria"),
            "materiales":        snap_count("materiales"),
            "presupuesto_mensual": snap_count("presupuesto_mensual"),
        }
        print(f"  conteos post-confirm: {snap_after}")
        diffs = {k: snap_after[k] - snap_before[k] for k in snap_before}
        print(f"  deltas: {diffs}")

        # Verificar estado final de daily_log
        final = conn.execute(
            "SELECT parse_estado, acciones_derivadas, parse_confirmado_por "
            "FROM daily_log WHERE id=?", (entry_id,)
        ).fetchone()
        assert final["parse_estado"] == "confirmed", \
            f"estado final inesperado: {final['parse_estado']}"
        assert final["acciones_derivadas"], "acciones_derivadas vacio"
        assert final["parse_confirmado_por"] == "smoke_test"
        print(f"  daily_log.parse_estado = 'confirmed' OK")
        print(f"  confirmado_por = {final['parse_confirmado_por']}")

        banner("A. SMOKE TEST parse+confirm: OK")

        # Caso B: UPDATE rollback con prev_values (sin llamadas al parser)
        test_update_rollback(conn, codigo)

        # Caso C: guards + validaciones regex (sin llamadas al parser)
        test_guards_y_validaciones(conn, codigo)

        banner("SMOKE TEST COMPLETO: A + B + C OK")

    finally:
        # Limpieza: solo revertir INSERTs hechos por este test.
        # Los UPDATEs no se deshacen (no guardamos snapshot previo aca).
        banner("Limpieza (rollback para no contaminar la DB)")
        deleted = {"asistencia": 0, "materiales": 0, "presupuesto": 0}
        if 'r2' in dir() and r2 and r2.get("ok"):
            for d in r2["diff"]:
                res = d["resultado"]
                if res["op"] != "insert":
                    continue  # update no se deshace; noop tampoco
                tabla = res["tabla"]
                if tabla == "asistencia_diaria":
                    deleted["asistencia"] += conn.execute(
                        "DELETE FROM asistencia_diaria WHERE id=?", (res["id"],)
                    ).rowcount
                elif tabla == "materiales":
                    deleted["materiales"] += conn.execute(
                        "DELETE FROM materiales WHERE id=?", (res["id"],)
                    ).rowcount
                elif tabla == "presupuesto_mensual":
                    deleted["presupuesto"] += conn.execute(
                        "DELETE FROM presupuesto_mensual WHERE id=?", (res["id"],)
                    ).rowcount
        conn.execute("DELETE FROM daily_log WHERE id=?", (entry_id,))
        conn.commit()
        print(f"  deleted: {deleted}, entry={entry_id}")
        print("  nota: UPDATES no se deshacen automaticamente (ej. asistencia).")
        conn.close()


if __name__ == "__main__":
    main()
