"""Unit tests for PDF converter module."""

from __future__ import annotations

import re
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, Mock, patch

import pytest

from markitai.config import (
    ImageConfig,
    LLMConfig,
    MarkitaiConfig,
    OCRConfig,
    ScreenshotConfig,
)
from markitai.converter.base import ConvertResult, ExtractedImage, FileFormat
from markitai.converter.pdf import PdfConverter

if TYPE_CHECKING:
    pass


# Create a reusable mock for pymupdf module
def create_pymupdf_mock() -> Mock:
    """Create a mock pymupdf module with common attributes."""
    mock_pymupdf = Mock()
    mock_pix = Mock()
    mock_pix.width = 100
    mock_pix.height = 200
    mock_pymupdf.Pixmap.return_value = mock_pix
    mock_pymupdf.Matrix.return_value = Mock()
    return mock_pymupdf


class TestPdfConverterInit:
    """Tests for PdfConverter initialization."""

    def test_init_with_no_config(self) -> None:
        """Test PdfConverter initialization without config."""
        converter = PdfConverter()
        assert converter.config is None
        assert FileFormat.PDF in converter.supported_formats

    def test_init_with_config(self) -> None:
        """Test PdfConverter initialization with config."""
        config = MarkitaiConfig()
        converter = PdfConverter(config)
        assert converter.config is config

    def test_supported_formats(self) -> None:
        """Test that PDF format is supported."""
        converter = PdfConverter()
        assert converter.supported_formats == [FileFormat.PDF]

    def test_can_convert_pdf(self) -> None:
        """Test can_convert method for PDF files."""
        converter = PdfConverter()
        assert converter.can_convert("test.pdf") is True
        assert converter.can_convert("test.PDF") is True
        assert converter.can_convert(Path("document.pdf")) is True

    def test_cannot_convert_non_pdf(self) -> None:
        """Test can_convert method for non-PDF files."""
        converter = PdfConverter()
        assert converter.can_convert("test.docx") is False
        assert converter.can_convert("test.txt") is False
        assert converter.can_convert("test.pptx") is False


class TestFixImagePaths:
    """Tests for _fix_image_paths helper method."""

    def test_fix_absolute_paths(self) -> None:
        """Test fixing absolute image paths to relative."""
        converter = PdfConverter()
        image_path = Path("/tmp/output/.markitai/assets")

        markdown = "![](C:/tmp/output/.markitai/assets/image1.jpg)"
        # On POSIX, this won't match since the path uses Windows format
        # Test with POSIX path
        markdown = f"![]({image_path.as_posix()}/image1.jpg)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![](.markitai/assets/image1.jpg)"

    def test_fix_multiple_images(self) -> None:
        """Test fixing multiple image paths."""
        converter = PdfConverter()
        image_path = Path("/home/user/output/.markitai/assets")

        markdown = f"""# Document

![First image]({image_path.as_posix()}/image1.png)

Some text here.

![Second image]({image_path.as_posix()}/image2.jpg)
"""
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert "![First image](.markitai/assets/image1.png)" in result
        assert "![Second image](.markitai/assets/image2.jpg)" in result
        assert image_path.as_posix() not in result

    def test_preserve_alt_text(self) -> None:
        """Test that alt text is preserved when fixing paths."""
        converter = PdfConverter()
        image_path = Path("/tmp/.markitai/assets")

        markdown = f"![Alt text with spaces]({image_path.as_posix()}/image.png)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![Alt text with spaces](.markitai/assets/image.png)"

    def test_empty_alt_text(self) -> None:
        """Test fixing paths with empty alt text."""
        converter = PdfConverter()
        image_path = Path("/tmp/.markitai/assets")

        markdown = f"![]({image_path.as_posix()}/image.png)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![](.markitai/assets/image.png)"

    def test_no_change_for_relative_paths(self) -> None:
        """Test that relative paths are not changed incorrectly."""
        converter = PdfConverter()
        image_path = Path("/different/path")

        markdown = "![](.markitai/assets/image.png)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![](.markitai/assets/image.png)"

    def test_special_characters_in_path(self) -> None:
        """Test handling of special regex characters in path."""
        converter = PdfConverter()
        # Path with special regex characters
        image_path = Path("/tmp/test[1]/.markitai/assets")

        markdown = f"![]({image_path.as_posix()}/image.png)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![](.markitai/assets/image.png)"

    def test_cross_platform_path(self) -> None:
        """Test that POSIX paths work on all platforms."""
        converter = PdfConverter()
        # Use a path that would be different on Windows vs POSIX
        image_path = Path("/home/user/documents/output/.markitai/assets")

        markdown = f"![test]({image_path.as_posix()}/document.pdf-1-0.jpg)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![test](.markitai/assets/document.pdf-1-0.jpg)"

    def test_filename_with_parentheses(self) -> None:
        """Filenames with parentheses must not break path fixing."""
        converter = PdfConverter()
        image_path = Path("/tmp/.markitai/assets")
        markdown = f"![]({image_path.as_posix()}/report(final).pdf-0-0.jpg)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![](.markitai/assets/report(final).pdf-0-0.jpg)"

    def test_filename_with_dollar_sign(self) -> None:
        """Filenames with $ must not break path fixing."""
        converter = PdfConverter()
        image_path = Path("/tmp/.markitai/assets")
        markdown = f"![]({image_path.as_posix()}/price$100.pdf-0-0.jpg)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![](.markitai/assets/price$100.pdf-0-0.jpg)"

    def test_filename_with_spaces(self) -> None:
        """Filenames with spaces must not break path fixing."""
        converter = PdfConverter()
        image_path = Path("/tmp/.markitai/assets")
        markdown = f"![alt text]({image_path.as_posix()}/my document.pdf-0-0.jpg)"
        result = converter._fix_image_paths(markdown, image_path)  # type: ignore[reportAttributeAccessIssue]
        assert result == "![alt text](.markitai/assets/my document.pdf-0-0.jpg)"


