"""Unit tests for PDFHighlighter.compute_chunk_bboxes_batch (Deck #76).

Replaces the legacy `highlight_chunks_batch`-+-base64 pipeline that inflated
Qdrant payloads with per-chunk PNG screenshots. The new path returns
normalized bounding boxes only.
"""

from __future__ import annotations

import pymupdf
import pytest

from nextcloud_mcp_server.search.pdf_highlighter import PDFHighlighter


def _make_pdf(pages: list[str]) -> bytes:
    """Build an in-memory PDF whose pages contain the given text."""
    doc = pymupdf.open()
    for body in pages:
        page = doc.new_page(width=595, height=842)  # A4
        page.insert_text((50, 50), body)
    pdf_bytes = doc.tobytes()
    doc.close()
    return pdf_bytes


def _page_boundaries(pages: list[str]) -> tuple[list[dict], str]:
    """Build (page_boundaries, full_text) compatible with the highlighter API."""
    boundaries: list[dict] = []
    cursor = 0
    parts: list[str] = []
    for i, body in enumerate(pages, start=1):
        end = cursor + len(body)
        boundaries.append({"page": i, "start_offset": cursor, "end_offset": end})
        parts.append(body)
        cursor = end
    return boundaries, "".join(parts)


@pytest.mark.unit
def test_compute_chunk_bboxes_returns_normalized_rects():
    """Each returned bbox should be 4 floats in [0, 1] tagged with the page."""
    pages = [
        "Chapter 1: Introduction. Nextcloud is a self-hosted collaboration platform "
        "covering installation, configuration and maintenance topics.",
        "Chapter 2: Installation. Download the package, extract it to the web "
        "server directory, and configure the database connection.",
    ]
    pdf_bytes = _make_pdf(pages)
    boundaries, full_text = _page_boundaries(pages)

    chunks = [
        (
            0,
            0,
            len(pages[0]),
            1,
            "Chapter 1: Introduction. Nextcloud is a self-hosted collaboration platform.",
        ),
        (
            1,
            len(pages[0]),
            len(pages[0]) + len(pages[1]),
            2,
            "Chapter 2: Installation. Download the package.",
        ),
    ]

    results = PDFHighlighter.compute_chunk_bboxes_batch(
        pdf_bytes=pdf_bytes,
        chunks=chunks,
        page_boundaries=boundaries,
        full_text=full_text,
    )

    assert set(results) == {0, 1}

    bboxes_p1, page_p1 = results[0]
    bboxes_p2, page_p2 = results[1]

    assert page_p1 == 1
    assert page_p2 == 2

    for rects in (bboxes_p1, bboxes_p2):
        assert len(rects) >= 1
        for rect in rects:
            assert len(rect) == 4
            x0, y0, x1, y1 = rect
            assert 0.0 <= x0 < x1 <= 1.0
            assert 0.0 <= y0 < y1 <= 1.0


@pytest.mark.unit
def test_compute_chunk_bboxes_empty_input():
    assert (
        PDFHighlighter.compute_chunk_bboxes_batch(
            pdf_bytes=b"",
            chunks=[],
            page_boundaries=[],
            full_text="",
        )
        == {}
    )


@pytest.mark.unit
def test_compute_chunk_bboxes_omits_when_offsets_out_of_range():
    """Chunks whose offsets fall outside every page boundary are omitted.

    Verifies the docstring contract: *"Chunks whose bbox cannot be located
    are omitted from the result."* (path: ``find_chunk_page`` returns None).
    """
    pages = ["Page one body text content here for the test."]
    pdf_bytes = _make_pdf(pages)
    boundaries, full_text = _page_boundaries(pages)

    # Offsets way beyond the document end — no page boundary matches.
    out_of_range_start = len(full_text) + 1000
    out_of_range_end = out_of_range_start + 50
    chunks = [(0, out_of_range_start, out_of_range_end, 1, "irrelevant")]

    results = PDFHighlighter.compute_chunk_bboxes_batch(
        pdf_bytes=pdf_bytes,
        chunks=chunks,
        page_boundaries=boundaries,
        full_text=full_text,
    )

    assert results == {}


@pytest.mark.unit
def test_compute_chunk_bboxes_omits_when_text_not_in_pdf():
    """Chunks whose page-relative text isn't on the page are omitted.

    Verifies the second omission path: ``_find_chunk_bbox`` returns None
    when the supplied text cannot be located on the rendered page.
    """
    pages = ["Hello world."]
    pdf_bytes = _make_pdf(pages)
    # Build boundaries from the real text but pass a *different* full_text
    # so the page-relative slice is content that does not exist in the PDF.
    boundaries, _ = _page_boundaries(pages)
    bogus_full_text = "Z" * len(pages[0])

    chunks = [(0, 0, len(pages[0]), 1, "ignored")]

    results = PDFHighlighter.compute_chunk_bboxes_batch(
        pdf_bytes=pdf_bytes,
        chunks=chunks,
        page_boundaries=boundaries,
        full_text=bogus_full_text,
    )

    assert results == {}


@pytest.mark.unit
@pytest.mark.parametrize("page_index", [0, 1])
def test_compute_chunk_bboxes_assigns_correct_page(page_index: int):
    """Verify the page number returned matches the page the chunk lives on."""
    pages = [
        "Page one talks about apples and oranges in detail.",
        "Page two discusses bananas and grapes thoroughly.",
    ]
    pdf_bytes = _make_pdf(pages)
    boundaries, full_text = _page_boundaries(pages)

    if page_index == 0:
        chunk_text = "apples and oranges"
        offsets = (0, len(pages[0]))
    else:
        chunk_text = "bananas and grapes"
        offsets = (len(pages[0]), len(pages[0]) + len(pages[1]))

    chunks = [(0, offsets[0], offsets[1], page_index + 1, chunk_text)]

    results = PDFHighlighter.compute_chunk_bboxes_batch(
        pdf_bytes=pdf_bytes,
        chunks=chunks,
        page_boundaries=boundaries,
        full_text=full_text,
    )

    assert results, "expected a bbox for the chunk"
    _, page_num = results[0]
    assert page_num == page_index + 1


@pytest.mark.unit
def test_compute_chunk_bboxes_handles_unordered_page_boundaries():
    """Page lookup must match by ``page`` key, not by list position.

    Regression guard: an earlier implementation indexed
    ``page_boundaries[page_num - 1]``, which silently produces a wrong
    bbox if boundaries are passed out of order. Reverse the boundaries
    and assert the result is identical to the in-order case.
    """
    pages = [
        "Page one talks about apples and oranges in detail.",
        "Page two discusses bananas and grapes thoroughly.",
    ]
    pdf_bytes = _make_pdf(pages)
    boundaries, full_text = _page_boundaries(pages)

    chunks = [
        (0, 0, len(pages[0]), 1, "apples and oranges"),
        (
            1,
            len(pages[0]),
            len(pages[0]) + len(pages[1]),
            2,
            "bananas and grapes",
        ),
    ]

    in_order = PDFHighlighter.compute_chunk_bboxes_batch(
        pdf_bytes=pdf_bytes,
        chunks=chunks,
        page_boundaries=boundaries,
        full_text=full_text,
    )
    reversed_order = PDFHighlighter.compute_chunk_bboxes_batch(
        pdf_bytes=pdf_bytes,
        chunks=chunks,
        page_boundaries=list(reversed(boundaries)),
        full_text=full_text,
    )

    assert in_order == reversed_order
    assert reversed_order[0][1] == 1
    assert reversed_order[1][1] == 2
