"""
Unit tests for Phase 4: HTML Rendering Mode components.

Tests cover:
- SpaceRenderer base class
- RenderConfig and ThemeConfig
- MarkdownToHTMLRenderer
- SpaceRenderingService
- Cache integration
"""

import pytest
from unittest.mock import Mock, MagicMock, patch
from pathlib import Path

from markitect.spaces.rendering import (
    SpaceRenderer,
    RenderConfig,
    RenderResult,
    RenderFormat,
    ThemeConfig,
    CompositeRenderer,
    MarkdownToHTMLRenderer,
    HTMLRendererFactory,
    THEME_PROPERTIES,
    combine_theme_properties,
    SpaceRenderingService,
    SpaceRenderingServiceBuilder,
)
from markitect.spaces.transclusion import (
    RenderCache,
    ReferenceGraph,
)
from markitect.spaces.events import EventBus, SpaceEventType


class TestRenderFormat:
    """Tests for RenderFormat enum."""

    def test_html_format(self):
        """Test HTML format value."""
        assert RenderFormat.HTML.value == "html"

    def test_format_members(self):
        """Test all format members exist."""
        assert RenderFormat.HTML
        assert RenderFormat.PDF
        assert RenderFormat.DOCX
        assert RenderFormat.LATEX


class TestThemeConfig:
    """Tests for ThemeConfig dataclass."""

    def test_default_values(self):
        """Test default configuration."""
        config = ThemeConfig()
        assert config.name == "default"
        assert config.layers == ["basic"]
        assert config.custom_css is None
        assert config.custom_properties == {}

    def test_custom_config(self):
        """Test custom configuration."""
        config = ThemeConfig(
            name="custom",
            layers=["github", "dark"],
            custom_css="body { color: red; }",
            custom_properties={"max_width": "1000px"},
        )
        assert config.name == "custom"
        assert config.layers == ["github", "dark"]
        assert "color: red" in config.custom_css
        assert config.custom_properties["max_width"] == "1000px"


class TestRenderConfig:
    """Tests for RenderConfig dataclass."""

    def test_default_values(self):
        """Test default configuration."""
        config = RenderConfig()
        assert config.format == RenderFormat.HTML
        assert config.include_toc is False
        assert config.highlight_code is True
        assert config.sanitize_html is True
        assert config.link_target_blank is True

    def test_custom_config(self):
        """Test custom configuration."""
        theme = ThemeConfig(name="github")
        config = RenderConfig(
            format=RenderFormat.HTML,
            theme=theme,
            include_toc=True,
            image_max_width="800px",
        )
        assert config.theme.name == "github"
        assert config.include_toc is True
        assert config.image_max_width == "800px"


class TestRenderResult:
    """Tests for RenderResult dataclass."""

    def test_creation(self):
        """Test result creation."""
        result = RenderResult(
            content="<h1>Test</h1>",
            format=RenderFormat.HTML,
            content_hash="abc123",
            source_hash="def456",
            document_id="doc-1",
            space_id="space-1",
        )
        assert result.content == "<h1>Test</h1>"
        assert result.format == RenderFormat.HTML
        assert result.document_id == "doc-1"
        assert result.space_id == "space-1"
        assert result.dependencies == set()

    def test_with_dependencies(self):
        """Test result with dependencies."""
        result = RenderResult(
            content="<p>Test</p>",
            format=RenderFormat.HTML,
            content_hash="abc",
            source_hash="def",
            document_id="doc-1",
            space_id="space-1",
            dependencies={"doc-2", "doc-3"},
        )
        assert len(result.dependencies) == 2
        assert "doc-2" in result.dependencies

    def test_compute_hash(self):
        """Test hash computation."""
        hash1 = RenderResult.compute_hash("test content")
        hash2 = RenderResult.compute_hash("test content")
        hash3 = RenderResult.compute_hash("different content")

        assert hash1 == hash2
        assert hash1 != hash3
        assert len(hash1) == 16  # SHA256 truncated to 16 chars


