"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21/22) + REQ-273..275 (v1.23):
S&P theme + slide render pipeline + deck-refinement tests.

v1.20 validates:
  - The mermaid theme JSON contains the S&P colors
  - Every .mmd has a corresponding .png
  - The render_slides.sh script exists and is executable
  - The CI workflow file exists

v1.21/22 adds (REQ-245,251,252):
  - Deck renamed to nova-autonomous-cloud-delivery*
  - No maturity badges in the Marp deck
  - No version in the Marp footer/title slide
  - 20 main + 1 appendix slides
  - No D-###/REQ-###/internal .py paths in audience-facing slides

v1.23 (REQ-273,274,275) — single-document + dual-PPTX + image-inlining pipeline:
  - The plain `.md` is gone; `*-marp.md` is the sole source of truth.
  - Marp deck uses `theme: default` + an inline `style:` block (S&P colors).
  - `nova-sp-theme.css` is RETAINED AS REFERENCE (not loaded at render).
  - HTML has base64-inlined images (zero `src="assets/` references).
  - A second PPTX (`*-python.pptx`) is produced by `scripts/render_pptx.py`.
  - Speaker notes live as `<!-- Speaker notes: ... -->` HTML comments.
  - Benefit callouts use `<div class="benefit">` (no `**Benefit:**` prefixes).
  - The purged term "penetrate" is absent repo-wide.
