"""Tests for command router module.

Tests CommandRouter parse, get_commands, and get_command_def methods.
All fields asserted on returned objects. Minimum 1 error test per 2 happy path.
Pure logic tests - no database mocking needed.
"""

from __future__ import annotations

import pytest
from hypothesis import given
from hypothesis import strategies as st

from unfamiliar.tui.commands import COMMANDS, CommandDef, CommandRouter
from unfamiliar.tui.themes.film_noir.theme import FILM_NOIR_THEME
from unfamiliar.tui.themes.registry import ThemeRegistry
from unfamiliar.tui.views import CommandInfo


@pytest.fixture
def router(theme_registry: ThemeRegistry) -> CommandRouter:
    """Create a CommandRouter with Film Noir theme loaded."""
    theme = theme_registry.get("film_noir")
    return CommandRouter(theme)


class TestCommandRouterParse:
    """Tests for CommandRouter.parse method.

    Parse converts raw user input into (canonical_command, args) tuple.
    It handles: themed aliases, multi-word commands, arguments, case, whitespace.
    """

    def test_canonical_command_returns_itself(self, router: CommandRouter):
        """Canonical command 'status' returns itself with empty args."""
        result = router.parse("status")

        assert result == ("status", [])
        assert result[0] == "status"
        assert result[1] == []

    def test_themed_alias_resolves_to_canonical(self, router: CommandRouter):
        """Themed alias 'investigate' resolves to canonical 'next'."""
        result = router.parse("investigate")

        assert result == ("next", [])
        assert result[0] == "next"
        assert result[1] == []

    def test_multi_word_alias_file_report_resolves(self, router: CommandRouter):
        """Multi-word alias 'file report' resolves to canonical 'complete'."""
        result = router.parse("file report")

        assert result == ("complete", [])
        assert result[0] == "complete"
        assert result[1] == []

    def test_multi_word_alias_case_board_resolves(self, router: CommandRouter):
        """Multi-word alias 'case board' resolves to canonical 'status'."""
        result = router.parse("case board")

        assert result == ("status", [])
        assert result[0] == "status"
        assert result[1] == []

    def test_multi_word_alias_close_the_case_resolves(self, router: CommandRouter):
        """Multi-word alias 'close the case' resolves to canonical 'end campaign'."""
        result = router.parse("close the case")

        assert result == ("end campaign", [])
        assert result[0] == "end campaign"
        assert result[1] == []

    def test_multi_word_alias_with_argument(self, router: CommandRouter):
        """Multi-word alias 'break in 42' resolves to 'unlock' with arg '42'."""
        result = router.parse("break in 42")

        assert result == ("unlock", ["42"])
        assert result[0] == "unlock"
        assert result[1] == ["42"]

    def test_canonical_command_with_argument(self, router: CommandRouter):
        """Canonical command 'next 3' returns 'next' with arg '3'."""
        result = router.parse("next 3")

        assert result == ("next", ["3"])
        assert result[0] == "next"
        assert result[1] == ["3"]

    def test_canonical_multi_word_command(self, router: CommandRouter):
        """Multi-word canonical 'end campaign' returns itself with empty args."""
        result = router.parse("end campaign")

        assert result == ("end campaign", [])
        assert result[0] == "end campaign"
        assert result[1] == []

    def test_canonical_multi_word_with_args(self, router: CommandRouter):
        """Multi-word canonical 'mission 5' returns 'mission' with arg '5'."""
        result = router.parse("mission 5")

        assert result == ("mission", ["5"])
        assert result[0] == "mission"
        assert result[1] == ["5"]

    def test_unknown_command_returns_unknown(self, router: CommandRouter):
        """Unknown command 'gibberish' returns ('unknown', ['gibberish'])."""
        result = router.parse("gibberish")

        assert result == ("unknown", ["gibberish"])
        assert result[0] == "unknown"
        assert result[1] == ["gibberish"]

    def test_empty_input_returns_unknown(self, router: CommandRouter):
        """Empty input returns ('unknown', [''])."""
        result = router.parse("")

        assert result == ("unknown", [""])
        assert result[0] == "unknown"
        assert result[1] == [""]

    def test_whitespace_only_returns_unknown(self, router: CommandRouter):
        """Whitespace-only input returns ('unknown', [''])."""
        result = router.parse("   ")

        assert result == ("unknown", [""])
        assert result[0] == "unknown"
        assert result[1] == [""]

    def test_case_insensitive_uppercase(self, router: CommandRouter):
        """Uppercase 'STATUS' returns lowercase canonical 'status'."""
        result = router.parse("STATUS")

        assert result == ("status", [])
        assert result[0] == "status"
        assert result[1] == []

    def test_case_insensitive_mixed_case(self, router: CommandRouter):
        """Mixed case 'InVeStIgAtE' returns canonical 'next'."""
        result = router.parse("InVeStIgAtE")

        assert result == ("next", [])
        assert result[0] == "next"
        assert result[1] == []

    def test_extra_whitespace_stripped(self, router: CommandRouter):
        """Input with leading/trailing whitespace '  status  ' returns 'status'."""
        result = router.parse("  status  ")

        assert result == ("status", [])
        assert result[0] == "status"
        assert result[1] == []

    def test_multiple_args_extracted(self, router: CommandRouter):
        """Command with multiple args 'next 1 2 3' extracts all args."""
        result = router.parse("next 1 2 3")

        assert result == ("next", ["1", "2", "3"])
        assert result[0] == "next"
        assert result[1] == ["1", "2", "3"]

    def test_partial_alias_not_matched(self, router: CommandRouter):
        """Partial alias 'file' (partial of 'file report') returns unknown."""
        result = router.parse("file")

        assert result == ("unknown", ["file"])
        assert result[0] == "unknown"
        assert result[1] == ["file"]

    def test_alias_takes_precedence_over_canonical(self, router: CommandRouter):
        """Themed alias 'lead' resolves to canonical 'mission', not unknown."""
        result = router.parse("lead")

        assert result == ("mission", [])
        assert result[0] == "mission"
        assert result[1] == []


