"""
Tests for web/services/pdf_service.py

Tests cover:
- PDFService initialization
- Markdown to HTML conversion
- Markdown to PDF conversion
- Error handling
- Singleton pattern
"""

import pytest
from unittest.mock import Mock, patch


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

    def test_pdf_service_init(self):
        """Test PDFService initializes with minimal CSS."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()

        assert service.minimal_css is not None

    def test_pdf_service_css_contains_a4(self):
        """Test that CSS sets A4 page size."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()

        # The CSS string should be accessible
        assert service.minimal_css is not None

    def test_minimal_css_includes_emoji_font_families(self):
        """Body and code font stacks must fall back to an emoji font.

        Without an emoji family in the stack, WeasyPrint renders emoji
        codepoints as "tofu" boxes because Arial and the CJK fonts have
        no emoji coverage. Regression test for the emoji font stack.
        """
        from local_deep_research.web.services.pdf_service import MINIMAL_CSS

        assert "Noto Color Emoji" in MINIMAL_CSS
        assert "Noto Emoji" in MINIMAL_CSS

        # Both the body and code/mono stacks should have emoji fallbacks.
        # Pull out just the font-family declarations to make the assertion
        # resilient to other CSS edits.
        import re

        font_family_blocks = re.findall(r"font-family:\s*([^;]+);", MINIMAL_CSS)
        assert len(font_family_blocks) >= 2, (
            "expected at least body + code font-family declarations"
        )
        for block in font_family_blocks:
            assert "Noto Color Emoji" in block, (
                f"font-family block missing color emoji fallback: {block!r}"
            )
            assert "Noto Emoji" in block, (
                f"font-family block missing monochrome emoji fallback: {block!r}"
            )


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

    def test_markdown_to_html_basic(self):
        """Test basic markdown conversion."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "# Hello World\n\nThis is a **test**."

        html = service._markdown_to_html(markdown)

        # h1 may have an id attribute added by markdown TOC extension
        assert "Hello World</h1>" in html
        assert "<strong>test</strong>" in html
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_markdown_to_html_with_title(self):
        """Test conversion with title."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "Content here"

        html = service._markdown_to_html(markdown, title="Test Title")

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

    def test_markdown_to_html_escapes_html_in_title(self):
        """Test that HTML special characters in title are escaped."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()

        html = service._markdown_to_html(
            "content", title='<script>alert("xss")</script>'
        )

        assert "<script>" not in html
        assert "&lt;script&gt;" in html

    def test_markdown_to_html_escapes_html_in_metadata(self):
        """Test that HTML special characters in metadata are escaped."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        metadata = {"<b>key</b>": '<img src="x" onerror="alert(1)">'}

        html = service._markdown_to_html("content", metadata=metadata)

        assert "<b>key</b>" not in html
        assert "&lt;b&gt;key&lt;/b&gt;" in html
        assert 'onerror="alert(1)"' not in html

    def test_markdown_to_html_with_metadata(self):
        """Test conversion with metadata."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "Content"
        metadata = {"author": "Test Author", "date": "2024-01-01"}

        html = service._markdown_to_html(markdown, metadata=metadata)

        assert 'name="author"' in html
        assert 'content="Test Author"' in html
        assert 'name="date"' in html

    def test_markdown_to_html_tables(self):
        """Test table markdown is converted."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = """
| Header 1 | Header 2 |
|----------|----------|
| Cell 1   | Cell 2   |
"""
        html = service._markdown_to_html(markdown)

        assert "<table>" in html
        assert "<th>" in html or "<td>" in html

    def test_markdown_to_html_code_blocks(self):
        """Test fenced code blocks are converted."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = """
