"""Tests for the MISSINGNESS chapter.

Covers the Definition of Done for this chapter:
  * Activates (non-None Chapter with the expected sections) when the profile has
    missing data, building the co-occurrence from the per-row is-null mask.
  * Returns None when the table has no missing data at all (edge case).
  * Registers the MCAR/MAR/missingness glossary terms.
  * The DuckDB push-down path covers categorical columns (not only numeric),
    so a categorical column that co-misses with a numeric one is detected.
"""

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 import model  # noqa: E402
from datascience.automatic_eda.chapters.missingness import (  # noqa: E402
    build_missingness,
)


def _titles(chapter):
    """Collect heading texts and table/figure titles for assertions."""
    out = []
    for b in chapter.blocks:
        kind = getattr(b, "kind", None)
        if kind == "heading":
            out.append(("heading", getattr(b, "text", "")))
        elif kind in ("data_table", "kv_table"):
            out.append((kind, getattr(b, "title", "")))
        elif kind == "group":
            for inner in getattr(b, "blocks", []):
                ik = getattr(inner, "kind", None)
                if ik == "heading":
                    out.append(("heading", getattr(inner, "text", "")))
                elif ik in ("data_table", "kv_table"):
                    out.append((ik, getattr(inner, "title", "")))
                elif ik == "figure":
                    out.append(("figure", getattr(inner, "caption", "")))
        elif kind == "figure":
            out.append(("figure", getattr(b, "caption", "")))
    return out


def _all_text(chapter):
    parts = []
    def walk(blocks):
        for b in blocks:
            for attr in ("text", "title", "note", "caption"):
                v = getattr(b, attr, None)
                if v:
                    parts.append(str(v))
            if getattr(b, "kind", None) == "group":
                walk(getattr(b, "blocks", []))
    walk(chapter.blocks)
    return "\n".join(parts)


def test_returns_none_when_no_missing_data():
    profile = {
        "n_rows": 4,
        "null_cell_pct": 0.0,
        "columns": [
            {"name": "a", "null_count": 0, "null_pct": 0.0, "n_rows": 4},
            {"name": "b", "null_count": 0, "null_pct": 0.0, "n_rows": 4},
        ],
    }
    assert build_missingness(profile, {}) is None


def test_activates_with_cooccurrence_via_raw_numeric():
    # a and b are missing in EXACTLY the same rows (0,1,2) -> perfect absence
    # correlation. c has no nulls. No db_path -> the chapter falls back to the
    # numeric raw_numeric mask.
    profile = {
        "n_rows": 6,
        "null_cell_pct": (0.5 + 0.5 + 0.0) / 3.0,
        "columns": [
            {"name": "a", "null_count": 3, "null_pct": 0.5, "n_rows": 6},
            {"name": "b", "null_count": 3, "null_pct": 0.5, "n_rows": 6},
            {"name": "c", "null_count": 0, "null_pct": 0.0, "n_rows": 6},
        ],
    }
    glossary = model.GlossaryCollector()
    ctx = {
        "raw_numeric": {
            "a": [None, None, None, 1.0, 2.0, 3.0],
            "b": [None, None, None, 4.0, 5.0, 6.0],
        },
        "glossary": glossary,
    }
    ch = build_missingness(profile, ctx)
    assert ch is not None
    assert ch.id == "missingness"
    assert ch.blocks

    titles = _titles(ch)
    headings = {t for (k, t) in titles if k == "heading"}
    # Core sections present.
    assert any("Cuánto y dónde" in h for h in headings)
    assert any("Faltantes por columna" in h for h in headings)
    assert any("Co-ocurrencia" in h for h in headings)
    assert any("MCAR" in h for h in headings)
    # A summary KVTable, a ranking DataTable, a co-occurrence figure and the
    # pairs table all exist.
    kinds = {k for (k, _) in titles}
    assert "kv_table" in kinds
    assert "data_table" in kinds
    assert "figure" in kinds

    # Glossary terms registered.
    keys = {t["key"] for t in glossary.terms()}
    assert {"missingness", "mcar", "mar"} <= keys

    # The MCAR/MAR note reads the co-occurrence; with a perfect overlap it must
    # flag the non-random (MAR) reading.
    text = _all_text(ch)
    assert "MAR" in text


def test_db_pushdown_covers_categorical_column(tmp_path):
    """The is-null mask push-down must cover a categorical column, so a
    categorical that co-misses with a numeric one shows up in the pairs."""
    import duckdb

    db = str(tmp_path / "miss.duckdb")
    con = duckdb.connect(db)
    con.execute("CREATE TABLE t (num1 DOUBLE, num2 DOUBLE, cat VARCHAR)")
    # num1 and cat are NULL together in the first 4 of 10 rows; num2 never null.
    rows = []
    for i in range(10):
        if i < 4:
            rows.append((None, float(i), None))
        else:
            rows.append((float(i), float(i), f"c{i}"))
    con.executemany("INSERT INTO t VALUES (?,?,?)", rows)
    con.close()

    profile = {
        "n_rows": 10,
        "null_cell_pct": (0.4 + 0.0 + 0.4) / 3.0,
        "columns": [
            {"name": "num1", "null_count": 4, "null_pct": 0.4, "n_rows": 10},
            {"name": "num2", "null_count": 0, "null_pct": 0.0, "n_rows": 10},
            {"name": "cat", "null_count": 4, "null_pct": 0.4, "n_rows": 10},
        ],
    }
    ctx = {"db_path": db, "table": "t", "glossary": model.GlossaryCollector()}
    ch = build_missingness(profile, ctx)
    assert ch is not None

    # The pairs table must mention both num1 and cat (they co-miss perfectly),
    # which is only possible if the mask covered the categorical column.
    text = _all_text(ch)
    assert "num1" in text and "cat" in text
    # Co-occurrence section + a pairs data table exist.
    titles = _titles(ch)
    assert any("co-faltan" in (t or "").lower() for (k, t) in titles)