class TestCombineThemeProperties:
    """Tests for theme property combination."""

    def test_single_layer(self):
        """Test single theme layer."""
        props = combine_theme_properties(["default"])
        assert "font_family" in props
        assert "body_color" in props

    def test_multiple_layers(self):
        """Test multiple theme layers."""
        props = combine_theme_properties(["default", "github"])
        # GitHub layer should override default
        assert props["max_width"] == "980px"

    def test_unknown_layer(self):
        """Test with unknown layer."""
        props = combine_theme_properties(["nonexistent"])
        assert props == {}

    def test_empty_layers(self):
        """Test with no layers."""
        props = combine_theme_properties([])
        assert props == {}


class TestMarkdownToHTMLRenderer:
    """Tests for MarkdownToHTMLRenderer."""

    def test_supported_formats(self):
        """Test supported formats."""
        renderer = MarkdownToHTMLRenderer()
        assert RenderFormat.HTML in renderer.supported_formats
        assert len(renderer.supported_formats) == 1

    def test_simple_render(self):
        """Test simple markdown rendering."""
        renderer = MarkdownToHTMLRenderer()
        result = renderer.render(
            content="# Hello World\n\nThis is a paragraph.",
            document_id="doc-1",
            space_id="space-1",
        )

        assert result.format == RenderFormat.HTML
        assert "<h1" in result.content
        assert "Hello World" in result.content
        assert "<p>" in result.content
        assert result.document_id == "doc-1"
        assert result.space_id == "space-1"

    def test_render_with_code(self):
        """Test rendering code blocks."""
        renderer = MarkdownToHTMLRenderer()
        markdown = """
# Code Example

```python
def hello():
    print("Hello")
```
"""
        result = renderer.render(
            content=markdown,
            document_id="doc-1",
            space_id="space-1",
        )

        assert "<pre>" in result.content or "<code>" in result.content

    def test_render_with_links(self):
        """Test link rendering with target blank."""
        renderer = MarkdownToHTMLRenderer()
        result = renderer.render(
            content="Visit [Google](https://google.com)",
            document_id="doc-1",
            space_id="space-1",
        )

        assert "https://google.com" in result.content
        assert 'target="_blank"' in result.content

    def test_render_with_toc(self):
        """Test table of contents generation."""
        config = RenderConfig(include_toc=True)
        renderer = MarkdownToHTMLRenderer(config)

        markdown = """
# Main Title

## Section 1

Content here.

## Section 2

More content.
"""
        result = renderer.render(
            content=markdown,
            document_id="doc-1",
            space_id="space-1",
        )

        assert 'class="toc"' in result.content
        assert "Contents" in result.content

    def test_render_with_dependencies(self):
        """Test dependencies are tracked."""
        renderer = MarkdownToHTMLRenderer()
        result = renderer.render(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
            dependencies={"dep-1", "dep-2"},
        )

        assert len(result.dependencies) == 2
        assert "dep-1" in result.dependencies

    def test_content_hash_computation(self):
        """Test that hashes are computed."""
        renderer = MarkdownToHTMLRenderer()
        result = renderer.render(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
        )

        assert result.content_hash
        assert result.source_hash
        assert len(result.content_hash) == 16

    def test_different_themes(self):
        """Test different theme configurations."""
        github_config = RenderConfig(theme=ThemeConfig(name="github", layers=["github"]))
        dark_config = RenderConfig(theme=ThemeConfig(name="dark", layers=["dark"]))

        github_renderer = MarkdownToHTMLRenderer(github_config)
        dark_renderer = MarkdownToHTMLRenderer(dark_config)

        github_result = github_renderer.render("# Test", "doc", "space")
        dark_result = dark_renderer.render("# Test", "doc", "space")

        # Results should be different due to different themes
        assert github_result.content != dark_result.content
        assert "#24292e" in github_result.content  # GitHub body color
        assert "#c9d1d9" in dark_result.content  # Dark mode text color