```python
def hello():
    print("Hello")
```
"""
        html = service._markdown_to_html(markdown)

        assert "<pre>" in html or "<code>" in html

    def test_markdown_to_html_includes_footer(self):
        """Test that footer with LDR attribution is included."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "Test content"

        html = service._markdown_to_html(markdown)

        assert "Local Deep Research" in html or "LDR" in html


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

    def test_markdown_to_pdf_returns_bytes(self):
        """Test that PDF conversion returns bytes."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "# Test Document\n\nThis is a test."

        pdf_bytes = service.markdown_to_pdf(markdown)

        assert isinstance(pdf_bytes, bytes)
        assert len(pdf_bytes) > 0
        # PDF files start with %PDF
        assert pdf_bytes[:4] == b"%PDF"

    def test_markdown_to_pdf_with_title(self):
        """Test PDF conversion with title."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "Content here"

        pdf_bytes = service.markdown_to_pdf(markdown, title="My Document")

        assert isinstance(pdf_bytes, bytes)
        assert len(pdf_bytes) > 0

    def test_markdown_to_pdf_with_metadata(self):
        """Test PDF conversion with metadata."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "Content"
        metadata = {"author": "Test Author"}

        pdf_bytes = service.markdown_to_pdf(markdown, metadata=metadata)

        assert isinstance(pdf_bytes, bytes)

    def test_markdown_to_pdf_with_custom_css(self):
        """Test PDF conversion with custom CSS."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = "# Styled Content"
        custom_css = "body { color: red; }"

        pdf_bytes = service.markdown_to_pdf(markdown, custom_css=custom_css)

        assert isinstance(pdf_bytes, bytes)
        assert len(pdf_bytes) > 0

    def test_custom_css_layers_on_top_of_default(self):
        """custom_css must extend, not replace, the default stylesheet.

        Regression test: previously markdown_to_pdf used an if/else that
        dropped self.minimal_css entirely when custom_css was provided,
        silently losing the CJK and emoji font fallbacks. Verify the
        default stylesheet is always passed to WeasyPrint.
        """
        from unittest.mock import patch, MagicMock
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        custom_css = "body { color: red; }"

        with (
            patch(
                "local_deep_research.web.services.pdf_service.CSS",
                side_effect=lambda **kwargs: MagicMock(**kwargs),
            ),
            patch(
                "local_deep_research.web.services.pdf_service.HTML"
            ) as mock_html,
        ):
            mock_html.return_value.write_pdf.return_value = b"%PDF-1.7"

            service.markdown_to_pdf("# x", custom_css=custom_css)

            # write_pdf must receive both the default and the custom CSS.
            call_kwargs = mock_html.return_value.write_pdf.call_args.kwargs
            stylesheets = call_kwargs.get("stylesheets", [])
            assert len(stylesheets) == 2, (
                f"expected [default, custom]; got {len(stylesheets)} sheets"
            )
            assert stylesheets[0] is service.minimal_css, (
                "default MINIMAL_CSS must always be applied first"
            )

    def test_markdown_to_pdf_complex_document(self):
        """Test PDF conversion with complex document."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = """
# Research Report

## Introduction

This is a comprehensive research document.

### Key Findings

1. First finding
2. Second finding
3. Third finding

### Data Table

| Metric | Value |
|--------|-------|
| A      | 100   |
| B      | 200   |

## Conclusion

The research concludes that **testing is important**.

```python
def analyze():
    return True
```
"""
        pdf_bytes = service.markdown_to_pdf(
            markdown,
            title="Research Report",
            metadata={"author": "Research Team", "date": "2024"},
        )

        assert isinstance(pdf_bytes, bytes)
        assert len(pdf_bytes) > 100  # Should be a reasonable size

    def test_markdown_to_pdf_empty_content(self):
        """Test PDF conversion with empty content."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = ""

        pdf_bytes = service.markdown_to_pdf(markdown)

        # Should still generate valid PDF
        assert isinstance(pdf_bytes, bytes)
        assert pdf_bytes[:4] == b"%PDF"


