# tests/unit/middleware/test_theme_context.py
"""
Comprehensive unit tests for ThemeContextMiddleware and ThemeContextManager.

Tests cover:
- Theme loading and caching
- Default theme structure and validation
- Store-specific theme retrieval
- Fallback to default theme
- Middleware integration
- Edge cases and error handling
"""

from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest
from fastapi import Request

from middleware.theme_context import (
    ThemeContextManager,
    ThemeContextMiddleware,
    get_current_theme,
)


@pytest.mark.unit
class TestThemeContextManager:
    """Test suite for ThemeContextManager."""

    def test_get_default_theme_structure(self):
        """Test default theme has correct structure."""
        theme = ThemeContextManager.get_default_theme()

        assert "theme_name" in theme
        assert "colors" in theme
        assert "fonts" in theme
        assert "branding" in theme
        assert "layout" in theme
        assert "social_links" in theme
        assert "css_variables" in theme

    def test_get_default_theme_colors(self):
        """Test default theme has all required colors."""
        theme = ThemeContextManager.get_default_theme()

        required_colors = [
            "primary",
            "secondary",
            "accent",
            "background",
            "text",
            "border",
        ]
        for color in required_colors:
            assert color in theme["colors"]
            assert theme["colors"][color].startswith("#")

    def test_get_default_theme_fonts(self):
        """Test default theme has font configuration."""
        theme = ThemeContextManager.get_default_theme()

        assert "heading" in theme["fonts"]
        assert "body" in theme["fonts"]
        assert isinstance(theme["fonts"]["heading"], str)
        assert isinstance(theme["fonts"]["body"], str)

    def test_get_default_theme_branding(self):
        """Test default theme branding structure."""
        theme = ThemeContextManager.get_default_theme()

        assert "logo" in theme["branding"]
        assert "logo_dark" in theme["branding"]
        assert "favicon" in theme["branding"]
        assert "banner" in theme["branding"]

    def test_get_default_theme_css_variables(self):
        """Test default theme has CSS variables."""
        theme = ThemeContextManager.get_default_theme()

        assert "--color-primary" in theme["css_variables"]
        assert "--font-heading" in theme["css_variables"]
        assert "--font-body" in theme["css_variables"]

    def test_get_store_theme_with_custom_theme(self):
        """Test getting store-specific theme."""
        mock_db = Mock()
        mock_theme = Mock()

        # Mock to_dict to return actual dictionary
        custom_theme_dict = {"theme_name": "custom", "colors": {"primary": "#ff0000"}}
        mock_theme.to_dict.return_value = custom_theme_dict

        # Correct filter chain: query().filter().first()
        mock_db.query.return_value.filter.return_value.first.return_value = mock_theme

        theme = ThemeContextManager.get_store_theme(mock_db, store_id=1)

        assert theme["theme_name"] == "custom"
        assert theme["colors"]["primary"] == "#ff0000"
        mock_theme.to_dict.assert_called_once()

    def test_get_store_theme_fallback_to_default(self):
        """Test falling back to default theme when no custom theme exists."""
        mock_db = Mock()
        # Correct filter chain: query().filter().first()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        theme = ThemeContextManager.get_store_theme(mock_db, store_id=1)

        # Verify it returns a dict (not a Mock)
        assert isinstance(theme, dict)
        assert theme["theme_name"] == "default"
        assert "colors" in theme
        assert "fonts" in theme

    def test_get_store_theme_inactive_theme(self):
        """Test that inactive themes are not returned."""
        mock_db = Mock()
        # Correct filter chain: query().filter().first()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        theme = ThemeContextManager.get_store_theme(mock_db, store_id=1)

        # Should return default theme (actual dict)
        assert isinstance(theme, dict)
        assert theme["theme_name"] == "default"


@pytest.mark.unit
class TestThemeContextMiddleware:
    """Test suite for ThemeContextMiddleware."""

    @pytest.mark.asyncio
    async def test_middleware_loads_theme_for_store(self):
        """Test middleware loads theme when store exists."""
        middleware = ThemeContextMiddleware(app=None)

        request = Mock(spec=Request)
        mock_store = Mock()
        mock_store.id = 1
        mock_store.name = "Test Store"
        request.state = Mock(store=mock_store)

        call_next = AsyncMock(return_value=Mock())

        mock_db = MagicMock()
        mock_theme = {"theme_name": "test_theme"}

        with (
            patch("middleware.theme_context.get_db", return_value=iter([mock_db])),
            patch.object(
                ThemeContextManager, "get_store_theme", return_value=mock_theme
            ),
        ):
            await middleware.dispatch(request, call_next)

        assert request.state.theme == mock_theme
        call_next.assert_called_once_with(request)

    @pytest.mark.asyncio
    async def test_middleware_uses_default_theme_no_store(self):
        """Test middleware uses default theme when no store."""
        middleware = ThemeContextMiddleware(app=None)

        request = Mock(spec=Request)
        request.state = Mock(store=None)

        call_next = AsyncMock(return_value=Mock())

        await middleware.dispatch(request, call_next)

        assert hasattr(request.state, "theme")
        assert request.state.theme["theme_name"] == "default"
        call_next.assert_called_once()

    @pytest.mark.asyncio
    async def test_middleware_handles_theme_loading_error(self):
        """Test middleware handles errors gracefully."""
        middleware = ThemeContextMiddleware(app=None)

        request = Mock(spec=Request)
        mock_store = Mock(id=1, name="Test Store")
        request.state = Mock(store=mock_store)

        call_next = AsyncMock(return_value=Mock())

        mock_db = MagicMock()

        with (
            patch("middleware.theme_context.get_db", return_value=iter([mock_db])),
            patch.object(
                ThemeContextManager,
                "get_store_theme",
                side_effect=Exception("DB Error"),
            ),
        ):
            await middleware.dispatch(request, call_next)

        # Should fallback to default theme
        assert request.state.theme["theme_name"] == "default"
        call_next.assert_called_once()

    def test_get_current_theme_exists(self):
        """Test getting current theme when it exists."""
        request = Mock(spec=Request)
        test_theme = {"theme_name": "test"}
        request.state.theme = test_theme

        theme = get_current_theme(request)

        assert theme == test_theme

    def test_get_current_theme_default(self):
        """Test getting theme returns default when not set."""
        request = Mock(spec=Request)
        request.state = Mock(spec=[])  # No theme attribute

        theme = get_current_theme(request)

        assert theme["theme_name"] == "default"


@pytest.mark.unit
class TestThemeEdgeCases:
    """Test suite for edge cases and special scenarios."""

    @pytest.mark.asyncio
    async def test_middleware_closes_db_connection(self):
        """Test middleware properly closes database connection."""
        middleware = ThemeContextMiddleware(app=None)

        request = Mock(spec=Request)
        mock_store = Mock(id=1, name="Test")
        request.state = Mock(store=mock_store)

        call_next = AsyncMock(return_value=Mock())

        mock_db = MagicMock()

        with patch("middleware.theme_context.get_db", return_value=iter([mock_db])):
            await middleware.dispatch(request, call_next)

        # Verify database was closed
        mock_db.close.assert_called_once()

    def test_theme_default_immutability(self):
        """Test that getting default theme doesn't share state."""
        theme1 = ThemeContextManager.get_default_theme()
        theme2 = ThemeContextManager.get_default_theme()

        # Modify theme1
        theme1["colors"]["primary"] = "#000000"

        # theme2 should not be affected (if properly implemented)
        # Note: This test documents expected behavior
        assert theme2["colors"]["primary"] == "#6366f1"
