"""Tests for pyos/input_handlers.py — handle_text_box_input + handle_scroll_list_input."""

from pyos.Attrs import NORMAL, BOLD, DIM, UNDERLINE, REVERSE
import queue

import pytest

from pyos import Keys
from pyos.EventTypes import KeyStroke, ScrollChange, TextBoxChange, TextBoxSubmit
from pyos.input_handlers import handle_text_box_input, handle_scroll_list_input


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_text_ctx(text="", cursor_index=0):
    return {"text": text, "cursor_index": cursor_index}


def _key(code):
    return KeyStroke(code)


def _drain_events(q):
    events = []
    while not q.empty():
        events.append(q.get_nowait())
    return events


# ===========================================================================
# handle_text_box_input
# ===========================================================================

class TestTextBoxInput:
    """Happy-path tests."""

    def test_type_char_updates_text_and_cursor(self):
        ctx = _make_text_ctx("ab", cursor_index=1)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(ord("X")), eq)
        assert ctx["text"] == "aXb"
        assert ctx["cursor_index"] == 2
        events = _drain_events(eq)
        assert len(events) == 1
        assert isinstance(events[0], TextBoxChange)

    def test_backspace_deletes_char(self):
        ctx = _make_text_ctx("abc", cursor_index=2)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.BACKSPACE), eq)
        assert ctx["text"] == "ac"
        assert ctx["cursor_index"] == 1
        events = _drain_events(eq)
        assert any(isinstance(e, TextBoxChange) for e in events)

    def test_left_moves_cursor(self):
        ctx = _make_text_ctx("abc", cursor_index=2)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.LEFT), eq)
        assert ctx["cursor_index"] == 1

    def test_right_moves_cursor(self):
        ctx = _make_text_ctx("abc", cursor_index=1)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.RIGHT), eq)
        assert ctx["cursor_index"] == 2

    def test_home_moves_to_start(self):
        ctx = _make_text_ctx("abc", cursor_index=2)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.HOME), eq)
        assert ctx["cursor_index"] == 0

    def test_end_moves_to_end(self):
        ctx = _make_text_ctx("abc", cursor_index=0)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.END), eq)
        assert ctx["cursor_index"] == 3

    def test_enter_emits_submit(self):
        ctx = _make_text_ctx("hello", cursor_index=2)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.ENTER), eq)
        assert ctx["cursor_index"] == 5
        events = _drain_events(eq)
        assert any(isinstance(e, TextBoxSubmit) for e in events)


class TestTextBoxInputEdge:
    """Edge cases and boundary conditions."""

    def test_backspace_at_zero_is_noop(self):
        ctx = _make_text_ctx("abc", cursor_index=0)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.BACKSPACE), eq)
        assert ctx["text"] == "abc"
        assert ctx["cursor_index"] == 0
        # No TextBoxChange emitted
        events = _drain_events(eq)
        assert not any(isinstance(e, TextBoxChange) for e in events)

    def test_left_at_zero_is_noop(self):
        ctx = _make_text_ctx("abc", cursor_index=0)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.LEFT), eq)
        assert ctx["cursor_index"] == 0

    def test_right_at_end_is_noop(self):
        ctx = _make_text_ctx("abc", cursor_index=3)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.RIGHT), eq)
        assert ctx["cursor_index"] == 3

    def test_type_into_empty_context_no_text_key(self):
        """Context with no "text" key — should initialise text correctly."""
        ctx = {}
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(ord("A")), eq)
        assert ctx["text"] == "A"
        assert ctx["cursor_index"] == 1

    def test_cursor_beyond_text_length(self):
        """Stale cursor_index > len(text) — should not crash."""
        ctx = _make_text_ctx("ab", cursor_index=99)
        eq = queue.Queue()
        # LEFT should still work (cursor > 0)
        handle_text_box_input("tb", ctx, _key(Keys.LEFT), eq)
        assert ctx["cursor_index"] == 98

    def test_nonprintable_key_ignored(self):
        ctx = _make_text_ctx("abc", cursor_index=1)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.F5), eq)
        assert ctx["text"] == "abc"
        assert ctx["cursor_index"] == 1
        events = _drain_events(eq)
        assert len(events) == 0

    def test_rapid_backspace_on_single_char(self):
        ctx = _make_text_ctx("x", cursor_index=1)
        eq = queue.Queue()
        handle_text_box_input("tb", ctx, _key(Keys.BACKSPACE), eq)
        handle_text_box_input("tb", ctx, _key(Keys.BACKSPACE), eq)
        handle_text_box_input("tb", ctx, _key(Keys.BACKSPACE), eq)
        assert ctx["cursor_index"] >= 0
        assert ctx["text"] == ""


# ===========================================================================
# handle_scroll_list_input
# ===========================================================================

class TestScrollListInput:
    """Happy-path tests for scroll list input handler."""

    def test_down_moves_selected_index(self):
        ctx = {"items": ["a", "b", "c"], "selected_index": 0}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(Keys.DOWN), eq)
        assert ctx["selected_index"] == 1
        events = _drain_events(eq)
        assert any(isinstance(e, ScrollChange) for e in events)

    def test_up_moves_selected_index(self):
        ctx = {"items": ["a", "b", "c"], "selected_index": 2}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(Keys.UP), eq)
        assert ctx["selected_index"] == 1

    def test_up_at_zero_stays(self):
        ctx = {"items": ["a", "b", "c"], "selected_index": 0}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(Keys.UP), eq)
        assert ctx["selected_index"] == 0

    def test_down_at_last_stays(self):
        ctx = {"items": ["a", "b", "c"], "selected_index": 2}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(Keys.DOWN), eq)
        assert ctx["selected_index"] == 2

    def test_scroll_change_emitted_on_every_key(self):
        ctx = {"items": ["a", "b"], "selected_index": 0}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(Keys.DOWN), eq)
        events = _drain_events(eq)
        assert any(isinstance(e, ScrollChange) for e in events)


class TestScrollListInputEdge:
    """Edge cases and adversarial scenarios."""

    def test_empty_items_scroll_down(self):
        """Empty items → scroll_down sets index to -1 (documents the behavior)."""
        ctx = {"items": []}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(Keys.DOWN), eq)
        assert ctx["selected_index"] == -1

    def test_unrecognized_key_still_emits_scroll_change(self):
        """Even keys that don't move the index still emit ScrollChange."""
        ctx = {"items": ["a", "b"], "selected_index": 0}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(ord("z")), eq)
        events = _drain_events(eq)
        assert any(isinstance(e, ScrollChange) for e in events)

    def test_menu_navigation_left_right(self):
        """With selected_item set, LEFT/RIGHT navigate the menu."""
        menu_ctx = {"items": [{"text": "a"}, {"text": "b"}, {"text": "c"}], "selected_index": 0}
        ctx = {"items": ["x"], "selected_index": 0, "selected_item": "x", "menu": menu_ctx}
        eq = queue.Queue()
        handle_scroll_list_input("sl", ctx, _key(Keys.RIGHT), eq)
        assert menu_ctx["selected_index"] == 1
        handle_scroll_list_input("sl", ctx, _key(Keys.LEFT), eq)
        assert menu_ctx["selected_index"] == 0

    def test_empty_menu_items_crash(self):
        """Empty menu items → ZeroDivisionError from modulo (documents the crash)."""
        menu_ctx = {"items": [], "selected_index": 0}
        ctx = {"items": ["x"], "selected_index": 0, "selected_item": "x", "menu": menu_ctx}
        eq = queue.Queue()
        with pytest.raises(ZeroDivisionError):
            handle_scroll_list_input("sl", ctx, _key(Keys.RIGHT), eq)
