"""
Tests for Track B3.6 — PDF drawing preservation.

Covers:
  - Color backgrounds (e.g. "Avis important" box on light blue) are
    preserved through translation (not erased by redaction).
  - Title overflow: long translated titles expand horizontally + vertically
    to avoid overlapping the next block.
  - Transparent redaction (fill=None) is used when a block overlaps a
    drawing, to preserve the drawing.
"""
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock

import pytest
import importlib.util

_REPO_ROOT = Path(__file__).resolve().parent.parent.parent


def _load_pdf_translator():
    spec = importlib.util.spec_from_file_location(
        "pdf_translator_under_test",
        _REPO_ROOT / "translators" / "pdf_translator.py",
    )
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


@pytest.fixture(scope="module")
def pdf_mod():
    return _load_pdf_translator()


# ============================================================================
# Constants
# ============================================================================

class TestB3_6Constants:
    def test_vertical_expansion_6x(self, pdf_mod):
        """MAX_VERTICAL_EXPANSION should be 6x (B3.6) to handle long titles."""
        assert pdf_mod.MAX_VERTICAL_EXPANSION == pytest.approx(6.0, abs=0.1)

    def test_transparent_redaction_uses_fill_none(self, pdf_mod):
        """When a block overlaps a drawing, redaction uses fill=None (transparent)."""
        import fitz
        page = MagicMock()
        # Return one drawing (a colored rect) for get_drawings()
        drawing = MagicMock()
        drawing.get.return_value = fitz.Rect(72, 480, 500, 550)
        drawing.get.side_effect = lambda key, default=None: {
            "rect": fitz.Rect(72, 480, 500, 550),
            "fill": (0.85, 0.92, 1.0),
        }.get(key, default)
        page.get_drawings = MagicMock(return_value=[drawing])
        page.add_redact_annot = MagicMock()

        # Simulate the logic from _process_pages_inplace
        drawing_rects = []
        for d in page.get_drawings():
            r = d.get("rect")
            if r is not None and d.get("fill") is not None:
                drawing_rects.append(fitz.Rect(r))

        # Block that intersects the drawing
        block_bbox = fitz.Rect(85, 500, 200, 520)
        intersects = any(
            block_bbox.intersects(dr) for dr in drawing_rects
        )
        assert intersects is True

        if intersects:
            page.add_redact_annot(block_bbox, fill=None)
        else:
            page.add_redact_annot(block_bbox, fill=(1, 1, 1))

        # Verify the redaction was added with fill=None (transparent)
        call_kwargs = page.add_redact_annot.call_args[1]
        assert call_kwargs.get("fill") is None, (
            f"Expected fill=None for block overlapping drawing, "
            f"got fill={call_kwargs.get('fill')}"
        )

    def test_opaque_redaction_for_text_only_blocks(self, pdf_mod):
        """For blocks WITHOUT drawings, redaction uses white fill (opaque)."""
        import fitz
        page = MagicMock()
        page.get_drawings = MagicMock(return_value=[])  # no drawings
        page.add_redact_annot = MagicMock()

        drawing_rects = []
        for d in page.get_drawings():
            r = d.get("rect")
            if r is not None and d.get("fill") is not None:
                drawing_rects.append(fitz.Rect(r))

        block_bbox = fitz.Rect(100, 200, 400, 220)
        intersects = any(
            block_bbox.intersects(dr) for dr in drawing_rects
        )
        assert intersects is False  # no drawings → no intersection

        if intersects:
            page.add_redact_annot(block_bbox, fill=None)
        else:
            page.add_redact_annot(block_bbox, fill=(1, 1, 1))

        call_kwargs = page.add_redact_annot.call_args[1]
        assert call_kwargs.get("fill") == (1, 1, 1), (
            f"Expected white fill for text-only block, got {call_kwargs.get('fill')}"
        )


# ============================================================================
# Real PDF test
# ============================================================================

