"""Tests for the font-embed engine's orchestrator wiring in
scripts/remediate_pdf.py: edits["font_embed"] on both branches, and the
structural fidelity gate (FONT_EMBED_DELTA / FONT_EMBED_STRUCT_PCT via
fidelity_gate.structural_diff) that applies only when a font was actually
embedded (plan §3, §4). Every other edit still uses fidelity_gate.pixel_diff
at exact-zero.

CI-light: mocks pdf_font_embed/pdf_font_repair/verapdf_gate/fidelity_gate —
no vendored ODL/java/verapdf, mirrors test_remediate_pdf_fontflag.py.
"""

import os
import shutil
import sys

import fitz  # PyMuPDF
import pytest

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

import fidelity_gate  # noqa: E402
import remediate_pdf as rp  # noqa: E402


def _tiny_pdf(path, text="Hello world"):
    doc = fitz.open()
    page = doc.new_page(width=200, height=200)
    page.insert_text((10, 20), text, fontsize=10)
    doc.save(str(path))
    doc.close()
    return path


def _fake_run_e2(work_input, input_path, output_path, judgment, title, lang, notes):
    shutil.copy(str(work_input), str(output_path))
    return {"promotions": [], "alts_set": {}, "tables_flagged": []}, title or "T"


ZERO_REPAIR = {"charsets_deleted": 0, "cmaps_built": 0, "cmaps_rewritten": 0}


def _stub_common(monkeypatch, embedded_count, worst_pct, compliant=True, width_fixed_count=0):
    calls = {}

    monkeypatch.setattr(rp, "_run_e2", _fake_run_e2)
    monkeypatch.setattr(rp.pdf_font_embed, "repair_missing_fonts",
                         lambda pdf: {"embedded": embedded_count, "skipped_no_substitute": 0,
                                       "skipped_type3": 0, "skipped_embedded": 0})
    monkeypatch.setattr(rp.pdf_font_embed, "fix_degenerate_widths",
                         lambda pdf: {"fixed": width_fixed_count, "skipped_uncertain_mapping": 0})
    monkeypatch.setattr(rp.pdf_font_repair, "repair_fonts", lambda pdf: dict(ZERO_REPAIR))
    monkeypatch.setattr(rp.verapdf_gate, "run_verapdf",
                         lambda *_a, **_k: {"compliant": compliant, "failed_summaries": []})

    def fake_pixel_diff(a, b, **kwargs):
        calls["pixel_diff_threshold_pct"] = kwargs.get("threshold_pct")
        return {"identical": worst_pct <= kwargs.get("threshold_pct", 0.0),
                "worst_pct": worst_pct, "page_count": 1, "pages_diff": [], "dpi": 110}

    def fake_structural_diff(a, b, **kwargs):
        calls["structural_delta"] = kwargs.get("delta")
        calls["structural_threshold_pct"] = kwargs.get("threshold_pct")
        return {"identical": worst_pct <= kwargs.get("threshold_pct", 0.0),
                "worst_pct": worst_pct, "page_count": 1, "pages_diff": [], "dpi": 110,
                "delta": kwargs.get("delta")}

    monkeypatch.setattr(rp.fidelity_gate, "pixel_diff", fake_pixel_diff)
    monkeypatch.setattr(rp.fidelity_gate, "structural_diff", fake_structural_diff)
    return calls


