"""Tests for scripts/pdf_table_weave.py convert_tables_to_lists.

CI-light: pure pikepdf fixtures built in-memory, no vendored ODL/java/verapdf.
"""

import os
import sys

import pikepdf

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

import pdf_table_weave as ptw


def _cell(pdf, text=None):
    kids = []
    if text is not None:
        p = pdf.make_indirect(pikepdf.Dictionary(
            {"/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/P"),
             "/ActualText": pikepdf.String(text)}))
        kids.append(p)
    return pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/TD"),
        "/K": pikepdf.Array(kids) if kids else pikepdf.Array([]),
    }))


def _row(pdf, cells):
    return pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/TR"),
        "/K": pikepdf.Array(cells),
    }))


def _table(pdf, rows, caption=None):
    kids = ([caption] if caption is not None else []) + list(rows)
    return pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/Table"),
        "/K": pikepdf.Array(kids),
    }))


def _group(pdf, tag, rows):
    """A /THead, /TBody, or /TFoot row-grouping struct elem — the PDF spec's
    standard tagged-table shape (/Table -> /TBody -> /TR -> /TD)."""
    return pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name(tag),
        "/K": pikepdf.Array(rows),
    }))


def _doc_with_tables(pdf, tables):
    doc = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/Document"),
        "/K": pikepdf.Array(tables),
    }))
    root = pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructTreeRoot"), "/K": doc,
    })
    pdf.Root.StructTreeRoot = root
    return doc


def _text_of(li):
    """Read the /ActualText tagged onto an /LI's re-parented content."""
    body = ptw._kids(li)[0]
    p = ptw._kids(body)[0]
    return str(p["/ActualText"])


def _two_table_doc():
    pdf = pikepdf.new()
    # Word grid authored to read down each column: col0=[a1,a2], col1=[b1,b2]
    t0 = _table(pdf, [
        _row(pdf, [_cell(pdf, "a1"), _cell(pdf, "b1")]),
        _row(pdf, [_cell(pdf, "a2"), _cell(pdf, "b2")]),
    ])
    t1 = _table(pdf, [
        _row(pdf, [_cell(pdf, "Name"), _cell(pdf, "Score")]),
        _row(pdf, [_cell(pdf, "Alice"), _cell(pdf, "9")]),
    ])
    _doc_with_tables(pdf, [t0, t1])
    return pdf


def test_row_order_default_produces_row_major_li_and_removes_table():
    pdf = _two_table_doc()
    result = ptw.convert_tables_to_lists(pdf, [{"index": 0}])
    assert result["converted"] == 1
    assert result["items_created"] == 4
    assert result["skipped"] == []

    doc = pdf.Root.StructTreeRoot["/K"]
    kids = ptw._kids(doc)
    assert str(kids[0]["/S"]) == "/L"  # /Table gone, /L present in same slot
    assert str(kids[1]["/S"]) == "/Table"  # table 1 untouched (not in specs)

    li_items = ptw._kids(kids[0])
    assert [str(li["/S"]) for li in li_items] == ["/LI"] * 4
    assert [_text_of(li) for li in li_items] == ["a1", "b1", "a2", "b2"]  # row-major


def test_column_order_emits_column_major_sequence():
    pdf = _two_table_doc()
    result = ptw.convert_tables_to_lists(pdf, [{"index": 0, "order": "column"}])
    assert result["converted"] == 1

    doc = pdf.Root.StructTreeRoot["/K"]
    lst = ptw._kids(doc)[0]
    li_items = ptw._kids(lst)
    assert [_text_of(li) for li in li_items] == ["a1", "a2", "b1", "b2"]  # column-major


def test_content_structelems_reparented_not_rebuilt():
    pdf = _two_table_doc()
    tables = ptw._find_tables(pdf)
    original_p = ptw._kids(ptw._kids(ptw._kids(tables[0])[0])[0])[0]
    original_key = original_p.objgen

    ptw.convert_tables_to_lists(pdf, [{"index": 0}])

    doc = pdf.Root.StructTreeRoot["/K"]
    lst = ptw._kids(doc)[0]
    li0 = ptw._kids(lst)[0]
    body = ptw._kids(li0)[0]
    p = ptw._kids(body)[0]
    assert p.objgen == original_key  # same object, reparented under /LBody
    assert str(p["/S"]) == "/P"


def test_opt_in_table_not_in_specs_keeps_td():
    pdf = _two_table_doc()
    ptw.convert_tables_to_lists(pdf, [{"index": 0}])

    doc = pdf.Root.StructTreeRoot["/K"]
    t1 = ptw._kids(doc)[1]
    assert str(t1["/S"]) == "/Table"
    row0 = ptw._kids(ptw._kids(t1)[0])
    for cell in row0:
        assert str(cell["/S"]) == "/TD"


