"""Tests for scripts/mcid_text.py — content-stream MCID -> text extraction.

CI-light: pure pikepdf fixtures built in-memory, 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 mcid_text as mt  # noqa: E402


def _page_with_content(content: bytes):
    pdf = pikepdf.new()
    page = pdf.add_blank_page(page_size=(200, 200))
    page.Contents = pdf.make_stream(content)
    return pdf, page


# ── page_mcid_text: basic mapping ───────────────────────────────────────────

def test_page_mcid_text_maps_two_sequences():
    pdf, page = _page_with_content(
        b"/P <</MCID 0>> BDC\nBT (Hello) Tj ET\nEMC\n"
        b"/Span <</MCID 1>> BDC\nBT [(Wor) 20 (ld)] TJ ET\nEMC\n"
    )
    assert mt.page_mcid_text(page) == {0: "Hello", 1: "World"}
    pdf.close()


def test_page_mcid_text_accepts_raw_page_object_too():
    pdf, page = _page_with_content(b"/P <</MCID 0>> BDC\nBT (Hi) Tj ET\nEMC\n")
    assert mt.page_mcid_text(page.obj) == {0: "Hi"}
    pdf.close()


# ── nested BDC attributes to innermost MCID ─────────────────────────────────

def test_nested_bdc_attributes_text_to_innermost_mcid():
    pdf, page = _page_with_content(
        b"/Sect <</MCID 0>> BDC\n"
        b"BT (Outer) Tj ET\n"
        b"/Span <</MCID 1>> BDC\n"
        b"BT (Inner) Tj ET\n"
        b"EMC\n"
        b"BT (Tail) Tj ET\n"
        b"EMC\n"
    )
    result = mt.page_mcid_text(page)
    assert result[1] == "Inner"
    assert result[0] == "OuterTail"
    pdf.close()


# ── malformed content stream never raises ───────────────────────────────────

def test_malformed_content_stream_returns_empty_dict_not_raise():
    pdf, page = _page_with_content(b"\xff\xfe not a real content stream ( unbalanced")
    assert mt.page_mcid_text(page) == {}
    pdf.close()


def test_page_with_no_contents_returns_empty_dict():
    pdf = pikepdf.new()
    page = pdf.add_blank_page(page_size=(200, 200))
    assert mt.page_mcid_text(page) == {}
    pdf.close()


# ── ToUnicode CMap decoding ──────────────────────────────────────────────────

_CMAP = b"""/CIDInit /ProcSet findresource begin
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
2 beginbfchar
<0001> <0048>
<0002> <0069>
endbfchar
1 beginbfrange
<0003> <0004> <006A>
endbfrange
end"""


def test_tounicode_mapped_text_decodes_correctly():
    pdf = pikepdf.new()
    page = pdf.add_blank_page(page_size=(200, 200))
    font = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/Font"), "/Subtype": pikepdf.Name("/Type0"),
        "/Encoding": pikepdf.Name("/Identity-H"),
        "/ToUnicode": pdf.make_stream(_CMAP),
    }))
    page.obj["/Resources"] = pikepdf.Dictionary(
        {"/Font": pikepdf.Dictionary({"/F1": font})})
    # codes 0001 0002 0003 0004 -> H i j k  ("Hijk")
    page.Contents = pdf.make_stream(
        b"/P <</MCID 0>> BDC\nBT /F1 12 Tf <0001000200030004> Tj ET\nEMC\n")
    assert mt.page_mcid_text(page) == {0: "Hijk"}
    pdf.close()


# ── WinAnsi high-byte fallback (no ToUnicode) ───────────────────────────────

def test_winansi_high_byte_decodes_via_encoding_fallback():
    pdf = pikepdf.new()
    page = pdf.add_blank_page(page_size=(200, 200))
    font = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/Font"), "/Subtype": pikepdf.Name("/Type1"),
        "/Encoding": pikepdf.Name("/WinAnsiEncoding"),
        "/BaseFont": pikepdf.Name("/Helvetica"),
    }))
    page.obj["/Resources"] = pikepdf.Dictionary(
        {"/Font": pikepdf.Dictionary({"/F1": font})})
    # 0xE9 in WinAnsi/cp1252 is é
    page.Contents = pdf.make_stream(
        b"/P <</MCID 0>> BDC\nBT /F1 12 Tf (caf\xe9) Tj ET\nEMC\n")
    assert mt.page_mcid_text(page) == {0: "café"}
    pdf.close()


def test_macroman_encoding_uses_mac_roman_codec():
    pdf = pikepdf.new()
    page = pdf.add_blank_page(page_size=(200, 200))
    font = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/Font"), "/Subtype": pikepdf.Name("/Type1"),
        "/Encoding": pikepdf.Name("/MacRomanEncoding"),
        "/BaseFont": pikepdf.Name("/Helvetica"),
    }))
    page.obj["/Resources"] = pikepdf.Dictionary(
        {"/Font": pikepdf.Dictionary({"/F1": font})})
    # 0x8E in MacRoman is é (different byte than WinAnsi's 0xE9)
    page.Contents = pdf.make_stream(
        b"/P <</MCID 0>> BDC\nBT /F1 12 Tf (caf\x8e) Tj ET\nEMC\n")
    assert mt.page_mcid_text(page) == {0: "café"}
    pdf.close()


# ── caching correctness across distinct Pdf instances (regression) ─────────

def test_cache_does_not_collide_across_different_pdf_instances():
    """Two freshly-created single-page PDFs deterministically allocate the
    same low object numbers for their page — objgen alone is NOT a safe
    cache key across Pdf instances (pikepdf.Object equality/objgen ignores
    owning Pdf). This must not leak file A's text onto file B's page."""
    pdf_a, page_a = _page_with_content(b"/P <</MCID 0>> BDC\nBT (FromA) Tj ET\nEMC\n")
    pdf_b, page_b = _page_with_content(b"/P <</MCID 0>> BDC\nBT (FromB) Tj ET\nEMC\n")
    assert page_a.obj.objgen == page_b.obj.objgen  # same objgen, different Pdf
    assert mt.page_mcid_text(page_a) == {0: "FromA"}
    assert mt.page_mcid_text(page_b) == {0: "FromB"}
    pdf_a.close()
    pdf_b.close()


def test_sweep_of_many_closed_pdfs_never_crashes_or_cross_contaminates():
    """Simulates the real batch-sweep shape (judgment_eval.py's
    build_candidates(): open, extract, close, repeat) — the module-level
    cache accumulates references to CLOSED Pdf instances across the sweep,
    and later lookups must scan past those stale entries without raising
    or (worse) segfaulting, and without ever returning a wrong file's text."""
    for i in range(15):
        pdf, page = _page_with_content(f"/P <</MCID 0>> BDC\nBT (File{i}) Tj ET\nEMC\n".encode())
        assert mt.page_mcid_text(page) == {0: f"File{i}"}
        pdf.close()

    pdf, page = _page_with_content(b"/P <</MCID 0>> BDC\nBT (Final) Tj ET\nEMC\n")
    assert mt.page_mcid_text(page) == {0: "Final"}
    pdf.close()
