import pytest

pypdf = pytest.importorskip("pypdf", reason="pypdf not installed")


class TestPdfPluginStructure:
    def test_import_plugin(self) -> None:
        from elizaos_plugin_pdf import PdfPlugin

        assert PdfPlugin is not None

    def test_import_client(self) -> None:
        from elizaos_plugin_pdf import PdfClient

        assert PdfClient is not None

    def test_import_types(self) -> None:
        from elizaos_plugin_pdf import (
            PdfConversionResult,
            PdfDocumentInfo,
            PdfExtractionOptions,
        )

        assert PdfConversionResult is not None
        assert PdfExtractionOptions is not None
        assert PdfDocumentInfo is not None


class TestPdfPluginCreation:
    def test_create_plugin(self) -> None:
        from elizaos_plugin_pdf import PdfPlugin

        plugin = PdfPlugin()
        assert plugin is not None

    def test_get_pdf_plugin(self) -> None:
        from elizaos_plugin_pdf import get_pdf_plugin

        plugin = get_pdf_plugin()
        assert plugin is not None


class TestPdfTypes:
    def test_extraction_options(self) -> None:
        from elizaos_plugin_pdf import PdfExtractionOptions

        options = PdfExtractionOptions(
            start_page=1,
            end_page=10,
            preserve_whitespace=True,
        )
        assert options.start_page == 1
        assert options.end_page == 10

    def test_conversion_result_success(self) -> None:
        from elizaos_plugin_pdf import PdfConversionResult

        result = PdfConversionResult(
            success=True,
            text="Sample PDF content",
            page_count=5,
        )
        assert result.success is True
        assert result.text == "Sample PDF content"

    def test_conversion_result_error(self) -> None:
        from elizaos_plugin_pdf import PdfConversionResult

        result = PdfConversionResult(
            success=False,
            error="Failed to parse PDF",
        )
        assert result.success is False
        assert result.error == "Failed to parse PDF"


# ─── Real PDF processing tests ───

import asyncio
import tempfile
from pathlib import Path


def _create_valid_pdf_bytes(text: str = "Hello World") -> bytes:
    """Create a minimal valid PDF with text content and proper font resources."""
    offsets: list[int] = []
    parts: list[bytes] = []

    header = b"%PDF-1.4\n"
    parts.append(header)

    # Object 1: Catalog
    offsets.append(sum(len(p) for p in parts))
    parts.append(b"1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n")

    # Object 2: Pages
    offsets.append(sum(len(p) for p in parts))
    parts.append(b"2 0 obj\n<</Type /Pages /Kids [3 0 R] /Count 1>>\nendobj\n")

    # Object 3: Page with font resources
    offsets.append(sum(len(p) for p in parts))
    parts.append(
        b"3 0 obj\n"
        b"<</Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]"
        b" /Contents 4 0 R /Resources <</Font <</F1 5 0 R>>>>>>\n"
        b"endobj\n"
    )

    # Object 4: Content stream with text
    stream_content = f"BT /F1 12 Tf 100 700 Td ({text}) Tj ET".encode("latin-1")
    offsets.append(sum(len(p) for p in parts))
    stream_header = f"4 0 obj\n<</Length {len(stream_content)}>>\nstream\n".encode(
        "latin-1"
    )
    parts.append(stream_header + stream_content + b"\nendstream\nendobj\n")

    # Object 5: Font
    offsets.append(sum(len(p) for p in parts))
    parts.append(
        b"5 0 obj\n<</Type /Font /Subtype /Type1 /BaseFont /Helvetica>>\nendobj\n"
    )

    # Cross-reference table
    xref_offset = sum(len(p) for p in parts)
    xref_lines: list[bytes] = [b"xref\n0 6\n", b"0000000000 65535 f \n"]
    for offset in offsets:
        xref_lines.append(f"{offset:010d} 00000 n \n".encode("latin-1"))
    parts.extend(xref_lines)

    # Trailer
    parts.append(
        f"trailer\n<</Size 6 /Root 1 0 R>>\nstartxref\n{xref_offset}\n%%EOF".encode(
            "latin-1"
        )
    )

    return b"".join(parts)


