"""Tests for scripts/remediate_pptx.py (Engine E1 PPTX in-place remediation).

Same shape as test_remediate_docx.py: CI-light structural/validation/AC12
tests via zip/lxml, heavy (remediation_tools marker) tests for the real
LibreOffice appearance check and --export-pdf + veraPDF gate.
"""

import json
import os
import sys
import zipfile

import pytest
from lxml import etree

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "fixtures"))

import gen_office_fixtures as gf
import lo_export
import remediate_pptx as rp
import setup_remediation

REMEDIATION_TOOLS_READY = bool(setup_remediation.get_java() and setup_remediation.get_verapdf())
LO_READY = bool(setup_remediation.get_libreoffice())
heavy = pytest.mark.remediation_tools

DC_NS = "http://purl.org/dc/elements/1.1/"


def _zip_names(path):
    with zipfile.ZipFile(path) as z:
        return set(z.namelist())


def _zip_read(path, name):
    with zipfile.ZipFile(path) as z:
        return z.read(name)


# ── Judgment validation ─────────────────────────────────────────────────

class TestJudgmentValidation:
    def test_non_dict_judgment_rejected(self, tmp_path):
        p = gf.build_bad_pptx(tmp_path / "bad.pptx")
        with pytest.raises(ValueError, match="JSON object"):
            rp.remediate_pptx(p, judgment=["nope"])

    def test_alt_text_must_be_dict(self, tmp_path):
        p = gf.build_bad_pptx(tmp_path / "bad.pptx")
        with pytest.raises(ValueError, match="alt_text"):
            rp.remediate_pptx(p, judgment={"alt_text": ["nope"]})

    def test_slide_titles_must_be_dict(self, tmp_path):
        p = gf.build_bad_pptx(tmp_path / "bad.pptx")
        with pytest.raises(ValueError, match="slide_titles"):
            rp.remediate_pptx(p, judgment={"slide_titles": ["nope"]})

    def test_header_tables_must_be_list(self, tmp_path):
        p = gf.build_bad_pptx(tmp_path / "bad.pptx")
        with pytest.raises(ValueError, match="header_tables"):
            rp.remediate_pptx(p, judgment={"header_tables": "nope"})

    def test_missing_input_raises(self, tmp_path):
        with pytest.raises(RuntimeError, match="not found"):
            rp.remediate_pptx(tmp_path / "does_not_exist.pptx")


# ── Structural edits ─────────────────────────────────────────────────────

class TestStructuralEdits:
    JUDGMENT = {
        "doc_title": "Fixture Deck",
        "language": "en-US",
        "alt_text": {"0": "A small solid red square accent."},
        "slide_titles": {"1": "A Real Title"},
        "header_tables": [0],
    }

    def _run(self, tmp_path):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "bad.remediated.pptx"
        result = rp.remediate_pptx(src, out, judgment=self.JUDGMENT)
        return src, out, result

    def test_copies_input_never_touches_it(self, tmp_path):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        original_bytes = src.read_bytes()
        rp.remediate_pptx(src, tmp_path / "out.pptx", judgment=self.JUDGMENT)
        assert src.read_bytes() == original_bytes

    def test_alt_text_set_on_picture(self, tmp_path):
        _src, out, result = self._run(tmp_path)
        assert result["edits"]["alt_set"] == {0: "A small solid red square accent."}
        xml = etree.fromstring(_zip_read(out, "ppt/slides/slide3.xml"))
        cNvPr = xml.find(".//{*}nvPicPr/{*}cNvPr")
        assert cNvPr.get("descr") == "A small solid red square accent."

    def test_slide_title_set_where_placeholder_exists(self, tmp_path):
        _src, out, result = self._run(tmp_path)
        assert result["edits"]["titles_set"] == {1: "A Real Title"}
        xml = etree.fromstring(_zip_read(out, "ppt/slides/slide2.xml"))
        texts = [t.text for t in xml.iter("{*}t")]
        assert "A Real Title" in texts

    def test_slide_title_flagged_when_no_placeholder(self, tmp_path):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "out.pptx"
        result = rp.remediate_pptx(src, out, judgment={"slide_titles": {"0": "No placeholder here"}})
        assert result["edits"]["titles_set"] == {}
        assert any("no title/ctrTitle placeholder" in f for f in result["edits"]["flags"])

    def test_header_table_first_row_set(self, tmp_path):
        _src, out, result = self._run(tmp_path)
        assert result["edits"]["header_rows_set"] == [0]
        xml = etree.fromstring(_zip_read(out, "ppt/slides/slide4.xml"))
        tblPr = xml.find(".//{*}tbl/{*}tblPr")
        assert tblPr.get("firstRow") == "1"

    def test_title_and_language_set(self, tmp_path):
        _src, out, _result = self._run(tmp_path)
        core = etree.fromstring(_zip_read(out, "docProps/core.xml"))
        assert core.find(f"{{{DC_NS}}}title").text == "Fixture Deck"
        assert core.find(f"{{{DC_NS}}}language").text == "en-US"

    def test_out_of_range_indices_flagged_not_raised(self, tmp_path):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "out.pptx"
        judgment = {
            "alt_text": {"99": "nope"},
            "slide_titles": {"99": "nope"},
            "header_tables": [99],
        }
        result = rp.remediate_pptx(src, out, judgment=judgment)
        assert len(result["edits"]["flags"]) == 3