class TestEditsFontEmbedKey:
    def test_e2_path_records_font_embed_edits(self, tmp_path, monkeypatch):
        """Unit-level: _run_e2 itself sets edits["font_embed"] before
        font_repair — mirrors test_font_repair.py's ODL-mocked _run_e2 test,
        since a full remediate_pdf() run would need _run_e2 stubbed out
        entirely (which bypasses the line under test)."""
        p = _tiny_pdf(tmp_path / "untagged.pdf")

        def fake_run_odl(pdf_path, outdir, timeout=None):
            import json
            json_path = os.path.join(outdir, "manifest.json")
            with open(json_path, "w", encoding="utf-8") as f:
                json.dump({"kids": []}, f)
            tagged_path = os.path.join(outdir, "tagged.pdf")
            import pikepdf
            pikepdf.open(str(pdf_path)).save(tagged_path)
            return json_path, tagged_path

        monkeypatch.setattr(rp, "_run_odl", fake_run_odl)
        monkeypatch.setattr(rp.pdf_font_embed, "repair_missing_fonts",
                             lambda pdf: {"embedded": 1, "skipped_no_substitute": 0,
                                           "skipped_type3": 0, "skipped_embedded": 0})
        monkeypatch.setattr(rp.pdf_font_repair, "repair_fonts", lambda pdf: dict(ZERO_REPAIR))

        edits, _title = rp._run_e2(p, p, tmp_path / "out.pdf", {}, None, "en-US", [])

        assert edits["font_embed"]["embedded"] == 1

    def test_e3_path_records_font_embed_edits(self, tmp_path, monkeypatch):
        p = _tiny_pdf(tmp_path / "tagged.pdf")
        _stub_common(monkeypatch, embedded_count=0, worst_pct=0.0)

        result = rp.remediate_pdf(p, judgment={}, cls="digital-tagged")

        assert result["edits"]["font_embed"]["embedded"] == 0


class TestEditsFontWidthFixKey:
    """fix_degenerate_widths (7.21.5, in-place hsbw patch) is called on both
    branches immediately after repair_missing_fonts and before repair_fonts
    — same wiring shape as font_embed, but it does NOT feed the bounded
    structural fidelity gate (see TestBoundedFidelityGate below): unlike
    repair_missing_fonts's substitute-outline embed, it patches the
    original Type1 charstrings' width operand in place and never touches
    the outline, so it stays on the ordinary exact-zero pixel_diff gate."""

    def test_e2_path_records_font_width_fix_edits(self, tmp_path, monkeypatch):
        p = _tiny_pdf(tmp_path / "untagged.pdf")

        def fake_run_odl(pdf_path, outdir, timeout=None):
            import json
            json_path = os.path.join(outdir, "manifest.json")
            with open(json_path, "w", encoding="utf-8") as f:
                json.dump({"kids": []}, f)
            tagged_path = os.path.join(outdir, "tagged.pdf")
            import pikepdf
            pikepdf.open(str(pdf_path)).save(tagged_path)
            return json_path, tagged_path

        monkeypatch.setattr(rp, "_run_odl", fake_run_odl)
        monkeypatch.setattr(rp.pdf_font_embed, "repair_missing_fonts",
                             lambda pdf: {"embedded": 0, "skipped_no_substitute": 0,
                                           "skipped_type3": 0, "skipped_embedded": 0})
        monkeypatch.setattr(rp.pdf_font_embed, "fix_degenerate_widths",
                             lambda pdf: {"fixed": 4, "skipped_uncertain_mapping": 0})
        monkeypatch.setattr(rp.pdf_font_repair, "repair_fonts", lambda pdf: dict(ZERO_REPAIR))

        edits, _title = rp._run_e2(p, p, tmp_path / "out.pdf", {}, None, "en-US", [])

        assert edits["font_width_fix"] == {"fixed": 4, "skipped_uncertain_mapping": 0}

    def test_e3_path_records_font_width_fix_edits(self, tmp_path, monkeypatch):
        p = _tiny_pdf(tmp_path / "tagged.pdf")
        _stub_common(monkeypatch, embedded_count=0, worst_pct=0.0, width_fixed_count=4)

        result = rp.remediate_pdf(p, judgment={}, cls="digital-tagged")

        assert result["edits"]["font_width_fix"] == {"fixed": 4, "skipped_uncertain_mapping": 0}