class TestGetPDFService:
    """Tests for get_pdf_service singleton function."""

    def test_get_pdf_service_returns_instance(self):
        """Test that get_pdf_service returns a PDFService instance."""
        # Reset singleton for test
        import local_deep_research.web.services.pdf_service as pdf_module

        pdf_module._pdf_service = None

        from local_deep_research.web.services.pdf_service import get_pdf_service

        service = get_pdf_service()

        assert service is not None
        from local_deep_research.web.services.pdf_service import PDFService

        assert isinstance(service, PDFService)

    def test_get_pdf_service_singleton(self):
        """Test that get_pdf_service returns same instance."""
        # Reset singleton for test
        import local_deep_research.web.services.pdf_service as pdf_module

        pdf_module._pdf_service = None

        from local_deep_research.web.services.pdf_service import get_pdf_service

        service1 = get_pdf_service()
        service2 = get_pdf_service()

        assert service1 is service2


class TestPDFServiceErrorHandling:
    """Tests for error handling in PDF service."""

    def test_markdown_to_pdf_raises_on_error(self):
        """Test that exceptions are propagated."""
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()

        # Mock HTML to raise an exception
        with patch(
            "local_deep_research.web.services.pdf_service.HTML"
        ) as mock_html:
            mock_html.side_effect = Exception("Conversion error")

            with pytest.raises(Exception) as exc_info:
                service.markdown_to_pdf("test")

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


class TestMissingPDFDependencyError:
    """Tests for MissingPDFDependencyError behavior."""

    def test_is_runtime_error_subclass(self):
        """MissingPDFDependencyError must be a RuntimeError subclass.

        Documents the security boundary: the web route catches this specific
        class (not generic RuntimeError) so that other exporters' RuntimeErrors
        (e.g., pandoc stderr from ODT) don't leak into HTTP responses.
        """
        from local_deep_research.web.services.pdf_service import (
            MissingPDFDependencyError,
        )

        assert issubclass(MissingPDFDependencyError, RuntimeError)

    def test_get_pdf_service_raises_specific_class_when_unavailable(self):
        """get_pdf_service() raises MissingPDFDependencyError, not plain RuntimeError."""
        from local_deep_research.web.services import pdf_service as pdf_module
        from local_deep_research.web.services.pdf_service import (
            MissingPDFDependencyError,
            get_pdf_service,
        )

        with patch.object(pdf_module, "WEASYPRINT_AVAILABLE", False):
            with pytest.raises(MissingPDFDependencyError):
                get_pdf_service()


