"""
Tests for the multi-theme system.

These tests ensure:
1. All themes define all required CSS variables
2. No hardcoded colors exist in critical template/CSS/JS files
3. Theme switching works correctly
4. RGB variants are defined for rgba() usage
"""

import re
from pathlib import Path

import pytest

# Base paths
WEB_DIR = (
    Path(__file__).parent.parent.parent / "src" / "local_deep_research" / "web"
)
STATIC_DIR = WEB_DIR / "static"
TEMPLATES_DIR = WEB_DIR / "templates"
CSS_DIR = STATIC_DIR / "css"
JS_DIR = STATIC_DIR / "js"


# Required CSS variables that every theme must define
REQUIRED_VARIABLES = [
    # Background colors
    "--bg-primary",
    "--bg-secondary",
    "--bg-tertiary",
    # Accent colors
    "--accent-primary",
    "--accent-secondary",
    "--accent-tertiary",
    # Text colors
    "--text-primary",
    "--text-secondary",
    "--text-muted",
    # Border
    "--border-color",
    # Status colors
    "--success-color",
    "--warning-color",
    "--error-color",
]

# RGB variants required for rgba() usage
REQUIRED_RGB_VARIANTS = [
    "--bg-primary-rgb",
    "--bg-secondary-rgb",
    "--bg-tertiary-rgb",
    "--accent-primary-rgb",
    "--accent-secondary-rgb",
    "--accent-tertiary-rgb",
    "--text-primary-rgb",
    "--text-secondary-rgb",
    "--text-muted-rgb",
    "--border-color-rgb",
    "--success-color-rgb",
    "--warning-color-rgb",
    "--error-color-rgb",
]


# Auto-detect themes from theme registry (single source of truth)
def _get_themes_from_registry() -> list[str]:
    """Get theme names from the theme registry."""
    try:
        from local_deep_research.web.themes import theme_registry

        # Exclude 'system' virtual theme for CSS tests
        return sorted(
            [t for t in theme_registry.get_theme_ids() if t != "system"]
        )
    except ImportError:
        # Fallback to parsing CSS if registry not available
        themes = set()
        if CSS_DIR.exists():
            themes_css = CSS_DIR / "themes.css"
            if themes_css.exists():
                content = themes_css.read_text()
                pattern = r'\[data-theme="([^"]+)"\]\s*\{'
                matches = re.findall(pattern, content)
                themes = set(matches)
        return sorted(themes)


def _generate_combined_css() -> str:
    """Generate combined CSS from theme registry for testing."""
    try:
        from local_deep_research.web.themes import theme_registry

        return theme_registry.get_combined_css()
    except ImportError:
        # Fallback to reading existing themes.css
        themes_css = CSS_DIR / "themes.css"
        if themes_css.exists():
            return themes_css.read_text()
        return ""


# All themes are auto-detected from theme registry
ALL_THEMES = _get_themes_from_registry()

# Hex color pattern (matches #fff, #ffffff, #FFFFFF)
HEX_COLOR_PATTERN = re.compile(r"#[0-9a-fA-F]{3,8}\b")

# RGB/RGBA color pattern (matches rgb(x,x,x) and rgba(x,x,x,x))
RGBA_LITERAL_PATTERN = re.compile(
    r"rgba?\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*[\d.]+\s*)?\)"
)

# Files that are allowed to have some hardcoded colors (brand colors, intentional)
ALLOWED_HARDCODED_FILES = [
    "pdf-viewer.css",  # PDF viewer needs light background for readability
    "details.js",  # Chart colors may need to be fixed for data visualization
]

# Brand colors that are intentionally hardcoded (provider logos, etc.)
BRAND_COLOR_EXCEPTIONS = [
    "#10a37f",  # OpenAI green
    "#d97706",  # Anthropic orange
    "#4285f4",  # Google blue
    "#000000",  # Pure black (often intentional)
    "#ffffff",  # Pure white (often intentional)
    "#fff",  # Pure white shorthand
    "#000",  # Pure black shorthand
]


class TestThemeDefinitions:
    """Test that all themes are properly defined."""

    @pytest.fixture
    def themes_css_content(self):
        """Load or generate themes.css content."""
        # Use the generated combined CSS from theme registry
        content = _generate_combined_css()
        assert content, "Failed to load or generate themes CSS"
        return content

    def test_theme_registry_loads(self):
        """Verify theme registry loads themes correctly."""
        try:
            from local_deep_research.web.themes import theme_registry

            themes = theme_registry.themes
            assert len(themes) > 0, "No themes loaded from registry"
            assert "hashed" in themes, "Default 'hashed' theme not found"
            assert "system" in themes, "Virtual 'system' theme not found"
        except ImportError:
            pytest.skip("Theme registry not available")

    def test_themes_css_can_be_generated(self):
        """Verify combined CSS can be generated."""
        content = _generate_combined_css()
        assert content, "Failed to generate combined CSS"
        assert "[data-theme=" in content, (
            "Generated CSS has no theme definitions"
        )

    def test_all_themes_defined(self, themes_css_content):
        """Check that all themes are defined in the combined CSS."""
        for theme in ALL_THEMES:
            if theme == "hashed":
                # Hashed theme is defined both in :root and as [data-theme="hashed"]
                assert (
                    ":root" in themes_css_content
                    or '[data-theme="hashed"]' in themes_css_content
                ), "Default hashed theme not found"
            else:
                theme_selector = f'[data-theme="{theme}"]'
                assert theme_selector in themes_css_content, (
                    f"Theme '{theme}' not defined in combined CSS"
                )

    def test_required_variables_in_root(self, themes_css_content):
        """Check that :root defines all required variables."""
        # Extract :root block
        root_match = re.search(
            r":root[^{]*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}", themes_css_content
        )
        assert root_match, ":root block not found in themes.css"
        root_content = root_match.group(1)

        for var in REQUIRED_VARIABLES:
            assert var in root_content, (
                f"Required variable '{var}' not defined in :root"
            )

    def test_rgb_variants_in_root(self, themes_css_content):
        """Check that :root defines RGB variants for rgba() usage."""
        root_match = re.search(
            r":root[^{]*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}", themes_css_content
        )
        assert root_match, ":root block not found"
        root_content = root_match.group(1)

        for var in REQUIRED_RGB_VARIANTS:
            assert var in root_content, (
                f"RGB variant '{var}' not defined in :root"
            )

    @pytest.mark.parametrize("theme", ALL_THEMES)
    def test_theme_has_required_variables(self, themes_css_content, theme):
        """Check each theme defines all required variables."""
        theme_pattern = (
            rf'\[data-theme="{theme}"\]\s*\{{([^}}]+(?:\{{[^}}]*\}}[^}}]*)*)\}}'
        )
        theme_match = re.search(theme_pattern, themes_css_content)
        assert theme_match, f"Theme '{theme}' block not found"
        theme_content = theme_match.group(1)

        for var in REQUIRED_VARIABLES:
            assert var in theme_content, (
                f"Theme '{theme}' missing required variable '{var}'"
            )

    @pytest.mark.parametrize("theme", ALL_THEMES)
    def test_theme_has_rgb_variants(self, themes_css_content, theme):
        """Check each theme defines RGB variants."""
        theme_pattern = (
            rf'\[data-theme="{theme}"\]\s*\{{([^}}]+(?:\{{[^}}]*\}}[^}}]*)*)\}}'
        )
        theme_match = re.search(theme_pattern, themes_css_content)
        assert theme_match, f"Theme '{theme}' block not found"
        theme_content = theme_match.group(1)

        for var in REQUIRED_RGB_VARIANTS:
            assert var in theme_content, (
                f"Theme '{theme}' missing RGB variant '{var}'"
            )