class TestBoundedFidelityGate:
    def test_no_fonts_embedded_uses_exact_zero_pixel_diff(self, tmp_path, monkeypatch):
        p = _tiny_pdf(tmp_path / "tagged.pdf")
        calls = _stub_common(monkeypatch, embedded_count=0, worst_pct=0.0)

        result = rp.remediate_pdf(p, judgment={}, cls="digital-tagged")

        # default call, no threshold_pct kwarg passed to pixel_diff
        assert calls["pixel_diff_threshold_pct"] is None
        assert "structural_delta" not in calls  # structural_diff never called
        assert "font_embed_fidelity" not in result
        assert not any("fonts embedded" in n for n in result["notes"])

    def test_width_fix_alone_still_uses_exact_zero_pixel_diff(self, tmp_path, monkeypatch):
        """fix_degenerate_widths patches the Type1 charstring's wx in place
        (outline untouched) — unlike repair_missing_fonts's substitute
        outline, it never needs the structural-diff slack. A width fix
        with zero font_embed activity must stay on the strict gate."""
        p = _tiny_pdf(tmp_path / "tagged.pdf")
        calls = _stub_common(monkeypatch, embedded_count=0, worst_pct=0.0, width_fixed_count=4)

        result = rp.remediate_pdf(p, judgment={}, cls="digital-tagged")

        assert calls["pixel_diff_threshold_pct"] is None
        assert "structural_delta" not in calls  # structural_diff never called
        assert "font_embed_fidelity" not in result

    def test_fonts_embedded_uses_structural_diff_and_reports_worst_pct(self, tmp_path, monkeypatch):
        p = _tiny_pdf(tmp_path / "tagged.pdf")
        calls = _stub_common(monkeypatch, embedded_count=1, worst_pct=1.4)

        result = rp.remediate_pdf(p, judgment={}, cls="digital-tagged")

        assert calls["structural_delta"] == rp.FONT_EMBED_DELTA
        assert calls["structural_threshold_pct"] == rp.FONT_EMBED_STRUCT_PCT
        assert "pixel_diff_threshold_pct" not in calls  # pixel_diff never called on this path
        assert result["ok"] is True
        assert result["font_embed_fidelity"] == {
            "metric": "structural", "delta": rp.FONT_EMBED_DELTA,
            "threshold_pct": rp.FONT_EMBED_STRUCT_PCT, "worst_pct": 1.4}
        assert any("fonts embedded" in n and "1.4" in n for n in result["notes"])
        assert not any("pixel-identical" in n for n in result["notes"])

    def test_real_layout_break_still_fails_the_gate(self, tmp_path, monkeypatch):
        p = _tiny_pdf(tmp_path / "tagged.pdf")
        _stub_common(monkeypatch, embedded_count=1, worst_pct=40.0)

        result = rp.remediate_pdf(p, judgment={}, cls="digital-tagged")

        assert result["ok"] is False
        assert result["output"] is None
        assert result["rejected_output"] is not None
        assert os.path.exists(result["rejected_output"])


class TestStructuralDiff:
    def test_identical_pdf_is_zero_diff(self, tmp_path):
        p = _tiny_pdf(tmp_path / "a.pdf")

        result = fidelity_gate.structural_diff(str(p), str(p))

        assert result["identical"] is True
        assert result["worst_pct"] == 0.0
        assert result["pages_diff"] == []

    def test_page_count_mismatch_is_not_identical(self, tmp_path):
        a = _tiny_pdf(tmp_path / "a.pdf")
        doc = fitz.open()
        doc.new_page(width=200, height=200)
        doc.new_page(width=200, height=200)
        b = tmp_path / "b.pdf"
        doc.save(str(b))
        doc.close()

        result = fidelity_gate.structural_diff(str(a), str(b))

        assert result["identical"] is False
        assert result["worst_pct"] == 100.0
        assert result["reason"] == "page_count_mismatch"

    def test_structural_break_exceeds_default_threshold(self, tmp_path):
        def _dense_pdf(path, text):
            doc = fitz.open()
            page = doc.new_page(width=200, height=200)
            for i in range(15):
                page.insert_text((10, 20 + i * 12), text, fontsize=10)
            doc.save(str(path))
            doc.close()
            return path

        a = _dense_pdf(tmp_path / "a.pdf", "Hello world this is text")
        b = _dense_pdf(tmp_path / "b.pdf", "XXXXXXXXXXXXXXXXXXXXXXXXX")

        result = fidelity_gate.structural_diff(str(a), str(b))

        assert result["identical"] is False
        assert result["worst_pct"] > 3.5
