import importlib.util
import time
from pathlib import Path
from unittest.mock import Mock

import pytest

from fenrirscreenreader.core.tabCompletionManager import TabCompletionManager


def _build_env(old_text="", cursor=None, screen="pty"):
    if cursor is None:
        cursor = {"x": 0, "y": 0}
    input_manager = Mock()
    input_manager.get_last_event.return_value = {
        "event_name": "KEY_TAB",
        "event_state": 1,
    }
    input_manager.get_last_deepest_input.return_value = [["KEY_TAB"]]
    input_manager.get_last_input_time.return_value = time.time()
    env = {
        "runtime": {
            "InputManager": input_manager,
        },
        "screen": {
            "new_cursor": cursor.copy(),
            "new_content_text": old_text,
            "new_delta": "",
            "new_delta_is_typing": False,
            "newTTY": screen,
        },
        "commandBuffer": {},
    }
    manager = TabCompletionManager()
    manager.initialize(env)
    return manager, env, input_manager


def _load_large_insertion_module():
    module_path = (
        Path(__file__).resolve().parents[2]
        / "src"
        / "fenrirscreenreader"
        / "commands"
        / "onCursorChange"
        / "55000-large_insertion_echo.py"
    )
    spec = importlib.util.spec_from_file_location(
        "fenrir_large_insertion_echo", module_path
    )
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def _set_screen_update(env, text, cursor, delta="", typing=False):
    env["screen"]["new_content_text"] = text
    env["screen"]["new_cursor"] = cursor.copy()
    env["screen"]["new_delta"] = delta
    env["screen"]["new_delta_is_typing"] = typing


@pytest.mark.unit
def test_unique_completion_speaks_inserted_suffix():
    manager, env, _input_manager = _build_env(
        "cd Do".ljust(20), {"x": 5, "y": 0}
    )

    manager.capture_if_tab()
    _set_screen_update(
        env,
        "cd Documents/".ljust(20),
        {"x": 13, "y": 0},
        delta="cuments/",
        typing=True,
    )

    assert manager.process_update() == "cuments/"
    state = env["commandBuffer"]["tabCompletion"]
    assert state["pending"] is None
    assert state["lastProcessedDelta"] == "cuments/"


@pytest.mark.unit
def test_small_completion_speaks_inserted_suffix():
    manager, env, _input_manager = _build_env(
        "cd gi".ljust(20), {"x": 5, "y": 0}
    )

    manager.capture_if_tab()
    _set_screen_update(
        env,
        "cd git/".ljust(20),
        {"x": 7, "y": 0},
        delta="t/",
        typing=True,
    )

    assert manager.process_update() == "t/"


@pytest.mark.unit
def test_completion_processed_once_for_cursor_and_screen_update():
    manager, env, _input_manager = _build_env(
        "cd gi".ljust(20), {"x": 5, "y": 0}
    )

    manager.capture_if_tab()
    _set_screen_update(
        env,
        "cd git/".ljust(20),
        {"x": 7, "y": 0},
        delta="t/",
        typing=True,
    )

    assert manager.process_update() == "t/"
    assert manager.process_update() == ""


@pytest.mark.unit
def test_candidate_list_speaks_visible_list_without_cursor_advance():
    old_text = "\n".join(["$ cd Do".ljust(20), "".ljust(20), "".ljust(20)])
    manager, env, _input_manager = _build_env(old_text, {"x": 7, "y": 0})

    manager.capture_if_tab()
    _set_screen_update(
        env,
        "\n".join(
            [
                "$ cd Do".ljust(20),
                "Documents/ Downloads/".ljust(20),
                "$ cd Do".ljust(20),
            ]
        ),
        {"x": 7, "y": 2},
        delta="Documents/ Downloads/",
    )

    assert manager.process_update() == "Documents/ Downloads/"


@pytest.mark.unit
def test_no_screen_change_stays_silent_and_keeps_pending_briefly():
    manager, env, _input_manager = _build_env(
        "cd Do".ljust(20), {"x": 5, "y": 0}
    )

    manager.capture_if_tab()

    assert manager.process_update() == ""
    assert env["commandBuffer"]["tabCompletion"]["pending"] is not None


@pytest.mark.unit
def test_timeout_clears_pending_without_speech():
    manager, env, _input_manager = _build_env(
        "cd Do".ljust(20), {"x": 5, "y": 0}
    )

    manager.capture_if_tab()
    env["commandBuffer"]["tabCompletion"]["pending"]["timestamp"] = (
        time.time() - 1
    )
    _set_screen_update(
        env,
        "unrelated output".ljust(20),
        {"x": 0, "y": 0},
        delta="unrelated output",
    )

    assert manager.process_update() == ""
    assert env["commandBuffer"]["tabCompletion"]["pending"] is None