class TestCollectEmbeddedImages:
    """Tests for _collect_embedded_images helper method."""

    def test_collect_matching_images(self, tmp_path: Path) -> None:
        """Test collecting images that match the PDF filename pattern."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        # Create test image files with pymupdf4llm naming pattern
        (assets_dir / "test.pdf-0-0.png").touch()
        (assets_dir / "test.pdf-0-1.png").touch()
        (assets_dir / "test.pdf-1-0.png").touch()

        # Mock pymupdf module in sys.modules
        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "test.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert len(images) == 3
        # Check all images are ExtractedImage instances
        for img in images:
            assert isinstance(img, ExtractedImage)
            assert "test.pdf" in img.original_name

    def test_ignore_non_matching_images(self, tmp_path: Path) -> None:
        """Test that non-matching images are ignored."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        # Create matching and non-matching files
        (assets_dir / "test.pdf-0-0.png").touch()
        (assets_dir / "other.pdf-0-0.png").touch()  # Different PDF
        (assets_dir / "random_image.png").touch()  # No pattern
        (assets_dir / "test.pdf.png").touch()  # Wrong pattern

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "test.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert len(images) == 1
        assert images[0].original_name == "test.pdf-0-0.png"

    def test_handle_jpeg_extension(self, tmp_path: Path) -> None:
        """Test collecting images with jpeg extension."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        (assets_dir / "doc.pdf-0-0.jpg").touch()
        (assets_dir / "doc.pdf-0-1.jpeg").touch()

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "doc.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert len(images) == 2
        # Check MIME types are correct
        mime_types = {img.mime_type for img in images}
        assert "image/jpeg" in mime_types

    def test_index_calculation(self, tmp_path: Path) -> None:
        """Test that image index is calculated correctly from page and position."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        # Page 2 (0-indexed), image 3 on that page
        (assets_dir / "doc.pdf-2-3.png").touch()

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "doc.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert len(images) == 1
        # Index should be page_idx * 100 + img_idx = 2 * 100 + 3 = 203
        assert images[0].index == 203

    def test_empty_directory(self, tmp_path: Path) -> None:
        """Test collecting from empty directory."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        images = converter._collect_embedded_images(assets_dir, "test.pdf")  # type: ignore[reportAttributeAccessIssue]
        assert images == []

    def test_handle_dimension_error(self, tmp_path: Path) -> None:
        """Test graceful handling of dimension reading errors."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        (assets_dir / "test.pdf-0-0.png").touch()

        # Create a mock that raises an exception
        mock_pymupdf = Mock()
        mock_pymupdf.Pixmap.side_effect = Exception("Failed to read image")

        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "test.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert len(images) == 1
        assert images[0].width == 0
        assert images[0].height == 0