class TestHardcodedColors:
    """Test for hardcoded colors that should use theme variables."""

    def _get_files(self, directory: Path, pattern: str) -> list[Path]:
        """Get files matching pattern in directory."""
        if not directory.exists():
            return []
        return list(directory.rglob(pattern))

    def _extract_hardcoded_colors(
        self, content: str, file_path: Path
    ) -> list[tuple[str, int, str]]:
        """Extract hardcoded colors from content.

        Returns list of (color, line_number, line_content) tuples.
        """
        colors = []
        lines = content.split("\n")

        for i, line in enumerate(lines, 1):
            # Skip comments
            if line.strip().startswith("//") or line.strip().startswith("/*"):
                continue

            # Skip lines that use CSS variables correctly
            if "var(--" in line:
                continue

            # Find hex colors
            for match in HEX_COLOR_PATTERN.finditer(line):
                color = match.group().lower()
                if color not in [c.lower() for c in BRAND_COLOR_EXCEPTIONS]:
                    colors.append((color, i, line.strip()))

        return colors

    def _extract_literal_rgba(
        self, content: str, file_path: Path
    ) -> list[tuple[str, int, str]]:
        """Extract literal rgba() colors that should use CSS variables.

        Returns list of (color, line_number, line_content) tuples.
        """
        colors = []
        lines = content.split("\n")

        for i, line in enumerate(lines, 1):
            # Skip if already using CSS variable pattern: rgba(var(--xxx-rgb), 0.5)
            if "rgba(var(--" in line:
                continue

            # Skip comments
            if line.strip().startswith("//") or line.strip().startswith("/*"):
                continue

            # Find literal rgba() calls
            for match in RGBA_LITERAL_PATTERN.finditer(line):
                color = match.group()
                # Allow black/white rgba as they're often intentional
                if not any(
                    x in color
                    for x in [
                        "0, 0, 0",
                        "255, 255, 255",
                        "0,0,0",
                        "255,255,255",
                    ]
                ):
                    colors.append((color, i, line.strip()))

        return colors

    def test_templates_no_hardcoded_hex_colors(self):
        """Check HTML templates don't have hardcoded hex colors in style attributes."""
        templates = self._get_files(TEMPLATES_DIR, "*.html")
        issues = []

        for template in templates:
            if any(
                allowed in template.name for allowed in ALLOWED_HARDCODED_FILES
            ):
                continue

            content = template.read_text()

            # Focus on style attributes and inline styles
            style_pattern = re.compile(
                r'style\s*=\s*["\'][^"\']*#[0-9a-fA-F]{3,8}[^"\']*["\']'
            )
            for i, line in enumerate(content.split("\n"), 1):
                if style_pattern.search(line):
                    # Extract the actual color
                    colors = HEX_COLOR_PATTERN.findall(line)
                    for color in colors:
                        if color.lower() not in [
                            c.lower() for c in BRAND_COLOR_EXCEPTIONS
                        ]:
                            issues.append(
                                f"{template.name}:{i} - hardcoded color {color}"
                            )

        if issues:
            issue_sample = issues[:10]
            pytest.fail(
                f"Found {len(issues)} hardcoded colors in templates:\n"
                + "\n".join(issue_sample)
                + ("\n..." if len(issues) > 10 else "")
            )

    def test_css_files_no_literal_rgba(self):
        """Check CSS files don't have literal rgba() that should use variables."""
        css_files = self._get_files(CSS_DIR, "*.css")
        issues = []

        for css_file in css_files:
            # Skip themes.css (it defines the colors)
            if css_file.name == "themes.css":
                continue

            if any(
                allowed in css_file.name for allowed in ALLOWED_HARDCODED_FILES
            ):
                continue

            content = css_file.read_text()
            colors = self._extract_literal_rgba(content, css_file)

            for color, line_num, line in colors:
                issues.append(f"{css_file.name}:{line_num} - {color}")

        if issues:
            issue_sample = issues[:15]
            pytest.fail(
                f"Found {len(issues)} literal rgba() colors that should use CSS variables:\n"
                + "\n".join(issue_sample)
                + ("\n..." if len(issues) > 15 else "")
            )

    def test_js_files_no_inline_style_colors(self):
        """Check JS files don't set inline style colors directly."""
        js_files = self._get_files(JS_DIR, "*.js")
        issues = []

        # Pattern to find style.color = '#xxx' or style.backgroundColor = '#xxx'
        js_color_pattern = re.compile(
            r'\.style\.\w+\s*=\s*["\']#[0-9a-fA-F]{3,8}["\']'
        )

        for js_file in js_files:
            if any(
                allowed in js_file.name for allowed in ALLOWED_HARDCODED_FILES
            ):
                continue

            content = js_file.read_text()

            for i, line in enumerate(content.split("\n"), 1):
                if js_color_pattern.search(line):
                    issues.append(f"{js_file.name}:{i} - {line.strip()[:80]}")

        if issues:
            issue_sample = issues[:10]
            pytest.fail(
                f"Found {len(issues)} inline style color assignments in JS:\n"
                + "\n".join(issue_sample)
                + ("\n..." if len(issues) > 10 else "")
            )


class TestThemeVariableUsage:
    """Test that theme variables are used correctly throughout the codebase."""

    def test_styles_css_uses_variables(self):
        """Verify main styles.css uses theme variables."""
        styles_css = CSS_DIR / "styles.css"
        assert styles_css.exists(), "styles.css not found"

        content = styles_css.read_text()

        # Should use theme variables
        assert "var(--bg-primary)" in content, (
            "styles.css should use --bg-primary"
        )
        assert "var(--text-primary)" in content, (
            "styles.css should use --text-primary"
        )
        assert "var(--accent-primary)" in content, (
            "styles.css should use --accent-primary"
        )

    def test_base_html_has_theme_attribute(self):
        """Verify base.html supports data-theme attribute."""
        base_html = TEMPLATES_DIR / "base.html"
        assert base_html.exists(), "base.html not found"

        content = base_html.read_text()

        # Should have data-theme or theme initialization
        assert "data-theme" in content or "theme" in content.lower(), (
            "base.html should support theme attribute"
        )

    def test_theme_toggle_exists(self):
        """Verify theme toggle functionality exists."""
        # Check for theme-related JavaScript
        js_files = list(JS_DIR.rglob("*.js"))
        theme_js_found = False

        for js_file in js_files:
            content = js_file.read_text()
            if (
                "setTheme" in content
                or "data-theme" in content
                or "theme" in content.lower()
            ):
                theme_js_found = True
                break

        assert theme_js_found, (
            "No theme toggle functionality found in JavaScript"
        )


