"""
Comprehensive tests for PDFService.
Tests PDF generation, markdown conversion, CSS handling, and metadata.
"""

import io
import subprocess

import pytest
from unittest.mock import patch


def _host_has_cjk_fonts() -> bool:
    """True if fontconfig reports any CJK-capable family on the host.

    Used to gate the strong glyph-embedding assertion in
    test_handles_cjk_content_embeds_glyphs: Docker CI (where this PR
    installs fonts-noto-cjk) and properly-configured dev hosts run the
    assertion; bare pip/macOS/Windows installs without Noto CJK skip it.
    """
    try:
        result = subprocess.run(
            ["fc-list", ":lang=zh"],
            capture_output=True,
            text=True,
            timeout=5,
        )
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False
    return bool(result.stdout.strip())


class TestPDFServiceInit:
    """Tests for PDFService initialization."""

    def test_init_creates_minimal_css(self):
        """Test that initialization creates minimal CSS."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()

        assert service.minimal_css is not None

    def test_minimal_css_includes_page_settings(self):
        """Test that minimal CSS includes @page settings."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()

        # The CSS object wraps the string, so we check it was created
        assert service.minimal_css is not None


class TestPDFServiceMarkdownToHtml:
    """Tests for _markdown_to_html method."""

    @pytest.fixture
    def service(self):
        """Create PDFService instance."""
        from local_deep_research.web.services.pdf_service import PDFService

        return PDFService()

    def test_converts_basic_markdown(self, service, simple_markdown):
        """Test conversion of basic markdown to HTML."""
        html = service._markdown_to_html(simple_markdown)

        # Markdown adds id attribute to headings
        assert "<h1" in html and "Hello World</h1>" in html
        assert "<p>This is a test.</p>" in html

    def test_includes_doctype_and_structure(self, service, simple_markdown):
        """Test that HTML includes proper structure."""
        html = service._markdown_to_html(simple_markdown)

        assert "<!DOCTYPE html>" in html
        assert "<html>" in html
        assert "</html>" in html
        assert "<head>" in html
        assert "</head>" in html
        assert "<body>" in html
        assert "</body>" in html

    def test_includes_charset_meta(self, service, simple_markdown):
        """Test that HTML includes UTF-8 charset."""
        html = service._markdown_to_html(simple_markdown)

        assert '<meta charset="utf-8">' in html

    def test_includes_title_when_provided(self, service, simple_markdown):
        """Test that title is included when provided."""
        html = service._markdown_to_html(simple_markdown, title="Test Title")

        assert "<title>Test Title</title>" in html

    def test_excludes_title_when_not_provided(self, service, simple_markdown):
        """Test that no title tag when title not provided."""
        html = service._markdown_to_html(simple_markdown)

        assert "<title>" not in html

    def test_includes_metadata_as_meta_tags(
        self, service, simple_markdown, sample_metadata
    ):
        """Test that metadata is included as meta tags."""
        html = service._markdown_to_html(
            simple_markdown, metadata=sample_metadata
        )

        assert '<meta name="author" content="Test Author">' in html
        assert '<meta name="date" content="2024-01-15">' in html
        assert '<meta name="subject" content="Test Subject">' in html

    def test_includes_ldr_attribution_footer(self, service, simple_markdown):
        """Test that LDR attribution footer is included."""
        html = service._markdown_to_html(simple_markdown)

        assert "Generated by" in html
        assert "Local Deep Research" in html

    def test_converts_tables(self, service, sample_markdown):
        """Test that markdown tables are converted."""
        html = service._markdown_to_html(sample_markdown)

        assert "<table>" in html
        assert "<th>" in html
        assert "Column 1" in html

    def test_converts_code_blocks(self, service, sample_markdown):
        """Test that fenced code blocks are converted."""
        html = service._markdown_to_html(sample_markdown)

        assert "<code" in html or "<pre>" in html
        assert "def example():" in html

    def test_converts_lists(self, service, sample_markdown):
        """Test that lists are converted."""
        html = service._markdown_to_html(sample_markdown)

        assert "<li>" in html
        assert "Item 1" in html

    def test_converts_bold_and_italic(self, service, sample_markdown):
        """Test that bold and italic text are converted."""
        html = service._markdown_to_html(sample_markdown)

        assert "<strong>bold</strong>" in html
        assert "<em>italic</em>" in html

    def test_handles_special_characters(
        self, service, markdown_with_special_chars
    ):
        """Test handling of special characters."""
        html = service._markdown_to_html(markdown_with_special_chars)

        # Unicode should pass through
        assert "café" in html
        assert "naïve" in html
        assert "日本語" in html


