"""Tests for the TEXT DISTR chapter — DoD: golden + edges + degradation.

Self-contained: builds synthetic TableProfiles and feeds the raw text sample
in-memory through ``ctx['text_raw']`` (no DuckDB needed), so the suite is fast
and deterministic. Verifies that ``build_text_distr``:

- GOLDEN: with a long-text column, emits the chapter with its key blocks
  (length summary, word histogram, top-terms table, n-gram tables, language
  bars) and registers the clickable glossary terms; and that it renders inside
  the full document to both PDF and PPTX showing that content.
- EDGE (None): a dataset whose only string column is short labels (titanic-like
  ``Name``) yields ``None`` without raising — the existing report is untouched.
- EDGE (None): a column that passes the cheap char gate but whose documents are
  short (median words below the threshold) is rejected at the confirmation step.
- DEGRADATION: with ``langdetect`` / ``textstat`` / ``wordcloud`` unavailable,
  the chapter still builds (those pieces are omitted) and never raises.
"""

import builtins
import os
import tempfile

from pypdf import PdfReader
from pptx import Presentation

from datascience.automatic_eda.model import (
    DataTable, Figure, GlossaryCollector, Group, Heading, KVTable, Markdown,
    Note,
)
from datascience.automatic_eda.chapters.text_distr import (
    CHAPTER_ID, CHAPTER_VERSION, build_text_distr,
)
from datascience.automatic_eda.chapters_registry import build_document
from datascience.render_automatic_eda_pdf import render_automatic_eda_pdf
from datascience.render_automatic_eda_pptx import render_automatic_eda_pptx


# --------------------------------------------------------------------------- #
# Synthetic corpus + profiles.
# --------------------------------------------------------------------------- #
_ES = [
    "El producto llegó en perfecto estado y mucho antes de lo previsto por la tienda",
    "La calidad de los materiales es realmente excelente y se nota la diferencia al usarlo",
    "No me convenció del todo porque esperaba bastante más por el precio que pagué finalmente",
    "El servicio de atención al cliente fue rápido amable y resolvió mi problema sin demora",
    "Lo recomiendo totalmente ya que ha superado con creces todas mis expectativas iniciales",
]
_EN = [
    "The product arrived in perfect condition and much earlier than the store had promised me",
    "The build quality is genuinely outstanding and you can really feel the difference using it",
    "I was not fully convinced because I expected quite a lot more for the price i finally paid",
    "Customer support was fast friendly and solved my whole problem without any delay at all",
    "I highly recommend it since it has exceeded by far every one of my initial expectations",
]


def _long_reviews(n=40) -> list:
    """A corpus of long multi-sentence reviews (>= 20 words each), mixing two
    languages and including a few exact duplicates."""
    out = []
    for i in range(n):
        base = _ES if i % 3 != 0 else _EN  # mostly ES, some EN
        a = base[i % len(base)]
        b = base[(i + 2) % len(base)]
        out.append(f"{a}. {b}.")
    # Inject a couple of exact duplicates.
    out.append(out[0])
    out.append(out[1])
    return out


def _text_profile() -> dict:
    """Profile with a long free-text column (review) + a numeric + a short cat."""
    return {
        "table": "reviews",
        "source": "/data/reviews.duckdb",
        "profiled_at": "2026-06-30T10:00:00+00:00",
        "n_rows": 42,
        "n_cols": 3,
        "quality_score": 88.0,
        "columns": [
            {
                "name": "review",
                "inferred_type": "categorical",
                "categorical": {
                    "top": [{"value": "x", "count": 2, "pct": 0.05}],
                    "n_distinct": 40,
                    "len_mean": 180.0,
                    "len_min": 80,
                    "len_max": 220,
                },
            },
            {
                "name": "rating",
                "inferred_type": "numeric",
                "numeric": {"mean": 3.1, "median": 3.0, "std": 1.2,
                            "min": 1, "max": 5},
            },
            {
                "name": "product",
                "inferred_type": "categorical",
                "categorical": {
                    "top": [{"value": "teclado", "count": 10, "pct": 0.25}],
                    "n_distinct": 6,
                    "len_mean": 7.0,
                    "len_min": 5, "len_max": 11,
                },
            },
        ],
    }


def _no_text_profile() -> dict:
    """titanic-like: the only string column is short labels (Name ≈ 27 chars)."""
    return {
        "table": "titanic",
        "n_rows": 891,
        "n_cols": 3,
        "columns": [
            {"name": "Age", "inferred_type": "numeric",
             "numeric": {"mean": 29.7, "median": 28.0, "std": 14.5}},
            {"name": "Name", "inferred_type": "categorical",
             "categorical": {"top": [{"value": "Braund, Mr. Owen Harris",
                                      "count": 1, "pct": 0.001}],
                             "n_distinct": 891, "len_mean": 27.0,
                             "len_min": 12, "len_max": 82}},
            {"name": "Sex", "inferred_type": "categorical",
             "categorical": {"top": [{"value": "male", "count": 577,
                                      "pct": 0.65}],
                             "n_distinct": 2, "len_mean": 4.6,
                             "len_min": 4, "len_max": 6}},
        ],
    }