def _create_multi_page_pdf_bytes(pages_text: list[str]) -> bytes:
    """Create a multi-page PDF where each page has different text."""
    num_pages = len(pages_text)
    offsets: list[int] = []
    parts: list[bytes] = []

    header = b"%PDF-1.4\n"
    parts.append(header)

    # Object 1: Catalog
    offsets.append(sum(len(p) for p in parts))
    parts.append(b"1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n")

    # Object 2: Pages — Kids reference page objects starting at obj 4
    page_obj_nums = [4 + i * 2 for i in range(num_pages)]
    kids_str = " ".join(f"{n} 0 R" for n in page_obj_nums)
    offsets.append(sum(len(p) for p in parts))
    parts.append(
        f"2 0 obj\n<</Type /Pages /Kids [{kids_str}] /Count {num_pages}>>\nendobj\n".encode(
            "latin-1"
        )
    )

    # Object 3: Font
    offsets.append(sum(len(p) for p in parts))
    parts.append(
        b"3 0 obj\n<</Type /Font /Subtype /Type1 /BaseFont /Helvetica>>\nendobj\n"
    )

    # Pages and their content streams
    for i, text in enumerate(pages_text):
        page_num = 4 + i * 2
        content_num = 5 + i * 2

        # Page object
        offsets.append(sum(len(p) for p in parts))
        parts.append(
            f"{page_num} 0 obj\n"
            f"<</Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]"
            f" /Contents {content_num} 0 R /Resources <</Font <</F1 3 0 R>>>>>>\n"
            f"endobj\n".encode("latin-1")
        )

        # Content stream
        stream_content = f"BT /F1 12 Tf 100 700 Td ({text}) Tj ET".encode("latin-1")
        offsets.append(sum(len(p) for p in parts))
        stream_header = f"{content_num} 0 obj\n<</Length {len(stream_content)}>>\nstream\n".encode(
            "latin-1"
        )
        parts.append(stream_header + stream_content + b"\nendstream\nendobj\n")

    # Cross-reference table
    total_objects = 4 + 2 * num_pages  # 0(free) + Catalog + Pages + Font + 2*pages
    xref_offset = sum(len(p) for p in parts)
    xref_lines: list[bytes] = [
        f"xref\n0 {total_objects}\n".encode("latin-1"),
        b"0000000000 65535 f \n",
    ]
    for offset in offsets:
        xref_lines.append(f"{offset:010d} 00000 n \n".encode("latin-1"))
    parts.extend(xref_lines)

    parts.append(
        f"trailer\n<</Size {total_objects} /Root 1 0 R>>\nstartxref\n{xref_offset}\n%%EOF".encode(
            "latin-1"
        )
    )

    return b"".join(parts)


class TestPdfTextExtraction:
    """Tests for actual PDF text extraction."""

    @pytest.fixture()
    def client(self) -> "PdfClient":
        from elizaos_plugin_pdf.client import PdfClient

        return PdfClient()

    @pytest.fixture()
    def valid_pdf(self) -> bytes:
        return _create_valid_pdf_bytes("Hello World")

    def test_extract_text_returns_content(self, client: "PdfClient", valid_pdf: bytes) -> None:
        text: str = asyncio.run(client.extract_text(valid_pdf))
        assert text, "Extracted text should not be empty"
        assert "Hello" in text
        assert "World" in text

    def test_extract_text_with_options(self, client: "PdfClient", valid_pdf: bytes) -> None:
        from elizaos_plugin_pdf.types import PdfExtractionOptions

        opts = PdfExtractionOptions(clean_content=True)
        text: str = asyncio.run(client.extract_text(valid_pdf, options=opts))
        assert text, "Cleaned text should not be empty"

    def test_extract_text_from_file(self, client: "PdfClient", valid_pdf: bytes) -> None:
        with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
            f.write(valid_pdf)
            tmp_path = f.name

        try:
            text: str = asyncio.run(client.extract_text_from_file(tmp_path))
            assert text, "Extracted text from file should not be empty"
        finally:
            Path(tmp_path).unlink(missing_ok=True)

    def test_extract_text_from_file_not_found(self, client: "PdfClient") -> None:
        with pytest.raises(FileNotFoundError):
            asyncio.run(client.extract_text_from_file("/nonexistent/path.pdf"))


