"""Tests for the two file-remediation gate-gap fixes:

1. pdf_tree_edit.tag_link_annotations — Link annots missing /Contents
   (LibreOffice/ODL output) trip veraPDF 7.18.x; fix sets a deterministic
   /Contents fallback.
2. remediate_pdf E3->E2 fallthrough — a digital-tagged PDF that fails the E3
   metadata-only gate falls through to E2 re-tagging UNLESS the failure is a
   7.21.x embedded-font-program defect (unfixable by re-tagging), in which
   case it's flagged needs_manual instead of looped on.

CI-light only: pikepdf + mocks, no vendor java/verapdf/ODL required.
"""

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
import remediate_pdf as rp


# ── Fix 1: tag_link_annotations ─────────────────────────────────────────────

def _page_with_annots(pdf, annots):
    page = pdf.pages[0]
    page.obj["/Annots"] = pdf.make_indirect(pikepdf.Array(annots))
    return page


def _link_annot(pdf, uri=None, contents=None):
    d = pikepdf.Dictionary(Subtype=pikepdf.Name.Link)
    if uri is not None:
        d["/A"] = pikepdf.Dictionary(S=pikepdf.Name.URI, URI=uri)
    if contents is not None:
        d["/Contents"] = pikepdf.String(contents)
    return pdf.make_indirect(d)