class TestDarkLightThemes:
    """Test that dark and light themes have appropriate brightness levels."""

    # Light themes are identified by having "light" in name or specific names
    LIGHT_THEME_PATTERNS = ["light", "dawn", "sepia"]

    # Dynamically determine light/dark themes from ALL_THEMES
    @classmethod
    def _is_light_theme(cls, theme: str) -> bool:
        """Check if a theme is a light theme based on name patterns."""
        theme_lower = theme.lower()
        return any(
            pattern in theme_lower for pattern in cls.LIGHT_THEME_PATTERNS
        )

    # Auto-detect dark and light themes
    DARK_THEMES = [
        t
        for t in ALL_THEMES
        if not any(p in t.lower() for p in ["light", "dawn", "sepia"])
    ]
    LIGHT_THEMES = [
        t
        for t in ALL_THEMES
        if any(p in t.lower() for p in ["light", "dawn", "sepia"])
    ]

    @pytest.fixture
    def themes_css_content(self):
        """Combined theme CSS from the theme registry (single source of truth).

        Must NOT read ``static/css/themes.css`` directly: that file is
        auto-generated and gitignored (PR #3400), so a fresh checkout / CI
        run does not have it — which made these tests error with
        FileNotFoundError (or fail extraction against a separately-built
        copy). Generating from the registry is what the app serves and what
        the TestThemeDefinitions fixture already does.
        """
        return _generate_combined_css()

    def _get_luminance(self, hex_color: str) -> float:
        """Calculate relative luminance of a color (0-1 scale)."""
        r = int(hex_color[1:3], 16) / 255
        g = int(hex_color[3:5], 16) / 255
        b = int(hex_color[5:7], 16) / 255
        return 0.299 * r + 0.587 * g + 0.114 * b

    def _extract_theme_bg_primary(
        self, themes_css_content: str, theme: str
    ) -> str | None:
        """Extract --bg-primary value for a theme."""
        if theme == "dark":
            pattern = r":root[^{]*\{[^}]*--bg-primary:\s*(#[0-9a-fA-F]{6})"
        else:
            pattern = rf'\[data-theme="{theme}"\][^{{]*\{{[^}}]*--bg-primary:\s*(#[0-9a-fA-F]{{6}})'
        match = re.search(pattern, themes_css_content, re.DOTALL)
        return match.group(1) if match else None

    @pytest.mark.parametrize("theme", DARK_THEMES)
    def test_dark_theme_has_dark_background(self, themes_css_content, theme):
        """Dark themes should have low luminance backgrounds."""
        bg_color = self._extract_theme_bg_primary(themes_css_content, theme)
        assert bg_color, f"Could not extract --bg-primary for theme '{theme}'"
        luminance = self._get_luminance(bg_color)
        assert luminance < 0.35, (
            f"Dark theme '{theme}' has too bright background {bg_color} "
            f"(luminance: {luminance:.2f}, expected < 0.35)"
        )

    @pytest.mark.parametrize("theme", LIGHT_THEMES)
    def test_light_theme_has_light_background(self, themes_css_content, theme):
        """Light themes should have high luminance backgrounds."""
        bg_color = self._extract_theme_bg_primary(themes_css_content, theme)
        assert bg_color, f"Could not extract --bg-primary for theme '{theme}'"
        luminance = self._get_luminance(bg_color)
        assert luminance > 0.5, (
            f"Light theme '{theme}' has too dark background {bg_color} "
            f"(luminance: {luminance:.2f}, expected > 0.5)"
        )


class TestTextContrast:
    """Test that text colors have sufficient contrast against backgrounds."""

    @pytest.fixture
    def themes_css_content(self):
        """Combined theme CSS from the theme registry (single source of truth).

        Must NOT read ``static/css/themes.css`` directly: that file is
        auto-generated and gitignored (PR #3400), so a fresh checkout / CI
        run does not have it — which made these tests error with
        FileNotFoundError (or fail extraction against a separately-built
        copy). Generating from the registry is what the app serves and what
        the TestThemeDefinitions fixture already does.
        """
        return _generate_combined_css()

    def _get_luminance(self, hex_color: str) -> float:
        """Calculate relative luminance."""
        r = int(hex_color[1:3], 16) / 255
        g = int(hex_color[3:5], 16) / 255
        b = int(hex_color[5:7], 16) / 255
        return 0.299 * r + 0.587 * g + 0.114 * b

    def _contrast_ratio(self, l1: float, l2: float) -> float:
        """Calculate contrast ratio between two luminance values."""
        lighter = max(l1, l2)
        darker = min(l1, l2)
        return (lighter + 0.05) / (darker + 0.05)

    def test_root_text_contrast(self, themes_css_content):
        """Test that :root has adequate text-to-background contrast."""
        # Extract colors from :root
        bg_match = re.search(
            r":root[^{]*\{[^}]*--bg-primary:\s*(#[0-9a-fA-F]{6})",
            themes_css_content,
            re.DOTALL,
        )
        text_match = re.search(
            r":root[^{]*\{[^}]*--text-primary:\s*(#[0-9a-fA-F]{6})",
            themes_css_content,
            re.DOTALL,
        )

        assert bg_match and text_match, (
            "Could not find bg-primary or text-primary in :root"
        )

        bg_lum = self._get_luminance(bg_match.group(1))
        text_lum = self._get_luminance(text_match.group(1))
        ratio = self._contrast_ratio(bg_lum, text_lum)

        # WCAG AA requires 4.5:1 for normal text
        assert ratio >= 4.5, (
            f"Insufficient contrast ratio in :root: {ratio:.2f} "
            f"(bg: {bg_match.group(1)}, text: {text_match.group(1)})"
        )


class TestComponentCoverage:
    """Test that key CSS files properly use theme variables."""

    CSS_FILES_REQUIRING_THEME_VARS = [
        "styles.css",
        "collections.css",
        "settings.css",
    ]

    REQUIRED_VAR_USAGE = [
        "var(--bg-",
        "var(--text-",
        "var(--accent-",
        "var(--border-color)",
    ]

    @pytest.mark.parametrize("css_file", CSS_FILES_REQUIRING_THEME_VARS)
    def test_css_file_uses_theme_vars(self, css_file):
        """Check that critical CSS files use theme variables."""
        file_path = CSS_DIR / css_file
        if not file_path.exists():
            pytest.skip(f"{css_file} not found")

        content = file_path.read_text()
        missing_vars = []

        for var_pattern in self.REQUIRED_VAR_USAGE:
            if var_pattern not in content:
                missing_vars.append(var_pattern)

        assert not missing_vars, (
            f"{css_file} is missing theme variable usage: {missing_vars}"
        )