def test_caption_preserved_as_first_child_not_an_li():
    pdf = pikepdf.new()
    caption = pdf.make_indirect(pikepdf.Dictionary(
        {"/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/Caption")}))
    table = _table(pdf, [
        _row(pdf, [_cell(pdf, "a"), _cell(pdf, "b")]),
    ], caption=caption)
    _doc_with_tables(pdf, [table])

    result = ptw.convert_tables_to_lists(pdf, [{"index": 0}])
    assert result["converted"] == 1
    assert result["items_created"] == 2  # caption not counted as an item

    doc = pdf.Root.StructTreeRoot["/K"]
    lst = ptw._kids(doc)[0]
    kids = ptw._kids(lst)
    assert str(kids[0]["/S"]) == "/Caption"
    assert kids[0].objgen == caption.objgen
    assert [str(k["/S"]) for k in kids[1:]] == ["/LI", "/LI"]


def test_empty_cell_produces_no_li():
    pdf = pikepdf.new()
    table = _table(pdf, [
        _row(pdf, [_cell(pdf, "a"), _cell(pdf)]),  # second cell has no /K content
    ])
    _doc_with_tables(pdf, [table])

    result = ptw.convert_tables_to_lists(pdf, [{"index": 0}])
    assert result["items_created"] == 1

    doc = pdf.Root.StructTreeRoot["/K"]
    lst = ptw._kids(doc)[0]
    assert len(ptw._kids(lst)) == 1


def test_ragged_rows_column_order_no_indexerror():
    pdf = pikepdf.new()
    table = _table(pdf, [
        _row(pdf, [_cell(pdf, "a"), _cell(pdf, "b"), _cell(pdf, "c")]),
        _row(pdf, [_cell(pdf, "x"), _cell(pdf, "y")]),  # short row
    ])
    _doc_with_tables(pdf, [table])

    result = ptw.convert_tables_to_lists(pdf, [{"index": 0, "order": "column"}])
    assert result["converted"] == 1
    assert result["items_created"] == 5  # no IndexError, ragged row just contributes fewer

    doc = pdf.Root.StructTreeRoot["/K"]
    lst = ptw._kids(doc)[0]
    li_items = ptw._kids(lst)
    assert [_text_of(li) for li in li_items] == ["a", "x", "b", "y", "c"]


def test_out_of_range_index_is_skipped_not_raised():
    pdf = _two_table_doc()
    result = ptw.convert_tables_to_lists(pdf, [{"index": 99}])
    assert result["converted"] == 0
    assert result["items_created"] == 0
    assert len(result["skipped"]) == 1
    assert result["skipped"][0]["reason"] == "index out of range"

    # Nothing in the tree was touched.
    tables = ptw._find_tables(pdf)
    assert len(tables) == 2


# ── row-group traversal (/THead, /TBody, /TFoot) ────────────────────────────

def test_row_grouped_table_converts_correct_li_count_no_orphan_groups():
    pdf = pikepdf.new()
    thead = _group(pdf, "/THead", [_row(pdf, [_cell(pdf, "Name"), _cell(pdf, "Score")])])
    tbody = _group(pdf, "/TBody", [_row(pdf, [_cell(pdf, "Alice"), _cell(pdf, "9")])])
    table = pdf.make_indirect(pikepdf.Dictionary({
        "/Type": pikepdf.Name("/StructElem"), "/S": pikepdf.Name("/Table"),
        "/K": pikepdf.Array([thead, tbody]),
    }))
    _doc_with_tables(pdf, [table])

    result = ptw.convert_tables_to_lists(pdf, [{"index": 0}])
    assert result["converted"] == 1
    assert result["items_created"] == 4  # before the fix: 0 (no cell-bearing rows found)
    assert result["skipped"] == []

    doc = pdf.Root.StructTreeRoot["/K"]
    kids = ptw._kids(doc)
    assert len(kids) == 1
    assert str(kids[0]["/S"]) == "/L"  # /Table (and its THead/TBody kids) gone

    li_items = ptw._kids(kids[0])
    assert [str(li["/S"]) for li in li_items] == ["/LI"] * 4
    assert [_text_of(li) for li in li_items] == ["Name", "Score", "Alice", "9"]

    # No orphaned /THead or /TBody shell reachable anywhere in the tree —
    # the whole /Table (including its row groups) was replaced by /L.
    def _walk_tags(node, acc):
        acc.append(str(node.get("/S", "")))
        for k in ptw._kids(node):
            _walk_tags(k, acc)
    tags = []
    _walk_tags(doc, tags)
    assert "/THead" not in tags
    assert "/TBody" not in tags
    assert "/Table" not in tags
