"""Golden tests for the global render-quality features (issue: eda-render-quality).

Covers, with executable evidence:
  * High DPI: every embedded figure is rasterized at 220 dpi, so a phone reader
    can zoom in and still see crisp detail.
  * Wide table → image: a table too wide to be legible as text (e.g. a 19-column
    df.head) is rendered as one high-res image that scales to fit entirely, while
    a narrow table keeps its selectable-text/native-table rendering.
  * ``Group(layout="side_by_side")``: in PPTX the table and figure are placed in
    two columns of the same slide; in PDF the same group stacks vertically.
  * Backward compatibility: a Group without ``layout`` defaults to ``"stack"`` and
    a fitting table renders exactly as before.

Renderers are invoked for real; PDFs are inspected with PyMuPDF and PPTX decks
with python-pptx.
"""

from __future__ import annotations

import os
import tempfile

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402

import pytest  # noqa: E402

from datascience.automatic_eda import model  # noqa: E402
from datascience.automatic_eda.render_pdf_impl import (  # noqa: E402
    render_pdf, _RASTER_DPI as _PDF_DPI, _table_fits_as_text as _pdf_fits)
from datascience.automatic_eda.render_pptx_impl import (  # noqa: E402
    render_pptx, _RASTER_DPI as _PPTX_DPI, _table_fits_as_text as _pptx_fits)


# --------------------------------------------------------------------------- #
# Helpers.
# --------------------------------------------------------------------------- #
def _simple_fig():
    """A small, real matplotlib figure for the figure blocks."""
    fig, ax = plt.subplots(figsize=(4, 3))
    ax.plot([0, 1, 2, 3], [1, 3, 2, 4])
    ax.set_title("demo")
    return fig


def _wide_table(n_cols=19, n_rows=5):
    header = [f"columna_{i}" for i in range(n_cols)]
    rows = [[f"v{r}_{c}" for c in range(n_cols)] for r in range(n_rows)]
    return model.DataTable(header=header, rows=rows, title="Primeras filas")


def _narrow_table():
    return model.DataTable(header=["a", "b", "c"],
                           rows=[["1", "2", "3"], ["4", "5", "6"]],
                           title="Tabla estrecha")


def _chapter(blocks, cid="cap", title="Capítulo"):
    return [model.Chapter(id=cid, title=title, version="1.0.0", blocks=blocks)]


# --------------------------------------------------------------------------- #
# 1) High DPI — the unit constant and a real embedded image.
# --------------------------------------------------------------------------- #
def test_raster_dpi_is_high_both_renderers():
    assert _PDF_DPI >= 200, "el DPI del PDF debe ser alto (>=200)"
    assert _PPTX_DPI >= 200, "el DPI del PPTX debe ser alto (>=200)"


def test_pdf_embedded_figure_is_high_resolution(tmp_path):
    fitz = pytest.importorskip("fitz")
    out = str(tmp_path / "fig.pdf")
    res = render_pdf(_chapter([model.Figure(make=_simple_fig, caption="demo")]),
                     out, {"title": "T"})
    assert res["path"] == out
    doc = fitz.open(out)
    try:
        widths = []
        for page in doc:
            for img in page.get_images(full=True):
                xref = img[0]
                info = doc.extract_image(xref)
                widths.append(info.get("width", 0))
        assert widths, "no se incrustó ninguna imagen en el PDF"
        # A ~4" figure rasterized at 220 dpi is ~ >850 px wide. At the old 150 dpi
        # it would be ~600 px. The high-res threshold proves the DPI bump.
        assert max(widths) >= 800, \
            f"la figura embebida no es de alta resolución: {max(widths)} px"
    finally:
        doc.close()


# --------------------------------------------------------------------------- #
# 2) Wide table → image (PDF and PPTX); narrow table stays text.
# --------------------------------------------------------------------------- #
def test_fit_criterion_flags_wide_and_keeps_narrow():
    wide = _wide_table()
    narrow = _narrow_table()
    assert not _pdf_fits(wide.header, wide.rows), \
        "una tabla de 19 columnas debería NO caber como texto en A5"
    assert not _pptx_fits(wide.header, wide.rows), \
        "una tabla de 19 columnas debería NO caber como tabla nativa en 16:9"
    assert _pdf_fits(narrow.header, narrow.rows), \
        "una tabla de 3 columnas debería caber como texto en A5"
    assert _pptx_fits(narrow.header, narrow.rows), \
        "una tabla de 3 columnas debería caber como tabla nativa en 16:9"