class TestPDFServiceMarkdownToPdf:
    """Tests for markdown_to_pdf method."""

    @pytest.fixture
    def service(self):
        """Create PDFService instance."""
        from local_deep_research.web.services.pdf_service import PDFService

        return PDFService()

    def test_returns_bytes(self, service, simple_markdown):
        """Test that markdown_to_pdf returns bytes."""
        result = service.markdown_to_pdf(simple_markdown)

        assert isinstance(result, bytes)

    def test_returns_valid_pdf(self, service, simple_markdown):
        """Test that result is a valid PDF (starts with %PDF)."""
        result = service.markdown_to_pdf(simple_markdown)

        assert result.startswith(b"%PDF")

    def test_pdf_has_reasonable_size(self, service, simple_markdown):
        """Test that generated PDF has a reasonable size."""
        result = service.markdown_to_pdf(simple_markdown)

        # A simple PDF should be at least a few KB
        assert len(result) > 1000

    def test_applies_title(self, service, simple_markdown):
        """Test that title is applied to PDF."""
        result = service.markdown_to_pdf(simple_markdown, title="My Title")

        # PDF was generated successfully
        assert result.startswith(b"%PDF")
        assert len(result) > 0

    def test_applies_metadata(self, service, simple_markdown, sample_metadata):
        """Test that metadata is applied to PDF."""
        result = service.markdown_to_pdf(
            simple_markdown, metadata=sample_metadata
        )

        assert result.startswith(b"%PDF")

    def test_applies_custom_css(self, service, simple_markdown, custom_css):
        """Test that custom CSS is applied."""
        result = service.markdown_to_pdf(simple_markdown, custom_css=custom_css)

        assert result.startswith(b"%PDF")

    def test_uses_minimal_css_when_no_custom_css(
        self, service, simple_markdown
    ):
        """Test that minimal CSS is used by default."""
        result = service.markdown_to_pdf(simple_markdown)

        assert result.startswith(b"%PDF")

    def test_handles_empty_markdown(self, service):
        """Test handling of empty markdown."""
        result = service.markdown_to_pdf("")

        assert result.startswith(b"%PDF")

    def test_handles_large_markdown(self, service):
        """Test handling of large markdown content."""
        # Create a large markdown document
        large_content = "# Large Document\n\n"
        large_content += ("This is a paragraph. " * 100 + "\n\n") * 50

        result = service.markdown_to_pdf(large_content)

        assert result.startswith(b"%PDF")
        # Large content should produce larger PDF
        assert len(result) > 10000

    def test_handles_markdown_with_all_features(self, service, sample_markdown):
        """Test handling of markdown with tables, code, lists."""
        result = service.markdown_to_pdf(sample_markdown)

        assert result.startswith(b"%PDF")

    def test_handles_cjk_content(self, service):
        """Regression guard for #4055 — CJK text must not crash export.

        Glyph visibility depends on the host having CJK fonts installed
        (e.g. fonts-noto-cjk). This test pins the no-crash contract; the
        glyph-embedding contract is checked separately in
        test_handles_cjk_content_embeds_glyphs.
        """
        cjk_markdown = (
            "# 中文标题\n\n这是一个测试。\n\n"
            "## 日本語の見出し\n\nテスト本文。\n\n"
            "## 한국어 제목\n\n본문 테스트.\n"
        )

        result = service.markdown_to_pdf(cjk_markdown)

        assert result.startswith(b"%PDF")
        assert len(result) > 1000

    @pytest.mark.skipif(
        not _host_has_cjk_fonts(),
        reason="no CJK fonts on host; install fonts-noto-cjk to run",
    )
    def test_handles_cjk_content_embeds_glyphs(self, service):
        """End-to-end glyph check for #4055.

        Round-trips CJK text through markdown → PDF → text extraction.
        If no CJK-capable font is available to WeasyPrint, glyphs are
        dropped and pypdf's extract_text returns the rendered text
        without them.

        What this catches: removal of fonts-noto-cjk from the runtime
        Docker image (the load-bearing half of the fix) — pytest-tests
        runs inside that image, so the test fails when fonts are gone.

        What this does NOT catch: removal of the CSS CJK fallback list.
        On Linux, fontconfig auto-substitutes a glyph-bearing family
        even for a Latin-only CSS stack, so the assertion still passes.
        The CSS fallbacks matter on Windows/macOS, which CI doesn't run.
        """
        import pypdf

        cjk_markdown = (
            "# 中文标题\n\n这是一个测试。\n\n"
            "## 日本語の見出し\n\nテスト本文。\n\n"
            "## 한국어 제목\n\n본문 테스트.\n"
        )

        pdf_bytes = service.markdown_to_pdf(cjk_markdown)
        reader = pypdf.PdfReader(io.BytesIO(pdf_bytes))
        extracted = "".join(page.extract_text() for page in reader.pages)

        for phrase in (
            "中文标题",
            "这是一个测试",
            "日本語",
            "テスト",
            "한국어",
            "본문",
        ):
            assert phrase in extracted, (
                f"CJK glyphs missing from PDF: {phrase!r} not found in "
                f"extracted text — WeasyPrint could not match a glyph "
                f"(check that fonts-noto-cjk is installed on the host)"
            )

    def test_logs_pdf_size(self, service, simple_markdown):
        """Test that PDF size is logged."""
        with patch(
            "local_deep_research.web.services.pdf_service.logger"
        ) as mock_logger:
            service.markdown_to_pdf(simple_markdown)

            mock_logger.info.assert_called_once()
            call_args = mock_logger.info.call_args[0][0]
            assert "Generated PDF" in call_args
            assert "bytes" in call_args