class TestB3_6RealPDF:
    def test_colored_box_preserved_in_translation(self, tmp_path):
        """A PDF with a colored background box (e.g. "Avis important") should
        keep that color in the translated output."""
        import fitz

        # Build a minimal PDF: light blue box with text inside
        doc = fitz.open()
        page = doc.new_page()
        # Light blue background
        page.draw_rect(
            fitz.Rect(72, 480, 500, 550),
            color=(0.9, 0.95, 1.0),
            fill=(0.85, 0.92, 1.0),
        )
        page.insert_text((85, 500), "Important notice", fontsize=14, color=(0.1, 0.2, 0.5))

        in_path = tmp_path / "input.pdf"
        out_path = tmp_path / "output.pdf"
        doc.save(str(in_path))
        doc.close()

        # Translate
        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()
        translator.translate_file(
            in_path, out_path,
            target_language="fr",
            source_language="en",
        )

        # Verify the blue box is still there
        out_doc = fitz.open(str(out_path))
        out_page = out_doc[0]
        drawings = out_page.get_drawings()
        # Find a drawing with the blue fill
        blue_drawings = [
            d for d in drawings
            if d.get("fill") == pytest.approx((0.85, 0.92, 1.0), abs=0.01)
        ]
        assert len(blue_drawings) >= 1, (
            f"Blue background drawing was lost! Got {len(drawings)} drawings: "
            f"{[(d.get('rect'), d.get('fill')) for d in drawings]}"
        )
        out_doc.close()

    def test_title_does_not_overflow_into_next_block(self, tmp_path):
        """A long translated title should not overlap the next block."""
        import fitz
        doc = fitz.open()
        page = doc.new_page()
        # Title at top
        page.insert_text(
            (72, 80),
            "A very long title that should wrap to two lines in translation",
            fontsize=22,
        )
        # Next block at y=110 (very close — should be problematic)
        page.insert_text(
            (72, 110),
            "Subtitle text",
            fontsize=11,
        )

        in_path = tmp_path / "input.pdf"
        out_path = tmp_path / "output.pdf"
        doc.save(str(in_path))
        doc.close()

        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()
        translator.translate_file(
            in_path, out_path,
            target_language="fr",
            source_language="en",
        )

        # Verify the title doesn't overlap the subtitle
        out_doc = fitz.open(str(out_path))
        out_page = out_doc[0]
        text_dict = out_page.get_text("dict")

        # Get title bbox and subtitle bbox
        title_bbox = None
        subtitle_bbox = None
        for block in text_dict.get("blocks", []):
            if block.get("type") != 0:
                continue
            block_text = " ".join(
                s.get("text", "")
                for line in block.get("lines", [])
                for s in line.get("spans", [])
            ).lower()
            if "specification" in block_text or "sp" in block_text and "technique" in block_text:
                title_bbox = block.get("bbox")
            elif "version" in block_text or "subtitle" in block_text:
                subtitle_bbox = block.get("bbox")

        if title_bbox and subtitle_bbox:
            title_bottom = title_bbox[3]
            subtitle_top = subtitle_bbox[1]
            assert title_bottom <= subtitle_top + 5, (
                f"Title bottom ({title_bottom}) overlaps subtitle top ({subtitle_top}). "
                f"Title: {title_bbox}, Subtitle: {subtitle_bbox}"
            )
        out_doc.close()


# ============================================================================
# Drawing re-application (alternative strategy)
# ============================================================================

class TestB3_6DrawingReapplication:
    """The alternative strategy: re-apply original drawings on top of
    redaction. We use the cleaner strategy (transparent redaction) but
    keep this test for the legacy approach."""

    def test_drawings_collected_before_redaction(self, pdf_mod):
        """The page's drawings should be captured BEFORE the redaction."""
        import fitz
        page = MagicMock()
        page.get_drawings = MagicMock(return_value=[
            {"rect": fitz.Rect(0, 0, 100, 100), "fill": (1, 0, 0)},
        ])

        # Capture
        captured = []
        if hasattr(page, "get_drawings"):
            try:
                captured = list(page.get_drawings())
            except Exception:
                captured = []
        assert len(captured) == 1
        assert captured[0]["fill"] == (1, 0, 0)


# ============================================================================
# Track B3.7 — next-block-aware vertical expansion
# ============================================================================