class TestSemanticColorUsage:
    """Test that semantic colors (success, warning, error) are used correctly."""

    @pytest.fixture
    def all_css_content(self):
        """Load all CSS files content."""
        content = {}
        for css_file in CSS_DIR.rglob("*.css"):
            content[css_file.name] = css_file.read_text()
        return content

    def test_success_uses_success_color(self, all_css_content):
        """Classes with 'success' should use --success-color."""
        issues = []
        for filename, content in all_css_content.items():
            if filename == "themes.css":
                continue
            # Find .success or -success classes
            success_classes = re.findall(
                r"\.([\w-]*success[\w-]*)\s*\{([^}]+)\}", content
            )
            for class_name, class_content in success_classes:
                if (
                    "color:" in class_content
                    and "var(--success-color)" not in class_content
                ):
                    # Check if it's using success-color-rgb variant
                    if "var(--success-color-rgb)" not in class_content:
                        issues.append(f"{filename}: .{class_name}")

        # Allow a few exceptions but flag if too many
        if len(issues) > 3:
            pytest.fail(
                f"Success classes not using --success-color: {issues[:5]}"
            )

    def test_error_uses_error_color(self, all_css_content):
        """Classes with 'error' or 'danger' should use --error-color."""
        issues = []
        for filename, content in all_css_content.items():
            if filename == "themes.css":
                continue
            # Find .error or .danger classes
            error_classes = re.findall(
                r"\.([\w-]*(?:error|danger)[\w-]*)\s*\{([^}]+)\}", content
            )
            for class_name, class_content in error_classes:
                if "color:" in class_content:
                    if (
                        "var(--error-color)" not in class_content
                        and "var(--error-color-rgb)" not in class_content
                    ):
                        issues.append(f"{filename}: .{class_name}")

        if len(issues) > 3:
            pytest.fail(
                f"Error/danger classes not using --error-color: {issues[:5]}"
            )

    def test_warning_uses_warning_color(self, all_css_content):
        """Classes with 'warning' should use --warning-color."""
        issues = []
        for filename, content in all_css_content.items():
            if filename == "themes.css":
                continue
            warning_classes = re.findall(
                r"\.([\w-]*warning[\w-]*)\s*\{([^}]+)\}", content
            )
            for class_name, class_content in warning_classes:
                if "color:" in class_content:
                    if (
                        "var(--warning-color)" not in class_content
                        and "var(--warning-color-rgb)" not in class_content
                    ):
                        issues.append(f"{filename}: .{class_name}")

        if len(issues) > 3:
            pytest.fail(
                f"Warning classes not using --warning-color: {issues[:5]}"
            )


class TestCSSVariableFallbacks:
    """Test that CSS variable fallbacks are appropriate."""

    # Bootstrap colors that should NOT be used as fallbacks
    BOOTSTRAP_COLORS = [
        "#007bff",  # Bootstrap primary blue
        "#28a745",  # Bootstrap success green
        "#dc3545",  # Bootstrap danger red
        "#ffc107",  # Bootstrap warning yellow
        "#17a2b8",  # Bootstrap info
        "#6c757d",  # Bootstrap secondary
        "#343a40",  # Bootstrap dark
        "#f8f9fa",  # Bootstrap light
    ]

    def test_no_bootstrap_fallbacks_in_css(self):
        """CSS files should not use Bootstrap colors as fallbacks."""
        issues = []
        fallback_pattern = re.compile(r"var\([^,]+,\s*(#[0-9a-fA-F]{6})\)")

        for css_file in CSS_DIR.rglob("*.css"):
            if css_file.name == "themes.css":
                continue

            content = css_file.read_text()
            for match in fallback_pattern.finditer(content):
                fallback_color = match.group(1).lower()
                if fallback_color in [c.lower() for c in self.BOOTSTRAP_COLORS]:
                    issues.append(
                        f"{css_file.name}: Bootstrap color {fallback_color} as fallback"
                    )

        if issues:
            pytest.fail(
                "Found Bootstrap colors used as fallbacks:\n"
                + "\n".join(issues[:10])
            )

    def test_no_bootstrap_fallbacks_in_js(self):
        """JS files should not use Bootstrap colors as fallbacks."""
        issues = []
        fallback_pattern = re.compile(
            r"var\([^,]+,\s*['\"]?(#[0-9a-fA-F]{6})['\"]?\)"
        )

        for js_file in JS_DIR.rglob("*.js"):
            content = js_file.read_text()
            for match in fallback_pattern.finditer(content):
                fallback_color = match.group(1).lower()
                if fallback_color in [c.lower() for c in self.BOOTSTRAP_COLORS]:
                    issues.append(
                        f"{js_file.name}: Bootstrap color {fallback_color}"
                    )

        if issues:
            pytest.fail(
                "Found Bootstrap colors in JS fallbacks:\n"
                + "\n".join(issues[:10])
            )


class TestGradientTheming:
    """Test that gradients use theme variables."""

    def test_gradients_use_theme_vars(self):
        """Gradients should use theme variables, not hardcoded colors."""
        issues = []
        # Pattern for gradients with hardcoded colors (not var())
        gradient_pattern = re.compile(
            r"(?:linear|radial)-gradient\s*\([^)]*#[0-9a-fA-F]{3,8}[^)]*\)"
        )

        for css_file in CSS_DIR.rglob("*.css"):
            if css_file.name == "themes.css":
                continue

            content = css_file.read_text()
            for i, line in enumerate(content.split("\n"), 1):
                # Skip if using CSS variables
                if "var(--" in line:
                    continue
                if gradient_pattern.search(line):
                    issues.append(f"{css_file.name}:{i}")

        # Allow a small number for intentional gradients
        if len(issues) > 5:
            pytest.fail(
                f"Found {len(issues)} gradients with hardcoded colors:\n"
                + "\n".join(issues[:10])
            )


class TestBorderColorTheming:
    """Test that border colors use theme variables."""

    def test_borders_use_theme_colors(self):
        """Border declarations should use theme variables."""
        issues = []
        # Pattern for border with hardcoded color (not CSS custom property definitions)
        border_color_pattern = re.compile(
            r"border(?:-(?:top|right|bottom|left))?(?:-color)?:\s*[^;]*#[0-9a-fA-F]{3,8}"
        )

        for css_file in CSS_DIR.rglob("*.css"):
            if css_file.name == "themes.css":
                continue

            content = css_file.read_text()
            for i, line in enumerate(content.split("\n"), 1):
                # Skip lines using CSS variables
                if "var(--" in line:
                    continue
                # Skip CSS custom property definitions (theme-specific values)
                if line.strip().startswith("--"):
                    continue
                if border_color_pattern.search(line):
                    # Exclude black/white borders (often intentional)
                    if (
                        "#000" not in line.lower()
                        and "#fff" not in line.lower()
                    ):
                        issues.append(
                            f"{css_file.name}:{i}: {line.strip()[:60]}"
                        )

        if len(issues) > 10:
            pytest.fail(
                f"Found {len(issues)} borders with hardcoded colors:\n"
                + "\n".join(issues[:15])
            )