def test_wide_table_rendered_as_image_pdf(tmp_path):
    fitz = pytest.importorskip("fitz")
    out = str(tmp_path / "wide.pdf")
    res = render_pdf(_chapter([_wide_table()]), out, {"title": "T"})
    assert res["path"] == out
    doc = fitz.open(out)
    try:
        n_images = sum(len(page.get_images(full=True)) for page in doc)
        text = "".join(page.get_text() for page in doc)
    finally:
        doc.close()
    assert n_images >= 1, "la tabla ancha no se rasterizó como imagen en el PDF"
    # The cells are now inside the image, not selectable text. A unique cell value
    # must therefore NOT appear as extractable text (it lives in the picture).
    assert "v4_18" not in text, \
        "la tabla ancha sigue como texto seleccionable (no se hizo imagen)"


def test_narrow_table_stays_selectable_text_pdf(tmp_path):
    fitz = pytest.importorskip("fitz")
    out = str(tmp_path / "narrow.pdf")
    render_pdf(_chapter([_narrow_table()]), out, {"title": "T"})
    doc = fitz.open(out)
    try:
        text = "".join(page.get_text() for page in doc)
    finally:
        doc.close()
    # Narrow table is selectable text: its header/cells are extractable.
    for v in ("a", "b", "c", "1", "6"):
        assert v in text, f"la celda '{v}' debería ser texto seleccionable"


def test_wide_table_rendered_as_picture_pptx(tmp_path):
    pptx = pytest.importorskip("pptx")
    from pptx.enum.shapes import MSO_SHAPE_TYPE
    out = str(tmp_path / "wide.pptx")
    res = render_pptx(_chapter([_wide_table()]), out, {"title": "T"})
    assert res["path"] == out
    prs = pptx.Presentation(out)
    pics = sum(1 for s in prs.slides for sh in s.shapes
               if sh.shape_type == MSO_SHAPE_TYPE.PICTURE)
    assert pics >= 1, "la tabla ancha no se colocó como imagen en el PPTX"


# --------------------------------------------------------------------------- #
# 3) Group(layout="side_by_side"): two columns in PPTX, stacked in PDF.
# --------------------------------------------------------------------------- #
def _side_by_side_group():
    return model.Group(
        blocks=[model.Heading(text="Columna X", level=2),
                _narrow_table(),
                model.Figure(make=_simple_fig, caption="grafico")],
        layout="side_by_side")


def test_side_by_side_places_two_columns_pptx(tmp_path):
    pptx = pytest.importorskip("pptx")
    from pptx.enum.shapes import MSO_SHAPE_TYPE
    from pptx.util import Inches
    out = str(tmp_path / "sbs.pptx")
    render_pptx(_chapter([_side_by_side_group()]), out, {"title": "T"})
    prs = pptx.Presentation(out)
    # Find the slide that holds the pair (table image + figure image).
    centre_emu = int(Inches(13.333 / 2.0))
    placed = False
    for s in prs.slides:
        lefts = [sh.left for sh in s.shapes
                 if sh.shape_type == MSO_SHAPE_TYPE.PICTURE
                 and sh.left is not None]
        if len(lefts) >= 2:
            # one picture starts in the left half, another in the right half.
            if min(lefts) < centre_emu and max(lefts) > centre_emu:
                placed = True
                break
    assert placed, \
        "side_by_side no colocó tabla y figura en dos columnas de la misma slide"