class TestCommandRouterGetCommands:
    """Tests for CommandRouter.get_commands method.

    Get commands returns list of CommandInfo with themed aliases populated.
    """

    def test_returns_all_commands_with_aliases(self, router: CommandRouter):
        """get_commands returns list of CommandInfo with all fields populated."""
        result = router.get_commands()

        assert len(result) == 11
        assert all(isinstance(cmd, CommandInfo) for cmd in result)

        for cmd_info in result:
            assert isinstance(cmd_info.canonical, str)
            assert isinstance(cmd_info.alias, str)
            assert isinstance(cmd_info.description, str)
            assert len(cmd_info.canonical) > 0
            assert len(cmd_info.alias) > 0
            assert len(cmd_info.description) > 0

    def test_command_count_matches_defined_commands(self, router: CommandRouter):
        """get_commands returns exactly 10 commands matching COMMANDS constant."""
        result = router.get_commands()

        assert len(result) == len(COMMANDS)
        assert len(result) == 11

    def test_next_command_has_correct_alias(self, router: CommandRouter):
        """The 'next' command has alias 'investigate' in Film Noir theme."""
        result = router.get_commands()

        next_cmd = next((c for c in result if c.canonical == "next"), None)
        assert next_cmd is not None
        assert next_cmd.alias == "investigate"
        assert next_cmd.description == "Show available missions or start one by number"

    def test_complete_command_has_correct_alias(self, router: CommandRouter):
        """The 'complete' command has alias 'file report' in Film Noir theme."""
        result = router.get_commands()

        complete_cmd = next((c for c in result if c.canonical == "complete"), None)
        assert complete_cmd is not None
        assert complete_cmd.alias == "file report"
        assert complete_cmd.description == "Mark current mission as complete"

    def test_end_campaign_command_has_correct_alias(self, router: CommandRouter):
        """The 'end campaign' command has alias 'close the case' in Film Noir theme."""
        result = router.get_commands()

        end_cmd = next((c for c in result if c.canonical == "end campaign"), None)
        assert end_cmd is not None
        assert end_cmd.alias == "close the case"
        assert end_cmd.description == "End campaign with final report"