class TestJSThemeIntegration:
    """Test JavaScript files properly integrate with theme system."""

    def test_js_uses_css_variables_for_colors(self):
        """JS that sets colors should use CSS variables."""
        issues = []

        # Patterns that indicate proper CSS variable usage
        good_patterns = [
            "var(--",
            "getPropertyValue",
            "getComputedStyle",
        ]

        # Pattern for direct color assignment
        bad_pattern = re.compile(
            r'\.style\.\w*[Cc]olor\s*=\s*["\']#[0-9a-fA-F]{3,8}["\']'
        )

        for js_file in JS_DIR.rglob("*.js"):
            if "details.js" in js_file.name:  # Chart colors exception
                continue

            content = js_file.read_text()
            for i, line in enumerate(content.split("\n"), 1):
                if bad_pattern.search(line):
                    # Check if the line also uses CSS variables
                    if not any(p in line for p in good_patterns):
                        issues.append(f"{js_file.name}:{i}")

        if issues:
            pytest.fail(
                f"Found {len(issues)} JS lines setting colors directly:\n"
                + "\n".join(issues[:10])
            )

    def test_js_reads_css_variables_correctly(self):
        """JS should read CSS variables using getComputedStyle."""
        js_files_using_colors = []

        for js_file in JS_DIR.rglob("*.js"):
            content = js_file.read_text()
            if "getPropertyValue" in content and "--" in content:
                js_files_using_colors.append(js_file.name)

        # We should have at least some files reading CSS variables
        assert len(js_files_using_colors) >= 2, (
            "Expected at least 2 JS files to read CSS variables with getPropertyValue"
        )


class TestTemplateInlineStyles:
    """Extended tests for inline styles in templates."""

    def test_no_inline_background_colors(self):
        """Templates should not have inline background-color with hex values."""
        issues = []
        pattern = re.compile(
            r'style\s*=\s*["\'][^"\']*background(?:-color)?:\s*#[0-9a-fA-F]{3,8}'
        )

        for template in TEMPLATES_DIR.rglob("*.html"):
            content = template.read_text()
            for i, line in enumerate(content.split("\n"), 1):
                if "var(--" in line:
                    continue
                if pattern.search(line):
                    issues.append(f"{template.name}:{i}")

        if issues:
            pytest.fail(
                f"Found {len(issues)} inline background colors:\n"
                + "\n".join(issues[:10])
            )

    def test_no_inline_text_colors(self):
        """Templates should not have inline color with hex values."""
        issues = []
        # More specific pattern to avoid matching background-color
        pattern = re.compile(
            r'style\s*=\s*["\'][^"\']*(?<!background-)color:\s*#[0-9a-fA-F]{3,8}'
        )

        for template in TEMPLATES_DIR.rglob("*.html"):
            content = template.read_text()
            for i, line in enumerate(content.split("\n"), 1):
                if "var(--" in line:
                    continue
                if pattern.search(line):
                    issues.append(f"{template.name}:{i}")

        if issues:
            pytest.fail(
                f"Found {len(issues)} inline text colors:\n"
                + "\n".join(issues[:10])
            )


class TestThemeNamingConventions:
    """Test that theme names follow conventions."""

    @pytest.fixture
    def themes_css_content(self):
        """Combined theme CSS from the registry (single source of truth).

        Generated from the theme registry rather than read from the
        gitignored, auto-generated ``static/css/themes.css`` (PR #3400),
        which is absent on a fresh checkout / in CI.
        """
        return _generate_combined_css()

    def test_theme_names_are_lowercase_kebab(self, themes_css_content):
        """Theme names should be lowercase with hyphens."""
        theme_pattern = re.compile(r'\[data-theme="([^"]+)"\]')
        themes = theme_pattern.findall(themes_css_content)

        for theme in themes:
            assert theme == theme.lower(), (
                f"Theme '{theme}' should be lowercase"
            )
            assert "_" not in theme, (
                f"Theme '{theme}' should use hyphens, not underscores"
            )
            assert " " not in theme, (
                f"Theme '{theme}' should not contain spaces"
            )

    def test_theme_count(self, themes_css_content):
        """Should have at least 20 themes defined with data-theme attribute."""
        theme_pattern = re.compile(r'\[data-theme="([^"]+)"\]')
        themes = set(theme_pattern.findall(themes_css_content))

        # Should have a reasonable number of themes (at least original 20)
        assert len(themes) >= 20, (
            f"Expected at least 20 data-theme themes, found {len(themes)}: {themes}"
        )
        # Verify detected count matches ALL_THEMES
        assert len(ALL_THEMES) == len(themes), (
            f"ALL_THEMES count ({len(ALL_THEMES)}) doesn't match CSS ({len(themes)})"
        )


class TestAdditionalRGBVariants:
    """Additional tests for RGB variant correctness."""

    @pytest.fixture
    def themes_css_content(self):
        """Combined theme CSS from the registry (single source of truth).

        Generated from the theme registry rather than read from the
        gitignored, auto-generated ``static/css/themes.css`` (PR #3400),
        which is absent on a fresh checkout / in CI.
        """
        return _generate_combined_css()

    def test_all_required_rgb_variants_exist(self, themes_css_content):
        """Every required variable should have an RGB variant."""
        required_base_vars = [
            "bg-primary",
            "bg-secondary",
            "bg-tertiary",
            "accent-primary",
            "accent-secondary",
            "accent-tertiary",
            "text-primary",
            "text-secondary",
            "text-muted",
            "border-color",
            "success-color",
            "warning-color",
            "error-color",
        ]

        for var in required_base_vars:
            rgb_var = f"--{var}-rgb"
            assert rgb_var in themes_css_content, (
                f"Missing RGB variant: {rgb_var}"
            )

    def test_rgb_format_is_correct(self, themes_css_content):
        """RGB values should be in 'R, G, B' format (no parentheses)."""
        rgb_pattern = re.compile(r"--([\w-]+)-rgb:\s*([^;]+);")

        for match in rgb_pattern.finditer(themes_css_content):
            var_name = match.group(1)
            value = match.group(2).strip()

            # Should be three numbers separated by commas
            parts = value.split(",")
            assert len(parts) == 3, (
                f"--{var_name}-rgb should have 3 components: {value}"
            )

            for part in parts:
                try:
                    num = int(part.strip())
                    assert 0 <= num <= 255, (
                        f"--{var_name}-rgb component out of range: {num}"
                    )
                except ValueError:
                    pytest.fail(
                        f"--{var_name}-rgb has non-numeric component: {part}"
                    )


