"""Tests for theme configuration module.

Tests NarrativeConfig, ThemeConfig dataclasses, and theme registry functions.
All fields asserted on returned objects. Minimum 1 error test per 2 happy path.
"""

from __future__ import annotations

from dataclasses import FrozenInstanceError
from typing import TYPE_CHECKING

import pytest

from unfamiliar.tui.themes.base import NarrativeConfig

if TYPE_CHECKING:
    from unfamiliar.tui.themes.registry import ThemeRegistry


class TestNarrativeConfig:
    """Tests for NarrativeConfig frozen dataclass."""

    def test_narrative_config_is_frozen_dataclass(self):
        """NarrativeConfig is frozen and raises on field assignment."""
        config = NarrativeConfig(
            tone="Test tone",
            title_examples=["Title 1", "Title 2"],
            intro_style="Test intro",
            outro_style="Test outro",
        )

        assert config.tone == "Test tone"
        assert config.title_examples == ["Title 1", "Title 2"]
        assert config.intro_style == "Test intro"
        assert config.outro_style == "Test outro"

        with pytest.raises(FrozenInstanceError):
            # Intentionally assigning to frozen field to verify it raises
            config.tone = "modified"  # noqa

    def test_film_noir_narrative_has_all_fields_populated(
        self, theme_registry: ThemeRegistry
    ):
        """Film Noir narrative config has tone, 5 title examples,
        intro and outro styles.
        """
        theme = theme_registry.get("film_noir")
        narrative = theme.narrative

        assert narrative.tone == (
            "Film Noir detective investigation. Moody, atmospheric, hardboiled. "
            "The developer is a detective investigating an unfamiliar codebase."
        )
        assert narrative.title_examples == [
            "The Password Vault",
            "Shadows in the Login Flow",
            "Following the Data Trail",
            "The Dead Drop in the Config",
            "Stakeout on the Event Loop",
        ]
        assert narrative.intro_style == (
            "Set a moody, investigative tone. The detective discovers a new lead, "
            "finds a clue, or gets a tip from an informant."
        )
        assert narrative.outro_style == (
            "Provide closure like a case being solved. The detective pieces together "
            "what they've learned, connecting it to the bigger picture."
        )


class TestThemeConfig:
    """Tests for ThemeConfig vocab and alias methods."""

    def test_vocab_returns_themed_term_for_known_key(
        self, theme_registry: ThemeRegistry
    ):
        """vocab returns themed term when key exists in vocabulary mapping."""
        theme = theme_registry.get("film_noir")

        result = theme.vocab("mission")

        assert result == "lead"

    def test_vocab_returns_key_unchanged_for_unknown_key(
        self, theme_registry: ThemeRegistry
    ):
        """vocab returns the key itself when key not in vocabulary mapping."""
        theme = theme_registry.get("film_noir")

        result = theme.vocab("unknown_term")

        assert result == "unknown_term"

    def test_alias_returns_themed_alias_for_known_command(
        self, theme_registry: ThemeRegistry
    ):
        """alias returns themed command when key exists in aliases mapping."""
        theme = theme_registry.get("film_noir")

        result = theme.alias("next")

        assert result == "investigate"

    def test_alias_returns_key_unchanged_for_unknown_command(
        self, theme_registry: ThemeRegistry
    ):
        """alias returns the key itself when key not in aliases mapping."""
        theme = theme_registry.get("film_noir")

        result = theme.alias("unknown_cmd")

        assert result == "unknown_cmd"

    def test_vocab_with_empty_string_returns_empty_string(
        self, theme_registry: ThemeRegistry
    ):
        """vocab returns empty string unchanged when passed empty string."""
        theme = theme_registry.get("film_noir")

        result = theme.vocab("")

        assert result == ""

    def test_color_returns_hex_for_known_key(self, theme_registry: ThemeRegistry):
        """color returns hex code when key exists in colors mapping."""
        theme = theme_registry.get("film_noir")

        result = theme.color("accent")

        assert result == "#c9a227"

    def test_color_returns_key_unchanged_for_unknown_key(
        self, theme_registry: ThemeRegistry
    ):
        """color returns the key itself when key not in colors mapping."""
        theme = theme_registry.get("film_noir")

        result = theme.color("unknown_color")

        assert result == "unknown_color"

    def test_color_with_empty_string_returns_empty_string(
        self, theme_registry: ThemeRegistry
    ):
        """color returns empty string unchanged when passed empty string."""
        theme = theme_registry.get("film_noir")

        result = theme.color("")

        assert result == ""

    def test_color_all_film_noir_keys_present(self, theme_registry: ThemeRegistry):
        """All expected color keys are present in Film Noir theme."""
        theme = theme_registry.get("film_noir")

        expected_keys = [
            "accent",
            "accent_dim",
            "accent_bright",
            "text",
            "text_muted",
            "text_dim",
            "error",
            "divider",
            "success",
            "warning",
            "info",
        ]

        for key in expected_keys:
            assert key in theme.colors, f"Missing color key: {key}"
            assert theme.colors[key].startswith("#"), f"Color {key} is not a hex code"

    def test_decoration_returns_string_for_known_key(
        self, theme_registry: ThemeRegistry
    ):
        """decoration returns decoration string when key exists."""
        theme = theme_registry.get("film_noir")

        result = theme.decoration("divider")

        assert result == "━━━✦━━━"

    def test_decoration_returns_key_unchanged_for_unknown_key(
        self, theme_registry: ThemeRegistry
    ):
        """decoration returns the key itself when key not in decorations."""
        theme = theme_registry.get("film_noir")

        result = theme.decoration("unknown_decoration")

        assert result == "unknown_decoration"

    def test_decoration_all_film_noir_keys_present(self, theme_registry: ThemeRegistry):
        """All expected decoration keys are present in Film Noir theme."""
        theme = theme_registry.get("film_noir")

        expected_keys = [
            "divider",
            "divider_heavy",
            "bullet",
            "section_separator",
        ]

        for key in expected_keys:
            assert key in theme.decorations, f"Missing decoration key: {key}"
            assert len(theme.decorations[key]) > 0, f"Decoration {key} is empty"


