"""Tests for the PORTADA (cover) chapter — DoD: golden + edges + render.

Self-contained: builds synthetic TableProfiles (no DuckDB) so the suite is fast
and deterministic. Verifies the Fase 4b improvements:

1. The dataset size (N rows x M columns) is always shown BIG — as a level-2
   heading kept together with the dataset name in a ``Group`` — and is no longer
   a row of the metadata table.
2. Description and Granularity are resolved through a real cascade and are never
   the old empty placeholders: an explicit ``ctx`` value wins; otherwise the LLM
   block (``profile['llm']``) provides ``summary`` / ``row_meaning``; otherwise a
   short summary is derived from the profile and a "Cada fila es…" sentence from
   the key-candidate columns or the table shape.
3. The chapter degrades without raising on empty/None input.
4. It renders inside the full document to both PDF and PPTX showing that content.
"""

import os
import re
import tempfile

from pypdf import PdfReader
from pptx import Presentation

from datascience.automatic_eda.model import Group, Heading, KVTable, Markdown
from datascience.automatic_eda.chapters.portada import (
    CHAPTER_ID, CHAPTER_VERSION, build_portada,
)
from datascience.render_automatic_eda_pdf import render_automatic_eda_pdf
from datascience.render_automatic_eda_pptx import render_automatic_eda_pptx


def _profile(with_llm: bool = True, with_keys: bool = True) -> dict:
    prof = {
        "table": "titanic",
        "source": "/data/titanic.csv",
        "profiled_at": "2026-06-30T10:00:00+00:00",
        "n_rows": 891,
        "n_cols": 12,
        "quality_score": 78.0,
        "columns": [
            {"name": "PassengerId", "inferred_type": "numeric",
             "null_pct": 0.0, "numeric": {"mean": 446.0, "min": 1.0,
                                          "max": 891.0, "std": 257.0}},
            {"name": "Survived", "inferred_type": "numeric",
             "null_pct": 0.0, "numeric": {"mean": 0.38, "min": 0.0,
                                          "max": 1.0, "std": 0.49}},
            {"name": "Sex", "inferred_type": "categorical", "null_pct": 0.0,
             "categorical": {"top": [{"value": "male", "count": 577, "pct": 0.65},
                                     {"value": "female", "count": 314,
                                      "pct": 0.35}],
                             "mode": "male", "n_distinct": 2, "entropy": 0.93}},
        ],
    }
    if with_keys:
        prof["key_candidates"] = ["PassengerId"]
    if with_llm:
        prof["llm"] = {
            "summary": "Pasajeros del Titanic con su supervivencia y datos de viaje.",
            "row_meaning": "Cada fila es un pasajero del Titanic.",
            "dictionary": [], "pii": [], "cleaning": [], "analyses": [],
        }
    return prof


def _pdf_text(path: str) -> str:
    txt = "".join((pg.extract_text() or "") for pg in PdfReader(path).pages)
    return re.sub(r"\s+", " ", txt)


def _pptx_text(path: str) -> str:
    prs = Presentation(path)
    parts = []
    for sl in prs.slides:
        for sh in sl.shapes:
            if sh.has_text_frame:
                parts.append(sh.text_frame.text)
            if sh.has_table:
                tb = sh.table
                for r in range(len(tb.rows)):
                    for c in range(len(tb.columns)):
                        parts.append(tb.cell(r, c).text)
    return re.sub(r"\s+", " ", " ".join(parts))


def _markdown_after(blocks, heading_text):
    """Return the Markdown block that follows a Heading whose text matches."""
    for i, b in enumerate(blocks):
        if isinstance(b, Heading) and heading_text.lower() in b.text.lower():
            for nb in blocks[i + 1:]:
                if isinstance(nb, Markdown):
                    return nb
    return None