class TestColorConsistency:
    """Test color value consistency across themes."""

    @pytest.fixture
    def themes_css_content(self):
        """Combined theme CSS from the registry (single source of truth).

        Generated from the theme registry rather than read from the
        gitignored, auto-generated ``static/css/themes.css`` (PR #3400),
        which is absent on a fresh checkout / in CI.
        """
        return _generate_combined_css()

    def test_success_color_is_greenish(self, themes_css_content):
        """Success colors should be in the green spectrum."""
        # Extract success colors from all themes
        success_pattern = re.compile(r"--success-color:\s*(#[0-9a-fA-F]{6})")
        success_colors = success_pattern.findall(themes_css_content)

        for color in success_colors:
            # Green component should be relatively high
            r = int(color[1:3], 16)
            g = int(color[3:5], 16)

            # For success colors, green should generally be prominent
            # (allowing for some exceptions like high-contrast yellow-green)
            assert g >= r // 2 or g >= 100, (
                f"Success color {color} doesn't look greenish enough"
            )

    def test_error_color_is_reddish(self, themes_css_content):
        """Error colors should be in the red spectrum."""
        error_pattern = re.compile(r"--error-color:\s*(#[0-9a-fA-F]{6})")
        error_colors = error_pattern.findall(themes_css_content)

        for color in error_colors:
            r = int(color[1:3], 16)
            g = int(color[3:5], 16)
            b = int(color[5:7], 16)

            # Red component should be prominent
            assert r >= 150 or (r > g and r > b), (
                f"Error color {color} doesn't look reddish enough"
            )

    def test_rgb_values_match_hex(self, themes_css_content):
        """RGB variants should match their hex counterparts."""
        # Find pairs like --bg-primary: #121212 and --bg-primary-rgb: 18, 18, 18
        hex_pattern = re.compile(r"--(\w+(?:-\w+)*):\s*(#[0-9a-fA-F]{6})")
        rgb_pattern = re.compile(
            r"--(\w+(?:-\w+)*)-rgb:\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)"
        )

        hex_colors = {}
        rgb_colors = {}

        for match in hex_pattern.finditer(themes_css_content):
            var_name = match.group(1)
            hex_value = match.group(2)
            if var_name not in hex_colors:
                hex_colors[var_name] = []
            hex_colors[var_name].append(hex_value)

        for match in rgb_pattern.finditer(themes_css_content):
            var_name = match.group(1)
            rgb_value = (
                int(match.group(2)),
                int(match.group(3)),
                int(match.group(4)),
            )
            if var_name not in rgb_colors:
                rgb_colors[var_name] = []
            rgb_colors[var_name].append(rgb_value)

        # Check a sample of variables
        for var_name in ["bg-primary", "accent-primary", "success-color"]:
            if var_name in hex_colors and var_name in rgb_colors:
                for i, hex_val in enumerate(
                    hex_colors[var_name][:3]
                ):  # Check first 3
                    expected_r = int(hex_val[1:3], 16)
                    expected_g = int(hex_val[3:5], 16)
                    expected_b = int(hex_val[5:7], 16)

                    if i < len(rgb_colors[var_name]):
                        actual = rgb_colors[var_name][i]
                        assert actual == (expected_r, expected_g, expected_b), (
                            f"RGB mismatch for {var_name}: "
                            f"hex {hex_val} != rgb {actual}"
                        )


class TestShadowTheming:
    """Test that box shadows use theme-appropriate colors."""

    def test_shadows_use_rgba_variables(self):
        """Box shadows should use rgba with CSS variables, not hardcoded colors."""
        issues = []
        # Pattern for box-shadow with hardcoded rgba
        shadow_pattern = re.compile(
            r"box-shadow:\s*[^;]*rgba\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+",
            re.IGNORECASE,
        )

        for css_file in CSS_DIR.rglob("*.css"):
            if css_file.name == "themes.css":
                continue

            content = css_file.read_text()
            for i, line in enumerate(content.split("\n"), 1):
                # Skip if using CSS variable for rgba
                if "rgba(var(--" in line:
                    continue
                # Allow black shadows (common and intentional)
                if "rgba(0, 0, 0" in line or "rgba(0,0,0" in line:
                    continue
                if shadow_pattern.search(line):
                    issues.append(f"{css_file.name}:{i}")

        if len(issues) > 5:
            pytest.fail(
                f"Found {len(issues)} shadows with non-black hardcoded colors:\n"
                + "\n".join(issues[:10])
            )

    def test_no_hardcoded_shadow_colors_in_js(self):
        """JavaScript should not set hardcoded shadow colors."""
        issues = []
        shadow_pattern = re.compile(
            r"boxShadow\s*=\s*['\"].*?#[0-9a-fA-F]{3,8}", re.IGNORECASE
        )

        for js_file in JS_DIR.rglob("*.js"):
            content = js_file.read_text()
            for i, line in enumerate(content.split("\n"), 1):
                if shadow_pattern.search(line):
                    issues.append(f"{js_file.name}:{i}")

        if issues:
            pytest.fail(
                f"Found {len(issues)} hardcoded shadows in JS:\n"
                + "\n".join(issues[:10])
            )


class TestFocusStateTheming:
    """Test that focus states use theme colors."""

    def test_focus_outline_uses_theme_colors(self):
        """Focus outlines should use accent colors from theme."""
        focus_uses_var = False
        focus_pattern = re.compile(r":focus[^{]*\{[^}]*outline", re.DOTALL)

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            if focus_pattern.search(content):
                if "var(--accent-" in content or "var(--border-" in content:
                    focus_uses_var = True
                    break

        assert focus_uses_var, (
            "Focus states should use theme variables for outline colors"
        )

    def test_focus_visible_defined(self):
        """Should have :focus-visible styles for keyboard navigation."""
        focus_visible_found = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            if ":focus-visible" in content:
                focus_visible_found = True
                break

        assert focus_visible_found, (
            "Should define :focus-visible styles for accessibility"
        )


class TestFormElementTheming:
    """Test that form elements use theme colors."""

    def test_input_uses_theme_colors(self):
        """Input elements should use theme background and text colors."""
        input_pattern = re.compile(
            r"input\s*(?:\[[^\]]*\])?\s*\{[^}]+\}", re.DOTALL
        )

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            matches = input_pattern.findall(content)
            for match in matches:
                # Should use theme vars, not hardcoded colors
                if "background" in match and "#" in match:
                    if "var(--" not in match:
                        pytest.fail(
                            f"Input styles in {css_file.name} have hardcoded bg"
                        )

    def test_select_uses_theme_colors(self):
        """Select elements should use theme colors."""
        for css_file in CSS_DIR.rglob("*.css"):
            if css_file.name in ["custom_dropdown.css", "settings.css"]:
                content = css_file.read_text()
                if "select" in content.lower():
                    assert "var(--" in content, (
                        f"{css_file.name} should use theme variables for selects"
                    )
                    break


