from __future__ import annotations

from pathlib import Path

import pytest
from textual import events

from vibe.cli.autocompletion.base import CompletionResult, CompletionView
from vibe.cli.autocompletion.path_completion import PathCompletionController
from vibe.core.autocompletion.completers import PathCompleter


class StubView(CompletionView):
    def __init__(self) -> None:
        self.suggestions: list[tuple[list[tuple[str, str]], int]] = []
        self.clears = 0
        self.replacements: list[tuple[int, int, str]] = []

    def render_completion_suggestions(
        self, suggestions: list[tuple[str, str]], selected_index: int
    ) -> None:
        self.suggestions.append((suggestions, selected_index))

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

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


@pytest.fixture()
def file_tree(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    (tmp_path / "src" / "utils").mkdir(parents=True)
    (tmp_path / "src" / "main.py").write_text("", encoding="utf-8")
    (tmp_path / "src" / "core").mkdir(parents=True)
    (tmp_path / "src" / "core" / "logger.py").write_text("", encoding="utf-8")
    (tmp_path / "src" / "core" / "models.py").write_text("", encoding="utf-8")
    (tmp_path / "src" / "core" / "ports.py").write_text("", encoding="utf-8")
    (tmp_path / "src" / "core" / "sanitize.py").write_text("", encoding="utf-8")
    (tmp_path / "src" / "core" / "use_cases.py").write_text("", encoding="utf-8")
    (tmp_path / "src" / "core" / "validate.py").write_text("", encoding="utf-8")
    (tmp_path / "README.md").write_text("", encoding="utf-8")
    (tmp_path / ".env").write_text("", encoding="utf-8")
    monkeypatch.chdir(tmp_path)
    return tmp_path


def make_controller(
    max_entries_to_process: int | None = None, target_matches: int | None = None
) -> tuple[PathCompletionController, StubView]:
    completer_kwargs = {}
    if max_entries_to_process is not None:
        completer_kwargs["max_entries_to_process"] = max_entries_to_process
    if target_matches is not None:
        completer_kwargs["target_matches"] = target_matches

    completer = PathCompleter(**completer_kwargs)
    view = StubView()
    controller = PathCompletionController(completer, view)
    return controller, view


def test_lists_root_entries(file_tree: Path) -> None:
    controller, view = make_controller()

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

    suggestions, selected = view.suggestions[-1]
    assert selected == 0
    assert [alias for alias, _ in suggestions] == ["@README.md", "@src/"]


def test_suggests_hidden_entries_only_with_dot_prefix(file_tree: Path) -> None:
    controller, view = make_controller()

    controller.on_text_changed("@.", cursor_index=2)

    suggestions, _ = view.suggestions[-1]
    assert suggestions[0][0] == "@.env"


def test_lists_nested_entries_when_prefixing_with_folder_name(file_tree: Path) -> None:
    controller, view = make_controller()

    controller.on_text_changed("@src/", cursor_index=5)

    suggestions, _ = view.suggestions[-1]
    assert [alias for alias, _ in suggestions] == [
        "@src/core/",
        "@src/main.py",
        "@src/utils/",
    ]


def test_resets_when_fragment_invalid(file_tree: Path) -> None:
    controller, view = make_controller()

    controller.on_text_changed("@src", cursor_index=4)
    assert view.suggestions

    controller.on_text_changed("@src foo", cursor_index=8)
    assert view.clears == 1
    assert (
        controller.on_key(events.Key("tab", None), "@src foo", 8)
        is CompletionResult.IGNORED
    )


def test_applies_selected_completion_on_tab_keycode(file_tree: Path) -> None:
    controller, view = make_controller()

    controller.on_text_changed("@R", cursor_index=2)
    result = controller.on_key(events.Key("tab", None), "@R", 2)

    assert result is CompletionResult.HANDLED
    assert view.replacements == [(0, 2, "@README.md")]
    assert view.clears == 1


def test_applies_selected_completion_on_enter_keycode(file_tree: Path) -> None:
    controller, view = make_controller()
    controller.on_text_changed("@src/", cursor_index=5)
    controller.on_key(events.Key("down", None), "@src/", 5)

    result = controller.on_key(events.Key("enter", None), "@src/", 5)

    assert result is CompletionResult.HANDLED
    assert view.replacements == [(0, 5, "@src/main.py")]
    assert view.clears == 1


def test_navigates_and_cycles_across_suggestions(file_tree: Path) -> None:
    controller, view = make_controller()

    controller.on_text_changed("@src/", cursor_index=5)
    controller.on_key(events.Key("down", None), "@src/", 5)
    suggestions, selected_index = view.suggestions[-1]
    assert [alias for alias, _ in suggestions] == [
        "@src/core/",
        "@src/main.py",
        "@src/utils/",
    ]
    assert selected_index == 1
    controller.on_key(events.Key("up", None), "@src/", 5)
    suggestions, selected_index = view.suggestions[-1]
    assert selected_index == 0

    controller.on_key(events.Key("down", None), "@src/", 5)
    controller.on_key(events.Key("down", None), "@src/", 5)
    suggestions, selected_index = view.suggestions[-1]
    assert selected_index == 2

    controller.on_key(events.Key("down", None), "@src/", 5)
    suggestions, selected_index = view.suggestions[-1]
    assert selected_index == 0


def test_limits_suggestions_to_ten(file_tree: Path) -> None:
    (file_tree / "src" / "core" / "extra").mkdir(parents=True)
    [
        (file_tree / "src" / "core" / "extra" / f"extra_file_{i}.py").write_text(
            "", encoding="utf-8"
        )
        for i in range(1, 13)
    ]
    controller, view = make_controller()

    controller.on_text_changed("@src/core/extra/", cursor_index=16)
    suggestions, selected_index = view.suggestions[-1]
    assert len(suggestions) == 10
    assert [alias for alias, _ in suggestions] == [
        "@src/core/extra/extra_file_1.py",
        "@src/core/extra/extra_file_10.py",
        "@src/core/extra/extra_file_11.py",
        "@src/core/extra/extra_file_12.py",
        "@src/core/extra/extra_file_2.py",
        "@src/core/extra/extra_file_3.py",
        "@src/core/extra/extra_file_4.py",
        "@src/core/extra/extra_file_5.py",
        "@src/core/extra/extra_file_6.py",
        "@src/core/extra/extra_file_7.py",
    ]
    assert selected_index == 0


def test_does_not_handle_when_cursor_at_beginning_of_input(file_tree: Path) -> None:
    controller, _ = make_controller()

    assert not controller.can_handle("@file", cursor_index=0)
    assert not controller.can_handle("", cursor_index=0)
    assert not controller.can_handle("hello@file", cursor_index=0)


def test_does_not_handle_when_cursor_before_or_at_the_at_symbol(
    file_tree: Path,
) -> None:
    controller, _ = make_controller()

    assert not controller.can_handle("@file", cursor_index=0)
    assert not controller.can_handle("hello@file", cursor_index=5)


def test_does_handle_when_cursor_after_the_at_symbol_even_in_the_middle_of_the_input(
    file_tree: Path,
) -> None:
    controller, _ = make_controller()

    assert controller.can_handle("@file", cursor_index=1)
    assert controller.can_handle("hello @file", cursor_index=7)


def test_lists_immediate_children_when_path_ends_with_slash(file_tree: Path) -> None:
    controller, view = make_controller()

    controller.on_text_changed("@src/", cursor_index=5)

    suggestions, _ = view.suggestions[-1]
    assert [alias for alias, _ in suggestions] == [
        "@src/core/",
        "@src/main.py",
        "@src/utils/",
    ]


def test_respects_max_entries_to_process_limit(file_tree: Path) -> None:
    for i in range(30):
        (file_tree / f"file_{i:03d}.txt").write_text("", encoding="utf-8")

    controller, view = make_controller(max_entries_to_process=10)

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

    suggestions, _ = view.suggestions[-1]
    assert len(suggestions) <= 10


def test_respects_target_matches_limit_for_listing(file_tree: Path) -> None:
    for i in range(30):
        (file_tree / f"item_{i:03d}.txt").write_text("", encoding="utf-8")

    controller, view = make_controller(target_matches=5)

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

    suggestions, _ = view.suggestions[-1]
    assert len(suggestions) <= 5


def test_respects_target_matches_limit_for_fuzzy_search(file_tree: Path) -> None:
    for i in range(30):
        (file_tree / f"test_file_{i:03d}.py").write_text("", encoding="utf-8")

    controller, view = make_controller(target_matches=5)

    controller.on_text_changed("@test", cursor_index=5)

    suggestions, _ = view.suggestions[-1]
    assert len(suggestions) <= 5