def _flatten(blocks) -> list:
    """Recursively flatten Group blocks so tests can inspect leaf blocks."""
    out = []
    for b in blocks:
        if isinstance(b, Group):
            out.extend(_flatten(b.blocks))
        else:
            out.append(b)
    return out


# --------------------------------------------------------------------------- #
# Golden.
# --------------------------------------------------------------------------- #
def test_golden_activa_con_texto():
    glossary = GlossaryCollector()
    ctx = {"text_raw": {"review": _long_reviews()}, "glossary": glossary}
    ch = build_text_distr(_text_profile(), ctx)

    assert ch is not None, "el capítulo debe activarse con una columna de texto largo"
    assert ch.id == CHAPTER_ID
    assert ch.version == CHAPTER_VERSION
    leaves = _flatten(ch.blocks)
    kinds = [b.kind for b in leaves]
    assert "heading" in kinds
    assert "kv_table" in kinds          # summary
    assert "figure" in kinds            # histogram / bars
    assert "data_table" in kinds        # top terms + n-grams

    # KV summary mentions vocabulary metrics.
    kv = next(b for b in leaves if isinstance(b, KVTable))
    labels = " ".join(str(r[0]) for r in kv.rows)
    assert "TTR" in labels
    assert "Hapax" in labels or "hapax" in labels

    # There is a terms table and at least one n-gram table.
    titles = [getattr(b, "title", "") or "" for b in leaves
              if isinstance(b, DataTable)]
    assert any("Términos" in t for t in titles)
    assert any("Bigrama" in t for t in titles)

    # Glossary terms were registered (clickable destinations).
    assert glossary.has("ttr")
    assert glossary.has("hapax")


def test_golden_render_pdf_pptx():
    profile = _text_profile()
    ctx = {"text_raw": {"review": _long_reviews()},
           "dataset_name": "reviews"}
    chapters = build_document(profile, ctx)
    ids = [c.id for c in chapters]
    assert "text_distr" in ids, f"text_distr ausente en {ids}"

    with tempfile.TemporaryDirectory() as d:
        pdf = os.path.join(d, "t.pdf")
        pptx = os.path.join(d, "t.pptx")
        rp = render_automatic_eda_pdf(profile, pdf, {"title": "EDA", "ctx": ctx})
        rx = render_automatic_eda_pptx(profile, pptx, {"title": "EDA", "ctx": ctx})
        assert rp.get("path") and os.path.exists(pdf)
        assert rx.get("path") and os.path.exists(pptx)

        text = "\n".join(p.extract_text() or "" for p in PdfReader(pdf).pages)
        assert "Texto libre" in text or "TTR" in text

        prs = Presentation(pptx)
        ptext = []
        for slide in prs.slides:
            for shp in slide.shapes:
                if shp.has_text_frame:
                    ptext.append(shp.text_frame.text)
        joined = "\n".join(ptext)
        assert "Texto libre" in joined or "TTR" in joined


# --------------------------------------------------------------------------- #
# Edges — None.
# --------------------------------------------------------------------------- #
def test_edge_none_sin_texto_largo():
    # titanic-like: short labels only → chapter must not apply.
    assert build_text_distr(_no_text_profile(), {}) is None


def test_edge_none_palabras_cortas():
    # Char gate passes (len_mean high) but documents are short → confirmation
    # rejects them (median words below threshold).
    profile = _text_profile()
    short = ["palabra " * 3] * 30  # 3 words each, < _MIN_WORDS
    ctx = {"text_raw": {"review": short}}
    assert build_text_distr(profile, ctx) is None


def test_edge_none_empty_profile():
    assert build_text_distr({}, {}) is None
    assert build_text_distr(None, None) is None


# --------------------------------------------------------------------------- #
# Degradation — optional libs absent.
# --------------------------------------------------------------------------- #
def test_degradacion_sin_libs(monkeypatch):
    real_import = builtins.__import__
    blocked = ("langdetect", "textstat", "wordcloud", "datasketch")

    def fake_import(name, *a, **k):
        if name in blocked or any(name.startswith(b + ".") for b in blocked):
            raise ImportError(f"simulado: {name}")
        return real_import(name, *a, **k)

    monkeypatch.setattr(builtins, "__import__", fake_import)

    ctx = {"text_raw": {"review": _long_reviews()}}
    ch = build_text_distr(_text_profile(), ctx)
    # Still builds (the cheap, stdlib-only pieces remain) and never raises.
    assert ch is not None
    leaves = _flatten(ch.blocks)
    assert any(isinstance(b, KVTable) for b in leaves)
    assert any(isinstance(b, DataTable) for b in leaves)
    # A degradation note is present mentioning the missing optional libs.
    notes = " ".join(b.text for b in leaves if isinstance(b, Note))
    assert "langdetect" in notes or "textstat" in notes or "datasketch" in notes