class TestButtonTheming:
    """Test that buttons use theme colors appropriately."""

    def test_primary_button_uses_accent(self):
        """Primary buttons should use accent colors."""
        primary_btn_uses_accent = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            # Look for primary button definitions
            if ".btn-primary" in content or ".ldr-btn-primary" in content:
                if "var(--accent-primary)" in content:
                    primary_btn_uses_accent = True
                    break

        assert primary_btn_uses_accent, (
            "Primary buttons should use --accent-primary"
        )

    def test_button_hover_states_themed(self):
        """Button hover states should use theme colors."""
        hover_pattern = re.compile(r"\.btn[^{]*:hover\s*\{([^}]+)\}", re.DOTALL)

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            matches = hover_pattern.findall(content)
            for match in matches:
                if "#" in match and "var(--" not in match:
                    # Has hardcoded color without CSS variable
                    # Check if it's a brand exception
                    if not any(
                        brand.lower() in match.lower()
                        for brand in BRAND_COLOR_EXCEPTIONS
                    ):
                        pytest.fail(
                            f"Button hover in {css_file.name} has hardcoded color"
                        )


class TestModalTheming:
    """Test that modals and dialogs use theme colors."""

    def test_modal_content_uses_theme_bg(self):
        """Modal content should use theme background colors."""
        for template in TEMPLATES_DIR.rglob("*.html"):
            if "modal" in template.name.lower():
                content = template.read_text()
                # Should not have hardcoded background colors
                if (
                    "background-color: #" in content
                    or "background:#" in content
                ):
                    if "var(--" not in content:
                        pytest.fail(
                            f"Modal {template.name} has hardcoded background"
                        )


class TestLinkTheming:
    """Test that links use theme colors."""

    def test_links_use_accent_colors(self):
        """Links should use accent or dedicated link colors."""
        link_uses_theme = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            # Find anchor styles
            if re.search(r"\ba\s*\{", content) or re.search(
                r"\ba:link", content
            ):
                if "var(--accent-" in content or "var(--link-" in content:
                    link_uses_theme = True
                    break

        assert link_uses_theme, "Links should use theme accent or link colors"


class TestScrollbarTheming:
    """Test scrollbar theming for webkit browsers."""

    def test_scrollbar_uses_theme_colors(self):
        """Custom scrollbars should use theme colors."""
        scrollbar_uses_theme = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            if "::-webkit-scrollbar" in content:
                # Check if it uses theme variables
                if "var(--" in content:
                    scrollbar_uses_theme = True
                    break

        # Scrollbar styling is optional but should use theme vars if present
        if not scrollbar_uses_theme:
            # Check if any scrollbar styling exists
            for css_file in CSS_DIR.rglob("*.css"):
                if "::-webkit-scrollbar" in css_file.read_text():
                    pytest.fail(
                        "Scrollbar styles found but not using theme variables"
                    )


class TestTransitionAndAnimation:
    """Test that color transitions work with theme switching."""

    def test_theme_transition_defined(self):
        """Should have smooth transitions for theme changes."""
        transition_found = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            # Look for color/background transitions
            if re.search(
                r"transition[^;]*(?:color|background|border)[^;]*;", content
            ):
                transition_found = True
                break

        assert transition_found, (
            "Should have CSS transitions for smooth theme switching"
        )

    def test_no_color_flash_on_load(self):
        """Theme should be applied before content is visible."""
        base_html = TEMPLATES_DIR / "base.html"
        if not base_html.exists():
            pytest.skip("base.html not found")

        content = base_html.read_text()

        # Theme initialization should happen early (in head or early body)
        head_content = (
            content.split("</head>")[0] if "</head>" in content else ""
        )

        # Should have theme detection/setting in head
        theme_in_head = (
            "data-theme" in head_content
            or "theme" in head_content.lower()
            or "localStorage" in head_content
        )

        assert theme_in_head, (
            "Theme should be initialized in <head> to prevent flash"
        )


class TestCodeBlockTheming:
    """Test that code blocks and pre elements use theme colors."""

    def test_code_blocks_themed(self):
        """Code blocks should use theme-appropriate colors."""
        code_themed = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            if re.search(r"\bcode\b|\bpre\b", content):
                if "var(--" in content:
                    code_themed = True
                    break

        assert code_themed, "Code blocks should use theme variables"


class TestLoadingStateTheming:
    """Test that loading states use theme colors."""

    def test_spinner_uses_theme_colors(self):
        """Loading spinners should use theme accent color."""
        spinner_patterns = ["spinner", "loading", "loader"]
        spinner_themed = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            for pattern in spinner_patterns:
                if pattern in content.lower():
                    if "var(--accent-" in content:
                        spinner_themed = True
                        break
            if spinner_themed:
                break

        assert spinner_themed, "Spinners should use theme accent colors"


class TestNavTheming:
    """Test that navigation uses theme colors."""

    def test_nav_uses_theme_colors(self):
        """Navigation should use theme colors."""
        nav_themed = False

        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            if ".nav" in content or "nav " in content:
                if "var(--" in content:
                    nav_themed = True
                    break

        assert nav_themed, "Navigation should use theme variables"


