"""Tests for InputHistory — command history buffer and line editing."""

from __future__ import annotations

import io
from collections import deque

import pytest

from h2g2.engine.input_history import InputHistory

# Escape sequences for arrow keys
UP = "\x1b[A"
DOWN = "\x1b[B"
RIGHT = "\x1b[C"
LEFT = "\x1b[D"
HOME = "\x1b[H"
END = "\x1b[F"
DELETE = "\x1b[3~"
BACKSPACE = "\x7f"
ENTER = "\r"
CTRL_D = "\x04"
CTRL_C = "\x03"


class FakeInputHistory(InputHistory):
    """InputHistory subclass that reads from a character queue
    and captures terminal output instead of touching the real terminal."""

    def __init__(self, chars: str = "") -> None:
        super().__init__()
        self._chars: deque[str] = deque(chars)
        self.output = io.StringIO()

    def feed(self, chars: str) -> None:
        self._chars.extend(chars)

    def _read_char(self, fd: int) -> str:
        return self._chars.popleft()

    def input(self, prompt: str = "> ") -> str:
        """Bypass terminal raw-mode setup; drive _read_line directly."""
        self.output.write(prompt)
        line = self._read_line(0, prompt)
        if line:
            self._history.append(line)
        return line

    @staticmethod
    def _redraw(buf: list[str], cursor: int, prompt: str) -> None:
        # Suppress terminal escape codes during tests
        pass


# ---------------------------------------------------------------------------
# History buffer basics
# ---------------------------------------------------------------------------

class TestHistoryGrowsDynamically:
    def test_single_entry(self):
        h = FakeInputHistory("hello" + ENTER)
        assert h.input() == "hello"
        assert h._history == ["hello"]

    def test_multiple_entries(self):
        h = FakeInputHistory()
        for cmd in ["north", "take lamp", "inventory"]:
            h.feed(cmd + ENTER)
            h.input()
        assert h._history == ["north", "take lamp", "inventory"]

    def test_empty_input_not_stored(self):
        h = FakeInputHistory(ENTER)
        assert h.input() == ""
        assert h._history == []

    def test_grows_unbounded(self):
        h = FakeInputHistory()
        for i in range(500):
            h.feed(f"cmd{i}" + ENTER)
            h.input()
        assert len(h._history) == 500
        assert h._history[0] == "cmd0"
        assert h._history[499] == "cmd499"


# ---------------------------------------------------------------------------
# Up/Down arrow navigation
# ---------------------------------------------------------------------------

class TestUpDownNavigation:
    def _make_history(self, *commands: str) -> FakeInputHistory:
        h = FakeInputHistory()
        for cmd in commands:
            h.feed(cmd + ENTER)
            h.input()
        return h

    def test_up_recalls_last_command(self):
        h = self._make_history("look", "north")
        h.feed(UP + ENTER)
        assert h.input() == "north"

    def test_up_twice_recalls_older(self):
        h = self._make_history("look", "north", "take lamp")
        h.feed(UP + UP + ENTER)
        assert h.input() == "north"

    def test_up_then_down_returns_to_newer(self):
        h = self._make_history("look", "north")
        h.feed(UP + UP + DOWN + ENTER)
        assert h.input() == "north"

    def test_down_past_end_restores_current_input(self):
        h = self._make_history("look")
        # Type "hel", arrow up (recalls "look"), arrow down (restores "hel")
        h.feed("hel" + UP + DOWN + ENTER)
        assert h.input() == "hel"

    def test_up_at_top_stays_at_oldest(self):
        h = self._make_history("only")
        h.feed(UP + UP + UP + ENTER)
        assert h.input() == "only"

    def test_down_with_no_history_is_noop(self):
        h = FakeInputHistory(DOWN + "hello" + ENTER)
        assert h.input() == "hello"

    def test_up_with_no_history_is_noop(self):
        h = FakeInputHistory(UP + "hello" + ENTER)
        assert h.input() == "hello"

    def test_browse_full_history(self):
        h = self._make_history("a", "b", "c")
        # Go all the way up (c -> b -> a), then submit
        h.feed(UP + UP + UP + ENTER)
        assert h.input() == "a"

    def test_recalled_command_added_to_history(self):
        h = self._make_history("look")
        h.feed(UP + ENTER)
        result = h.input()
        assert result == "look"
        # "look" is now in history twice (original + re-submitted)
        assert h._history == ["look", "look"]


# ---------------------------------------------------------------------------
# Line editing
# ---------------------------------------------------------------------------

class TestLineEditing:
    def test_backspace_deletes_char(self):
        h = FakeInputHistory("helloo" + BACKSPACE + ENTER)
        assert h.input() == "hello"

    def test_backspace_at_start_is_noop(self):
        h = FakeInputHistory(BACKSPACE + "hi" + ENTER)
        assert h.input() == "hi"

    def test_left_arrow_and_insert(self):
        # Type "hllo", left 3 times to position after 'h', insert 'e'
        h = FakeInputHistory("hllo" + LEFT + LEFT + LEFT + "e" + ENTER)
        assert h.input() == "hello"

    def test_right_arrow(self):
        # Type "hllo", left 3, right 1, insert 'e' (between l and l)
        h = FakeInputHistory("hllo" + LEFT + LEFT + LEFT + RIGHT + "e" + ENTER)
        assert h.input() == "hlelo"

    def test_home_moves_to_start(self):
        h = FakeInputHistory("ello" + HOME + "h" + ENTER)
        assert h.input() == "hello"

    def test_end_moves_to_end(self):
        h = FakeInputHistory("hel" + HOME + END + "lo" + ENTER)
        assert h.input() == "hello"

    def test_delete_key(self):
        # Type "hello", left twice (cursor before 'l' 'o'), delete removes 'l'
        h = FakeInputHistory("hello" + LEFT + LEFT + DELETE + ENTER)
        assert h.input() == "helo"

    def test_delete_at_end_is_noop(self):
        h = FakeInputHistory("hi" + DELETE + ENTER)
        assert h.input() == "hi"

    def test_backspace_mid_line(self):
        # Type "helxlo", left 2 (before 'l' 'o'), backspace removes 'x'...
        # Actually: "helxlo" then LEFT LEFT puts cursor before "lo", backspace removes 'x'
        h = FakeInputHistory("helxlo" + LEFT + LEFT + BACKSPACE + ENTER)
        assert h.input() == "hello"


# ---------------------------------------------------------------------------
# Ctrl-D / Ctrl-C
# ---------------------------------------------------------------------------

class TestControlKeys:
    def test_ctrl_d_on_empty_raises_eof(self):
        h = FakeInputHistory(CTRL_D)
        with pytest.raises(EOFError):
            h.input()

    def test_ctrl_d_with_content_is_ignored(self):
        h = FakeInputHistory("hi" + CTRL_D + ENTER)
        assert h.input() == "hi"

    def test_ctrl_c_raises_keyboard_interrupt(self):
        h = FakeInputHistory(CTRL_C)
        with pytest.raises(KeyboardInterrupt):
            h.input()
