"""Tests del mapa central de dependencias por capítulo (chapter_deps).

Todas las funciones bajo prueba son PURAS (sin I/O): se ejercitan directamente
sin DuckDB ni renderizado. Cubren la resolución de requisitos (golden + edges),
la validación de ids y los helpers de eficiencia (qué cómputo se salta).
"""

import os
import sys

_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)

from datascience.automatic_eda.chapter_deps import (  # noqa: E402
    ALWAYS_PRESENT,
    CHAPTER_DEPS,
    DATA_CTX_KEYS,
    needs_render_ctx,
    resolve_ctx_data_keys,
    resolve_profile_flags,
    resolve_requirements,
    validate_chapter_ids,
)
from datascience.automatic_eda.chapters_registry import CHAPTER_ORDER  # noqa: E402


# --------------------------------------------------------------------------- #
# El mapa cubre CHAPTER_ORDER entero (sin huecos ni claves de más).
# --------------------------------------------------------------------------- #
def test_chapter_deps_covers_every_chapter_in_order():
    assert set(CHAPTER_DEPS) == set(CHAPTER_ORDER), (
        "CHAPTER_DEPS debe declarar exactamente los ids de CHAPTER_ORDER")
    # Cada entrada tiene la forma esperada.
    for cid, dep in CHAPTER_DEPS.items():
        assert isinstance(dep.get("profile_flags"), list), cid
        assert isinstance(dep.get("ctx"), list), cid


# --------------------------------------------------------------------------- #
# resolve_requirements — golden: outliers exige run_models + raw_numeric.
# --------------------------------------------------------------------------- #
def test_resolve_outliers_requires_run_models_and_raw_numeric():
    req = resolve_requirements(["outliers"])
    assert "run_models" in req["profile_flags"]
    assert "raw_numeric" in req["ctx_keys"]
    assert "run_series" not in req["profile_flags"]
    assert "run_llm" not in req["profile_flags"]


def test_resolve_timeseries_requires_run_series():
    req = resolve_requirements(["timeseries"])
    assert req["profile_flags"] == {"run_series"}
    assert "timeseries_raw" in req["ctx_keys"]


def test_resolve_analisis_llm_requires_run_llm():
    assert resolve_requirements(["analisis_llm"])["profile_flags"] == {"run_llm"}


def test_resolve_union_of_several_chapters():
    req = resolve_requirements(["outliers", "timeseries", "analisis_llm"])
    assert req["profile_flags"] == {"run_models", "run_series", "run_llm"}


# --------------------------------------------------------------------------- #
# Eficiencia: capítulos que NO necesitan flags caros no los activan.
# --------------------------------------------------------------------------- #
def test_resolve_geospatial_needs_no_cost_flags():
    """geospatial sale de geo_points/raw_numeric del ctx, NO de los modelos."""
    req = resolve_requirements(["geospatial"])
    assert req["profile_flags"] == set(), \
        "geospatial no debe activar run_models/run_series/run_llm"
    assert "geo_points" in req["ctx_keys"]


def test_resolve_correlacion_needs_raw_numeric_but_no_models():
    req = resolve_requirements(["correlacion"])
    assert req["profile_flags"] == set()
    assert "raw_numeric" in req["ctx_keys"]


def test_always_present_chapters_add_no_requirements():
    """portada y glosario están siempre, pero no arrastran cómputo."""
    for cid in ALWAYS_PRESENT:
        req = resolve_requirements([cid])
        assert req["profile_flags"] == set()
        assert req["ctx_keys"] == set()


def test_resolve_profile_flags_shortcut():
    assert resolve_profile_flags(["modelos"]) == {"run_models"}
    assert resolve_profile_flags(["num_distr"]) == set()


# --------------------------------------------------------------------------- #
# needs_render_ctx — cuándo se puede saltar build_eda_render_ctx por completo.
# --------------------------------------------------------------------------- #
def test_needs_render_ctx_true_when_chapter_reads_raw_data():
    assert needs_render_ctx(["outliers"]) is True
    assert needs_render_ctx(["agregacion"]) is True  # db_path/table push-down
    assert needs_render_ctx(["timeseries"]) is True


def test_needs_render_ctx_false_for_purely_aggregated_chapters():
    """num_distr / cat_distr / calidad solo leen el profile agregado."""
    assert needs_render_ctx(["num_distr"]) is False
    assert needs_render_ctx(["cat_distr", "calidad"]) is False


# --------------------------------------------------------------------------- #
# resolve_ctx_data_keys — poda: qué claves de DATOS conservar (db_path/table no).
# --------------------------------------------------------------------------- #
def test_resolve_ctx_data_keys_outliers_keeps_only_raw_numeric():
    assert resolve_ctx_data_keys(["outliers"]) == {"raw_numeric"}


def test_resolve_ctx_data_keys_geospatial_keeps_geo_and_numeric():
    assert resolve_ctx_data_keys(["geospatial"]) == {"geo_points", "raw_numeric"}


def test_resolve_ctx_data_keys_aggregation_keeps_nothing_prunable():
    """agregacion usa db_path/table (siempre presentes), 0 claves podables."""
    assert resolve_ctx_data_keys(["agregacion"]) == set()


def test_resolve_ctx_data_keys_subset_of_data_keys():
    keep = resolve_ctx_data_keys(["overview", "timeseries", "geospatial"])
    assert keep <= set(DATA_CTX_KEYS)
    assert {"head_rows", "timeseries_raw", "geo_points", "raw_numeric"} == keep


# --------------------------------------------------------------------------- #
# validate_chapter_ids — separa válidos de desconocidos preservando orden.
# --------------------------------------------------------------------------- #
def test_validate_separates_known_and_unknown():
    out = validate_chapter_ids(["outliers", "nope", "timeseries", "ghost"],
                               CHAPTER_ORDER)
    assert out["valid"] == ["outliers", "timeseries"]
    assert out["unknown"] == ["nope", "ghost"]


def test_validate_all_known():
    out = validate_chapter_ids(["portada", "glosario"], CHAPTER_ORDER)
    assert out["unknown"] == []


# --------------------------------------------------------------------------- #
# Robustez: entradas raras nunca lanzan.
# --------------------------------------------------------------------------- #
def test_resolve_handles_none_and_empty():
    assert resolve_requirements(None)["profile_flags"] == set()
    assert resolve_requirements([])["profile_flags"] == set()
    # ids desconocidos se ignoran silenciosamente en la resolución.
    assert resolve_requirements(["no_existe"])["ctx_keys"] == set()


def test_resolve_accepts_single_string():
    assert resolve_requirements("outliers")["profile_flags"] == {"run_models"}
