"""Tests for the RELACIONES chapter — DoD: golden(s) + edges + no-cut render.

Two goldens covering the two real paths of the chapter:

- **Intra-table** (a single table, no db source for relations): the chapter shows
  the primary-key candidates from the profile and the heuristic foreign-key
  suggestions (name + cardinality), explicitly flagged as a heuristic. Renders to
  PDF and PPTX with nothing cut.
- **Inter-table** (a real DuckDB file with two related tables, customers/orders,
  with a declared FK): the chapter shows the declared keys, the containment-based
  FK candidates and the join graph (roles + a pasteable Mermaid diagram).

Edges: a profile with no key candidate and no FK-looking column returns None;
``None`` / ``{}`` profiles do not raise. The chapter registers its glossary terms.

Layers that depend on the sibling registry functions delegated alongside this
chapter (``detect_declared_keys_duckdb``, ``suggest_intratable_fk_candidates``)
are asserted **conditionally on the function being importable**, so the chapter's
honest-degradation contract is what is tested, never a hard dependency on import
timing.
"""

import os
import tempfile

import duckdb
from pptx import Presentation
from pypdf import PdfReader

from datascience.automatic_eda.chapters.relaciones import build_relaciones
from datascience.automatic_eda.model import Chapter, Group, GlossaryCollector
from datascience.render_automatic_eda_pdf import render_automatic_eda_pdf
from datascience.render_automatic_eda_pptx import render_automatic_eda_pptx

# The optional sibling functions: their layers are asserted only when present.
try:
    from datascience.detect_declared_keys_duckdb import detect_declared_keys_duckdb
except Exception:  # noqa: BLE001
    detect_declared_keys_duckdb = None
try:
    from datascience.suggest_intratable_fk_candidates import (
        suggest_intratable_fk_candidates,
    )
except Exception:  # noqa: BLE001
    suggest_intratable_fk_candidates = None


# --------------------------------------------------------------------------- #
# Helpers.
# --------------------------------------------------------------------------- #
def _flatten(blocks) -> list:
    """Flatten Group blocks so a test can inspect every leaf block."""
    out = []
    for b in blocks:
        if isinstance(b, Group):
            out.extend(_flatten(b.blocks))
        else:
            out.append(b)
    return out


def _text_of(chapter: Chapter) -> str:
    """Collect all visible text of a chapter's blocks into one string."""
    parts = []
    for b in _flatten(chapter.blocks):
        for attr in ("text", "title", "note"):
            v = getattr(b, attr, None)
            if isinstance(v, str):
                parts.append(v)
        header = getattr(b, "header", None)
        if isinstance(header, list):
            parts.extend(str(c) for c in header)
        rows = getattr(b, "rows", None)
        if isinstance(rows, list):
            for r in rows:
                if isinstance(r, (list, tuple)):
                    parts.extend(str(c) for c in r)
                else:
                    parts.append(str(r))
    return "\n".join(parts)


def _render_both(chapter: Chapter, tag: str):
    """Render the chapter to PDF and PPTX; return (pdf_text, n_slides)."""
    tmp = tempfile.mkdtemp(prefix=f"relaciones_{tag}_")
    pdf_path = os.path.join(tmp, "out.pdf")
    pptx_path = os.path.join(tmp, "out.pptx")
    meta = {"title": f"EDA — {tag}"}
    render_automatic_eda_pdf([chapter], pdf_path, meta)
    render_automatic_eda_pptx([chapter], pptx_path, meta)
    assert os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0
    assert os.path.exists(pptx_path) and os.path.getsize(pptx_path) > 0
    text = "".join(p.extract_text() or "" for p in PdfReader(pdf_path).pages)
    n_slides = len(Presentation(pptx_path).slides)
    return text, n_slides


# --------------------------------------------------------------------------- #
# Fixtures.
# --------------------------------------------------------------------------- #
def _titanic_profile() -> dict:
    """A single-table profile: a PK candidate + a column that looks like a FK."""
    return {
        "table": "titanic",
        "source": "/data/titanic.csv",
        "n_rows": 891,
        "n_cols": 4,
        "key_candidates": ["PassengerId"],
        "columns": [
            {"name": "PassengerId", "inferred_type": "numeric",
             "physical_type": "BIGINT", "distinct_count": 891,
             "unique_pct": 1.0, "flags": ["possible_id"]},
            {"name": "ticket_id", "inferred_type": "numeric",
             "physical_type": "BIGINT", "distinct_count": 681,
             "unique_pct": 0.76, "flags": []},
            {"name": "fare", "inferred_type": "numeric",
             "physical_type": "DOUBLE", "distinct_count": 248,
             "unique_pct": 0.28, "flags": []},
            {"name": "sex", "inferred_type": "categorical",
             "physical_type": "VARCHAR", "distinct_count": 2,
             "unique_pct": 0.002, "flags": []},
        ],
    }


def _make_relational_db(path: str) -> None:
    """Create a small DuckDB with customers(id) <- orders(customer_id), real FK."""
    con = duckdb.connect(path)
    con.execute("CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT)")
    con.execute(
        "CREATE TABLE orders(id INTEGER PRIMARY KEY, "
        "customer_id INTEGER REFERENCES customers(id), amount DOUBLE)")
    con.execute("INSERT INTO customers VALUES "
                "(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')")
    con.execute("INSERT INTO orders VALUES "
                "(1,1,10.0),(2,1,20.0),(3,2,30.0),(4,3,40.0),"
                "(5,3,50.0),(6,4,60.0),(7,5,70.0),(8,2,80.0)")
    con.close()


