"""CI-light tests for scripts/pdf_tree_edit.py — pikepdf only, no vendor java/
verapdf/ODL and no macOS Vision required."""

import json
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


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


def _hand_built_pdf():
    """2 figures + H1 + H3 (no gap-free ordering) under one /Document node,
    matching the task's canonical example."""
    pdf = pikepdf.new()
    pdf.add_blank_page(page_size=(200, 200))
    h1 = _struct_elem(pdf, S=pikepdf.Name.H1, K=[0])
    fig1 = _struct_elem(pdf, S=pikepdf.Name.Figure, K=[1], Alt="placeholder")
    h3 = _struct_elem(pdf, S=pikepdf.Name.H3, K=[2])
    fig2 = _struct_elem(pdf, S=pikepdf.Name.Figure, K=[3], Alt="placeholder")
    doc = _struct_elem(pdf, S=pikepdf.Name.Document, K=[h1, fig1, h3, fig2])
    struct_root = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.StructTreeRoot, K=[doc]))
    pdf.Root.StructTreeRoot = struct_root
    return pdf


class TestWalk:
    def test_walk_elems_none_returns_leaves_only_no_document_wrapper(self):
        pdf = _hand_built_pdf()
        types = [str(n.get("/S")) for n in pte.walk_elems(pdf, None)]
        assert types == ["/H1", "/Figure", "/H3", "/Figure"]

    def test_walk_elems_filters_by_type(self):
        pdf = _hand_built_pdf()
        figs = pte.walk_elems(pdf, "/Figure")
        assert len(figs) == 2
        assert all(str(f.get("/S")) == "/Figure" for f in figs)

    def test_walk_figures_is_walk_elems_figure_shortcut(self):
        pdf = _hand_built_pdf()
        assert len(pte.walk_figures(pdf)) == 2

    def test_no_struct_tree_root_returns_empty(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        assert pte.walk_elems(pdf, None) == []
        assert pte.walk_figures(pdf) == []


class TestSetAlts:
    def test_applies_by_figure_depth_first_index(self):
        pdf = _hand_built_pdf()
        applied = pte.set_alts(pdf, {0: "first figure alt", 1: "second figure alt"})
        assert applied == {0: "first figure alt", 1: "second figure alt"}
        figs = pte.walk_figures(pdf)
        assert str(figs[0].get("/Alt")) == "first figure alt"
        assert str(figs[1].get("/Alt")) == "second figure alt"

    def test_out_of_range_index_skipped(self):
        pdf = _hand_built_pdf()
        applied = pte.set_alts(pdf, {5: "no such figure"})
        assert applied == {}

    def test_string_keys_from_json_judgment_coerced_to_int(self):
        pdf = _hand_built_pdf()
        applied = pte.set_alts(pdf, {"0": "alt via string key"})
        assert applied == {0: "alt via string key"}


class TestMatchFigures:
    def test_matches_nearest_same_page_by_center_distance(self):
        odl_images = [{"page": 1, "bbox": [0, 0, 10, 10]}, {"page": 1, "bbox": [100, 100, 110, 110]}]
        manifest = [{"page": 1, "bbox": [95, 95, 115, 115]}, {"page": 1, "bbox": [1, 1, 9, 9]}]
        result = pte.match_figures(odl_images, manifest)
        assert result == [1, 0]

    def test_no_same_page_candidate_is_none(self):
        odl_images = [{"page": 1, "bbox": [0, 0, 10, 10]}]
        manifest = [{"page": 2, "bbox": [0, 0, 10, 10]}]
        assert pte.match_figures(odl_images, manifest) == [None]


class TestNormalizeHeadingLevels:
    def test_h1_figure_h3_figure_normalizes_to_h1_h2(self):
        """The task's canonical example: 2 figures + H1 + H3 -> normalize -> H1 + H2."""
        pdf = _hand_built_pdf()
        promotions = pte.normalize_heading_levels(pdf)
        types = [str(n.get("/S")) for n in pte.walk_elems(pdf, None)]
        assert types == ["/H1", "/Figure", "/H2", "/Figure"]
        assert promotions == [{"index": 2, "from": "/H3", "to": "/H2"}]

    def test_no_op_when_no_skips(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        h1 = _struct_elem(pdf, S=pikepdf.Name.H1, K=[0])
        h2 = _struct_elem(pdf, S=pikepdf.Name.H2, K=[1])
        doc = _struct_elem(pdf, S=pikepdf.Name.Document, K=[h1, h2])
        pdf.Root.StructTreeRoot = pdf.make_indirect(
            pikepdf.Dictionary(Type=pikepdf.Name.StructTreeRoot, K=[doc]))
        assert pte.normalize_heading_levels(pdf) == []

    def test_retag_changes_s(self):
        pdf = _hand_built_pdf()
        h3 = pte.walk_elems(pdf, "/H3")[0]
        pte.retag(pdf, h3, "/H2")
        assert str(h3.get("/S")) == "/H2"

    def test_retag_prepends_slash(self):
        pdf = _hand_built_pdf()
        h3 = pte.walk_elems(pdf, "/H3")[0]
        pte.retag(pdf, h3, "H4")
        assert str(h3.get("/S")) == "/H4"


class TestSetMetadata:
    def test_sets_catalog_keys_verified_via_reload(self, tmp_path):
        pdf = _hand_built_pdf()
        pte.set_metadata(pdf, "My Fixture Title", "en-US")
        out = tmp_path / "out.pdf"
        pdf.save(str(out))
        pdf.close()

        reloaded = pikepdf.open(str(out))
        assert bool(reloaded.Root.MarkInfo.Marked) is True
        assert bool(reloaded.Root.ViewerPreferences.DisplayDocTitle) is True
        assert str(reloaded.Root.Lang) == "en-US"
        assert str(reloaded.docinfo["/Title"]) == "My Fixture Title"
        with reloaded.open_metadata() as meta:
            assert meta["dc:title"] == "My Fixture Title"
            assert meta["pdfuaid:part"] == "1"
        for page in reloaded.pages:
            assert str(page.Tabs) == "/S"


class TestLoadOdlJson:
    def test_flattens_and_converts_bbox_top_down(self, tmp_path):
        canned = {
            "file name": "x.pdf",
            "kids": [
                {
                    "type": "heading", "pdfua_tag": "H1", "page number": 1,
                    "bounding box": [72.0, 700.0, 300.0, 740.0], "content": "Title",
                },
                {
                    "type": "image", "pdfua_tag": "Figure", "page number": 1,
                    "bounding box": [72.0, 100.0, 172.0, 200.0], "source": "img1.png",
                },
            ],
        }
        p = tmp_path / "canned.json"
        p.write_text(json.dumps(canned), encoding="utf-8")

        nodes = pte.load_odl_json(p)
        assert len(nodes) == 2
        assert nodes[0]["type"] == "heading"
        assert nodes[0]["pdfua_tag"] == "H1"
        assert nodes[0]["content"] == "Title"
        # top-down: y_td = page_height - y_bu; default page_height=792 (letter)
        assert nodes[0]["bbox"] == [72.0, 792 - 740.0, 300.0, 792 - 700.0]
        assert nodes[1]["bbox"] == [72.0, 792 - 200.0, 172.0, 792 - 100.0]

    def test_page_heights_override(self, tmp_path):
        canned = {"kids": [{"type": "paragraph", "page number": 1,
                             "bounding box": [0, 0, 10, 10]}]}
        p = tmp_path / "canned.json"
        p.write_text(json.dumps(canned), encoding="utf-8")
        nodes = pte.load_odl_json(p, page_heights={1: 500})
        assert nodes[0]["bbox"] == [0, 500 - 10, 10, 500 - 0]

    def test_nested_kids_flattened_depth_first(self, tmp_path):
        canned = {"kids": [
            {"type": "list", "page number": 1, "bounding box": None,
             "kids": [{"type": "list_item", "page number": 1, "bounding box": [0, 0, 5, 5]}]},
            {"type": "paragraph", "page number": 1, "bounding box": [0, 10, 5, 15]},
        ]}
        p = tmp_path / "canned.json"
        p.write_text(json.dumps(canned), encoding="utf-8")
        nodes = pte.load_odl_json(p)
        assert [n["type"] for n in nodes] == ["list", "list_item", "paragraph"]
