"""Tests for the three PDF/UA structure fixes added to pdf_tree_edit.py:
remove_circular_rolemap (veraPDF 7.1 circular-mapping), wrap_link_annotations
(veraPDF 7.18.5 Link struct/OBJR wrapping), and resolve_unreachable_content
(veraPDF 7.1 unmarked/orphaned content -> attach real text, artifact
decoration). CI-light: pure pikepdf fixtures, no vendored ODL/java/verapdf.
"""
import os
import sys

import pikepdf
import pytest

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

import pdf_tree_edit as pte  # noqa: E402
import reading_order as ro  # noqa: E402


def _struct_elem(pdf, **kw):
    return pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.StructElem, **kw))


class TestRemoveCircularRolemap:
    def test_no_struct_tree_root_is_noop(self):
        pdf = pikepdf.new()
        assert pte.remove_circular_rolemap(pdf) == []

    def test_no_rolemap_is_noop(self):
        pdf = pikepdf.new()
        pdf.Root.StructTreeRoot = pdf.make_indirect(
            pikepdf.Dictionary({"/Type": pikepdf.Name("/StructTreeRoot"), "/K": pikepdf.Array([])}))
        assert pte.remove_circular_rolemap(pdf) == []

    def test_removes_self_mapping_entries(self):
        pdf = pikepdf.new()
        pdf.Root.StructTreeRoot = pdf.make_indirect(pikepdf.Dictionary({
            "/Type": pikepdf.Name("/StructTreeRoot"),
            "/K": pikepdf.Array([]),
            "/RoleMap": pikepdf.Dictionary({
                "/Document": pikepdf.Name("/Document"),
                "/Figure": pikepdf.Name("/Figure"),
                "/CustomBlock": pikepdf.Name("/P"),
            }),
        }))
        removed = pte.remove_circular_rolemap(pdf)
        assert set(removed) == {"Document", "Figure"}
        rm = pdf.Root.StructTreeRoot["/RoleMap"]
        assert list(rm.keys()) == ["/CustomBlock"]

    def test_removes_multi_hop_cycle(self):
        pdf = pikepdf.new()
        pdf.Root.StructTreeRoot = pdf.make_indirect(pikepdf.Dictionary({
            "/Type": pikepdf.Name("/StructTreeRoot"),
            "/K": pikepdf.Array([]),
            "/RoleMap": pikepdf.Dictionary({
                "/Foo": pikepdf.Name("/Bar"),
                "/Bar": pikepdf.Name("/Foo"),
            }),
        }))
        removed = pte.remove_circular_rolemap(pdf)
        assert set(removed) == {"Foo", "Bar"}
        assert "/RoleMap" not in pdf.Root.StructTreeRoot

    def test_deletes_rolemap_when_empty_after_cleanup(self):
        pdf = pikepdf.new()
        pdf.Root.StructTreeRoot = pdf.make_indirect(pikepdf.Dictionary({
            "/Type": pikepdf.Name("/StructTreeRoot"),
            "/K": pikepdf.Array([]),
            "/RoleMap": pikepdf.Dictionary({"/Link": pikepdf.Name("/Link")}),
        }))
        pte.remove_circular_rolemap(pdf)
        assert "/RoleMap" not in pdf.Root.StructTreeRoot

    def test_never_touches_legitimate_nonstandard_entries(self):
        pdf = pikepdf.new()
        pdf.Root.StructTreeRoot = pdf.make_indirect(pikepdf.Dictionary({
            "/Type": pikepdf.Name("/StructTreeRoot"),
            "/K": pikepdf.Array([]),
            "/RoleMap": pikepdf.Dictionary({
                "/Heading_A": pikepdf.Name("/H3"),
                "/Body_Text": pikepdf.Name("/P"),
            }),
        }))
        removed = pte.remove_circular_rolemap(pdf)
        assert removed == []
        rm = pdf.Root.StructTreeRoot["/RoleMap"]
        assert set(rm.keys()) == {"/Heading_A", "/Body_Text"}