class TestHTMLRendererFactory:
    """Tests for HTMLRendererFactory."""

    def test_create_default(self):
        """Test default renderer creation."""
        renderer = HTMLRendererFactory.create_default()
        assert isinstance(renderer, MarkdownToHTMLRenderer)

    def test_create_github_style(self):
        """Test GitHub-style renderer."""
        renderer = HTMLRendererFactory.create_github_style()
        assert renderer.config.theme.name == "github"

    def test_create_academic_style(self):
        """Test academic-style renderer."""
        renderer = HTMLRendererFactory.create_academic_style()
        assert renderer.config.theme.name == "academic"
        assert renderer.config.include_toc is True

    def test_create_minimal_style(self):
        """Test minimal-style renderer."""
        renderer = HTMLRendererFactory.create_minimal_style()
        assert renderer.config.theme.name == "minimal"

    def test_create_dark_mode(self):
        """Test dark mode renderer."""
        renderer = HTMLRendererFactory.create_dark_mode()
        assert renderer.config.theme.name == "dark"


class TestCompositeRenderer:
    """Tests for CompositeRenderer."""

    def test_register_renderer(self):
        """Test registering a renderer."""
        composite = CompositeRenderer()
        html_renderer = MarkdownToHTMLRenderer()

        composite.register(html_renderer)

        assert composite.get_renderer(RenderFormat.HTML) is html_renderer

    def test_supported_formats(self):
        """Test listing supported formats."""
        composite = CompositeRenderer()
        html_renderer = MarkdownToHTMLRenderer()

        composite.register(html_renderer)

        formats = composite.supported_formats()
        assert RenderFormat.HTML in formats

    def test_render_via_composite(self):
        """Test rendering through composite."""
        composite = CompositeRenderer()
        composite.register(MarkdownToHTMLRenderer())

        result = composite.render(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
            format=RenderFormat.HTML,
        )

        assert result.format == RenderFormat.HTML
        assert "<h1" in result.content

    def test_no_renderer_for_format(self):
        """Test error when no renderer for format."""
        composite = CompositeRenderer()

        with pytest.raises(ValueError) as exc:
            composite.render(
                content="test",
                document_id="doc-1",
                space_id="space-1",
                format=RenderFormat.PDF,
            )

        assert "No renderer registered" in str(exc.value)