class TestPdfPageCount:
    """Tests for PDF page counting."""

    @pytest.fixture()
    def client(self) -> "PdfClient":
        from elizaos_plugin_pdf.client import PdfClient

        return PdfClient()

    def test_single_page_count(self, client: "PdfClient") -> None:
        pdf_bytes = _create_valid_pdf_bytes("Single Page")
        count: int = asyncio.run(client.get_page_count(pdf_bytes))
        assert count == 1

    def test_multi_page_count(self, client: "PdfClient") -> None:
        pdf_bytes = _create_multi_page_pdf_bytes(["Page One", "Page Two", "Page Three"])
        count: int = asyncio.run(client.get_page_count(pdf_bytes))
        assert count == 3


class TestPdfDocumentInfo:
    """Tests for PDF document info extraction."""

    @pytest.fixture()
    def client(self) -> "PdfClient":
        from elizaos_plugin_pdf.client import PdfClient

        return PdfClient()

    def test_get_document_info(self, client: "PdfClient") -> None:
        pdf_bytes = _create_valid_pdf_bytes("Document Info Test")
        info = asyncio.run(client.get_document_info(pdf_bytes))
        assert info.page_count == 1
        assert info.text, "Document text should not be empty"
        assert len(info.pages) == 1
        assert info.pages[0].page_number == 1
        assert info.pages[0].width > 0
        assert info.pages[0].height > 0

    def test_get_document_info_multi_page(self, client: "PdfClient") -> None:
        pdf_bytes = _create_multi_page_pdf_bytes(["First", "Second"])
        info = asyncio.run(client.get_document_info(pdf_bytes))
        assert info.page_count == 2
        assert len(info.pages) == 2
        assert info.pages[0].page_number == 1
        assert info.pages[1].page_number == 2

    def test_get_document_info_has_metadata(self, client: "PdfClient") -> None:
        pdf_bytes = _create_valid_pdf_bytes("Metadata Test")
        info = asyncio.run(client.get_document_info(pdf_bytes))
        assert info.metadata is not None


class TestPdfErrorHandling:
    """Tests for PDF error handling with invalid input."""

    @pytest.fixture()
    def client(self) -> "PdfClient":
        from elizaos_plugin_pdf.client import PdfClient

        return PdfClient()

    def test_extract_text_invalid_bytes(self, client: "PdfClient") -> None:
        from elizaos_plugin_pdf.client import PdfError

        with pytest.raises((PdfError, Exception)):
            asyncio.run(client.extract_text(b"not a valid pdf"))

    def test_convert_pdf_to_text_invalid_bytes(self, client: "PdfClient") -> None:
        result = asyncio.run(client.convert_pdf_to_text(b"not a valid pdf"))
        assert result.success is False
        assert result.error is not None

    def test_get_document_info_invalid_bytes(self, client: "PdfClient") -> None:
        from elizaos_plugin_pdf.client import PdfError

        with pytest.raises((PdfError, Exception)):
            asyncio.run(client.get_document_info(b"not a valid pdf"))


class TestPdfConversion:
    """Tests for PDF-to-text conversion result."""

    @pytest.fixture()
    def client(self) -> "PdfClient":
        from elizaos_plugin_pdf.client import PdfClient

        return PdfClient()

    def test_convert_pdf_to_text_success(self, client: "PdfClient") -> None:
        pdf_bytes = _create_valid_pdf_bytes("Conversion Test")
        result = asyncio.run(client.convert_pdf_to_text(pdf_bytes))
        assert result.success is True
        assert result.text is not None
        assert result.page_count == 1

    def test_convert_pdf_to_text_multi_page(self, client: "PdfClient") -> None:
        pdf_bytes = _create_multi_page_pdf_bytes(["Alpha", "Beta"])
        result = asyncio.run(client.convert_pdf_to_text(pdf_bytes))
        assert result.success is True
        assert result.page_count == 2