class TestWrapLinkAnnotations:
    def _build(self, flat_nodes=None):
        pdf = pikepdf.new()
        page = pdf.add_blank_page(page_size=(612, 792))
        page.Contents = pdf.make_stream(b"/P <</MCID 0>> BDC BT (Hello world) Tj ET EMC\n")
        link_annot = pdf.make_indirect(pikepdf.Dictionary({
            "/Type": pikepdf.Name("/Annot"), "/Subtype": pikepdf.Name("/Link"),
            "/Rect": pikepdf.Array([100, 700, 200, 720]),
            "/A": pikepdf.Dictionary({"/S": pikepdf.Name("/URI"),
                                       "/URI": pikepdf.String("https://example.com")}),
        }))
        page.Annots = pikepdf.Array([link_annot])
        page.StructParents = 0
        p_elem = _struct_elem(pdf, S=pikepdf.Name.P, Pg=page.obj, K=0)
        doc_elem = _struct_elem(pdf, S=pikepdf.Name.Document, K=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, pikepdf.Array([p_elem])])}),
            "/ParentTreeNextKey": 1,
        }))
        return pdf, link_annot

    def test_no_struct_tree_root_is_noop(self):
        pdf = pikepdf.new()
        result = pte.wrap_link_annotations(pdf)
        assert result == {"wrapped": 0, "contents_set": 0, "contents_missing": 0}

    def test_wraps_unwrapped_link_and_resolves_contents_from_flat_nodes(self):
        flat_nodes = [{"page": 1, "bbox": [95, 65, 205, 95], "content": "click here", "type": "text"}]
        pdf, annot = self._build(flat_nodes)
        result = pte.wrap_link_annotations(pdf, flat_nodes)
        assert result == {"wrapped": 1, "contents_set": 1, "contents_missing": 0}

        links = pte.walk_elems(pdf, "/Link")
        assert len(links) == 1
        k = links[0]["/K"]
        assert str(k.get("/Type")) == "/OBJR"
        assert k.get("/Obj").objgen == annot.objgen
        assert int(annot["/StructParent"]) >= 0
        assert str(annot["/Contents"]) == "click here"

    def test_contents_missing_when_no_flat_nodes_overlap(self):
        pdf, annot = self._build()
        result = pte.wrap_link_annotations(pdf, None)
        assert result["wrapped"] == 1
        assert result["contents_set"] == 0
        assert result["contents_missing"] == 1
        assert "/Contents" not in annot

    def test_already_wrapped_link_is_skipped(self):
        pdf, annot = self._build()
        first = pte.wrap_link_annotations(pdf)
        assert first["wrapped"] == 1
        second = pte.wrap_link_annotations(pdf)
        assert second == {"wrapped": 0, "contents_set": 0, "contents_missing": 0}
        # exactly one Link struct elem exists -- never double-wrapped
        assert len(pte.walk_elems(pdf, "/Link")) == 1

    def test_existing_contents_never_overwritten(self):
        pdf, annot = self._build()
        annot["/Contents"] = pikepdf.String("original description")
        flat_nodes = [{"page": 1, "bbox": [95, 65, 205, 95], "content": "different text", "type": "text"}]
        pte.wrap_link_annotations(pdf, flat_nodes)
        assert str(annot["/Contents"]) == "original description"


class TestResolveUnreachableContent:
    def _build(self):
        pdf = pikepdf.new()
        page = pdf.add_blank_page(page_size=(612, 792))
        content = (
            b"/P <</MCID 0>> BDC BT (Reachable paragraph) Tj ET EMC\n"
            b"1 0 0 RG 10 10 50 50 re S\n"
            b"/Artifact BMC EMC\n"
            b"BT (Orphan bare text run) Tj ET\n"
            b"/Span <</MCID 1>> BDC BT (Orphaned but real text) Tj ET EMC\n"
            b"/Span <</MCID 2>> BDC 0 0 1 RG 20 20 30 30 re f EMC\n"
        )
        page.Contents = pdf.make_stream(content)
        page.StructParents = 0
        p_elem = _struct_elem(pdf, S=pikepdf.Name.P, Pg=page.obj, K=0)
        doc_elem = _struct_elem(pdf, S=pikepdf.Name.Document, K=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, pikepdf.Array([p_elem])])}),
            "/ParentTreeNextKey": 1,
        }))
        return pdf

    def test_no_struct_tree_root_is_noop(self):
        pdf = pikepdf.new()
        assert pte.resolve_unreachable_content(pdf) == {"attached": 0, "artifacted": 0}

    def test_attaches_text_artifacts_decoration(self):
        pdf = self._build()
        result = pte.resolve_unreachable_content(pdf)
        assert result == {"attached": 2, "artifacted": 2}

    def test_attached_text_joins_reading_order(self):
        pdf = self._build()
        pte.resolve_unreachable_content(pdf)
        texts = ro.struct_order_text(pdf)
        assert "Reachable paragraph" in texts
        assert "Orphan bare text run" in texts
        assert "Orphaned but real text" in texts

    def test_reachable_content_untouched(self):
        pdf = self._build()
        pte.resolve_unreachable_content(pdf)
        # the original leaf /P (mcid 0) still owns exactly its own kid
        root = pdf.Root.StructTreeRoot
        doc_elem = root["/K"][0]
        first_child = doc_elem["/K"][0]
        assert int(first_child["/K"]) == 0

    def test_idempotent_second_run_is_noop(self):
        pdf = self._build()
        pte.resolve_unreachable_content(pdf)
        second = pte.resolve_unreachable_content(pdf)
        assert second == {"attached": 0, "artifacted": 0}

    def test_no_flat_parenttree_is_noop(self):
        pdf = pikepdf.new()
        page = pdf.add_blank_page(page_size=(612, 792))
        page.Contents = pdf.make_stream(b"BT (bare text) Tj ET\n")
        pdf.Root.StructTreeRoot = pdf.make_indirect(pikepdf.Dictionary({
            "/Type": pikepdf.Name("/StructTreeRoot"), "/K": pikepdf.Array([]),
        }))
        assert pte.resolve_unreachable_content(pdf) == {"attached": 0, "artifacted": 0}
