"""Tests del modo `only_chapters` del pipeline render_automatic_eda.

Cubre la tarea de "capítulos sueltos con resolución de dependencias":

  - Golden (DuckDB real): pedir SOLO un capítulo genera un documento con solo
    portada + ese capítulo + glosario, y el capítulo llega COMPLETO porque la
    resolución de dependencias activó el cómputo que necesita aunque el caller
    no lo pidiera (outliers → run_models + raw_numeric → IsolationForest poblado;
    timeseries → run_series; correlacion → raw_numeric).
  - Eficiencia: pedir un capítulo que NO necesita flags caros (geospatial) no los
    activa, y un capítulo puramente agregado (num_distr) ni siquiera construye el
    ctx de datos crudos.
  - Edge: id desconocido / lista vacía / no-lista devuelven error claro sin
    lanzar; only_chapters=None mantiene el comportamiento histórico.
"""

import json
import os
import random
import sys
from datetime import date, timedelta

_HERE = os.path.dirname(os.path.abspath(__file__))
_FUNCTIONS = os.path.abspath(os.path.join(_HERE, "..", ".."))  # python/functions
if _FUNCTIONS not in sys.path:
    sys.path.insert(0, _FUNCTIONS)

import duckdb  # noqa: E402

from pipelines.render_automatic_eda import render_automatic_eda  # noqa: E402


def _make_db_models(path):
    """DB con fecha + 3 numéricas continuas en 3 clusters gaussianos.

    Garantiza material para outliers/modelos (>=2 numéricas → IsolationForest),
    timeseries (columna DATE) y correlacion (numéricas). Mismo shape que el
    fixture del test del pipeline base.
    """
    con = duckdb.connect(path)
    con.execute("CREATE TABLE pts (d DATE, grp VARCHAR, x1 DOUBLE, x2 DOUBLE, x3 DOUBLE)")
    random.seed(42)
    centers = [(0.0, 0.0, 0.0), (10.0, 10.0, 10.0), (20.0, 5.0, 15.0)]
    d0 = date(2024, 1, 1)
    rows = []
    for i in range(150):
        cx, cy, cz = centers[i % 3]
        rows.append((
            d0 + timedelta(days=i), f"g{i % 3}",
            round(cx + random.gauss(0, 1.0), 4),
            round(cy + random.gauss(0, 1.0), 4),
            round(cz + random.gauss(0, 1.0), 4),
        ))
    con.executemany("INSERT INTO pts VALUES (?,?,?,?,?)", rows)
    con.close()


def _manifest_chapters(result):
    with open(result["manifest_path"], encoding="utf-8") as fh:
        return set((json.load(fh).get("chapters") or {}).keys())


# --------------------------------------------------------------------------- #
# GOLDEN — outliers suelto: IsolationForest poblado por resolución de deps.
# --------------------------------------------------------------------------- #
def test_only_outliers_isolation_forest_populated_without_explicit_run_models(tmp_path):
    """El corazón de la tarea: pedir SOLO 'outliers' sin run_models explícito
    activa run_models por dependencias y conserva ctx['raw_numeric'], de modo que
    el bloque multivariante (Isolation Forest) sale con datos, no degradado."""
    db = str(tmp_path / "pts.duckdb")
    _make_db_models(db)
    out = str(tmp_path / "out")

    # NB: no se pasa run_models — la resolución de dependencias debe activarlo.
    r = render_automatic_eda(db, "pts", only_chapters=["outliers"],
                             out_dir=out, basename="only_outliers")
    assert r["status"] == "ok", r.get("error")
    assert r["only_chapters"] == ["outliers"]

    # Documento = portada + outliers + glosario, nada más.
    assert _manifest_chapters(r) == {"portada", "outliers", "glosario"}

    # El multivariante salió POBLADO (no la nota de degradación). Se comprueba en
    # el Markdown (mismo documento por capítulos, texto plano fiable).
    md = open(r["aeda_md_path"], encoding="utf-8").read()
    assert "Filas atípicas (multivariante)" in md
    assert "Filas analizadas" in md, "el Isolation Forest no trae su tabla poblada"
    assert "No se pudo analizar la anomalía multivariante" not in md, \
        "el bloque multivariante salió degradado pese a resolver las deps"

    # La resolución activó run_models → el perfil trae el bloque de modelos.
    assert ((r["profile"] or {}).get("models") or {}).get("outliers") is not None


# --------------------------------------------------------------------------- #
# GOLDEN — timeseries suelto activa run_series.
# --------------------------------------------------------------------------- #
def test_only_timeseries_activates_run_series(tmp_path):
    db = str(tmp_path / "pts.duckdb")
    _make_db_models(db)
    out = str(tmp_path / "out")

    r = render_automatic_eda(db, "pts", only_chapters=["timeseries"],
                             out_dir=out, basename="only_ts")
    assert r["status"] == "ok", r.get("error")
    assert "timeseries" in _manifest_chapters(r)
    assert "modelos" not in _manifest_chapters(r)
    # run_series resuelto por deps → el perfil trae el análisis de serie.
    assert (r["profile"] or {}).get("series") is not None, \
        "only_chapters=['timeseries'] debe activar run_series"