class TestCommandRouterGetCommandDef:
    """Tests for CommandRouter.get_command_def method.

    Get command definition returns CommandDef or None for non-existent.
    """

    def test_returns_definition_for_existing_command(self, router: CommandRouter):
        """get_command_def('next') returns fully populated CommandDef."""
        result = router.get_command_def("next")

        assert result is not None
        assert isinstance(result, CommandDef)
        assert result.name == "next"
        assert result.description == "Show available missions or start one by number"
        assert result.accepts_args is True

    def test_returns_definition_for_multi_word_command(self, router: CommandRouter):
        """get_command_def('end campaign') returns CommandDef for multi-word command."""
        result = router.get_command_def("end campaign")

        assert result is not None
        assert isinstance(result, CommandDef)
        assert result.name == "end campaign"
        assert result.description == "End campaign with final report"
        assert result.accepts_args is False

    def test_returns_none_for_nonexistent_command(self, router: CommandRouter):
        """get_command_def('nonexistent') returns None."""
        result = router.get_command_def("nonexistent")

        assert result is None

    def test_returns_none_for_empty_string(self, router: CommandRouter):
        """get_command_def('') returns None for empty string."""
        result = router.get_command_def("")

        assert result is None

    def test_returns_correct_accepts_args_for_each_command(self, router: CommandRouter):
        """Commands have correct accepts_args values per COMMANDS definition."""
        commands_with_args = ["next", "mission", "unlock"]
        commands_without_args = [
            "complete",
            "status",
            "briefing",
            "notebook",
            "help",
            "quit",
            "end campaign",
        ]

        for cmd_name in commands_with_args:
            cmd_def = router.get_command_def(cmd_name)
            assert cmd_def is not None
            assert cmd_def.accepts_args is True, f"{cmd_name} should accept args"

        for cmd_name in commands_without_args:
            cmd_def = router.get_command_def(cmd_name)
            assert cmd_def is not None
            assert cmd_def.accepts_args is False, f"{cmd_name} should not accept args"


class TestCommandRouterParseProperty:
    """Property-based tests for CommandRouter.parse invariants."""

    @given(text=st.text(max_size=100))
    def test_parse_always_returns_two_tuple(self, text: str):
        """parse always returns a 2-tuple of (str, list) for any input."""
        registry = ThemeRegistry()
        registry.register(FILM_NOIR_THEME)
        theme = registry.get("film_noir")
        router = CommandRouter(theme)
        result = router.parse(text)

        assert isinstance(result, tuple)
        assert len(result) == 2
        assert isinstance(result[0], str)
        assert isinstance(result[1], list)

    @given(text=st.text(max_size=100))
    def test_parse_first_element_never_empty_for_non_empty_input(self, text: str):
        """For non-empty input after strip, first element is never empty string."""
        registry = ThemeRegistry()
        registry.register(FILM_NOIR_THEME)
        theme = registry.get("film_noir")
        router = CommandRouter(theme)
        result = router.parse(text)

        stripped = text.strip()
        if stripped:
            assert result[0] != "" or result[0] == "unknown"

    @given(text=st.text(max_size=100))
    def test_parse_args_always_contains_strings(self, text: str):
        """All elements in the args list are strings."""
        registry = ThemeRegistry()
        registry.register(FILM_NOIR_THEME)
        theme = registry.get("film_noir")
        router = CommandRouter(theme)
        result = router.parse(text)

        for arg in result[1]:
            assert isinstance(arg, str)