"""
import re
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
PRESENTATIONS = ROOT / "docs" / "presentations"
ASSETS = PRESENTATIONS / "assets"
THEME_CSS = ASSETS / "nova-sp-theme.css"
THEME_JSON = ASSETS / "mmd" / "sp-theme.json"
MARP_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
SOURCE_MD = PRESENTATIONS / "nova-autonomous-cloud-delivery.md"
HTML = PRESENTATIONS / "nova-autonomous-cloud-delivery.html"
PYTHON_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery-python.pptx"
MARP_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery.pptx"
RENDER_SCRIPT = ROOT / "scripts" / "render_slides.sh"
SLIDES_WORKFLOW = ROOT / ".github" / "workflows" / "slides.yml"


def _frontmatter(text: str) -> str:
    """Return the Marp frontmatter block (between the first two `---`)."""
    fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
    assert fm_match, "Marp frontmatter not found"
    return fm_match.group(1)


# --- S&P theme reference + mermaid theme -------------------------------

def test_sp_theme_css_exists():
    """REQ-239: nova-sp-theme.css exists (retained as a reference)."""
    assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}"


def test_nova_sp_theme_css_retained_as_reference():
    """REQ-274: nova-sp-theme.css is retained as a REFERENCE only and is
    explicitly NOT loaded at render time (the live styling is the inline
    `style:` block in the -marp.md frontmatter)."""
    assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}"
    css = THEME_CSS.read_text()
    assert "not loaded at render" in css.lower(), \
        "nova-sp-theme.css does not document itself as 'not loaded at render'"


def test_sp_theme_css_has_snp_colors():
    """REQ-239: the reference CSS still carries S&P Red and Black."""
    css = THEME_CSS.read_text()
    assert "#D6002A" in css, "S&P Red (#D6002A) missing from theme CSS"
    assert "#1B1B1B" in css, "S&P Black (#1B1B1B) missing from theme CSS"


def test_sp_theme_json_has_snp_colors():
    """The mermaid theme JSON has S&P colors (mermaid PNGs are S&P-themed)."""
    json_text = THEME_JSON.read_text()
    assert "#D6002A" in json_text, "S&P Red missing from mermaid theme"
    assert "#1B1B1B" in json_text, "S&P Black missing from mermaid theme"


# --- Marp deck: theme + inline style ----------------------------------

def test_marp_deck_uses_default_theme():
    """REQ-274: the Marp deck frontmatter uses `theme: default` (not the
    retired `theme: nova-sp`). S&P styling is delivered by the inline
    `style:` block, not the standalone CSS."""
    frontmatter = _frontmatter(MARP_DECK.read_text())
    assert re.search(r"^theme:\s*default\s*$", frontmatter, re.MULTILINE), \
        "Marp deck does not set `theme: default` in the frontmatter"
    assert "nova-sp" not in frontmatter, \
        "Marp deck still references the retired `nova-sp` theme"


def test_marp_deck_has_sp_inline_style():
    """REQ-274: the inline `style:` block carries the S&P properties
    (#D6002A, #1B1B1B, and the `section.title` rule)."""
    frontmatter = _frontmatter(MARP_DECK.read_text())
    assert "style:" in frontmatter, "frontmatter has no inline `style:` block"
    # The inline style block extends past the frontmatter close in Marp
    # (the `style:` value is a multi-line YAML literal). Read the whole
    # deck so we capture the full style block.
    deck = MARP_DECK.read_text()
    assert "#D6002A" in deck, "inline style: block missing #D6002A"
    assert "#1B1B1B" in deck, "inline style: block missing #1B1B1B"
    assert "section.title" in deck, \
        "inline style: block missing the `section.title` rule"


def test_marp_deck_no_badges():
    """REQ-252: no maturity badges in the Marp deck."""
    text = MARP_DECK.read_text()
    assert "badge" not in text, "Marp deck still contains badge spans"


def test_marp_deck_no_version_in_footer():
    """REQ-251: no version (v1.x) in the Marp frontmatter footer/header."""
    frontmatter = _frontmatter(MARP_DECK.read_text())
    assert not re.search(r"v1\.\d+", frontmatter), \
        f"Marp frontmatter still contains a version: {frontmatter}"
    assert "Act %" not in frontmatter, \
        "Marp frontmatter still contains 'Act %{page}' artifact"


def test_marp_deck_title_slide_no_version_subtitle():
    """REQ-251: the title slide does not carry a version subtitle."""
    text = MARP_DECK.read_text()
    after_fm = text.split("---\n", 2)[2] if text.startswith("---") else text
    first_slide = after_fm.split("\n---\n")[0]
    assert "v1.18" not in first_slide, \
        "Title slide still contains 'v1.18' subtitle"
    assert "Citizen Developer & Production-Grade Guidance" not in first_slide, \
        "Title slide still contains the old version subtitle"


def test_marp_deck_title_is_autonomous_cloud_delivery():
    """REQ-245: the deck title is 'Nova — The Autonomous Cloud Delivery Platform'."""
    text = MARP_DECK.read_text()
    assert "Autonomous Cloud Delivery Platform" in text, \
        "Deck title is not 'Autonomous Cloud Delivery Platform'"
    assert "No-Humans Infrastructure Platform" not in text, \
        "Deck still carries the old 'No-Humans Infrastructure Platform' title"


def test_marp_deck_slide_count():
    """REQ-245/261: 20 main slides + 1 appendix = 21 slide sections
    (22 rendered sections incl. the H1 title slide)."""
    text = MARP_DECK.read_text()
    main_slides = re.findall(r"^## Slide ", text, re.MULTILINE)
    appendix_slides = re.findall(r"^## Appendix ", text, re.MULTILINE)
    assert len(main_slides) == 20, \
        f"expected 20 main slides, found {len(main_slides)}"
    assert len(appendix_slides) == 1, \
        f"expected 1 appendix slide, found {len(appendix_slides)}"


def test_marp_deck_no_internal_citations():
    """REQ-252: no D-### decision IDs, REQ-### requirement IDs, or internal
    .py file paths in the audience-facing Marp deck SLIDE BODIES. Internal
    provenance is allowed inside `<!-- ... -->` HTML comments (speaker
    notes / talking points), which Marp excludes from the rendered slide."""
    text = MARP_DECK.read_text()
    # Strip HTML comments (speaker notes + talking points) before checking.
    body = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
    assert not re.search(r"\bD-\d{3}\b", body), \
        "Marp deck slide body contains D-### decision IDs"
    assert not re.search(r"\bREQ-\d{3}\b", body), \
        "Marp deck slide body contains REQ-### requirement IDs"
    assert not re.search(r"\b(outbox_writer|confidence_signal|hitl_gates|"
                          r"attestation_matrix|checkov_adapter|infracost_adapter|"
                          r"contract_resolver|run_platform)\.py\b", body), \
        "Marp deck slide body contains internal .py file paths"


# --- Speaker notes + benefit callouts (REQ-274) ----------------------

def test_speaker_notes_as_html_comments():
    """REQ-274: speaker notes are embedded as `<!-- Speaker notes: ... -->`
    HTML comments (Marp excludes HTML comments from the rendered slide;
    the comments are for authors/presenters). Expect >= 20 (one per main
    slide) + the appendix slide."""
    text = MARP_DECK.read_text()
    count = len(re.findall(r"<!-- Speaker notes:", text))
    assert count >= 20, \
        f"expected >=20 `<!-- Speaker notes:` comments, found {count}"


def test_benefit_callouts_use_class():
    """REQ-274: benefit callouts use `<div class="benefit">` (>= 21
    occurrences — one per slide section incl. the title slide) and zero
    `**Benefit:**` text prefixes."""
    text = MARP_DECK.read_text()
    class_count = text.count('class="benefit"')
    assert class_count >= 21, \
        f"expected >=21 `class=\"benefit\"` callouts, found {class_count}"
    assert "**Benefit:**" not in text, \
        "Marp deck still uses the retired `**Benefit:**` prefix"


# --- Single source of truth (REQ-274) --------------------------------

def test_single_source_of_truth():
    """REQ-274: the plain `nova-autonomous-cloud-delivery.md` is deleted;
    `nova-autonomous-cloud-delivery-marp.md` is the sole source of truth."""
    assert not SOURCE_MD.exists(), \
        f"plain source markdown still exists (should be deleted): {SOURCE_MD}"
    assert MARP_DECK.is_file(), \
        f"Marp deck (sole source of truth) not found: {MARP_DECK}"


# --- Purged term (REQ-274) -------------------------------------------

def test_no_purged_loaded_term():
    """REQ-274: the purged term 'penetrate' (case-insensitive, any
    inflection: penetrate, penetrating, penetration, ...) is absent
    from docs/, .ciagent/PROJECT.md, and .ciagent/CLARIFY.md."""
    targets = [
        ROOT / "docs",
        ROOT / ".ciagent" / "PROJECT.md",
        ROOT / ".ciagent" / "CLARIFY.md",
    ]
    hits = []
    for target in targets:
        if target.is_dir():
            for path in target.rglob("*"):
                if not path.is_file():
                    continue
                if path.suffix in {".png", ".pptx", ".html", ".zip", ".json"}:
                    continue
                try:
                    if "penetrat" in path.read_text().lower():
                        hits.append(str(path))
                except (UnicodeDecodeError, OSError):
                    continue
        elif target.is_file():
            try:
                if "penetrat" in target.read_text().lower():
                    hits.append(str(target))
            except (UnicodeDecodeError, OSError):
                hits.append(f"<unreadable {target}>")
    assert not hits, \
        f"purged term 'penetrate' still present in: {hits}"


# --- Render script ---------------------------------------------------

def test_render_slides_script_exists():
    """REQ-240: render_slides.sh exists and is executable."""
    assert RENDER_SCRIPT.is_file(), "render_slides.sh not found"
    import os
    assert os.access(RENDER_SCRIPT, os.X_OK), "render_slides.sh not executable"


def test_render_slides_script_renders_mermaid():
    """REQ-240: render_slides.sh renders mermaid diagrams."""
    text = RENDER_SCRIPT.read_text()
    assert "mermaid-cli" in text or "mmdc" in text, \
        "render_slides.sh does not invoke mermaid-cli"
    assert "sp-theme.json" in text, \
        "render_slides.sh does not reference sp-theme.json"


def test_render_slides_script_renders_marp():
    """REQ-240: render_slides.sh renders Marp HTML + PPTX."""
    text = RENDER_SCRIPT.read_text()
    assert "marp-cli" in text, "render_slides.sh does not invoke marp-cli"
    assert ".html" in text, "render_slides.sh does not produce HTML"
    assert ".pptx" in text, "render_slides.sh does not produce PPTX"


def test_render_slides_default_deck_renamed():
    """REQ-245: render_slides.sh default deck is nova-autonomous-cloud-delivery."""
    text = RENDER_SCRIPT.read_text()
    assert "nova-autonomous-cloud-delivery" in text, \
        "render_slides.sh does not default to nova-autonomous-cloud-delivery"


def test_render_slides_has_2x_scale():
    """REQ-258: render_slides.sh uses -s 2 (2x scale) and -b transparent."""
    text = RENDER_SCRIPT.read_text()
    assert "-s 2" in text, "render_slides.sh does not use -s 2 (2x scale)"
    assert "-b transparent" in text, \
        "render_slides.sh does not use -b transparent"


def test_render_slides_pins_cli_versions():
    """REQ-257: render_slides.sh pins marp-cli and mermaid-cli versions
    (no @latest). REQ-273: pyproject.toml declares python-pptx in the
    `slides` optional-dependency group."""
    text = RENDER_SCRIPT.read_text()
    assert "marp-cli@" in text, "render_slides.sh does not pin marp-cli"
    assert "mermaid-cli@" in text, \
        "render_slides.sh does not pin mermaid-cli"
    assert "@latest" not in text, \
        "render_slides.sh still uses @latest (not pinned)"
    pyproject = (ROOT / "pyproject.toml").read_text()
    assert "python-pptx" in pyproject, \
        "pyproject.toml does not declare python-pptx"
    # python-pptx is in the [project.optional-dependencies] `slides` group.
    # Locate the optional-dependencies table block, then check the `slides`
    # array within it.
    block_match = re.search(
        r"\[project\.optional-dependencies\](.*?)(?=\n\[|\Z)",
        pyproject, re.DOTALL)
    assert block_match, \
        "pyproject.toml has no [project.optional-dependencies] table"
    block = block_match.group(1)
    slides_match = re.search(r"slides\s*=\s*\[([^\]]*)\]", block, re.DOTALL)
    assert slides_match, \
        "pyproject.toml has no `slides` optional-dependency group"
    assert "python-pptx" in slides_match.group(1), \
        "python-pptx is not in the `slides` optional-dependency group"


def test_render_deck_removed():
    """REQ-257: render_deck.sh has been deleted (produced unthemed output)."""
    old_script = ROOT / "scripts" / "render_deck.sh"
    assert not old_script.exists(), \
        "render_deck.sh still exists (should be deleted — produced unthemed output)"


# --- CI workflow (REQ-273) -------------------------------------------

def test_slides_ci_workflow_exists():
    """REQ-241: CI workflow for slides exists."""
    assert SLIDES_WORKFLOW.is_file(), "slides.yml workflow not found"


def test_slides_ci_workflow_triggers_on_presentations():
    """REQ-241: CI workflow triggers on docs/presentations/ changes."""
    text = SLIDES_WORKFLOW.read_text()
    assert "docs/presentations" in text, \
        "slides.yml does not trigger on docs/presentations/"
    assert "render_slides.sh" in text, \
        "slides.yml does not invoke render_slides.sh"


def test_slides_ci_workflow_installs_python_pptx():
    """REQ-273: CI workflow installs python-pptx (via the `slides` extra)."""
    text = SLIDES_WORKFLOW.read_text()
    assert "python-pptx" in text or "[slides]" in text, \
        "slides.yml does not install python-pptx / the slides extra"
    assert "setup-python" in text, \
        "slides.yml has no setup-python step"


def test_slides_ci_workflow_pins_cli_versions():
    """REQ-273: CI workflow pins marp-cli + mermaid-cli (no @latest)."""
    text = SLIDES_WORKFLOW.read_text()
    assert "marp-cli@4.5.0" in text, \
        "slides.yml does not pin @marp-team/marp-cli@4.5.0"
    assert "mermaid-cli@11.16.0" in text, \
        "slides.yml does not pin @mermaid-js/mermaid-cli@11.16.0"
    assert "@latest" not in text, \
        "slides.yml still uses @latest (not pinned)"


def test_slides_ci_workflow_stages_python_pptx():
    """REQ-273: CI workflow `git add` list includes *-python.pptx."""
    text = SLIDES_WORKFLOW.read_text()
    assert "*-python.pptx" in text, \
        "slides.yml git-add list does not stage *-python.pptx"


# --- Mermaid PNGs ----------------------------------------------------

def test_every_mmd_has_png():
    """REQ-240: every .mmd file has a corresponding .png."""
    mmd_dir = ASSETS / "mmd"
    png_dir = ASSETS / "png"
    if not mmd_dir.is_dir():
        pytest.skip("no .mmd directory")
    mmd_files = sorted(mmd_dir.glob("*.mmd"))
    assert len(mmd_files) > 0, "no .mmd files found"
    missing = []
    for mmd in mmd_files:
        png = png_dir / f"{mmd.stem}.png"
        if not png.is_file():
            missing.append(mmd.name)
    assert not missing, f"PNGs missing for: {missing}"


def test_png_aspect_ratios_sane():
    """REQ-259/260: PNGs referenced in the marp deck have aspect ratios
    in [0.4, 4.0] (suitable for 16:9 slides)."""
    import struct
    deck_text = MARP_DECK.read_text()
    referenced = re.findall(r'!\[[^\]]*\]\(assets/png/([^)]+\.png)\)', deck_text)
    assert referenced, "no PNGs referenced in the marp deck"
    for png_name in referenced:
        png_path = ASSETS / "png" / png_name
        assert png_path.is_file(), f"referenced PNG not found: {png_name}"
        with open(png_path, "rb") as fh:
            data = fh.read(24)
        assert data[:8] == b"\x89PNG\r\n\x1a\n", f"{png_name} is not a PNG"
        w = struct.unpack(">I", data[16:20])[0]
        h = struct.unpack(">I", data[20:24])[0]
        ar = w / h
        assert 0.4 <= ar <= 4.0, \
            f"{png_name} aspect ratio {ar:.2f} outside [0.4, 4.0] ({w}x{h})"


# --- HTML: theme embed + image inlining + slide count ----------------

def test_html_embeds_theme():
    """REQ-262/274: the committed HTML embeds the S&P theme as literal
    S&P colors (#D6002A — not just the --sp-red variable) + padding."""
    html = HTML.read_text()
    assert "#D6002A" in html, \
        "committed HTML does not embed the literal S&P Red (#D6002A)"
    assert "padding:" in html, "committed HTML does not embed padding rule"


def test_html_images_inlined_as_base64():
    """REQ-268/274: the rendered HTML is self-contained — zero
    `src="assets/` references and at least one `data:image` per image
    referenced in the -marp.md deck."""
    html = HTML.read_text()
    assert len(re.findall(r'src=["\']assets/', html)) == 0, \
        "HTML still references external `assets/` images (not inlined)"
    deck_text = MARP_DECK.read_text()
    image_count = len(re.findall(r'!\[[^\]]*\]\(assets/', deck_text))
    assert image_count > 0, "no images referenced in the marp deck"
    data_uri_count = html.count("data:image")
    assert data_uri_count >= image_count, \
        f"HTML has {data_uri_count} data:image URIs but the deck " \
        f"references {image_count} images (should be >=)"


def test_html_slide_count_matches_marp():
    """REQ-262: the committed HTML <section> count matches the marp deck
    slide count (title + 20 main + 1 appendix = 22)."""
    html = HTML.read_text()
    section_count = html.count("<section ")
    deck_text = MARP_DECK.read_text()
    main_slides = len(re.findall(r"^## Slide ", deck_text, re.MULTILINE))
    appendix_slides = len(re.findall(r"^## Appendix ", deck_text, re.MULTILINE))
    expected = main_slides + appendix_slides + 1
    assert section_count == expected, \
        f"HTML has {section_count} sections, expected {expected} " \
        f"({main_slides} main + {appendix_slides} appendix + 1 title)"


# --- python-pptx artifact (REQ-273/274) ------------------------------

def test_python_pptx_exists():
    """REQ-273/274: the python-pptx PPTX exists and is a valid OOXML zip
    (the PPTX/zip signature `PK\x03\x04`)."""
    assert PYTHON_PPTX.is_file(), \
        f"python-pptx PPTX not found: {PYTHON_PPTX}"
    with open(PYTHON_PPTX, "rb") as fh:
        sig = fh.read(4)
    assert sig == b"PK\x03\x04", \
        f"python-pptx PPTX is not a valid zip (bad signature: {sig!r})"


# --- README (REQ-275) ------------------------------------------------

def test_readme_no_retired_decks():
    """REQ-243: presentations README does not list retired decks."""
    readme = (PRESENTATIONS / "README.md").read_text()
    assert "how-the-platform-works" not in readme, \
        "README still references retired 'how-the-platform-works' deck"
    assert "the-developer-experience" not in readme, \
        "README still references retired 'the-developer-experience' deck"


def test_readme_no_old_deck_name():
    """REQ-245: README references the new deck name, not the old one."""
    readme = (PRESENTATIONS / "README.md").read_text()
    assert "nova-autonomous-cloud-delivery" in readme, \
        "README does not reference nova-autonomous-cloud-delivery"


def test_old_deck_files_removed():
    """REQ-245: the old nova-no-humans-platform* files are gone."""
    old_files = sorted(PRESENTATIONS.glob("nova-no-humans-platform*"))
    assert not old_files, f"old deck files still present: {old_files}"