def test_side_by_side_stacks_in_pdf(tmp_path):
    fitz = pytest.importorskip("fitz")
    out = str(tmp_path / "sbs.pdf")
    res = render_pdf(_chapter([_side_by_side_group()]), out, {"title": "T"})
    assert res["path"] == out and res["n_pages"] >= 1
    doc = fitz.open(out)
    try:
        n_images = sum(len(page.get_images(full=True)) for page in doc)
        text = "".join(page.get_text() for page in doc)
    finally:
        doc.close()
    # PDF stacks: the narrow table stays selectable text (1 of its cells is
    # extractable) and the figure is the single embedded image — not a 2-column
    # pair of pictures like PPTX.
    assert n_images == 1, "el PDF no debería usar el layout de dos imágenes"
    assert "Columna X" in text and "1" in text, \
        "la tabla del grupo debería seguir como texto apilado en el PDF"


# --------------------------------------------------------------------------- #
# 4) Backward compatibility — default layout stacks, fitting table unchanged.
# --------------------------------------------------------------------------- #
def test_group_default_layout_is_stack():
    g = model.Group(blocks=[_narrow_table()])
    assert g.layout == "stack", "el layout por defecto debe ser 'stack'"


# --------------------------------------------------------------------------- #
# 5) Clickable cover index ("Índice") → chapter first page/slide.
# --------------------------------------------------------------------------- #
def _doc_with_index():
    portada = model.Chapter(id="portada", title="Portada", version="1.0.0",
                            blocks=[model.Heading(text="Índice", level=2),
                                    model.TocEntry(label="Distribuciones",
                                                   target_id="Distribuciones")])
    cap = model.Chapter(id="num", title="Distribuciones", version="1.0.0",
                        blocks=[model.Markdown(text="contenido del capítulo")])
    return [portada, cap]


def test_cover_index_is_clickable_pdf(tmp_path):
    fitz = pytest.importorskip("fitz")
    out = str(tmp_path / "idx.pdf")
    res = render_pdf(_doc_with_index(), out, {"title": "T"})
    assert res["path"] == out
    doc = fitz.open(out)
    try:
        # The cover (page 0) must carry a GOTO link jumping to a later page.
        goto = [lk for lk in doc[0].get_links()
                if lk.get("kind") == fitz.LINK_GOTO and lk.get("page", 0) > 0]
    finally:
        doc.close()
    assert goto, "el índice de la portada no produjo enlaces clicables en el PDF"


def test_cover_index_shows_heading_pdf(tmp_path):
    fitz = pytest.importorskip("fitz")
    out = str(tmp_path / "idxh.pdf")
    render_pdf(_doc_with_index(), out, {"title": "T"})
    doc = fitz.open(out)
    try:
        text = "".join(page.get_text() for page in doc)
    finally:
        doc.close()
    assert "Índice" in text, "la portada no muestra el encabezado 'Índice'"
    assert "Este informe incluye" not in text, \
        "la portada aún muestra el texto antiguo 'Este informe incluye'"


def test_cover_index_is_clickable_pptx(tmp_path):
    pptx = pytest.importorskip("pptx")
    out = str(tmp_path / "idx.pptx")
    render_pptx(_doc_with_index(), out, {"title": "T"})
    prs = pptx.Presentation(out)
    cover_xml = prs.slides[0]._element.xml
    assert "hlinksldjump" in cover_xml, \
        "el índice de la portada no produjo un salto de slide nativo en el PPTX"


def test_default_group_renders_like_before_pptx(tmp_path):
    pptx = pytest.importorskip("pptx")
    from pptx.enum.shapes import MSO_SHAPE_TYPE
    out = str(tmp_path / "stack.pptx")
    grp = model.Group(blocks=[model.Heading(text="Y", level=2),
                              _narrow_table(),
                              model.Figure(make=_simple_fig, caption="g")])
    render_pptx(_chapter([grp]), out, {"title": "T"})
    prs = pptx.Presentation(out)
    # Stacked group: the narrow table is a NATIVE table (selectable), and there is
    # exactly one picture (the figure) — not the two-image side-by-side layout.
    n_tables = sum(1 for s in prs.slides for sh in s.shapes if sh.has_table)
    n_pics = sum(1 for s in prs.slides for sh in s.shapes
                 if sh.shape_type == MSO_SHAPE_TYPE.PICTURE)
    assert n_tables >= 1, "el grupo apilado debería usar una tabla nativa"
    assert n_pics == 1, "el grupo apilado no debería duplicar imágenes"