class TestSpaceRenderingService:
    """Tests for SpaceRenderingService."""

    def test_default_initialization(self):
        """Test default service initialization."""
        service = SpaceRenderingService()

        assert service.renderer is not None
        assert service.render_cache is not None
        assert service.reference_graph is not None

    def test_render_document(self):
        """Test basic document rendering."""
        service = SpaceRenderingService()

        result = service.render_document(
            content="# Hello\n\nWorld",
            document_id="doc-1",
            space_id="space-1",
        )

        assert result.format == RenderFormat.HTML
        assert "Hello" in result.content
        assert result.document_id == "doc-1"

    def test_render_caching(self):
        """Test that results are cached."""
        service = SpaceRenderingService()

        # First render
        result1 = service.render_document(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
        )

        # Second render - should use cache
        result2 = service.render_document(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
        )

        # Get cached entry
        cached = service.get_cached_render("doc-1")
        assert cached is not None

    def test_force_refresh_bypasses_cache(self):
        """Test force refresh skips cache."""
        service = SpaceRenderingService()

        # First render
        service.render_document(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
        )

        # Force refresh with different content
        result = service.render_document(
            content="# New Content",
            document_id="doc-1",
            space_id="space-1",
            force_refresh=True,
        )

        assert "New Content" in result.content

    def test_render_with_variables(self):
        """Test rendering with variable substitution."""
        service = SpaceRenderingService()

        result = service.render_document(
            content="Hello {{name}}!",
            document_id="doc-1",
            space_id="space-1",
            variables={"name": "World"},
        )

        assert "World" in result.content

    def test_render_documents_batch(self):
        """Test batch rendering."""
        service = SpaceRenderingService()

        documents = [
            {"id": "doc-1", "content": "# Doc 1"},
            {"id": "doc-2", "content": "# Doc 2"},
            {"id": "doc-3", "content": "# Doc 3"},
        ]

        results = service.render_documents(
            documents=documents,
            space_id="space-1",
        )

        assert len(results) == 3
        assert "doc-1" in results
        assert "doc-2" in results
        assert "doc-3" in results

    def test_invalidate_document(self):
        """Test cache invalidation."""
        service = SpaceRenderingService()

        # Render a document
        service.render_document(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
        )

        # Invalidate
        invalidated = service.invalidate_document("doc-1", "space-1")

        assert "doc-1" in invalidated
        assert service.get_cached_render("doc-1") is None

    def test_invalidate_space(self):
        """Test invalidating entire space."""
        service = SpaceRenderingService()

        # Render multiple documents
        service.render_document("# Doc 1", "doc-1", "space-1")
        service.render_document("# Doc 2", "doc-2", "space-1")

        # Invalidate space
        count = service.invalidate_space("space-1")

        assert count == 2

    def test_render_statistics(self):
        """Test getting render statistics."""
        service = SpaceRenderingService()

        # Render some documents
        service.render_document("# Doc 1", "doc-1", "space-1")
        service.render_document("# Doc 2", "doc-2", "space-1")

        stats = service.get_render_statistics("space-1")

        assert "cached_documents" in stats
        assert "render_format" in stats
        assert stats["cached_documents"] == 2

    def test_event_emission(self):
        """Test that events are emitted during rendering."""
        event_bus = EventBus()
        events = []

        def capture_event(event):
            events.append(event)

        event_bus.subscribe(SpaceEventType.RENDER_STARTED, capture_event)
        event_bus.subscribe(SpaceEventType.RENDER_COMPLETED, capture_event)

        service = SpaceRenderingService(event_bus=event_bus)

        service.render_document(
            content="# Test",
            document_id="doc-1",
            space_id="space-1",
        )

        # Should have RENDER_STARTED and RENDER_COMPLETED events
        event_types = [e.event_type for e in events]
        assert SpaceEventType.RENDER_STARTED in event_types
        assert SpaceEventType.RENDER_COMPLETED in event_types


class TestSpaceRenderingServiceBuilder:
    """Tests for SpaceRenderingServiceBuilder."""

    def test_build_default(self):
        """Test building with defaults."""
        service = SpaceRenderingServiceBuilder().build()
        assert service.renderer is not None

    def test_with_html_renderer(self):
        """Test building with HTML renderer."""
        config = RenderConfig(theme=ThemeConfig(name="github"))
        service = (
            SpaceRenderingServiceBuilder()
            .with_html_renderer(config)
            .build()
        )
        assert service.renderer.config.theme.name == "github"

    def test_with_event_bus(self):
        """Test building with event bus."""
        bus = EventBus()
        service = (
            SpaceRenderingServiceBuilder()
            .with_event_bus(bus)
            .build()
        )
        assert service.event_bus is bus

    def test_with_cache(self):
        """Test building with custom cache."""
        cache = RenderCache()
        service = (
            SpaceRenderingServiceBuilder()
            .with_cache(cache)
            .build()
        )
        assert service.render_cache is cache

    def test_with_reference_graph(self):
        """Test building with reference graph."""
        graph = ReferenceGraph()
        service = (
            SpaceRenderingServiceBuilder()
            .with_reference_graph(graph)
            .build()
        )
        assert service.reference_graph is graph

    def test_fluent_chaining(self):
        """Test fluent builder chaining."""
        bus = EventBus()
        cache = RenderCache()
        graph = ReferenceGraph()

        service = (
            SpaceRenderingServiceBuilder()
            .with_html_renderer()
            .with_event_bus(bus)
            .with_cache(cache)
            .with_reference_graph(graph)
            .build()
        )

        assert service.event_bus is bus
        assert service.render_cache is cache
        assert service.reference_graph is graph