def _orders_profile() -> dict:
    """A profile for the `orders` table of the relational DB."""
    return {
        "table": "orders",
        "source": "orders",
        "n_rows": 8,
        "n_cols": 3,
        "key_candidates": ["id"],
        "columns": [
            {"name": "id", "inferred_type": "numeric", "physical_type": "INTEGER",
             "distinct_count": 8, "unique_pct": 1.0, "flags": ["possible_id"]},
            {"name": "customer_id", "inferred_type": "numeric",
             "physical_type": "INTEGER", "distinct_count": 5, "unique_pct": 0.625,
             "flags": []},
            {"name": "amount", "inferred_type": "numeric", "physical_type": "DOUBLE",
             "distinct_count": 8, "unique_pct": 1.0, "flags": []},
        ],
    }


# --------------------------------------------------------------------------- #
# Golden 1 — intra-table.
# --------------------------------------------------------------------------- #
def test_golden_intra_table_pk_and_fk_heuristic():
    """Single table: PK candidate shown; FK heuristic shown (if fn available);
    renders to PDF + PPTX with nothing cut."""
    prof = _titanic_profile()
    glossary = GlossaryCollector()
    # No db_path: only the profile-derived layers apply (no declared, no inter).
    chapter = build_relaciones(prof, {"glossary": glossary})

    assert isinstance(chapter, Chapter)
    assert chapter.id == "relaciones"
    text = _text_of(chapter)

    # PK candidate is always present (comes from the profile).
    assert "Candidatos a clave primaria" in text
    assert "PassengerId" in text

    # Glossary terms got registered.
    for key in ("pk", "fk", "cardinalidad"):
        assert glossary.has(key)

    # FK heuristic layer: present iff the delegated function is importable.
    if suggest_intratable_fk_candidates is not None:
        assert "Posibles claves foráneas" in text
        assert "ticket_id" in text
        # The float measure and the PK itself are NOT suggested as FKs.
        assert "Posibles FK por nombre" in text

    pdf_text, n_slides = _render_both(chapter, "intra")
    assert "PassengerId" in pdf_text
    assert n_slides >= 1


# --------------------------------------------------------------------------- #
# Golden 2 — inter-table (real DuckDB).
# --------------------------------------------------------------------------- #
def test_golden_inter_table_containment_and_join_graph():
    """Two related tables: declared FK (if fn available) + containment FK
    candidate + Mermaid join graph."""
    tmp = tempfile.mkdtemp(prefix="relaciones_db_")
    db_path = os.path.join(tmp, "shop.duckdb")
    _make_relational_db(db_path)

    prof = _orders_profile()
    glossary = GlossaryCollector()
    chapter = build_relaciones(
        prof, {"db_path": db_path, "table": "orders", "glossary": glossary})

    assert isinstance(chapter, Chapter)
    text = _text_of(chapter)

    # Inter-table containment FK candidate: customer_id -> customers.id. This path
    # uses infer_fk_containment_duckdb + build_join_graph, both already in the
    # registry, so it must be present.
    assert "Claves foráneas candidatas (inter-tabla)" in text
    assert "orders.customer_id" in text
    assert "customers.id" in text
    # Join graph with a pasteable Mermaid diagram.
    assert "Grafo de relaciones" in text
    assert "mermaid" in text
    assert "graph LR" in text
    assert "containment" in text.lower()

    # Declared-keys layer: present iff the delegated function is importable.
    if detect_declared_keys_duckdb is not None:
        assert "Claves declaradas en el esquema" in text
        assert "Claves foráneas declaradas" in text

    pdf_text, n_slides = _render_both(chapter, "inter")
    assert "customer_id" in pdf_text
    assert n_slides >= 1


# --------------------------------------------------------------------------- #
# Edges.
# --------------------------------------------------------------------------- #
def test_none_when_no_relations():
    """No key candidates, no FK-looking columns, no db source -> None."""
    prof = {
        "table": "flat", "n_rows": 100, "n_cols": 2, "key_candidates": [],
        "columns": [
            {"name": "value", "inferred_type": "numeric", "physical_type": "DOUBLE",
             "distinct_count": 50, "unique_pct": 0.5, "flags": []},
            {"name": "label", "inferred_type": "categorical",
             "physical_type": "VARCHAR", "distinct_count": 3, "unique_pct": 0.03,
             "flags": []},
        ],
    }
    assert build_relaciones(prof, {}) is None


def test_empty_and_none_profile_do_not_raise():
    """None / {} profile and missing ctx degrade to None without raising."""
    assert build_relaciones(None, None) is None
    assert build_relaciones({}, {}) is None
    assert build_relaciones({}, {"glossary": GlossaryCollector()}) is None


def test_pk_candidate_only_builds_chapter():
    """A profile with only a key candidate (no FK anything, no db) still builds:
    the relations chapter applies because there is a PK candidate to report."""
    prof = {
        "table": "t", "n_rows": 10, "n_cols": 1, "key_candidates": ["row_id"],
        "columns": [
            {"name": "row_id", "inferred_type": "numeric", "physical_type": "BIGINT",
             "distinct_count": 10, "unique_pct": 1.0, "flags": ["possible_id"]},
        ],
    }
    chapter = build_relaciones(prof, {})
    assert isinstance(chapter, Chapter)
    assert "Candidatos a clave primaria" in _text_of(chapter)