class TestSafeUrlFetcher:
    """Tests for the SSRF-guarded WeasyPrint url_fetcher (GHSA-fj2m-qvh9-jq4q).

    The custom fetcher blocks outbound requests to internal/private IPs and
    non-http(s) schemes, so URLs that reach the rendered HTML via the
    markdown body, citations, or any other channel cannot be weaponised
    into SSRF against cloud metadata or internal services.
    """

    def test_blocks_aws_metadata_endpoint(self):
        """AWS metadata IP is always blocked — critical SSRF target."""
        from local_deep_research.web.services.pdf_service import (
            UnsafePDFResourceURLError,
            _safe_url_fetcher,
        )

        with pytest.raises(UnsafePDFResourceURLError):
            _safe_url_fetcher(
                "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
            )

    def test_blocks_loopback_address(self):
        """Loopback addresses are blocked by default."""
        from local_deep_research.web.services.pdf_service import (
            UnsafePDFResourceURLError,
            _safe_url_fetcher,
        )

        with pytest.raises(UnsafePDFResourceURLError):
            _safe_url_fetcher("http://127.0.0.1:8080/admin")

    def test_blocks_rfc1918_private_ip(self):
        """RFC1918 private range is blocked."""
        from local_deep_research.web.services.pdf_service import (
            UnsafePDFResourceURLError,
            _safe_url_fetcher,
        )

        with pytest.raises(UnsafePDFResourceURLError):
            _safe_url_fetcher("http://10.0.0.1/internal-api")

    def test_blocks_non_http_scheme(self):
        """Non-http(s) schemes like file:// are blocked."""
        from local_deep_research.web.services.pdf_service import (
            UnsafePDFResourceURLError,
            _safe_url_fetcher,
        )

        with pytest.raises(UnsafePDFResourceURLError):
            _safe_url_fetcher("file:///etc/passwd")

    def test_unsafe_url_error_subclasses_valueerror(self):
        """UnsafePDFResourceURLError must inherit from ValueError.

        WeasyPrint's url_fetcher contract treats ValueError as a
        retrievable fetch failure — the offending resource is skipped
        and rendering continues. Breaking this subclass relationship
        would turn blocked URLs into hard render failures.
        """
        from local_deep_research.web.services.pdf_service import (
            UnsafePDFResourceURLError,
        )

        assert issubclass(UnsafePDFResourceURLError, ValueError)

    def test_validated_url_delegates_to_url_fetcher(self):
        """Validated URLs are forwarded to the module's URLFetcher instance."""
        from local_deep_research.web.services import pdf_service as pdf_module
        from local_deep_research.web.services.pdf_service import (
            _safe_url_fetcher,
        )

        pdf_module._ensure_weasyprint()  # populate the lazy _URL_FETCHER
        with (
            patch.object(pdf_module, "validate_url", return_value=True),
            patch.object(pdf_module._URL_FETCHER, "fetch") as mock_fetch,
        ):
            mock_fetch.return_value = Mock(
                status=200, url="https://example.com/image.png"
            )

            _safe_url_fetcher("https://example.com/image.png")

            mock_fetch.assert_called_once_with("https://example.com/image.png")

    def test_url_fetcher_disables_redirects(self):
        """The module-level URLFetcher must keep allow_redirects=False.

        validate_url only inspects the initial URL string, so a 30x
        response redirecting to the AWS metadata endpoint would bypass
        the SSRF guard if URLFetcher were to follow redirects.
        default_url_fetcher
        hard-coded this; after migrating away from it, we assert the
        posture explicitly.
        """
        from local_deep_research.web.services import pdf_service as pdf_module
        from urllib.request import HTTPRedirectHandler

        pdf_module._ensure_weasyprint()  # populate the lazy _URL_FETCHER
        redirect_handlers = [
            h
            for h in pdf_module._URL_FETCHER.handlers
            if isinstance(h, HTTPRedirectHandler)
        ]
        assert redirect_handlers == []

    def test_pdf_html_passes_fetcher_to_weasyprint(self):
        """markdown_to_pdf must wire _safe_url_fetcher into HTML(...).

        Regression guard: if a refactor drops the url_fetcher= kwarg,
        WeasyPrint silently reverts to its default fetcher and the
        SSRF guard disappears.
        """
        from local_deep_research.web.services import pdf_service as pdf_module
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()

        with patch.object(pdf_module, "HTML") as mock_html:
            mock_html.return_value.write_pdf.return_value = None
            service.markdown_to_pdf("content")

            mock_html.assert_called_once()
            assert (
                mock_html.call_args.kwargs.get("url_fetcher")
                is pdf_module._safe_url_fetcher
            )

    def test_render_succeeds_when_body_url_is_blocked(self):
        """End-to-end: a malicious URL in the markdown body doesn't break rendering.

        Covers the full chain (markdown passthrough → WeasyPrint → fetcher →
        ValueError → WeasyPrint skips resource → PDF still renders), which
        is what the SSRF guard relies on. Would fail if a future WeasyPrint
        upgrade changed the ValueError-as-skip contract, if Python-Markdown
        started stripping raw HTML, or if the url_fetcher got unwired.
        """
        from local_deep_research.web.services.pdf_service import PDFService

        service = PDFService()
        markdown = (
            "# Report\n\n"
            "Body text.\n\n"
            '<img src="http://169.254.169.254/latest/meta-data/" alt="x" />\n'
        )

        pdf_bytes = service.markdown_to_pdf(markdown)

        assert isinstance(pdf_bytes, bytes)
        assert pdf_bytes[:4] == b"%PDF"
        assert len(pdf_bytes) > 100