def test_golden_tamano_grande_y_textos_llm():
    ch = build_portada(_profile(), {})
    assert ch is not None
    assert ch.id == CHAPTER_ID
    assert ch.version == CHAPTER_VERSION

    # 1) Title + size kept together in a Group; size is a BIG level-2 heading.
    group = next(b for b in ch.blocks if isinstance(b, Group))
    inner = group.blocks
    assert isinstance(inner[0], Heading) and inner[0].level == 1
    assert inner[0].text == "titanic"
    size_h = next(b for b in inner if isinstance(b, Heading) and b.level == 2)
    assert "891" in size_h.text and "12" in size_h.text
    assert "filas" in size_h.text and "columnas" in size_h.text

    # 2) Size is no longer a row of the metadata table.
    kv = next(b for b in ch.blocks if isinstance(b, KVTable))
    labels = [r[0] for r in kv.rows]
    assert "Tamaño" not in labels
    assert "Fuente" in labels and "Calidad" in labels

    # 3) Description and Granularity come from the LLM block.
    desc = _markdown_after(ch.blocks, "Descripción")
    gran = _markdown_after(ch.blocks, "Granularidad")
    assert desc is not None and "Titanic" in desc.text
    assert gran is not None and gran.text.startswith("Cada fila es")
    assert "pasajero" in gran.text.lower()


def test_fallback_sin_llm_usa_keys_y_perfil():
    # No LLM block: description derived from the profile, granularity from keys.
    ch = build_portada(_profile(with_llm=False, with_keys=True), {})
    desc = _markdown_after(ch.blocks, "Descripción")
    gran = _markdown_after(ch.blocks, "Granularidad")
    # Description is the derived summary, never the old "pendiente" placeholder.
    assert "pendiente" not in desc.text.lower()
    assert "891" in desc.text and "columnas" in desc.text
    assert "numéricas" in desc.text or "categóricas" in desc.text
    # Granularity mentions the key candidate and starts with "Cada fila es".
    assert gran.text.startswith("Cada fila es")
    assert "PassengerId" in gran.text
    assert "…" not in gran.text  # the old ellipsis placeholder is gone.


def test_fallback_sin_llm_sin_keys_usa_forma():
    ch = build_portada(_profile(with_llm=False, with_keys=False), {})
    gran = _markdown_after(ch.blocks, "Granularidad")
    assert gran.text.startswith("Cada fila es")
    assert "titanic" in gran.text.lower()
    assert "pendiente" not in gran.text.lower()


def test_ctx_explicito_gana_sobre_llm():
    ctx = {"description": "Descripción manual.",
           "granularity": "Cada fila es una unidad manual."}
    ch = build_portada(_profile(), ctx)
    desc = _markdown_after(ch.blocks, "Descripción")
    gran = _markdown_after(ch.blocks, "Granularidad")
    assert desc.text == "Descripción manual."
    assert gran.text == "Cada fila es una unidad manual."


def test_edge_perfil_vacio_no_lanza():
    # Empty / None never raise; the cover still shows a size and real texts.
    for prof, ctx in (({}, {}), (None, None)):
        ch = build_portada(prof, ctx)
        assert ch is not None
        group = next(b for b in ch.blocks if isinstance(b, Group))
        size_h = next(b for b in group.blocks
                      if isinstance(b, Heading) and b.level == 2)
        assert "filas" in size_h.text and "columnas" in size_h.text
        desc = _markdown_after(ch.blocks, "Descripción")
        gran = _markdown_after(ch.blocks, "Granularidad")
        assert desc.text and "pendiente" not in desc.text.lower()
        assert gran.text.startswith("Cada fila es")


def test_golden_render_pdf_muestra_portada():
    prof = _profile()
    with tempfile.TemporaryDirectory() as d:
        out = os.path.join(d, "eda.pdf")
        res = render_automatic_eda_pdf(prof, out, {"title": "EDA"})
        assert res["path"] == out and os.path.exists(out)
        assert CHAPTER_ID in [c["id"] for c in res["chapters"]]
        txt = _pdf_text(out)
        assert "titanic" in txt.lower()
        assert "891" in txt and "filas" in txt and "columnas" in txt
        assert "Titanic" in txt          # LLM summary in the Description.
        assert "Cada fila es" in txt     # granularity sentence.


def test_golden_render_pptx_muestra_portada():
    prof = _profile()
    with tempfile.TemporaryDirectory() as d:
        out = os.path.join(d, "eda.pptx")
        res = render_automatic_eda_pptx(prof, out, {"title": "EDA"})
        assert res["path"] == out and os.path.exists(out)
        assert CHAPTER_ID in [c["id"] for c in res["chapters"]]
        txt = _pptx_text(out)
        assert "titanic" in txt.lower()
        assert "891" in txt and "columnas" in txt
        assert "Cada fila es" in txt