class TestB3_7NextBlockCeiling:
    """Track B3.7: the smart-fit logic must cap vertical expansion at
    the next block's y0, not at the page bottom. Otherwise, a long
    translated block can flow into the block below it."""

    def test_next_block_y_populated_for_each_block(self, pdf_mod):
        """For 3 vertically stacked blocks, the middle one should have
        the third's y0 as its next_block_y, and the last one the page bottom."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        page = MagicMock()
        page.rect = fitz.Rect(0, 0, 612, 792)
        blocks = [
            {"bbox": (72, 100, 540, 120), "font_size": 12, "text": "A"},
            {"bbox": (72, 200, 540, 220), "font_size": 12, "text": "B"},
            {"bbox": (72, 300, 540, 320), "font_size": 12, "text": "C"},
        ]
        translator._populate_next_block_y(blocks, page.rect)
        assert blocks[0]["_next_block_y"] == 198   # block 1 y0 - 2
        assert blocks[1]["_next_block_y"] == 298   # block 2 y0 - 2
        assert blocks[2]["_next_block_y"] == 774   # page bottom (792 - 18)

    def test_next_block_y_sorted_by_y0(self, pdf_mod):
        """If blocks are passed out of order, _populate_next_block_y
        must still produce correct next-block mappings."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        page = MagicMock()
        page.rect = fitz.Rect(0, 0, 612, 792)
        blocks = [
            {"bbox": (72, 300, 540, 320), "font_size": 12, "text": "C"},
            {"bbox": (72, 100, 540, 120), "font_size": 12, "text": "A"},
            {"bbox": (72, 200, 540, 220), "font_size": 12, "text": "B"},
        ]
        translator._populate_next_block_y(blocks, page.rect)
        # Find the 'A' block (y0=100) and verify it points to 'B' (y0=200)
        a_block = next(b for b in blocks if b["text"] == "A")
        assert a_block["_next_block_y"] == 198

    def test_long_block_does_not_overflow_into_neighbour(self, tmp_path):
        """End-to-end: a long French translation should not visually
        overlap the block below it."""
        import fitz
        doc = fitz.open()
        page = doc.new_page()
        # Block 1: a short English title that will translate to a long French title
        page.insert_textbox(
            fitz.Rect(72, 100, 540, 120),
            "Latest version",
            fontsize=12,
        )
        # Block 2: a TOC entry very close below
        page.insert_textbox(
            fitz.Rect(72, 130, 540, 145),
            "8. Troubleshooting",
            fontsize=12,
        )

        in_path = tmp_path / "input.pdf"
        out_path = tmp_path / "output.pdf"
        doc.save(str(in_path))
        doc.close()

        # Translate with a provider that returns a much longer French string
        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()
        mock = MagicMock()
        mock.translate_batch = MagicMock(
            side_effect=lambda texts, tgt, src: [
                "Pour la dernière version de ce document, veuillez consulter le site web officiel à l'adresse suivante : " + t
                if t and t != "8. Troubleshooting" else t
                for t in texts
            ]
        )
        translator.set_provider(mock)
        translator.translate_file(
            in_path, out_path,
            target_language="fr",
            source_language="en",
        )

        # Verify: no block bottom should exceed the next block's top
        out_doc = fitz.open(str(out_path))
        out_page = out_doc[0]
        text_blocks = [
            b for b in out_page.get_text("dict").get("blocks", [])
            if b.get("type") == 0
        ]
        # Sort by y0 and check that no two consecutive blocks overlap
        text_blocks.sort(key=lambda b: b["bbox"][1])
        for i in range(len(text_blocks) - 1):
            top_of_next = text_blocks[i + 1]["bbox"][1]
            bottom_of_curr = text_blocks[i]["bbox"][3]
            assert bottom_of_curr <= top_of_next + 1, (
                f"Block {i} bottom ({bottom_of_curr}) overlaps block {i+1} top ({top_of_next}). "
                f"Block {i}: {text_blocks[i]['bbox']}, Block {i+1}: {text_blocks[i+1]['bbox']}"
            )
        out_doc.close()


# ============================================================================
# Track B3.8 — column-aware next_block_y (generic for multi-column PDFs)
# ============================================================================