class TestThemeRegistry:
    """Tests for theme registry functions: get_theme, list_themes."""

    def test_get_theme_returns_film_noir(self, theme_registry: ThemeRegistry):
        """get_theme returns fully populated ThemeConfig for film_noir."""
        result = theme_registry.get("film_noir")

        assert result.name == "film_noir"
        assert result.display_name == "Film Noir"
        assert result.css_path.name == "noir.tcss"
        assert len(result.aliases) == 11
        assert len(result.vocabulary) == 20
        assert len(result.colors) == 11
        assert result.aliases["next"] == "investigate"
        assert result.aliases["complete"] == "file report"
        assert result.aliases["status"] == "case board"
        assert result.vocabulary["mission"] == "lead"
        assert result.vocabulary["campaign"] == "case"
        assert result.vocabulary["chain"] == "case thread"
        assert result.colors["accent"] == "#c9a227"
        assert result.colors["text"] == "#e8e4d9"
        assert result.narrative.tone == (
            "Film Noir detective investigation. Moody, atmospheric, hardboiled. "
            "The developer is a detective investigating an unfamiliar codebase."
        )
        assert len(result.narrative.title_examples) == 5
        assert result.decorations["divider"] == "━━━✦━━━"
        assert result.icons["available"] == "○"

    def test_get_theme_unknown_raises_value_error(self, theme_registry: ThemeRegistry):
        """get_theme raises ValueError for unknown theme with available
        themes in message.
        """
        with pytest.raises(ValueError) as exc_info:
            theme_registry.get("cyberpunk")

        error_message = str(exc_info.value)
        assert "Unknown theme: 'cyberpunk'" in error_message
        assert "film_noir" in error_message

    def test_list_themes_includes_film_noir(self, theme_registry: ThemeRegistry):
        """list_themes returns sorted list containing film_noir."""
        result = theme_registry.list()

        assert result == ["film_noir"]


class TestThemeConfigIcons:
    """Tests for ThemeConfig.icon() method."""

    def test_known_icon_returns_value(self, theme_registry: ThemeRegistry):
        """Known icon key returns configured value."""
        theme = theme_registry.get("film_noir")

        assert theme.icon("available") == "○"
        assert theme.icon("completed") == "●"
        assert theme.icon("locked") == "🔒"
        assert theme.icon("in_progress") == "◈"

    def test_unknown_icon_returns_key(self, theme_registry: ThemeRegistry):
        """Unknown icon key returns the key itself as fallback."""
        theme = theme_registry.get("film_noir")

        assert theme.icon("nonexistent") == "nonexistent"

    def test_all_film_noir_icons_present(self, theme_registry: ThemeRegistry):
        """All expected icon keys are present in Film Noir theme."""
        theme = theme_registry.get("film_noir")

        expected_keys = [
            "available",
            "completed",
            "locked",
            "in_progress",
            "warning",
            "error",
        ]

        for key in expected_keys:
            assert key in theme.icons, f"Missing icon key: {key}"
            assert len(theme.icons[key]) > 0, f"Icon {key} is empty"