class TestConvertBasic:
    """Tests for basic convert functionality with mocked pymupdf4llm."""

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_basic_pdf(self, mock_pymupdf4llm: Mock, tmp_path: Path) -> None:
        """Test basic PDF conversion."""
        # Setup mock
        mock_pymupdf4llm.to_markdown.return_value = [
            {"text": "# Page 1\n\nContent on page 1."},
            {"text": "# Page 2\n\nContent on page 2."},
        ]

        # Create a dummy PDF file
        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        assert isinstance(result, ConvertResult)
        assert "Page 1" in result.markdown
        assert "Page 2" in result.markdown
        # Check page markers are added
        assert "<!-- Page number: 1 -->" in result.markdown
        assert "<!-- Page number: 2 -->" in result.markdown
        assert result.metadata["format"] == "PDF"

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_with_output_dir(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test PDF conversion with output directory."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Test content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()
        output_dir = tmp_path / "output"
        output_dir.mkdir()

        converter = PdfConverter()
        _ = converter.convert(pdf_file, output_dir)

        # Check that assets directory was created
        assets_dir = output_dir / ".markitai" / "assets"
        assert assets_dir.exists()

        # Verify pymupdf4llm was called with correct image_path
        call_args = mock_pymupdf4llm.to_markdown.call_args
        assert str(assets_dir) in call_args[1]["image_path"]

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_uses_temp_dir_when_no_output(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test that temp directory is used when no output_dir specified."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        _ = converter.convert(pdf_file)

        # Verify pymupdf4llm was called (temp dir is created internally)
        mock_pymupdf4llm.to_markdown.assert_called_once()

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_demotes_text_heavy_picture_blocks_to_references(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Text-heavy PDF picture blocks should become reference-only assets."""
        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()
        output_dir = tmp_path / "output"
        output_dir.mkdir()

        assets_dir = output_dir / ".markitai" / "assets"

        def build_chunk(
            image_name: str,
            picture_text: str,
            page_number: int,
        ) -> dict[str, object]:
            image_ref = f"![]({(assets_dir / image_name).as_posix()})"
            picture_block = (
                f"{image_ref}\n\n"
                "**----- Start of picture text -----**<br>\n"
                f"{picture_text}"
                "**----- End of picture text -----**<br>\n"
            )
            text = f"Lead paragraph.\n\n{picture_block}\nTail paragraph."
            start = text.index(image_ref)
            end = start + len(picture_block)
            return {
                "metadata": {"page_number": page_number},
                "page_boxes": [
                    {"class": "text", "pos": (0, start)},
                    {"class": "picture", "pos": (start, end)},
                    {"class": "text", "pos": (end, len(text))},
                ],
                "text": text,
            }

        def fake_to_markdown(*args, **kwargs):
            del args
            image_path = Path(kwargs["image_path"])
            image_path.mkdir(parents=True, exist_ok=True)
            (image_path / "test.pdf-0001-10.jpg").touch()
            (image_path / "test.pdf-0002-04.jpg").touch()
            return [
                build_chunk(
                    "test.pdf-0001-10.jpg",
                    "12<br>\n10<br>\nColumn 1<br>\nRow 1 Row 2 Row 3 Row 4<br>\n",
                    1,
                ),
                build_chunk(
                    "test.pdf-0002-04.jpg",
                    (
                        "Header A Header B Header C<br>\n"
                        "1 In eleifend velit vitae libero sollicitudin euismod. "
                        "Lorem<br>\n"
                        "2 Cras fringilla ipsum magna, in fringilla dui commodo a. "
                        "Ipsum<br>\n"
                        "3 Aliquam erat volutpat. Lorem<br>\n"
                        "4 Fusce vitae vestibulum velit. Lorem<br>\n"
                    ),
                    2,
                ),
            ]

        mock_pymupdf4llm.to_markdown.side_effect = fake_to_markdown

        converter = PdfConverter()
        result = converter.convert(pdf_file, output_dir)

        assert ".markitai/assets/test.pdf-0001-10.jpg" in result.markdown
        assert ".markitai/assets/test.pdf-0002-04.jpg" not in result.markdown
        assert len(result.metadata["reference_images"]) == 1
        assert result.metadata["reference_images"][0]["page"] == 2
        assert (
            result.metadata["reference_images"][0]["rel_path"]
            == ".markitai/assets/test.pdf-0002-04.jpg"
        )

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_with_image_format_config(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test PDF conversion respects image format from config."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        # Config with webp format
        config = MarkitaiConfig(image=ImageConfig(format="webp"))
        converter = PdfConverter(config)
        _ = converter.convert(pdf_file)

        call_args = mock_pymupdf4llm.to_markdown.call_args
        assert call_args[1]["image_format"] == "webp"

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_jpeg_format_normalized(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test that jpeg format is normalized to jpg for pymupdf4llm."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        config = MarkitaiConfig(image=ImageConfig(format="jpeg"))
        converter = PdfConverter(config)
        _ = converter.convert(pdf_file)

        call_args = mock_pymupdf4llm.to_markdown.call_args
        assert call_args[1]["image_format"] == "jpg"

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_page_chunks_enabled(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test that page_chunks=True is passed to pymupdf4llm."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        _ = converter.convert(pdf_file)

        call_args = mock_pymupdf4llm.to_markdown.call_args
        assert call_args[1]["page_chunks"] is True
        assert call_args[1]["force_text"] is True

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_convert_disables_pymupdf4llm_ocr_when_markitai_ocr_disabled(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Non-OCR mode must pass use_ocr=False to suppress Tesseract probing."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        config = MarkitaiConfig(ocr=OCRConfig(enabled=False))
        converter = PdfConverter(config)
        converter.convert(pdf_file, tmp_path)

        call_args = mock_pymupdf4llm.to_markdown.call_args
        assert call_args[1]["use_ocr"] is False


class TestConvertWithOCR:
    """Tests for OCR conversion mode."""

    def test_ocr_mode_enabled(self, tmp_path: Path) -> None:
        """Test that OCR mode is triggered when config.ocr.enabled=True."""
        # Setup mock document
        mock_pymupdf = Mock()
        mock_doc = MagicMock()
        mock_doc.__len__ = Mock(return_value=2)
        mock_doc.__enter__ = Mock(return_value=mock_doc)
        mock_doc.__exit__ = Mock(return_value=None)
        mock_doc.close = Mock()
        mock_pymupdf.open.return_value = mock_doc

        # Setup OCR processor mock
        mock_ocr = Mock()
        mock_result = Mock()
        mock_result.text = "OCR extracted text"
        mock_ocr.recognize_pdf_page.return_value = mock_result

        # Create a mock OCRProcessor class
        mock_ocr_processor_cls = Mock(return_value=mock_ocr)

        # Create mock ocr module
        mock_ocr_module = Mock()
        mock_ocr_module.OCRProcessor = mock_ocr_processor_cls

        pdf_file = tmp_path / "scanned.pdf"
        pdf_file.write_bytes(b"%PDF-1.4 dummy content")

        # Enable OCR, disable screenshot
        config = MarkitaiConfig(
            ocr=OCRConfig(enabled=True),
            screenshot=ScreenshotConfig(enabled=False),
        )
        converter = PdfConverter(config)

        with patch.dict(
            sys.modules,
            {"pymupdf": mock_pymupdf, "markitai.ocr": mock_ocr_module},
        ):
            result = converter.convert(pdf_file)

        # Verify OCR was used
        assert result.metadata.get("ocr_used") is True
        mock_ocr.recognize_pdf_page.assert_called()

    def test_ocr_not_used_when_disabled(self, tmp_path: Path) -> None:
        """Test that OCR is not used when disabled."""
        with patch("markitai.converter.pdf.pymupdf4llm") as mock_pymupdf4llm:
            mock_pymupdf4llm.to_markdown.return_value = [{"text": "Normal text"}]

            pdf_file = tmp_path / "normal.pdf"
            pdf_file.touch()

            # OCR disabled (default)
            config = MarkitaiConfig(ocr=OCRConfig(enabled=False))
            converter = PdfConverter(config)
            result = converter.convert(pdf_file)

            # OCR should not be in metadata
            assert result.metadata.get("ocr_used") is not True


class TestConvertWithScreenshot:
    """Tests for screenshot rendering functionality."""

    def test_screenshot_enabled(self, tmp_path: Path) -> None:
        """Test screenshot rendering when enabled."""
        # Setup pymupdf mock
        mock_pymupdf = Mock()
        mock_page = Mock()
        mock_pix = Mock()
        mock_pix.samples = b"fake_pixel_data"
        mock_pix.width = 800
        mock_pix.height = 600
        mock_page.get_pixmap.return_value = mock_pix

        mock_doc = MagicMock()
        mock_doc.__len__ = Mock(return_value=1)
        mock_doc.__getitem__ = Mock(return_value=mock_page)
        mock_doc.close = Mock()
        mock_pymupdf.open.return_value = mock_doc
        mock_pymupdf.Matrix.return_value = Mock()

        # Setup ImageProcessor mock — return ((w,h), output_path)
        mock_img_processor = Mock()
        mock_img_processor.save_screenshot.side_effect = (
            lambda _samples, _w, _h, path, **_kw: ((800, 600), path)
        )

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()
        output_dir = tmp_path / "output"
        output_dir.mkdir()

        # Enable screenshot
        config = MarkitaiConfig(screenshot=ScreenshotConfig(enabled=True))
        converter = PdfConverter(config)

        with patch("markitai.converter.pdf.pymupdf4llm") as mock_pymupdf4llm:
            mock_pymupdf4llm.to_markdown.return_value = [{"text": "Page content"}]
            with (
                patch.dict(sys.modules, {"pymupdf": mock_pymupdf}),
                patch(
                    "markitai.converter.pdf.ImageProcessor",
                    return_value=mock_img_processor,
                ),
            ):
                result = converter.convert(pdf_file, output_dir)

        # Verify screenshot was created
        assert "page_images" in result.metadata
        assert result.metadata.get("pages") == 1
        mock_img_processor.save_screenshot.assert_called()

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_screenshot_disabled_no_rendering(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test that pages are not rendered when screenshot is disabled."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()
        output_dir = tmp_path / "output"
        output_dir.mkdir()

        # Screenshot disabled (default)
        config = MarkitaiConfig(screenshot=ScreenshotConfig(enabled=False))
        converter = PdfConverter(config)
        result = converter.convert(pdf_file, output_dir)

        # page_images should not be in metadata
        assert "page_images" not in result.metadata


class TestRenderPagesForLLM:
    """Tests for _render_pages_for_llm method (OCR + LLM mode)."""

    def test_render_for_llm_extracts_text_and_renders(self, tmp_path: Path) -> None:
        """Test that _render_pages_for_llm extracts text and renders pages."""
        # Setup pymupdf mock for page rendering
        mock_pymupdf = Mock()
        mock_page = Mock()
        mock_pix = Mock()
        mock_pix.samples = b"pixel_data"
        mock_pix.width = 1000
        mock_pix.height = 800
        mock_page.get_pixmap.return_value = mock_pix

        mock_doc = MagicMock()
        mock_doc.__len__ = Mock(return_value=2)
        mock_doc.__getitem__ = Mock(return_value=mock_page)
        mock_doc.__enter__ = Mock(return_value=mock_doc)
        mock_doc.__exit__ = Mock(return_value=None)
        mock_doc.close = Mock()
        mock_pymupdf.open.return_value = mock_doc
        mock_pymupdf.Matrix.return_value = Mock()

        # Setup ImageProcessor mock — return ((w,h), output_path)
        mock_img_processor = Mock()
        mock_img_processor.save_screenshot.side_effect = (
            lambda _samples, _w, _h, path, **_kw: ((1000, 800), path)
        )

        pdf_file = tmp_path / "document.pdf"
        pdf_file.touch()
        output_dir = tmp_path / "output"
        output_dir.mkdir()

        # Enable both OCR and LLM
        config = MarkitaiConfig(
            ocr=OCRConfig(enabled=True),
            llm=LLMConfig(enabled=True),
            screenshot=ScreenshotConfig(enabled=True),
        )
        converter = PdfConverter(config)

        with patch("markitai.converter.pdf.pymupdf4llm") as mock_pymupdf4llm:
            mock_pymupdf4llm.to_markdown.return_value = "Extracted markdown text"
            with (
                patch.dict(sys.modules, {"pymupdf": mock_pymupdf}),
                patch(
                    "markitai.converter.pdf.ImageProcessor",
                    return_value=mock_img_processor,
                ),
            ):
                result = converter.convert(pdf_file, output_dir)

        # Verify text was extracted
        assert "Extracted markdown text" in result.markdown
        # Verify screenshots were taken when screenshot is enabled
        assert "page_images" in result.metadata

    def test_render_pages_for_llm_disables_pymupdf4llm_builtin_ocr(
        self, tmp_path: Path
    ) -> None:
        """OCR+LLM text extraction must also pass use_ocr=False to pymupdf4llm."""
        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        config = MarkitaiConfig(
            ocr=OCRConfig(enabled=True),
            llm=LLMConfig(enabled=True),
        )
        converter = PdfConverter(config)

        with patch("markitai.converter.pdf.pymupdf4llm") as mock_pymupdf4llm:
            mock_pymupdf4llm.to_markdown.return_value = "Extracted text"
            with patch.object(converter, "_render_pages_parallel", return_value=[]):
                converter._render_pages_for_llm(pdf_file, tmp_path)  # type: ignore[reportAttributeAccessIssue]

        call_args = mock_pymupdf4llm.to_markdown.call_args
        assert call_args[1]["use_ocr"] is False


class TestImageCompression:
    """Tests for image compression during conversion."""

    def test_image_compression_when_enabled(self, tmp_path: Path) -> None:
        """Test that images are compressed when config.image.compress=True."""
        # Create a fake image file
        output_dir = tmp_path / "output"
        assets_dir = output_dir / ".markitai" / "assets"
        assets_dir.mkdir(parents=True)

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        # Create a test image that matches the expected pattern
        test_image = assets_dir / "test.pdf-0-0.png"
        test_image.write_bytes(b"fake_image_data")

        # Setup PIL Image mock
        mock_img = MagicMock()
        mock_img.size = (800, 600)
        mock_img.copy.return_value = mock_img

        # Setup ImageProcessor mock
        mock_img_processor = Mock()
        compressed_img = Mock()
        compressed_img.size = (800, 600)
        mock_img_processor.compress.return_value = (compressed_img, b"compressed_data")

        # Enable compression
        config = MarkitaiConfig(image=ImageConfig(compress=True, quality=75))
        converter = PdfConverter(config)

        with patch("markitai.converter.pdf.pymupdf4llm") as mock_pymupdf4llm:
            mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]
            with (
                patch("PIL.Image.open", return_value=mock_img),
                patch(
                    "markitai.converter.pdf.ImageProcessor",
                    return_value=mock_img_processor,
                ),
            ):
                _ = converter.convert(pdf_file, output_dir)

        # Compression should have been attempted
        # (the actual call depends on image file existing)


class TestMetadataGeneration:
    """Tests for metadata generation in convert results."""

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_basic_metadata(self, mock_pymupdf4llm: Mock, tmp_path: Path) -> None:
        """Test that basic metadata is generated correctly."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "document.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        assert result.metadata["source"] == str(pdf_file)
        assert result.metadata["format"] == "PDF"
        assert "images" in result.metadata

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_metadata_with_images(self, mock_pymupdf4llm: Mock, tmp_path: Path) -> None:
        """Test metadata includes image count."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        # images count should be in metadata
        assert "images" in result.metadata
        assert isinstance(result.metadata["images"], int)


class TestPageMarkers:
    """Tests for page marker formatting."""

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_page_markers_format(self, mock_pymupdf4llm: Mock, tmp_path: Path) -> None:
        """Test that page markers are in correct format."""
        mock_pymupdf4llm.to_markdown.return_value = [
            {"text": "Page 1 content"},
            {"text": "Page 2 content"},
            {"text": "Page 3 content"},
        ]

        pdf_file = tmp_path / "multi_page.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        # Check page markers format
        assert "<!-- Page number: 1 -->" in result.markdown
        assert "<!-- Page number: 2 -->" in result.markdown
        assert "<!-- Page number: 3 -->" in result.markdown

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_page_markers_have_blank_line_after(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test that page markers have blank line after for proper markdown formatting."""
        mock_pymupdf4llm.to_markdown.return_value = [{"text": "Content"}]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        # Should have format: "<!-- Page number: N -->\n\nContent"
        marker_pattern = r"<!-- Page number: \d+ -->\n\n"
        assert re.search(marker_pattern, result.markdown)


class TestWorkerCalculation:
    """Tests for adaptive worker count calculation in OCR mode."""

    def test_worker_count_small_file(self, tmp_path: Path) -> None:
        """Test worker count calculation for small files (<10MB)."""
        # Create a small file
        pdf_file = tmp_path / "small.pdf"
        pdf_file.write_bytes(b"x" * (5 * 1024 * 1024))  # 5 MB

        file_size_mb = pdf_file.stat().st_size / (1024 * 1024)
        assert file_size_mb < 10

        # Simulate worker calculation logic
        import os

        cpu_count = os.cpu_count() or 4
        total_pages = 10

        if file_size_mb < 10:
            max_workers = min(cpu_count // 2 or 2, total_pages, 6)
        elif file_size_mb < 50:
            max_workers = min(4, total_pages)
        else:
            max_workers = min(2, total_pages)

        max_workers = max(1, max_workers)

        # Should use up to cpu_count/2 workers, capped at 6
        assert 1 <= max_workers <= 6

    def test_worker_count_large_file(self, tmp_path: Path) -> None:
        """Test worker count for large files (>50MB) is limited."""
        # Simulate large file
        file_size_mb = 60  # 60 MB
        total_pages = 100

        if file_size_mb < 10:
            max_workers = min(4, total_pages, 6)
        elif file_size_mb < 50:
            max_workers = min(4, total_pages)
        else:
            max_workers = min(2, total_pages)

        max_workers = max(1, max_workers)

        # Large files should use at most 2 workers
        assert max_workers == 2


class TestChunkHandling:
    """Tests for handling page chunks from pymupdf4llm."""

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_dict_chunk_handling(self, mock_pymupdf4llm: Mock, tmp_path: Path) -> None:
        """Test handling of dict chunks from pymupdf4llm."""
        mock_pymupdf4llm.to_markdown.return_value = [
            {"text": "First page text", "metadata": {}},
            {"text": "Second page text", "metadata": {}},
        ]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        assert "First page text" in result.markdown
        assert "Second page text" in result.markdown

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_string_chunk_handling(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test handling of string chunks (fallback)."""
        # Some versions might return strings instead of dicts
        mock_pymupdf4llm.to_markdown.return_value = [
            "First page as string",
            "Second page as string",
        ]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        assert "First page as string" in result.markdown
        assert "Second page as string" in result.markdown

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_empty_page_handling(self, mock_pymupdf4llm: Mock, tmp_path: Path) -> None:
        """Test handling of empty pages."""
        mock_pymupdf4llm.to_markdown.return_value = [
            {"text": "Page with content"},
            {"text": ""},  # Empty page
            {"text": "Another page"},
        ]

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        converter = PdfConverter()
        result = converter.convert(pdf_file)

        # Should still have 3 page markers
        assert result.markdown.count("<!-- Page number:") == 3


class TestErrorHandling:
    """Tests for error handling scenarios."""

    @patch("markitai.converter.pdf.pymupdf4llm")
    def test_conversion_error_propagates(
        self, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Test that conversion errors propagate correctly."""
        mock_pymupdf4llm.to_markdown.side_effect = Exception("PDF parsing failed")

        pdf_file = tmp_path / "corrupted.pdf"
        pdf_file.touch()

        converter = PdfConverter()

        with pytest.raises(Exception, match="PDF parsing failed"):
            converter.convert(pdf_file)

    def test_ocr_import_error_handling(self, tmp_path: Path) -> None:
        """Test that ImportError is raised when pymupdf is not available in OCR mode."""
        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        config = MarkitaiConfig(
            ocr=OCRConfig(enabled=True), llm=LLMConfig(enabled=False)
        )
        _ = PdfConverter(config)

        # Remove pymupdf from modules to simulate import failure
        original_modules = sys.modules.copy()
        if "pymupdf" in sys.modules:
            del sys.modules["pymupdf"]

        # The actual import error behavior depends on how the module handles it
        # This test verifies the code path doesn't crash unexpectedly
        try:
            # Restore the module for other tests
            pass
        finally:
            sys.modules.update(original_modules)


class TestMIMETypeHandling:
    """Tests for MIME type handling in extracted images."""

    def test_png_mime_type(self, tmp_path: Path) -> None:
        """Test PNG MIME type is set correctly."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        (assets_dir / "doc.pdf-0-0.png").touch()

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "doc.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert images[0].mime_type == "image/png"

    def test_jpg_mime_type(self, tmp_path: Path) -> None:
        """Test JPG MIME type is set correctly."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        (assets_dir / "doc.pdf-0-0.jpg").touch()

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "doc.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert images[0].mime_type == "image/jpeg"

    def test_jpeg_mime_type(self, tmp_path: Path) -> None:
        """Test JPEG extension MIME type is set correctly."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        (assets_dir / "doc.pdf-0-0.jpeg").touch()

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "doc.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert images[0].mime_type == "image/jpeg"


class TestRenderPagesForLLMWithoutScreenshot:
    """Issue 1: OCR+LLM should render page images regardless of screenshot.enabled."""

    def test_ocr_llm_renders_pages_even_when_screenshot_disabled(
        self, tmp_path: Path
    ) -> None:
        """OCR+LLM path must produce page_images even if screenshot.enabled=False."""
        mock_pymupdf = Mock()
        mock_page = Mock()
        mock_pix = Mock()
        mock_pix.samples = b"pixel_data"
        mock_pix.width = 1000
        mock_pix.height = 800
        mock_page.get_pixmap.return_value = mock_pix

        mock_doc = MagicMock()
        mock_doc.__len__ = Mock(return_value=2)
        mock_doc.__getitem__ = Mock(return_value=mock_page)
        mock_doc.close = Mock()
        mock_pymupdf.open.return_value = mock_doc
        mock_pymupdf.Matrix.return_value = Mock()

        mock_img_processor = Mock()
        mock_img_processor.save_screenshot.side_effect = (
            lambda _samples, _w, _h, path, **_kw: ((1000, 800), path)
        )

        pdf_file = tmp_path / "document.pdf"
        pdf_file.touch()
        output_dir = tmp_path / "output"
        output_dir.mkdir()

        # OCR + LLM enabled, but screenshot.enabled=False (default)
        config = MarkitaiConfig(
            ocr=OCRConfig(enabled=True),
            llm=LLMConfig(enabled=True),
            screenshot=ScreenshotConfig(enabled=False),
        )
        converter = PdfConverter(config)

        with patch("markitai.converter.pdf.pymupdf4llm") as mock_pymupdf4llm:
            mock_pymupdf4llm.to_markdown.return_value = "Extracted text"
            with (
                patch.dict(sys.modules, {"pymupdf": mock_pymupdf}),
                patch(
                    "markitai.converter.pdf.ImageProcessor",
                    return_value=mock_img_processor,
                ),
            ):
                result = converter.convert(pdf_file, output_dir)

        # Even with screenshot.enabled=False, OCR+LLM must produce page images
        assert "page_images" in result.metadata
        assert len(result.metadata["page_images"]) == 2
        assert result.metadata.get("pages") == 2


class TestDanglingImagePaths:
    """Issue 2: No output_dir should not return dangling image paths."""

    def test_no_output_dir_images_not_dangling(self, tmp_path: Path) -> None:
        """When output_dir is None, returned images must not reference deleted temp paths."""
        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        captured_image_path: list[Path] = []

        def fake_to_markdown(**kwargs):
            """Simulate pymupdf4llm extracting an image into the image_path."""
            image_path = Path(kwargs["image_path"])
            captured_image_path.append(image_path)
            # Simulate pymupdf4llm creating an embedded image
            fake_img = image_path / f"{pdf_file.name}-0-0.png"
            fake_img.touch()
            return [{"text": f"![](/{image_path}/{pdf_file.name}-0-0.png)"}]

        converter = PdfConverter()

        with patch("markitai.converter.pdf.pymupdf4llm") as mock_pymupdf4llm:
            mock_pymupdf4llm.to_markdown.side_effect = lambda _doc, **kw: (
                fake_to_markdown(**kw)
            )
            result = converter.convert(pdf_file)  # no output_dir

        # After convert returns, any images should not reference deleted dirs
        for img in result.images:
            if img.path is not None:
                assert img.path.exists(), (
                    f"Image path {img.path} is dangling (temp dir deleted)"
                )


class TestEmbeddedImagesWebp:
    """Issue 4: _collect_embedded_images must handle webp format."""

    def test_collect_webp_images(self, tmp_path: Path) -> None:
        """webp images should be collected by _collect_embedded_images."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        (assets_dir / "doc.pdf-0-0.webp").touch()
        (assets_dir / "doc.pdf-1-0.webp").touch()

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "doc.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert len(images) == 2
        for img in images:
            assert img.mime_type == "image/webp"

    def test_collect_mixed_formats_including_webp(self, tmp_path: Path) -> None:
        """All supported formats (png, jpg, jpeg, webp) should be collected."""
        converter = PdfConverter()
        assets_dir = tmp_path / "assets"
        assets_dir.mkdir()

        (assets_dir / "doc.pdf-0-0.png").touch()
        (assets_dir / "doc.pdf-0-1.jpg").touch()
        (assets_dir / "doc.pdf-0-2.jpeg").touch()
        (assets_dir / "doc.pdf-0-3.webp").touch()

        mock_pymupdf = create_pymupdf_mock()
        with patch.dict(sys.modules, {"pymupdf": mock_pymupdf}):
            images = converter._collect_embedded_images(assets_dir, "doc.pdf")  # type: ignore[reportAttributeAccessIssue]

        assert len(images) == 4
        extensions = {img.original_name.rsplit(".", 1)[-1] for img in images}
        assert extensions == {"png", "jpg", "jpeg", "webp"}


class TestThreadPoolLimits:
    """Issue 3: Internal thread pools should have bounded worker counts."""

    def test_worker_count_never_exceeds_cap(self, tmp_path: Path) -> None:
        """_get_worker_count must always cap at a reasonable maximum."""
        converter = PdfConverter()

        # Create a small file to trigger the "small file" branch
        pdf_file = tmp_path / "small.pdf"
        pdf_file.write_bytes(b"x" * (1 * 1024 * 1024))  # 1 MB

        workers = converter._get_worker_count(pdf_file, task_count=1000)  # type: ignore[reportAttributeAccessIssue]
        # Even with many pages and small file, should not exceed 6
        assert workers <= 6

    def test_worker_count_minimum_one(self, tmp_path: Path) -> None:
        """Worker count should never be less than 1."""
        converter = PdfConverter()

        pdf_file = tmp_path / "tiny.pdf"
        pdf_file.write_bytes(b"x" * 100)  # Very small

        workers = converter._get_worker_count(pdf_file, task_count=0)  # type: ignore[reportAttributeAccessIssue]
        assert workers >= 1


class TestPdfTempDirCleanup:
    """Tests for temp_dir cleanup on exception paths."""

    @patch("markitai.converter.pdf.pymupdf4llm")
    @patch("markitai.converter.pdf.tempfile.mkdtemp")
    def test_temp_dir_cleaned_on_exception(
        self, mock_mkdtemp: Mock, mock_pymupdf4llm: Mock, tmp_path: Path
    ) -> None:
        """Temp directory is removed even when conversion throws."""
        leaked_dir = tmp_path / "leaked_temp"
        leaked_dir.mkdir()
        mock_mkdtemp.return_value = str(leaked_dir)
        mock_pymupdf4llm.to_markdown.side_effect = RuntimeError("corrupt PDF")

        converter = PdfConverter()
        with pytest.raises(RuntimeError, match="corrupt PDF"):
            converter.convert(Path("fake.pdf"), output_dir=None)

        assert not leaked_dir.exists(), "temp_dir should be cleaned up on exception"


class TestScreenshotExtensionConsistency:
    """save_screenshot may change .png to .jpg in the extreme fallback.

    PDF converter must use the actual path returned by save_screenshot
    so that ExtractedImage metadata points to the real file on disk.
    """

    def test_pdf_screenshot_uses_actual_path_from_save_screenshot(
        self, tmp_path: Path
    ) -> None:
        """When save_screenshot returns a different path (e.g. .jpg instead of .png),
        the PDF converter _render_pages_parallel must use that path in
        ExtractedImage and page_images metadata.
        """
        mock_pymupdf = Mock()
        mock_page = Mock()
        mock_pix = Mock()
        mock_pix.samples = b"fake_pixel_data"
        mock_pix.width = 800
        mock_pix.height = 600
        mock_page.get_pixmap.return_value = mock_pix

        mock_doc = MagicMock()
        mock_doc.__len__ = Mock(return_value=1)
        mock_doc.__getitem__ = Mock(return_value=mock_page)
        mock_doc.close = Mock()
        mock_pymupdf.open.return_value = mock_doc
        mock_pymupdf.Matrix.return_value = Mock()

        # Simulate save_screenshot returning a DIFFERENT path (.jpg instead of .png)
        # This happens when the extreme compression fallback kicks in
        screenshots_dir = tmp_path / "output" / ".markitai" / "screenshots"
        screenshots_dir.mkdir(parents=True, exist_ok=True)
        actual_jpg_path = screenshots_dir / "test.pdf.page0001.jpg"
        actual_jpg_path.write_bytes(b"\xff\xd8fake_jpeg")

        mock_img_processor = Mock()
        mock_img_processor.save_screenshot.return_value = (
            (800, 600),
            actual_jpg_path,
        )

        pdf_file = tmp_path / "test.pdf"
        pdf_file.touch()

        config = MarkitaiConfig(
            screenshot=ScreenshotConfig(enabled=True),
            image=ImageConfig(format="png"),
        )
        converter = PdfConverter(config)

        with (
            patch.dict(sys.modules, {"pymupdf": mock_pymupdf}),
            patch(
                "markitai.converter.pdf.ImageProcessor",
                return_value=mock_img_processor,
            ),
        ):
            results = converter._render_pages_parallel(  # type: ignore[reportAttributeAccessIssue]
                pdf_file, screenshots_dir, "png", max_workers=1
            )

        # The ExtractedImage path must match the actual file (jpg, not png)
        assert len(results) == 1
        extracted_img, page_info = results[0]
        assert extracted_img.path == actual_jpg_path
        assert extracted_img.original_name == "test.pdf.page0001.jpg"
        assert extracted_img.mime_type == "image/jpeg"

        # page_info metadata must also use the actual path
        assert page_info["path"] == str(actual_jpg_path)
        assert page_info["name"] == "test.pdf.page0001.jpg"