class TestHighContrastCompliance:
    """Test high contrast theme meets accessibility requirements."""

    @pytest.fixture
    def themes_css_content(self):
        """Combined theme CSS from the theme registry (single source of truth).

        Must NOT read ``static/css/themes.css`` directly: that file is
        auto-generated and gitignored (PR #3400), so a fresh checkout / CI
        run does not have it — which made these tests error with
        FileNotFoundError (or fail extraction against a separately-built
        copy). Generating from the registry is what the app serves and what
        the TestThemeDefinitions fixture already does.
        """
        return _generate_combined_css()

    def _get_luminance(self, hex_color: str) -> float:
        """Calculate relative luminance."""
        r = int(hex_color[1:3], 16) / 255
        g = int(hex_color[3:5], 16) / 255
        b = int(hex_color[5:7], 16) / 255
        return 0.299 * r + 0.587 * g + 0.114 * b

    def _contrast_ratio(self, l1: float, l2: float) -> float:
        """Calculate contrast ratio."""
        lighter = max(l1, l2)
        darker = min(l1, l2)
        return (lighter + 0.05) / (darker + 0.05)

    def _extract_theme_color(
        self, themes_css_content: str, theme: str, var_name: str
    ) -> str | None:
        """Extract a color variable from a theme."""
        if theme == "dark":
            pattern = rf":root[^{{]*\{{[^}}]*{var_name}:\s*(#[0-9a-fA-F]{{6}})"
        else:
            pattern = rf'\[data-theme="{theme}"\][^{{]*\{{[^}}]*{var_name}:\s*(#[0-9a-fA-F]{{6}})'
        match = re.search(pattern, themes_css_content, re.DOTALL)
        return match.group(1) if match else None

    def test_high_contrast_has_higher_ratio(self, themes_css_content):
        """High contrast theme should have higher contrast than regular dark."""
        # Get dark theme contrast
        dark_bg = self._extract_theme_color(
            themes_css_content, "dark", "--bg-primary"
        )
        dark_text = self._extract_theme_color(
            themes_css_content, "dark", "--text-primary"
        )

        # Get high-contrast theme contrast
        hc_bg = self._extract_theme_color(
            themes_css_content, "high-contrast", "--bg-primary"
        )
        hc_text = self._extract_theme_color(
            themes_css_content, "high-contrast", "--text-primary"
        )

        if not all([dark_bg, dark_text, hc_bg, hc_text]):
            pytest.skip("Could not extract colors for comparison")

        dark_ratio = self._contrast_ratio(
            self._get_luminance(dark_bg), self._get_luminance(dark_text)
        )
        hc_ratio = self._contrast_ratio(
            self._get_luminance(hc_bg), self._get_luminance(hc_text)
        )

        assert hc_ratio >= dark_ratio, (
            f"High contrast ({hc_ratio:.2f}) should have >= contrast than dark ({dark_ratio:.2f})"
        )

    def test_high_contrast_meets_aaa(self, themes_css_content):
        """High contrast theme should meet WCAG AAA (7:1) for text."""
        hc_bg = self._extract_theme_color(
            themes_css_content, "high-contrast", "--bg-primary"
        )
        hc_text = self._extract_theme_color(
            themes_css_content, "high-contrast", "--text-primary"
        )

        if not hc_bg or not hc_text:
            pytest.skip("Could not extract high-contrast colors")

        ratio = self._contrast_ratio(
            self._get_luminance(hc_bg), self._get_luminance(hc_text)
        )

        assert ratio >= 7.0, (
            f"High contrast should meet WCAG AAA (7:1), got {ratio:.2f}"
        )


class TestThemeLocalStorage:
    """Test theme persistence via localStorage."""

    def test_theme_saved_to_localstorage(self):
        """JavaScript should save theme preference to localStorage."""
        localstorage_theme_found = False

        for js_file in JS_DIR.rglob("*.js"):
            content = js_file.read_text()
            if "localStorage" in content and "theme" in content.lower():
                localstorage_theme_found = True
                break

        assert localstorage_theme_found, (
            "Theme should be saved to localStorage for persistence"
        )

    def test_theme_loaded_from_localstorage(self):
        """Theme should be loaded from localStorage on page load."""
        load_found = False

        for js_file in JS_DIR.rglob("*.js"):
            content = js_file.read_text()
            if "localStorage.getItem" in content and "theme" in content.lower():
                load_found = True
                break

        # Also check templates
        if not load_found:
            for template in TEMPLATES_DIR.rglob("*.html"):
                content = template.read_text()
                if "localStorage" in content and "theme" in content.lower():
                    load_found = True
                    break

        assert load_found, "Theme should be loaded from localStorage on init"


class TestThemeCSSSyntax:
    """Test CSS syntax is valid in theme definitions."""

    @pytest.fixture
    def themes_css_content(self):
        """Combined theme CSS from the registry (single source of truth).

        Generated from the theme registry rather than read from the
        gitignored, auto-generated ``static/css/themes.css`` (PR #3400),
        which is absent on a fresh checkout / in CI.
        """
        return _generate_combined_css()

    def test_no_unclosed_braces(self, themes_css_content):
        """CSS should have balanced braces."""
        open_braces = themes_css_content.count("{")
        close_braces = themes_css_content.count("}")

        assert open_braces == close_braces, (
            f"Unbalanced braces: {open_braces} open, {close_braces} close"
        )

    def test_no_duplicate_theme_definitions(self, themes_css_content):
        """Each theme's root definition should only appear once.

        Note: Additional selectors like [data-theme="x"] a { } for element
        overrides are allowed and expected - we only check for duplicate
        root theme variable definitions (selectors followed directly by {).
        """
        # Pattern for root theme definitions only (not element-specific like
        # [data-theme="x"] a or [data-theme="x"] button)
        # This matches [data-theme="x"] { but not [data-theme="x"] a {
        theme_pattern = re.compile(r'\[data-theme="([^"]+)"\]\s*\{')
        themes = theme_pattern.findall(themes_css_content)

        # Count occurrences
        from collections import Counter

        counts = Counter(themes)

        duplicates = [t for t, c in counts.items() if c > 1]
        assert not duplicates, f"Duplicate root theme definitions: {duplicates}"

    def test_variables_have_values(self, themes_css_content):
        """CSS variables should have values, not be empty."""
        empty_var_pattern = re.compile(r"--[\w-]+:\s*;")
        empty_vars = empty_var_pattern.findall(themes_css_content)

        assert not empty_vars, f"Empty variable definitions found: {empty_vars}"

    def test_hex_colors_are_valid(self, themes_css_content):
        """All hex colors should be valid 3, 6, or 8 character hex."""
        hex_pattern = re.compile(r"#([0-9a-fA-F]+)\b")

        for match in hex_pattern.finditer(themes_css_content):
            hex_value = match.group(1)
            assert len(hex_value) in [3, 6, 8], (
                f"Invalid hex color length: #{hex_value} ({len(hex_value)} chars)"
            )


class TestMediaQueryTheming:
    """Test media query handling for theme preferences."""

    def test_prefers_color_scheme_support(self):
        """Should support prefers-color-scheme media query."""
        prefers_found = False

        # Check CSS
        for css_file in CSS_DIR.rglob("*.css"):
            content = css_file.read_text()
            if "prefers-color-scheme" in content:
                prefers_found = True
                break

        # Check JS
        if not prefers_found:
            for js_file in JS_DIR.rglob("*.js"):
                content = js_file.read_text()
                if "prefers-color-scheme" in content:
                    prefers_found = True
                    break

        assert prefers_found, (
            "Should support prefers-color-scheme for system theme detection"
        )


class TestResponsiveTheming:
    """Test that themes work across different screen sizes."""

    def test_no_fixed_color_in_media_queries(self):
        """Media queries should not override with hardcoded colors."""
        issues = []
        media_pattern = re.compile(
            r"@media[^{]*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}", re.DOTALL
        )

        for css_file in CSS_DIR.rglob("*.css"):
            if css_file.name == "themes.css":
                continue

            content = css_file.read_text()
            for match in media_pattern.finditer(content):
                media_content = match.group(1)
                # Check for hardcoded colors
                if re.search(r":\s*#[0-9a-fA-F]{3,8}\s*;", media_content):
                    if "var(--" not in media_content:
                        issues.append(css_file.name)
                        break

        if len(issues) > 3:
            pytest.fail(f"Media queries with hardcoded colors: {issues[:5]}")


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
