"""Vector-figure wrap + role-map (v7.63.2 fixes from the STP 530 shakedown)."""
import sys
from pathlib import Path

import pikepdf
import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import pdf_tree_edit  # noqa: E402


def _tagged_pdf_with_unmarked_vectors(path):
    """1-page tagged PDF: one marked paragraph + one unmarked filled rect."""
    pdf = pikepdf.new()
    page = pdf.add_blank_page(page_size=(612, 792))
    content = (
        b"/P <</MCID 0>> BDC BT /F0 12 Tf 72 700 Td (hello) Tj ET EMC\n"
        b"1 0 0 rg 100 100 200 150 re B\n"  # unmarked paint (the vector chart)
    )
    page.Contents = pdf.make_stream(content)
    p_elem = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/P"),
        "/Pg": page.obj, "/K": 0}))
    doc_elem = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/Document"),
        "/K": pikepdf.Array([p_elem])}))
    p_elem["/P"] = doc_elem
    parent_arr = pikepdf.Array([p_elem])
    pdf.Root.StructTreeRoot = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructTreeRoot"),
        "/K": pikepdf.Array([doc_elem]),
        "/ParentTree": pikepdf.Dictionary({"/Nums": pikepdf.Array([0, parent_arr])}),
        "/ParentTreeNextKey": 1,
    }))
    doc_elem["/P"] = pdf.Root.StructTreeRoot
    page.StructParents = 0
    pdf.save(path)