@pytest.mark.unit
def test_non_tab_key_does_not_capture():
    manager, env, input_manager = _build_env(
        "cd Do".ljust(20), {"x": 5, "y": 0}
    )
    input_manager.get_last_event.return_value = {
        "event_name": "KEY_A",
        "event_state": 1,
    }
    input_manager.get_last_deepest_input.return_value = [["KEY_A"]]

    manager.capture_if_tab()

    assert env["commandBuffer"]["tabCompletion"]["pending"] is None


@pytest.mark.unit
def test_recent_tab_screen_update_works_without_key_input_snapshot():
    manager, env, input_manager = _build_env(
        "cd Documents/".ljust(20), {"x": 13, "y": 0}
    )
    input_manager.get_last_event.return_value = {
        "event_name": "KEY_TAB",
        "event_state": 0,
    }
    env["screen"]["old_content_text"] = "cd Do".ljust(20)
    env["screen"]["old_cursor"] = {"x": 5, "y": 0}
    env["commandBuffer"]["tabCompletion"]["pending"] = None
    _set_screen_update(
        env,
        "cd Documents/".ljust(20),
        {"x": 13, "y": 0},
        delta="cuments/",
        typing=True,
    )

    assert manager.process_update() == "cuments/"


@pytest.mark.unit
def test_recent_raw_pty_tab_speaks_short_completion_without_capture():
    manager, env, input_manager = _build_env(
        "cd Documents/".ljust(20), {"x": 13, "y": 0}
    )
    input_manager.get_last_deepest_input.return_value = ["KEY_TAB"]
    input_manager.get_last_event.return_value = None
    env["screen"]["old_content_text"] = "cd Docume".ljust(20)
    env["screen"]["old_cursor"] = {"x": 9, "y": 0}
    env["commandBuffer"]["tabCompletion"]["pending"] = None
    _set_screen_update(
        env,
        "cd Documents/".ljust(20),
        {"x": 13, "y": 0},
        delta="nts/",
        typing=True,
    )

    assert manager.process_update() == "nts/"


@pytest.mark.unit
def test_large_insertion_echo_speaks_pasted_cursor_text():
    large_insertion_module = _load_large_insertion_module()
    output_manager = Mock(present_text=Mock())
    settings_manager = Mock()
    settings_manager.get_setting_as_bool.return_value = False
    input_manager = Mock()
    input_manager.get_last_deepest_input.return_value = []
    input_manager.get_last_event.return_value = None
    screen_manager = Mock(
        is_screen_change=Mock(return_value=False),
        is_delta=Mock(return_value=True),
    )
    env = {
        "runtime": {
            "InputManager": input_manager,
            "OutputManager": output_manager,
            "ScreenManager": screen_manager,
            "SettingsManager": settings_manager,
        },
        "screen": {
            "old_cursor": {"x": 2, "y": 0},
            "new_cursor": {"x": 13, "y": 0},
            "new_delta": "hello world",
        },
    }
    command = large_insertion_module.command()
    command.initialize(env)

    command.run()

    output_manager.present_text.assert_called_once_with(
        "hello world",
        interrupt=True,
        announce_capital=True,
        flush=False,
    )


@pytest.mark.unit
def test_large_insertion_echo_defers_recent_tab_to_tab_completion():
    large_insertion_module = _load_large_insertion_module()
    output_manager = Mock(present_text=Mock())
    settings_manager = Mock()
    settings_manager.get_setting_as_bool.return_value = False
    input_manager = Mock()
    input_manager.get_last_deepest_input.return_value = [["KEY_TAB"]]
    input_manager.get_last_event.return_value = {
        "event_name": "KEY_TAB",
        "event_state": 1,
    }
    input_manager.get_last_input_time.return_value = time.time()
    screen_manager = Mock(
        is_screen_change=Mock(return_value=False),
        is_delta=Mock(return_value=True),
    )
    env = {
        "runtime": {
            "InputManager": input_manager,
            "OutputManager": output_manager,
            "ScreenManager": screen_manager,
            "SettingsManager": settings_manager,
        },
        "screen": {
            "old_cursor": {"x": 5, "y": 0},
            "new_cursor": {"x": 13, "y": 0},
            "new_delta": "cuments/",
        },
    }
    command = large_insertion_module.command()
    command.initialize(env)

    command.run()

    output_manager.present_text.assert_not_called()