class TestB3_8ColumnAwareNextBlockY:
    """Track B3.8: PDFs with multi-column layouts (journals, brochures,
    newspapers) must not have the 'next block' of a left-column block
    point to a right-column block.

    Also tests: edge cases like centered full-width headers, blocks at
    identical y0 in different columns, and the max_expand_y negative
    clamp."""

    def test_two_columns_get_separate_next_block_y(self, pdf_mod):
        """2-column layout: left column blocks must point to the next
        LEFT-column block, not the right-column block at the same y."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        page = MagicMock()
        page.rect = fitz.Rect(0, 0, 612, 792)

        # Left column: 3 blocks at x0=72
        # Right column: 3 blocks at x0=320
        blocks = [
            # Left column (top to bottom)
            {"bbox": (72, 100, 290, 120), "font_size": 12, "text": "L1"},
            {"bbox": (72, 200, 290, 220), "font_size": 12, "text": "L2"},
            {"bbox": (72, 300, 290, 320), "font_size": 12, "text": "L3"},
            # Right column (top to bottom)
            {"bbox": (320, 100, 540, 120), "font_size": 12, "text": "R1"},
            {"bbox": (320, 200, 540, 220), "font_size": 12, "text": "R2"},
            {"bbox": (320, 300, 540, 320), "font_size": 12, "text": "R3"},
        ]
        translator._populate_next_block_y(blocks, page.rect)

        # Left column: L1 -> L2, L2 -> L3, L3 -> page bottom
        l1 = next(b for b in blocks if b["text"] == "L1")
        l2 = next(b for b in blocks if b["text"] == "L2")
        l3 = next(b for b in blocks if b["text"] == "L3")
        assert l1["_next_block_y"] == 198   # L2 y0 - 2
        assert l2["_next_block_y"] == 298   # L3 y0 - 2
        assert l3["_next_block_y"] == 774   # page bottom

        # Right column: R1 -> R2, R2 -> R3, R3 -> page bottom
        r1 = next(b for b in blocks if b["text"] == "R1")
        r2 = next(b for b in blocks if b["text"] == "R2")
        r3 = next(b for b in blocks if b["text"] == "R3")
        assert r1["_next_block_y"] == 198
        assert r2["_next_block_y"] == 298
        assert r3["_next_block_y"] == 774

    def test_centered_full_width_header_gets_own_column(self, pdf_mod):
        """A full-width centered header (x0=200, x1=500) between two
        columns should be its own column. Its 'next block' should be
        the page bottom (no full-width blocks below)."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        page = MagicMock()
        page.rect = fitz.Rect(0, 0, 612, 792)

        blocks = [
            {"bbox": (200, 50, 500, 70), "font_size": 18, "text": "HEADER"},
            {"bbox": (72, 100, 290, 120), "font_size": 12, "text": "L1"},
            {"bbox": (320, 100, 540, 120), "font_size": 12, "text": "R1"},
        ]
        translator._populate_next_block_y(blocks, page.rect)

        header = next(b for b in blocks if b["text"] == "HEADER")
        # Header is its own column, so its next_block_y is page bottom
        assert header["_next_block_y"] == 774

    def test_three_columns(self, pdf_mod):
        """3-column layout (e.g. newspaper) should also be handled."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        page = MagicMock()
        page.rect = fitz.Rect(0, 0, 612, 792)

        blocks = [
            {"bbox": (72, 100, 230, 120), "text": "C1a"},
            {"bbox": (72, 200, 230, 220), "text": "C1b"},
            {"bbox": (250, 100, 410, 120), "text": "C2a"},
            {"bbox": (250, 200, 410, 220), "text": "C2b"},
            {"bbox": (430, 100, 590, 120), "text": "C3a"},
            {"bbox": (430, 200, 590, 220), "text": "C3b"},
        ]
        translator._populate_next_block_y(blocks, page.rect)

        c1b = next(b for b in blocks if b["text"] == "C1b")
        c2b = next(b for b in blocks if b["text"] == "C2b")
        c3b = next(b for b in blocks if b["text"] == "C3b")
        # Each column's last block goes to page bottom
        assert c1b["_next_block_y"] == 774
        assert c2b["_next_block_y"] == 774
        assert c3b["_next_block_y"] == 774

    def test_single_block_page(self, pdf_mod):
        """A page with a single block should have its next_block_y = page bottom."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        page = MagicMock()
        page.rect = fitz.Rect(0, 0, 612, 792)

        blocks = [{"bbox": (72, 100, 540, 120), "text": "ONLY"}]
        translator._populate_next_block_y(blocks, page.rect)
        assert blocks[0]["_next_block_y"] == 774

    def test_empty_block_list(self, pdf_mod):
        """An empty block list should not crash."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        page = MagicMock()
        page.rect = fitz.Rect(0, 0, 612, 792)

        blocks = []
        # Should be a no-op
        translator._populate_next_block_y(blocks, page.rect)
        assert blocks == []


class TestB3_8MaxExpandYNegativeClamp:
    """Track B3.8: max_expand_y must never be negative (would create
    an invalid fitz.Rect with y1 < y0)."""

    def test_max_expand_y_clamped_to_zero(self, pdf_mod):
        """When next_block_y sits above the current block, the
        expansion should be clamped to 0 (no expansion)."""
        import fitz
        # Use a real PDFTranslator to test the full smart-fit path
        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()

        # Build a doc and check the actual computation path
        # by inspecting the logic
        original_rect = fitz.Rect(72, 100, 540, 120)  # height 20
        # Simulate next_block_y ABOVE original_rect.y1 (impossible normally,
        # but covers the edge case where extracted blocks have weird ordering)
        next_block_y = 50  # above y0=100
        max_expand_y = max(
            0.0,
            min(
                next_block_y - original_rect.y1,  # 50 - 120 = -70
                original_rect.height * 6.0,        # 120
            ),
        )
        assert max_expand_y == 0.0, (
            f"Expected max_expand_y to be clamped to 0, got {max_expand_y}"
        )

    def test_two_column_pdf_translation_end_to_end(self, tmp_path):
        """End-to-end: a 2-column PDF with a centered full-width title
        translates cleanly. PyMuPDF merges the 2 columns per y into a
        single horizontal block, but the column-aware next_block_y logic
        still applies (each horizontal block is its own row)."""
        import fitz
        doc = fitz.open()
        page = doc.new_page(width=612, height=792)
        # Centered title (full-width)
        page.insert_text((200, 70), "CENTERED HEADER", fontsize=18)
        # 3 rows of 2-column content
        for i, y in enumerate([150, 250, 350]):
            page.insert_text((72, y), f"Left col {i+1}", fontsize=12)
            page.insert_text((320, y), f"Right col {i+1}", fontsize=12)

        in_path = tmp_path / "input_2col.pdf"
        out_path = tmp_path / "output_2col.pdf"
        doc.save(str(in_path))
        doc.close()

        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()

        # Build a mock that handles BOTH the new-style
        # (list of TranslationRequest) and legacy-style call signatures.
        def _flexible_translate(*args, **kwargs):
            if args and hasattr(args[0], "__iter__") and not isinstance(args[0], str):
                first = args[0][0] if len(args[0]) else None
                if first is not None and hasattr(first, "text"):
                    class _R:
                        def __init__(self, t): self.translated_text = t
                    return [_R(r.text) for r in args[0]]
            return list(args[0]) if args else []

        mock = MagicMock()
        mock.__class__.__name__ = "MagicMock"  # ensure is_new_style detection
        mock.translate_batch = MagicMock(side_effect=_flexible_translate)
        translator.set_provider(mock)
        translator.translate_file(
            in_path, out_path,
            target_language="fr",
            source_language="en",
        )

        # Verify: 4 blocks present (1 title + 3 horizontal merged blocks)
        out_doc = fitz.open(str(out_path))
        out_page = out_doc[0]
        text_blocks = [
            b for b in out_page.get_text("dict").get("blocks", [])
            if b.get("type") == 0
        ]
        assert len(text_blocks) >= 4, (
            f"Expected >= 4 blocks (1 title + 3 content rows), got {len(text_blocks)}"
        )

        # The title's next_block_y must point to the first content row,
        # not the page bottom (the title is at y=70, so it should have
        # room to expand to ~y=150-2=148).
        # We can't directly read _next_block_y from the output, but we
        # can verify the title's bbox is preserved at y~70, not pushed
        # down to y>700.
        title_block = text_blocks[0]
        assert title_block["bbox"][1] < 100, (
            f"Title was pushed down (y0={title_block['bbox'][1]}), "
            f"expected near 70"
        )
        out_doc.close()


# ============================================================================
# Track B3.9 — table row preservation (column structure survives translation)
# ============================================================================

class TestB3_9TableRowSplitting:
    """Track B3.9: a PDF 'block' that contains multiple LINES at the
    SAME y but different x positions is a table row. The extractor must
    split it into one sub-block per line so the column structure
    survives translation.

    Without B3.9, the whole row is treated as one paragraph and
    `insert_textbox` writes the text left-aligned at x0, collapsing
    all columns into one."""

    def test_horizontal_layout_detected(self, pdf_mod):
        """A block with lines at the same y and different x is
        detected as a horizontal layout (table row)."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        # Mock page that returns one block with 3 lines at same y
        mock_page = MagicMock()
        mock_page.get_text.return_value = {
            "blocks": [{
                "type": 0,
                "bbox": (90, 188, 460, 203),
                "lines": [
                    {"bbox": (90, 188, 162, 203), "spans": [
                        {"text": "Col 1", "size": 12, "font": "helv",
                         "color": 0, "flags": 0, "origin": (90, 188)},
                    ]},
                    {"bbox": (220, 188, 292, 203), "spans": [
                        {"text": "Col 2", "size": 12, "font": "helv",
                         "color": 0, "flags": 0, "origin": (220, 188)},
                    ]},
                    {"bbox": (350, 188, 460, 203), "spans": [
                        {"text": "Col 3", "size": 12, "font": "helv",
                         "color": 0, "flags": 0, "origin": (350, 188)},
                    ]},
                ],
            }],
        }
        blocks = translator._extract_text_blocks(mock_page)
        # Should be split into 3 separate blocks (one per column)
        assert len(blocks) == 3, f"Expected 3 blocks, got {len(blocks)}"
        assert blocks[0]["text"] == "Col 1"
        assert blocks[1]["text"] == "Col 2"
        assert blocks[2]["text"] == "Col 3"
        # Each block should have its own bbox (not the merged row bbox)
        assert blocks[0]["bbox"][0] == 90
        assert blocks[1]["bbox"][0] == 220
        assert blocks[2]["bbox"][0] == 350

    def test_vertical_layout_kept_as_one_block(self, pdf_mod):
        """A block with lines at DIFFERENT y (same x) is a multi-line
        paragraph. It must NOT be split."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        mock_page = MagicMock()
        mock_page.get_text.return_value = {
            "blocks": [{
                "type": 0,
                "bbox": (72, 100, 540, 150),
                "lines": [
                    {"bbox": (72, 100, 540, 115), "spans": [
                        {"text": "Line 1", "size": 12, "font": "helv",
                         "color": 0, "flags": 0, "origin": (72, 100)},
                    ]},
                    {"bbox": (72, 120, 540, 135), "spans": [
                        {"text": "Line 2", "size": 12, "font": "helv",
                         "color": 0, "flags": 0, "origin": (72, 120)},
                    ]},
                    {"bbox": (72, 140, 540, 150), "spans": [
                        {"text": "Line 3", "size": 12, "font": "helv",
                         "color": 0, "flags": 0, "origin": (72, 140)},
                    ]},
                ],
            }],
        }
        blocks = translator._extract_text_blocks(mock_page)
        # Should remain 1 block with the 3 lines joined
        assert len(blocks) == 1
        assert blocks[0]["text"] == "Line 1\nLine 2\nLine 3"
        assert blocks[0]["line_count"] == 3

    def test_single_line_block_unchanged(self, pdf_mod):
        """A block with a single line is unchanged (no split)."""
        import fitz
        translator = pdf_mod.PDFTranslator()
        mock_page = MagicMock()
        mock_page.get_text.return_value = {
            "blocks": [{
                "type": 0,
                "bbox": (72, 100, 300, 115),
                "lines": [
                    {"bbox": (72, 100, 300, 115), "spans": [
                        {"text": "Just one line", "size": 12, "font": "helv",
                         "color": 0, "flags": 0, "origin": (72, 100)},
                    ]},
                ],
            }],
        }
        blocks = translator._extract_text_blocks(mock_page)
        assert len(blocks) == 1
        assert blocks[0]["text"] == "Just one line"

    def test_table_cell_each_at_own_x(self, tmp_path):
        """End-to-end: a 3-column table in a PDF must be translated
        with each cell staying at its original x position.

        Note: PyMuPDF re-groups the cells into row-blocks when reading
        the output back (3 lines per block), but each LINE (cell) must
        be at its original x position. The test checks the line x0
        values, not the block count.
        """
        import fitz
        doc = fitz.open()
        page = doc.new_page(width=612, height=792)
        # Build a 3-col table row by inserting text at 3 x positions on
        # the same y. PyMuPDF groups them as a single block with 3 lines.
        page.insert_text((90, 200), "Name", fontsize=12)
        page.insert_text((220, 200), "Score", fontsize=12)
        page.insert_text((350, 200), "Rank", fontsize=12)
        page.insert_text((90, 220), "Alice", fontsize=12)
        page.insert_text((220, 220), "95", fontsize=12)
        page.insert_text((350, 220), "1", fontsize=12)

        in_path = tmp_path / "input_table.pdf"
        out_path = tmp_path / "output_table.pdf"
        doc.save(str(in_path))
        doc.close()

        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()

        def _flexible_translate(*args, **kwargs):
            if args and hasattr(args[0], "__iter__") and not isinstance(args[0], str):
                first = args[0][0] if len(args[0]) else None
                if first is not None and hasattr(first, "text"):
                    class _R:
                        def __init__(self, t): self.translated_text = t
                    return [_R(r.text) for r in args[0]]
            return list(args[0]) if args else []

        mock = MagicMock()
        mock.__class__.__name__ = "MagicMock"
        mock.translate_batch = MagicMock(side_effect=_flexible_translate)
        translator.set_provider(mock)
        translator.translate_file(
            in_path, out_path,
            target_language="fr",
            source_language="en",
        )

        out_doc = fitz.open(str(out_path))
        out_page = out_doc[0]
        text_blocks = [
            b for b in out_page.get_text("dict").get("blocks", [])
            if b.get("type") == 0
        ]
        # We should see 2 row-blocks, each containing 3 cell-lines
        assert len(text_blocks) == 2, (
            f"Expected 2 row-blocks, got {len(text_blocks)}"
        )
        for row_block in text_blocks:
            lines = row_block.get("lines", [])
            assert len(lines) == 3, (
                f"Row block should have 3 cells, got {len(lines)}"
            )
            # Each cell's x0 should be one of the 3 column positions
            cell_x0s = sorted(line["bbox"][0] for line in lines)
            assert cell_x0s == [90.0, 220.0, 350.0], (
                f"Cell x0s are {cell_x0s}, expected [90, 220, 350]"
            )
        out_doc.close()


# ============================================================================
# Track B3.10 — code-block / callout preservation (drawing-covered blocks)
# ============================================================================

class TestB3_10DrawingCoveredBlocks:
    """Track B3.10: when a block is covered by a colored background
    drawing (code block, callout box, info box), each line must stay
    at its own y position. The merge logic must NOT collapse them
    into a single block, because the surrounding drawing has a fixed
    extent that the rewritten text must align with.

    Without B3.10, the merge logic joins all 5 code lines into one
    block, the smart-fit shrinks the font to fit, and the rewritten
    text no longer aligns with the background drawing — half the code
    spills outside the box, the box appears clipped, etc.
    """

    def test_code_block_lines_marked_no_merge(self, tmp_path):
        """A code block (multiple lines, all covered by a background
        drawing) should be split into per-line blocks with _no_merge=True."""
        import fitz
        doc = fitz.open()
        page = doc.new_page(width=612, height=792)
        # Add a light-gray background drawing
        page.draw_rect(
            fitz.Rect(72, 290, 540, 400),
            color=(0.9, 0.9, 0.9),
            fill=(0.95, 0.95, 0.95),
        )
        # 5 code lines inside the drawing (use larger spacing for
        # reliable y separation after PyMuPDF baseline adjustment)
        for i, y in enumerate([305, 325, 345, 365, 385]):
            page.insert_text((85, y), f"line {i+1}", fontsize=10)

        in_path = tmp_path / "input_code.pdf"
        doc.save(str(in_path))
        doc.close()

        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()
        in_doc = fitz.open(str(in_path))
        blocks = translator._extract_text_blocks(in_doc[0])
        in_doc.close()

        # Should have 5 separate blocks, each with no_merge=True
        code_blocks = [b for b in blocks if b.get("_no_merge")]
        assert len(code_blocks) == 5, (
            f"Expected 5 code blocks with no_merge=True, got {len(code_blocks)}"
        )
        # Each should have a distinct y position (within 1pt of each other)
        y_positions = [b["bbox"][1] for b in code_blocks]
        # Sort and check gaps are >= ~16pt (line spacing)
        sorted_ys = sorted(y_positions)
        for i in range(len(sorted_ys) - 1):
            gap = sorted_ys[i + 1] - sorted_ys[i]
            assert gap > 10, (
                f"y positions too close: {sorted_ys}, gap={gap}"
            )

    def test_paragraph_not_marked_no_merge(self, tmp_path):
        """A regular paragraph (no background drawing) should NOT
        be marked as no_merge. The merge logic should still combine
        consecutive lines into a single block."""
        import fitz
        doc = fitz.open()
        page = doc.new_page(width=612, height=792)
        # 3 lines, no background drawing
        page.insert_text((72, 100), "Line 1", fontsize=12)
        page.insert_text((72, 120), "Line 2", fontsize=12)
        page.insert_text((72, 140), "Line 3", fontsize=12)

        in_path = tmp_path / "input_para.pdf"
        doc.save(str(in_path))
        doc.close()

        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()
        in_doc = fitz.open(str(in_path))
        blocks = translator._extract_text_blocks(in_doc[0])
        merged = translator._merge_adjacent_blocks(blocks, in_doc[0].rect)
        in_doc.close()

        # All 3 lines should be merged into 1 block
        assert len(merged) == 1, (
            f"Expected 1 merged paragraph, got {len(merged)}"
        )
        assert "Line 1" in merged[0]["text"]
        assert "Line 2" in merged[0]["text"]
        assert "Line 3" in merged[0]["text"]

    def test_code_block_end_to_end_preserves_lines(self, tmp_path):
        """End-to-end: a code block translated must keep each line
        at its original y position, inside the background drawing."""
        import fitz
        doc = fitz.open()
        page = doc.new_page(width=612, height=792)
        # Background drawing
        page.draw_rect(
            fitz.Rect(72, 290, 540, 410),
            color=(0.9, 0.9, 0.9),
            fill=(0.95, 0.95, 0.95),
        )
        # 5 code lines (use larger spacing for reliable separation)
        code_lines = [
            "$ npm install",
            "$ npm start",
            "$ curl http://localhost:3000",
            "$ echo done",
            "$ exit",
        ]
        for i, line in enumerate(code_lines):
            page.insert_text((85, 305 + i * 20), line, fontsize=10)

        in_path = tmp_path / "input_code_e2e.pdf"
        out_path = tmp_path / "output_code_e2e.pdf"
        doc.save(str(in_path))
        doc.close()

        from translators.pdf_translator import PDFTranslator
        translator = PDFTranslator()

        def _flex(*args, **kwargs):
            if args and hasattr(args[0], "__iter__") and not isinstance(args[0], str):
                first = args[0][0] if len(args[0]) else None
                if first is not None and hasattr(first, "text"):
                    class _R:
                        def __init__(self, t): self.translated_text = t
                    return [_R(r.text) for r in args[0]]
            return list(args[0]) if args else []

        mock = MagicMock()
        mock.__class__.__name__ = "MagicMock"
        mock.translate_batch = MagicMock(side_effect=_flex)
        translator.set_provider(mock)
        translator.translate_file(
            in_path, out_path,
            target_language="fr",
            source_language="en",
        )

        out_doc = fitz.open(str(out_path))
        out_page = out_doc[0]
        text_blocks = [
            b for b in out_page.get_text("dict").get("blocks", [])
            if b.get("type") == 0
        ]
        # Should have 5 separate code-line blocks, each at its own y
        code_blocks = [
            b for b in text_blocks
            if b["bbox"][0] == 85 and b["bbox"][1] >= 290
        ]
        assert len(code_blocks) == 5, (
            f"Expected 5 separate code lines in output, got {len(code_blocks)}"
        )
        # Each line should be at a unique y position (no merging)
        y_positions = [b["bbox"][1] for b in code_blocks]
        assert len(set(y_positions)) == 5, (
            f"Code lines were collapsed to fewer y positions: {y_positions}"
        )
        # All code lines should be inside the background drawing
        for b in code_blocks:
            y = b["bbox"][1]
            assert 290 <= y <= 400, (
                f"Code line at y={y} is outside the background drawing "
                f"(y range 290-410)"
            )
        out_doc.close()