def test_detect_unmarked_paint_pages(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    assert pdf_tree_edit.detect_unmarked_paint_pages(pdf) == {1: 1}


def test_wrap_page_vectors_as_figure(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    mcids = pdf_tree_edit.wrap_page_vectors_as_figure(pdf, 1, "a red rectangle chart")
    out = tmp_path / "out.pdf"
    pdf.save(out)
    pdf2 = pikepdf.open(out)
    assert pdf_tree_edit.detect_unmarked_paint_pages(pdf2) == {}
    figs = pdf_tree_edit.walk_figures(pdf2)
    assert len(figs) == 1 and str(figs[0].Alt) == "a red rectangle chart"
    assert mcids == [1] and int(figs[0].K) == 1  # above existing MCID 0
    nums = pdf2.Root.StructTreeRoot.ParentTree.Nums
    assert len(nums[1]) == 2  # ParentTree array grew by the new elem


def test_wrap_raises_without_unmarked_paint(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    pdf_tree_edit.wrap_page_vectors_as_figure(pdf, 1, "x")
    with pytest.raises(RuntimeError, match="no unmarked paint"):
        pdf_tree_edit.wrap_page_vectors_as_figure(pdf, 1, "y")


def test_ensure_role_map(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    # standard-only tree -> no-op
    assert pdf_tree_edit.ensure_role_map(pdf) == []
    assert pdf.Root.StructTreeRoot.get("/RoleMap") is None
    # inject a non-standard type
    doc = pdf.Root.StructTreeRoot.K[0]
    doc.K[0]["/S"] = pikepdf.Name("/TextChunk")
    assert pdf_tree_edit.ensure_role_map(pdf) == ["TextChunk"]
    assert str(pdf.Root.StructTreeRoot.RoleMap["/TextChunk"]) == "/P"
    # idempotent
    assert pdf_tree_edit.ensure_role_map(pdf) == []


def test_judgment_vector_figures_validation():
    import remediate_pdf
    with pytest.raises(ValueError, match="vector_figures"):
        remediate_pdf._validate_judgment({"vector_figures": ["not", "a", "dict"]})
    remediate_pdf._validate_judgment({"vector_figures": {"5": "alt"}})  # ok


def test_wrap_sparse_mcids_no_collision(tmp_path):
    """Review finding 1: existing sparse MCID (5) must not collide."""
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    page = pdf.pages[0]
    page.Contents = pdf.make_stream(
        b"/P <</MCID 5>> BDC BT (x) Tj ET EMC\n1 0 0 rg 10 10 50 50 re B\n")
    pdf.Root.StructTreeRoot.K[0].K[0]["/K"] = 5
    mcids = pdf_tree_edit.wrap_page_vectors_as_figure(pdf, 1, "chart")
    assert mcids == [6]  # max existing (5) + 1, not len(arr)==1
    arr = pdf.Root.StructTreeRoot.ParentTree.Nums[1]
    assert len(arr) == 7  # null-padded to index 6
    assert str(arr[6].S) == "/Figure"


def test_wrap_multiple_clusters_distinct_mcids(tmp_path):
    """Review finding 4: separated paint runs get distinct MCIDs, one elem."""
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    pdf.pages[0].Contents = pdf.make_stream(
        b"1 0 0 rg 10 10 50 50 re B\n"
        b"/P <</MCID 0>> BDC BT (mid) Tj ET EMC\n"
        b"0 1 0 rg 70 70 50 50 re B\n")
    mcids = pdf_tree_edit.wrap_page_vectors_as_figure(pdf, 1, "two-part chart")
    assert len(mcids) == 2 and mcids[0] != mcids[1]
    figs = pdf_tree_edit.walk_figures(pdf)
    ks = [int(k) for k in figs[0].K]
    assert ks == mcids


def test_wrap_failure_leaves_tree_unmutated(tmp_path):
    """Review finding 2: no dangling Figure elems when nothing to wrap."""
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    pdf.pages[0].Contents = pdf.make_stream(
        b"/P <</MCID 0>> BDC BT (only marked) Tj ET EMC\n")
    before = len(pdf_tree_edit.walk_figures(pdf))
    arr_len = len(pdf.Root.StructTreeRoot.ParentTree.Nums[1])
    with pytest.raises(RuntimeError, match="no unmarked paint"):
        pdf_tree_edit.wrap_page_vectors_as_figure(pdf, 1, "x")
    assert len(pdf_tree_edit.walk_figures(pdf)) == before
    assert len(pdf.Root.StructTreeRoot.ParentTree.Nums[1]) == arr_len


def test_ensure_role_map_cycle_guard(tmp_path):
    """Review finding 3: cyclic /K must terminate, not hang."""
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    doc = pdf.Root.StructTreeRoot.K[0]
    child = doc.K[0]
    child["/K"] = pikepdf.Array([doc])  # cycle: child -> doc -> child
    assert pdf_tree_edit.ensure_role_map(pdf) == []  # terminates


def test_detect_surfaces_parse_errors(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    pdf.pages[0].Contents = pdf.make_stream(b"\xff\xfe not a stream q[[[")
    res = pdf_tree_edit.detect_unmarked_paint_pages(pdf)
    assert res.get(1) == "parse-error"  # the sentinel, not a paint count


def test_artifact_page_unmarked(tmp_path):
    """Decorative path: unmarked paint becomes /Artifact; tagged content untouched."""
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    n = pdf_tree_edit.artifact_page_unmarked(pdf, 1)
    assert n == 1
    assert pdf_tree_edit.detect_unmarked_paint_pages(pdf) == {}
    # no Figure was created (decorative != described)
    assert pdf_tree_edit.walk_figures(pdf) == []
    # marked paragraph content still present exactly once (semantic check —
    # unparse_content_stream re-serializes with different whitespace)
    p_bdcs = artifact_bmcs = 0
    for instr in pikepdf.parse_content_stream(pdf.pages[0]):
        op = str(instr.operator)
        if op == "BDC" and str(instr.operands[0]) == "/P":
            p_bdcs += 1
        if op == "BMC" and str(instr.operands[0]) == "/Artifact":
            artifact_bmcs += 1
    assert p_bdcs == 1 and artifact_bmcs == 1


def test_artifact_page_no_unmarked_is_noop(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    pdf.pages[0].Contents = pdf.make_stream(
        b"/P <</MCID 0>> BDC BT (only marked) Tj ET EMC\n")
    before = pdf.pages[0].Contents.read_bytes()
    assert pdf_tree_edit.artifact_page_unmarked(pdf, 1) == 0
    assert pdf.pages[0].Contents.read_bytes() == before


def test_judgment_vector_artifacts_validation():
    import remediate_pdf
    with pytest.raises(ValueError, match="vector_artifacts"):
        remediate_pdf._validate_judgment({"vector_artifacts": {"1": True}})
    remediate_pdf._validate_judgment({"vector_artifacts": [1, 2, 3]})  # ok


def test_judgment_figure_artifact_overlap_rejected():
    import remediate_pdf
    with pytest.raises(ValueError, match="BOTH"):
        remediate_pdf._validate_judgment(
            {"vector_figures": {"3": "alt"}, "vector_artifacts": [2, 3]})
    remediate_pdf._validate_judgment(
        {"vector_figures": {"3": "alt"}, "vector_artifacts": [2]})  # disjoint ok


def test_artifact_wraps_unmarked_inline_image(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    pdf.pages[0].Contents = pdf.make_stream(
        b"/P <</MCID 0>> BDC BT (text) Tj ET EMC\n"
        b"q BI /W 1 /H 1 /CS /G /BPC 8 ID \x00 EI Q\n")
    n = pdf_tree_edit.artifact_page_unmarked(pdf, 1)
    assert n == 1
    assert pdf_tree_edit.detect_unmarked_paint_pages(pdf) == {}
    # inline image survives the round-trip
    kinds = [type(i).__name__ for i in pikepdf.parse_content_stream(pdf.pages[0])]
    assert "ContentStreamInlineImage" in kinds


def test_marked_inline_image_untouched(tmp_path):
    p = tmp_path / "v.pdf"
    _tagged_pdf_with_unmarked_vectors(p)
    pdf = pikepdf.open(p)
    pdf.pages[0].Contents = pdf.make_stream(
        b"/Figure <</MCID 0>> BDC q BI /W 1 /H 1 /CS /G /BPC 8 ID \x00 EI Q EMC\n")
    assert pdf_tree_edit.artifact_page_unmarked(pdf, 1) == 0
    assert pdf_tree_edit.detect_unmarked_paint_pages(pdf) == {}
