from __future__ import annotations

from typing import NamedTuple

from textual import events

from vibe.cli.autocompletion.base import CompletionResult, CompletionView
from vibe.cli.autocompletion.slash_command import SlashCommandController
from vibe.core.autocompletion.completers import CommandCompleter


class Suggestion(NamedTuple):
    alias: str
    description: str


class SuggestionEvent(NamedTuple):
    suggestions: list[Suggestion]
    selected_index: int


class Replacement(NamedTuple):
    start: int
    end: int
    replacement: str


class StubView(CompletionView):
    def __init__(self) -> None:
        self.suggestion_events: list[SuggestionEvent] = []
        self.reset_count = 0
        self.replacements: list[Replacement] = []

    def render_completion_suggestions(
        self, suggestions: list[tuple[str, str]], selected_index: int
    ) -> None:
        typed = [Suggestion(alias, description) for alias, description in suggestions]
        self.suggestion_events.append(SuggestionEvent(typed, selected_index))

    def clear_completion_suggestions(self) -> None:
        self.reset_count += 1

    def replace_completion_range(
        self, start: int, end: int, replacement: str, *, suppress_update: bool = False
    ) -> None:
        self.replacements.append(Replacement(start, end, replacement))


def key_event(key: str) -> events.Key:
    return events.Key(key, character=None)


def make_controller(
    *, prefix: str | None = None
) -> tuple[SlashCommandController, StubView]:
    commands = [
        ("/config", "Show current configuration"),
        ("/compact", "Compact history"),
        ("/help", "Display help"),
        ("/config", "Override description"),
        ("/summarize", "Summarize history"),
        ("/logpath", "Show log path"),
        ("/exit", "Exit application"),
        ("/vim", "Toggle vim keybindings"),
    ]
    completer = CommandCompleter(lambda: commands)
    view = StubView()
    controller = SlashCommandController(completer, view)

    if prefix is not None:
        controller.on_text_changed(prefix, cursor_index=len(prefix))
        view.suggestion_events.clear()

    return controller, view


def test_on_text_change_emits_matching_suggestions_in_insertion_order_and_ignores_duplicates() -> (
    None
):
    controller, view = make_controller(prefix="/c")

    controller.on_text_changed("/c", cursor_index=2)

    suggestions, selected = view.suggestion_events[-1]
    assert suggestions == [
        Suggestion("/config", "Override description"),
        Suggestion("/compact", "Compact history"),
    ]
    assert selected == 0


def test_on_text_change_filters_suggestions_case_insensitively() -> None:
    controller, view = make_controller(prefix="/c")

    controller.on_text_changed("/CO", cursor_index=3)

    suggestions, _ = view.suggestion_events[-1]
    assert [suggestion.alias for suggestion in suggestions] == ["/config", "/compact"]


def test_on_text_change_clears_suggestions_when_no_matches() -> None:
    controller, view = make_controller(prefix="/c")

    controller.on_text_changed("/c", cursor_index=2)
    controller.on_text_changed("config", cursor_index=6)

    assert view.reset_count >= 1


def test_on_text_change_limits_the_number_of_results_and_preserves_insertion_order() -> (
    None
):
    controller, view = make_controller(prefix="/")

    controller.on_text_changed("/", cursor_index=1)

    suggestions, selected_index = view.suggestion_events[-1]
    assert len(suggestions) == 7
    assert [suggestion.alias for suggestion in suggestions] == [
        "/help",
        "/config",
        "/compact",
        "/summarize",
        "/logpath",
        "/exit",
        "/vim",
    ]


def test_on_key_tab_applies_selected_completion() -> None:
    controller, view = make_controller(prefix="/c")

    result = controller.on_key(key_event("tab"), text="/c", cursor_index=2)

    assert result is CompletionResult.HANDLED
    assert view.replacements == [Replacement(0, 2, "/config")]
    assert view.reset_count == 1


def test_on_key_down_and_up_cycle_selection() -> None:
    controller, view = make_controller(prefix="/c")

    controller.on_key(key_event("down"), text="/c", cursor_index=2)
    suggestions, selected_index = view.suggestion_events[-1]
    assert selected_index == 1

    controller.on_key(key_event("down"), text="/c", cursor_index=2)
    suggestions, selected_index = view.suggestion_events[-1]
    assert selected_index == 0

    controller.on_key(key_event("up"), text="/c", cursor_index=2)
    suggestions, selected_index = view.suggestion_events[-1]
    assert selected_index == 1
    assert [suggestion.alias for suggestion in suggestions] == ["/config", "/compact"]


def test_on_key_enter_submits_selected_completion() -> None:
    controller, view = make_controller(prefix="/c")

    controller.on_key(key_event("down"), text="/c", cursor_index=2)

    result = controller.on_key(key_event("enter"), text="/c", cursor_index=2)

    assert result is CompletionResult.SUBMIT
    assert view.replacements == [Replacement(0, 2, "/compact")]
    assert view.reset_count == 1