# --------------------------------------------------------------------------- #
# GOLDEN — correlacion suelto construye raw_numeric (sin activar modelos).
# --------------------------------------------------------------------------- #
def test_only_correlacion_builds_raw_numeric_without_models(tmp_path):
    db = str(tmp_path / "pts.duckdb")
    _make_db_models(db)
    out = str(tmp_path / "out")

    r = render_automatic_eda(db, "pts", only_chapters=["correlacion"],
                             out_dir=out, basename="only_corr")
    assert r["status"] == "ok", r.get("error")
    assert _manifest_chapters(r) == {"portada", "correlacion", "glosario"}
    # Eficiencia: correlacion no necesita los modelos → no se corrieron.
    assert ((r["profile"] or {}).get("models") or {}).get("outliers") is None
    assert (r["profile"] or {}).get("series") is None


# --------------------------------------------------------------------------- #
# Eficiencia y precedencia — vía stub (sin DuckDB).
# --------------------------------------------------------------------------- #
def _patch(monkeypatch, cap):
    import pipelines.render_automatic_eda as mod

    def fake_pt(db, t, **kw):
        cap["run_models"] = kw.get("run_models")
        cap["run_series"] = kw.get("run_series")
        cap["run_llm"] = kw.get("run_llm")
        return {"status": "ok", "profile": {"columns": []}}

    def fake_ctx(db, t, prof, **kw):
        cap["ctx_called"] = True
        return {"db_path": db, "table": t}

    cap["ctx_called"] = False
    monkeypatch.setattr(mod, "profile_table", fake_pt)
    monkeypatch.setattr(mod, "build_eda_render_ctx", fake_ctx)
    monkeypatch.setattr(mod, "render_automatic_eda_pdf",
                        lambda *a, **k: {"path": "x.pdf", "n_pages": 1,
                                         "manifest_path": "m.json"})
    monkeypatch.setattr(mod, "render_automatic_eda_pptx",
                        lambda *a, **k: {"path": "x.pptx", "n_slides": 1})
    monkeypatch.setattr(mod, "render_automatic_eda_markdown",
                        lambda *a, **k: {"path": "x.md", "n_chars": 1})


def test_only_geospatial_does_not_activate_cost_flags(monkeypatch):
    """Eficiencia: pedir solo geospatial NO corre modelos/serie/LLM."""
    cap = {}
    _patch(monkeypatch, cap)
    render_automatic_eda("db", "t", only_chapters=["geospatial"])
    assert cap["run_models"] is False
    assert cap["run_series"] is False
    assert cap["run_llm"] is False


def test_only_outliers_activates_run_models_via_deps(monkeypatch):
    cap = {}
    _patch(monkeypatch, cap)
    render_automatic_eda("db", "t", only_chapters=["outliers"])
    assert cap["run_models"] is True
    assert cap["run_series"] is False


def test_explicit_flag_overrides_dependency_resolution(monkeypatch):
    """run_models=False explícito gana, aunque outliers lo pediría por deps."""
    cap = {}
    _patch(monkeypatch, cap)
    render_automatic_eda("db", "t", only_chapters=["outliers"], run_models=False)
    assert cap["run_models"] is False


def test_purely_aggregated_chapter_skips_render_ctx(monkeypatch):
    """num_distr solo lee el profile → build_eda_render_ctx no se llama."""
    cap = {}
    _patch(monkeypatch, cap)
    render_automatic_eda("db", "t", only_chapters=["num_distr"])
    assert cap["ctx_called"] is False, \
        "num_distr no necesita datos crudos: el ctx no debe construirse"


def test_chapter_that_needs_ctx_builds_it(monkeypatch):
    cap = {}
    _patch(monkeypatch, cap)
    render_automatic_eda("db", "t", only_chapters=["outliers"])
    assert cap["ctx_called"] is True


# --------------------------------------------------------------------------- #
# EDGE — errores claros sin lanzar.
# --------------------------------------------------------------------------- #
def test_unknown_chapter_id_returns_clear_error(tmp_path):
    r = render_automatic_eda(str(tmp_path / "x.duckdb"), "t",
                             only_chapters=["no_existe"])
    assert r["status"] == "error"
    assert "no_existe" in r["error"]
    assert "Capítulos válidos" in r["error"]
    # Algún id válido conocido aparece en la lista.
    assert "outliers" in r["error"]


def test_empty_only_list_returns_error(tmp_path):
    r = render_automatic_eda(str(tmp_path / "x.duckdb"), "t", only_chapters=[])
    assert r["status"] == "error"
    assert "vac" in r["error"].lower()


def test_only_chapters_not_a_list_returns_error(tmp_path):
    r = render_automatic_eda(str(tmp_path / "x.duckdb"), "t",
                             only_chapters="outliers")
    assert r["status"] == "error"


def test_only_none_keeps_full_document(tmp_path):
    """Retro-compat: only_chapters=None genera el documento completo."""
    db = str(tmp_path / "pts.duckdb")
    _make_db_models(db)
    out = str(tmp_path / "out")
    r = render_automatic_eda(db, "pts", out_dir=out, basename="full")
    assert r["status"] == "ok", r.get("error")
    chapters = _manifest_chapters(r)
    # Documento completo: muchos más capítulos que portada/glosario.
    assert {"portada", "glosario", "overview", "correlacion"} <= chapters
    assert len(chapters) > 4