class TestBasicMarkdownParsing:
    """Tests for basic markdown parsing fallback."""

    def test_headings(self):
        """Test heading parsing."""
        renderer = MarkdownToHTMLRenderer()
        markdown = "# H1\n## H2\n### H3"
        result = renderer.render(markdown, "doc", "space")

        assert "<h1" in result.content
        assert "<h2" in result.content
        assert "<h3" in result.content

    def test_bold_italic(self):
        """Test bold and italic."""
        renderer = MarkdownToHTMLRenderer()
        markdown = "**bold** and *italic*"
        result = renderer.render(markdown, "doc", "space")

        assert "<strong>bold</strong>" in result.content or "bold" in result.content

    def test_code_blocks(self):
        """Test code block parsing."""
        renderer = MarkdownToHTMLRenderer()
        markdown = "```python\nprint('hello')\n```"
        result = renderer.render(markdown, "doc", "space")

        assert "<code" in result.content or "<pre" in result.content

    def test_lists(self):
        """Test list parsing."""
        renderer = MarkdownToHTMLRenderer()
        markdown = "- Item 1\n- Item 2\n- Item 3"
        result = renderer.render(markdown, "doc", "space")

        assert "<li>" in result.content or "Item" in result.content

    def test_blockquotes(self):
        """Test blockquote parsing."""
        renderer = MarkdownToHTMLRenderer()
        markdown = "> This is a quote"
        result = renderer.render(markdown, "doc", "space")

        assert "<blockquote" in result.content or "quote" in result.content

    def test_horizontal_rule(self):
        """Test horizontal rule."""
        renderer = MarkdownToHTMLRenderer()
        markdown = "Text\n\n---\n\nMore text"
        result = renderer.render(markdown, "doc", "space")

        assert "<hr" in result.content


class TestThemeProperties:
    """Tests for theme property definitions."""

    def test_default_theme_exists(self):
        """Test default theme is defined."""
        assert "default" in THEME_PROPERTIES

    def test_github_theme_exists(self):
        """Test GitHub theme is defined."""
        assert "github" in THEME_PROPERTIES

    def test_dark_theme_exists(self):
        """Test dark theme is defined."""
        assert "dark" in THEME_PROPERTIES

    def test_minimal_theme_exists(self):
        """Test minimal theme is defined."""
        assert "minimal" in THEME_PROPERTIES

    def test_academic_theme_exists(self):
        """Test academic theme is defined."""
        assert "academic" in THEME_PROPERTIES

    def test_theme_has_required_properties(self):
        """Test themes have required properties."""
        required = ["font_family", "body_color", "body_background"]
        for theme_name, props in THEME_PROPERTIES.items():
            for prop in required:
                assert prop in props, f"{theme_name} missing {prop}"


class TestRenderIntegration:
    """Integration tests for rendering pipeline."""

    def test_full_render_pipeline(self):
        """Test complete render pipeline with cache."""
        event_bus = EventBus()
        cache = RenderCache()
        graph = ReferenceGraph()

        service = (
            SpaceRenderingServiceBuilder()
            .with_html_renderer()
            .with_event_bus(event_bus)
            .with_cache(cache)
            .with_reference_graph(graph)
            .build()
        )

        # First render
        result1 = service.render_document(
            content="# Hello World",
            document_id="doc-1",
            space_id="space-1",
        )

        assert result1.format == RenderFormat.HTML
        assert "Hello World" in result1.content

        # Check cached
        assert cache.get("doc-1") is not None

        # Second render should use cache
        result2 = service.render_document(
            content="# Hello World",
            document_id="doc-1",
            space_id="space-1",
        )

        # Invalidate
        invalidated = service.invalidate_document("doc-1", "space-1")
        assert "doc-1" in invalidated
        assert cache.get("doc-1") is None

    def test_variable_substitution_in_render(self):
        """Test variable substitution during render."""
        service = SpaceRenderingService()

        content = "# Welcome {{user}}\n\nToday is {{day}}."
        variables = {"user": "Alice", "day": "Monday"}

        result = service.render_document(
            content=content,
            document_id="doc-1",
            space_id="space-1",
            variables=variables,
        )

        assert "Alice" in result.content
        assert "Monday" in result.content