def test_callable_entries_updates_completions_dynamically() -> None:
    """Test that CommandCompleter with a callable updates entries when the callable returns different values.

    This simulates config reload where available skills change.
    """
    available_skills: list[tuple[str, str]] = []

    def get_entries() -> list[tuple[str, str]]:
        base_commands = [("/help", "Display help"), ("/config", "Show configuration")]
        return base_commands + available_skills

    completer = CommandCompleter(get_entries)
    view = StubView()
    controller = SlashCommandController(completer, view)

    # Initially, only base commands are available
    controller.on_text_changed("/", cursor_index=1)
    suggestions, _ = view.suggestion_events[-1]
    assert [s.alias for s in suggestions] == ["/help", "/config"]

    # Simulate config reload: add a skill
    available_skills.append(("/summarize", "Summarize the conversation"))

    # Now completions should include the new skill
    controller.on_text_changed("/", cursor_index=1)
    suggestions, _ = view.suggestion_events[-1]
    assert [s.alias for s in suggestions] == ["/help", "/config", "/summarize"]

    # And searching for "/s" should find the new skill
    controller.on_text_changed("/s", cursor_index=2)
    suggestions, _ = view.suggestion_events[-1]
    assert [s.alias for s in suggestions] == ["/summarize"]
    assert suggestions[0].description == "Summarize the conversation"


def test_tab_on_slash_command_with_args_replaces_only_head() -> None:
    controller, view = make_controller()
    text = "/compact some args"
    controller.on_text_changed(text, cursor_index=len(text))

    result = controller.on_key(key_event("tab"), text=text, cursor_index=len(text))

    assert result is CompletionResult.HANDLED
    assert view.replacements == [Replacement(0, 8, "/compact")]


def test_enter_on_slash_command_with_args_submits_with_head_only_replacement() -> None:
    controller, view = make_controller()
    text = "/compact some args"
    controller.on_text_changed(text, cursor_index=len(text))

    result = controller.on_key(key_event("enter"), text=text, cursor_index=len(text))

    assert result is CompletionResult.SUBMIT
    assert view.replacements == [Replacement(0, 8, "/compact")]


def test_on_text_change_matches_substring_not_just_prefix() -> None:
    controller, view = make_controller()

    controller.on_text_changed("/path", cursor_index=5)

    suggestions, _ = view.suggestion_events[-1]
    assert any(s.alias == "/logpath" for s in suggestions)


def test_on_text_change_matches_middle_segment_of_hyphenated_command() -> None:
    commands = [
        ("/foo-bar-skill", "A skill"),
        ("/baz-bar-other", "Another skill"),
        ("/unrelated", "No match"),
    ]
    completer = CommandCompleter(lambda: commands)
    view = StubView()
    controller = SlashCommandController(completer, view)

    controller.on_text_changed("/bar", cursor_index=4)

    suggestions, _ = view.suggestion_events[-1]
    aliases = [s.alias for s in suggestions]
    assert "/foo-bar-skill" in aliases
    assert "/baz-bar-other" in aliases
    assert "/unrelated" not in aliases


def test_on_text_change_fuzzy_matches_scattered_characters() -> None:
    controller, view = make_controller()

    controller.on_text_changed("/sm", cursor_index=3)

    suggestions, _ = view.suggestion_events[-1]
    assert any(s.alias == "/summarize" for s in suggestions)


def test_on_text_change_fuzzy_ranks_prefix_matches_higher() -> None:
    commands = [("/zoo-config", "Zoo config"), ("/config", "Main config")]
    completer = CommandCompleter(lambda: commands)
    view = StubView()
    controller = SlashCommandController(completer, view)

    controller.on_text_changed("/config", cursor_index=7)

    suggestions, _ = view.suggestion_events[-1]
    aliases = [s.alias for s in suggestions]
    assert aliases.index("/config") < aliases.index("/zoo-config")


def test_callable_entries_reflects_enabled_disabled_skills() -> None:
    """Test that skill enable/disable changes are reflected in completions.

    This simulates the scenario where a user changes enabled_skills in config
    and runs /reload.
    """
    enabled_skills: set[str] = {"commit", "review"}

    all_skills = [
        ("/commit", "Create a git commit"),
        ("/review", "Review code changes"),
        ("/deploy", "Deploy to production"),
    ]

    def get_entries() -> list[tuple[str, str]]:
        return [(name, desc) for name, desc in all_skills if name[1:] in enabled_skills]

    completer = CommandCompleter(get_entries)
    view = StubView()
    controller = SlashCommandController(completer, view)

    # Initially only commit and review are enabled
    controller.on_text_changed("/", cursor_index=1)
    suggestions, _ = view.suggestion_events[-1]
    assert [s.alias for s in suggestions] == ["/commit", "/review"]

    # Simulate config reload: enable deploy, disable commit
    enabled_skills.discard("commit")
    enabled_skills.add("deploy")

    # Now completions should reflect the change
    controller.on_text_changed("/", cursor_index=1)
    suggestions, _ = view.suggestion_events[-1]
    assert [s.alias for s in suggestions] == ["/review", "/deploy"]