class TestPDFServiceErrorHandling:
    """Tests for error handling in PDFService."""

    @pytest.fixture
    def service(self):
        """Create PDFService instance."""
        from local_deep_research.web.services.pdf_service import PDFService

        return PDFService()

    def test_raises_exception_on_html_error(self, service):
        """Test that exceptions are raised on HTML conversion errors."""
        with patch.object(
            service, "_markdown_to_html", side_effect=Exception("HTML error")
        ):
            with pytest.raises(Exception) as exc_info:
                service.markdown_to_pdf("test")

            assert "HTML error" in str(exc_info.value)

    def test_logs_exception_on_error(self, service):
        """Test that exceptions are logged."""
        with patch.object(
            service, "_markdown_to_html", side_effect=Exception("Test error")
        ):
            with patch(
                "local_deep_research.web.services.pdf_service.logger"
            ) as mock_logger:
                with pytest.raises(Exception):
                    service.markdown_to_pdf("test")

                mock_logger.exception.assert_called_once()


class TestPDFServiceSingleton:
    """Tests for get_pdf_service singleton."""

    def test_get_pdf_service_returns_instance(self):
        """Test that get_pdf_service returns a PDFService instance."""
        from local_deep_research.web.services.pdf_service import (
            get_pdf_service,
            PDFService,
        )

        service = get_pdf_service()

        assert isinstance(service, PDFService)

    def test_get_pdf_service_returns_same_instance(self):
        """Test that get_pdf_service returns the same instance."""
        from local_deep_research.web.services.pdf_service import (
            get_pdf_service,
        )

        service1 = get_pdf_service()
        service2 = get_pdf_service()

        assert service1 is service2

    def test_singleton_can_be_reset(self):
        """Test that singleton can be reset for testing."""
        import local_deep_research.web.services.pdf_service as pdf_module

        # Reset singleton
        pdf_module._pdf_service = None

        service1 = pdf_module.get_pdf_service()

        # Reset again
        pdf_module._pdf_service = None

        service2 = pdf_module.get_pdf_service()

        # Different instances after reset
        assert service1 is not service2