# ── AC12 preservation — unrelated parts byte-identical ──────────────────

class TestAC12Preservation:
    def test_unrelated_parts_byte_identical(self, tmp_path):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "bad.remediated.pptx"
        judgment = {
            "doc_title": "T", "language": "en-US",
            "alt_text": {"0": "alt"},
            "slide_titles": {"1": "Title"},
            "header_tables": [0],
        }
        rp.remediate_pptx(src, out, judgment=judgment)

        names_in = _zip_names(src)
        names_out = _zip_names(out)
        assert names_in == names_out

        # only the touched slides + core.xml may differ: slide2 (title),
        # slide3 (alt text), slide4 (header row) -- slide1 (untouched
        # textbox slide) must stay byte-identical.
        changed = {"ppt/slides/slide2.xml", "ppt/slides/slide3.xml",
                   "ppt/slides/slide4.xml", "docProps/core.xml"}
        for name in names_in:
            in_bytes = _zip_read(src, name)
            out_bytes = _zip_read(out, name)
            if name in changed:
                assert in_bytes != out_bytes, f"{name} expected to change but didn't"
            else:
                assert in_bytes == out_bytes, f"{name} unexpectedly differs"

    def test_no_edit_copy_untouched_slides_identical(self, tmp_path):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "noop.pptx"
        rp.remediate_pptx(src, out, judgment={})
        for i in range(1, 5):
            name = f"ppt/slides/slide{i}.xml"
            assert _zip_read(src, name) == _zip_read(out, name)


# ── Appearance-check skip path ───────────────────────────────────────────

class TestAppearanceCheckSkip:
    def test_skipped_when_soffice_absent(self, tmp_path, monkeypatch):
        monkeypatch.setattr(lo_export, "get_soffice", lambda: None)
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "out.pptx"

        result = rp.remediate_pptx(src, out, judgment={"alt_text": {"0": "alt"}})

        assert result["appearance"] == {
            "checked": False,
            "reason": "LibreOffice not installed — appearance check skipped",
        }
        assert result["ok"] is True


# ── CLI ──────────────────────────────────────────────────────────────────

class TestCli:
    def test_main_json_output_on_success(self, tmp_path, monkeypatch, capsys):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "out.pptx"
        monkeypatch.setattr(lo_export, "get_soffice", lambda: None)
        monkeypatch.setattr(sys, "argv",
                             ["remediate_pptx.py", "--input", str(src), "--output", str(out), "--json"])
        with pytest.raises(SystemExit) as exc:
            rp.main()
        assert exc.value.code == 0
        payload = json.loads(capsys.readouterr().out)
        assert payload["ok"] is True

    def test_main_errors_on_bad_judgment_file(self, tmp_path, monkeypatch):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        bad_judgment = tmp_path / "judgment.json"
        bad_judgment.write_text("{not valid json")
        monkeypatch.setattr(sys, "argv",
                             ["remediate_pptx.py", "--input", str(src), "--judgment", str(bad_judgment)])
        with pytest.raises(SystemExit) as exc:
            rp.main()
        assert exc.value.code == 2


# ── Heavy: real LibreOffice appearance check + export ───────────────────

@heavy
@pytest.mark.skipif(not LO_READY, reason="vendored LibreOffice not installed")
class TestAppearanceCheckLive:
    def test_invisible_alt_text_only_edit_is_identical(self, tmp_path):
        """alt_text is pure metadata (cNvPr descr) — no visible change at
        all, so this is the clean "near zero" case."""
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "out.pptx"

        result = rp.remediate_pptx(src, out, judgment={"alt_text": {"0": "A red square accent."}})

        assert result["appearance"]["checked"] is True
        assert result["appearance"]["identical_within_threshold"] is True
        assert result["ok"] is True

    def test_no_edit_copy_is_identical(self, tmp_path):
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "noop.pptx"

        result = rp.remediate_pptx(src, out, judgment={})

        assert result["appearance"]["identical_within_threshold"] is True


@heavy
@pytest.mark.skipif(not (LO_READY and REMEDIATION_TOOLS_READY),
                     reason="vendored LibreOffice/java/veraPDF not installed")
class TestExportPdfLive:
    def test_export_leg_runs_verapdf(self, tmp_path):
        """Exercise the --export-pdf path end to end (Impress + artifact
        postfix + veraPDF). Not asserting COMPLIANT here -- AC11's
        well-formed-deck compliance leg lives in test_lo_export.py; this
        confirms remediate_pptx.py wires export + gate correctly regardless
        of the fixture's own compliance state."""
        src = gf.build_bad_pptx(tmp_path / "bad.pptx")
        out = tmp_path / "bad.remediated.pptx"
        export_out = tmp_path / "bad.remediated.pdf"

        result = rp.remediate_pptx(
            src, out, judgment={"alt_text": {"0": "A red square accent."}}, export_pdf=export_out
        )

        assert result["export"]["output"] == str(export_out)
        assert "compliant" in result["export"]["verapdf"]
        assert result["export"]["verapdf"]["matterhorn_human_checkpoints"] == 47