class TestTagLinkAnnotations:
    def test_no_annots_page_is_noop(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        assert pte.tag_link_annotations(pdf) == 0

    def test_link_missing_contents_gets_uri_derived_fallback(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        annot = _link_annot(pdf, uri="https://example.edu/reading")
        _page_with_annots(pdf, [annot])

        fixed = pte.tag_link_annotations(pdf)

        assert fixed == 1
        assert str(annot["/Contents"]) == "Link: https://example.edu/reading"

    def test_link_with_no_uri_gets_bare_fallback(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        annot = _link_annot(pdf)  # no /A at all
        _page_with_annots(pdf, [annot])

        fixed = pte.tag_link_annotations(pdf)

        assert fixed == 1
        assert str(annot["/Contents"]) == "Link"

    def test_link_with_existing_contents_untouched(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        annot = _link_annot(pdf, uri="https://example.edu/x", contents="Existing description")
        _page_with_annots(pdf, [annot])

        fixed = pte.tag_link_annotations(pdf)

        assert fixed == 0
        assert str(annot["/Contents"]) == "Existing description"

    def test_non_link_annot_ignored(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        d = pdf.make_indirect(pikepdf.Dictionary(Subtype=pikepdf.Name.Text))
        _page_with_annots(pdf, [d])

        assert pte.tag_link_annotations(pdf) == 0
        assert "/Contents" not in d

    def test_mixed_annots_only_fixes_the_bare_link(self):
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        fixed_already = _link_annot(pdf, uri="https://x.edu", contents="already fine")
        needs_fix = _link_annot(pdf, uri="https://y.edu")
        other = pdf.make_indirect(pikepdf.Dictionary(Subtype=pikepdf.Name.Text))
        _page_with_annots(pdf, [fixed_already, needs_fix, other])

        assert pte.tag_link_annotations(pdf) == 1
        assert str(needs_fix["/Contents"]) == "Link: https://y.edu"


# ── Fix 2: _has_font_defects ────────────────────────────────────────────────

class TestHasFontDefects:
    def test_font_defect_clause_detected(self):
        vera = {"failed_summaries": [{"clause": "7.21.7"}, {"clause": "7.1.1"}]}
        assert rp._has_font_defects(vera) is True

    def test_only_structural_clauses_no_font_defect(self):
        vera = {"failed_summaries": [{"clause": "7.1"}, {"clause": "7.3"}, {"clause": "7.18.5"}]}
        assert rp._has_font_defects(vera) is False

    def test_empty_summaries_no_font_defect(self):
        assert rp._has_font_defects({"failed_summaries": []}) is False
        assert rp._has_font_defects({}) is False
        assert rp._has_font_defects(None) is False

    @pytest.mark.parametrize("clause", ["7.21.4.1", "7.21.4.2", "7.21"])
    def test_various_721_clauses_detected(self, clause):
        assert rp._has_font_defects({"failed_summaries": [{"clause": clause}]}) is True


# ── Fix 2: E3->E2 routing (mocked, no vendor tools) ─────────────────────────

def _tiny_tagged_pdf(path):
    """A minimal PDF with a StructTreeRoot -> classifies digital-tagged."""
    pdf = pikepdf.new()
    pdf.add_blank_page(page_size=(200, 200))
    struct_root = pdf.make_indirect(pikepdf.Dictionary(Type=pikepdf.Name.StructTreeRoot, K=[]))
    pdf.Root.StructTreeRoot = struct_root
    pdf.save(str(path))
    pdf.close()
    return path


class TestE3FallthroughRouting:
    def test_e3_compliant_no_fallthrough(self, tmp_path, monkeypatch):
        p = _tiny_tagged_pdf(tmp_path / "in.pdf")
        out = tmp_path / "out.pdf"

        monkeypatch.setattr(rp.verapdf_gate, "run_verapdf",
                             lambda path: {"compliant": True, "failed_summaries": []})
        monkeypatch.setattr(rp.fidelity_gate, "pixel_diff",
                             lambda a, b: {"identical": True})
        e2_called = []
        monkeypatch.setattr(rp, "_run_e2", lambda *a, **kw: e2_called.append(1))

        result = rp.remediate_pdf(p, out, cls="digital-tagged")

        assert e2_called == []
        assert result["ok"] is True
        assert result["engine"] == "E3-metadata-patch"
        assert result["needs_manual"] is False
        assert result["manual_reason"] is None

    def test_e3_fails_with_font_defect_flags_needs_manual_no_e2(self, tmp_path, monkeypatch):
        p = _tiny_tagged_pdf(tmp_path / "in.pdf")
        out = tmp_path / "out.pdf"

        monkeypatch.setattr(rp.verapdf_gate, "run_verapdf", lambda path: {
            "compliant": False, "failed_summaries": [{"clause": "7.21.7"}],
        })
        monkeypatch.setattr(rp.fidelity_gate, "pixel_diff", lambda a, b: {"identical": True})
        e2_called = []
        monkeypatch.setattr(rp, "_run_e2", lambda *a, **kw: e2_called.append(1))

        result = rp.remediate_pdf(p, out, cls="digital-tagged")

        assert e2_called == [], "E2 must never run on a font-program defect — unfixable by re-tag"
        assert result["ok"] is False
        assert result["needs_manual"] is True
        assert "font-program" in result["manual_reason"]
        assert result["engine"] == "E3-metadata-patch"
        assert result["rejected_output"]

    def test_e3_fails_without_font_defect_falls_through_to_e2(self, tmp_path, monkeypatch):
        p = _tiny_tagged_pdf(tmp_path / "in.pdf")
        out = tmp_path / "out.pdf"

        vera_calls = [
            {"compliant": False, "failed_summaries": [{"clause": "7.1"}, {"clause": "7.18.5"}]},
            {"compliant": True, "failed_summaries": []},
        ]
        monkeypatch.setattr(rp.verapdf_gate, "run_verapdf", lambda path: vera_calls.pop(0))
        monkeypatch.setattr(rp.fidelity_gate, "pixel_diff", lambda a, b: {"identical": True})

        e2_called = []

        def fake_e2(work_input, input_path, output_path, judgment, title, lang, notes):
            e2_called.append(1)
            out.write_bytes(p.read_bytes())  # E2 "wrote" the output
            return {"promotions": [], "alts_set": {}, "tables_flagged": []}, title

        monkeypatch.setattr(rp, "_run_e2", fake_e2)

        result = rp.remediate_pdf(p, out, cls="digital-tagged")

        assert e2_called == [1]
        assert result["ok"] is True
        assert result["engine"] == "E3-metadata-patch->E2-insitu"
        assert any("falling through to E2" in n for n in result["notes"])
        assert result["needs_manual"] is False
        assert result["manual_reason"] is None

    def test_e3_fails_e2_fallthrough_also_fails_flags_needs_manual(self, tmp_path, monkeypatch):
        p = _tiny_tagged_pdf(tmp_path / "in.pdf")
        out = tmp_path / "out.pdf"

        vera_calls = [
            {"compliant": False, "failed_summaries": [{"clause": "7.1"}]},
            {"compliant": False, "failed_summaries": [{"clause": "7.1"}]},
        ]
        monkeypatch.setattr(rp.verapdf_gate, "run_verapdf", lambda path: vera_calls.pop(0))
        monkeypatch.setattr(rp.fidelity_gate, "pixel_diff", lambda a, b: {"identical": True})

        e2_called = []

        def fake_e2(work_input, input_path, output_path, judgment, title, lang, notes):
            e2_called.append(1)
            out.write_bytes(p.read_bytes())
            return {"promotions": [], "alts_set": {}, "tables_flagged": []}, title

        monkeypatch.setattr(rp, "_run_e2", fake_e2)

        result = rp.remediate_pdf(p, out, cls="digital-tagged")

        assert e2_called == [1]
        assert result["ok"] is False
        assert result["needs_manual"] is True
        assert "structural remediation incomplete" in result["manual_reason"]
        assert result["rejected_output"]

    def test_e2_direct_route_unaffected_needs_manual_defaults_false(self, tmp_path, monkeypatch):
        """Untagged-class route (E2-direct) never touches the fallthrough
        logic — needs_manual/manual_reason still default correctly."""
        pdf = pikepdf.new()
        pdf.add_blank_page(page_size=(200, 200))
        p = tmp_path / "in.pdf"
        pdf.save(str(p))
        pdf.close()
        out = tmp_path / "out.pdf"

        monkeypatch.setattr(rp.verapdf_gate, "run_verapdf",
                             lambda path: {"compliant": True, "failed_summaries": []})
        monkeypatch.setattr(rp.fidelity_gate, "pixel_diff", lambda a, b: {"identical": True})

        def fake_e2(work_input, input_path, output_path, judgment, title, lang, notes):
            out.write_bytes(p.read_bytes())
            return {"promotions": [], "alts_set": {}, "tables_flagged": []}, title

        monkeypatch.setattr(rp, "_run_e2", fake_e2)

        result = rp.remediate_pdf(p, out, cls="digital-untagged")

        assert result["ok"] is True
        assert result["engine"] == "E2-insitu"
        assert result["needs_manual"] is False
        assert result["manual_reason"] is None
