"""Tests for Kitty keyboard protocol CSI parsing and PTY integration."""

import os
import time
import pytest
from queue import Empty
from functools import partial

from pyos.Application import Application
from pyos.EventTypes import KeyStroke, KeyRelease, KeyState, StopApplication
from pyos import Keys


# ---------------------------------------------------------------------------
# Unit tests for _decode_kitty_u
# ---------------------------------------------------------------------------

class TestDecodeKittyU:
    """Test CSI <params> u parsing."""

    def setup_method(self):
        self.app = Application()

    def test_simple_press(self):
        event = self.app._decode_kitty_u("97")
        assert isinstance(event, KeyStroke)
        assert event.key == 97
        assert event.state == KeyState.PRESSED

    def test_press_with_modifiers(self):
        event = self.app._decode_kitty_u("97;1")
        assert isinstance(event, KeyStroke)
        assert event.key == 97
        assert event.state == KeyState.PRESSED

    def test_explicit_press_event_type(self):
        event = self.app._decode_kitty_u("97;1:1")
        assert isinstance(event, KeyStroke)
        assert event.key == 97
        assert event.state == KeyState.PRESSED

    def test_repeat_event_type(self):
        event = self.app._decode_kitty_u("97;1:2")
        assert isinstance(event, KeyStroke)
        assert event.key == 97
        assert event.state == KeyState.REPEATED

    def test_release_event_type(self):
        event = self.app._decode_kitty_u("97;1:3")
        assert isinstance(event, KeyRelease)
        assert event.key == 97

    def test_esc_key(self):
        event = self.app._decode_kitty_u("27")
        assert isinstance(event, KeyStroke)
        assert event.key == 27

    def test_ctrl_c_codepoint_3(self):
        event = self.app._decode_kitty_u("3")
        assert isinstance(event, StopApplication)

    def test_ctrl_c_with_modifier(self):
        # codepoint 99 ('c') with Ctrl modifier (4+1=5)
        event = self.app._decode_kitty_u("99;5")
        assert isinstance(event, StopApplication)

    def test_invalid_codepoint(self):
        event = self.app._decode_kitty_u("abc")
        assert event is None

    def test_release_of_letter(self):
        # 'd' release
        event = self.app._decode_kitty_u("100;1:3")
        assert isinstance(event, KeyRelease)
        assert event.key == 100


# ---------------------------------------------------------------------------
# Unit tests for _decode_kitty_csi
# ---------------------------------------------------------------------------

class TestDecodeKittyCsi:
    """Test enhanced CSI sequences for special keys."""

    def setup_method(self):
        self.app = Application()

    def test_arrow_up_press(self):
        event = self.app._decode_kitty_csi("", "A")
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.UP

    def test_arrow_down_press(self):
        event = self.app._decode_kitty_csi("", "B")
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.DOWN

    def test_arrow_left_press(self):
        event = self.app._decode_kitty_csi("", "D")
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.LEFT

    def test_arrow_right_press(self):
        event = self.app._decode_kitty_csi("", "C")
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.RIGHT

    def test_arrow_up_release(self):
        event = self.app._decode_kitty_csi("1;1:3", "A")
        assert isinstance(event, KeyRelease)
        assert event.key == Keys.UP

    def test_arrow_with_repeat(self):
        event = self.app._decode_kitty_csi("1;1:2", "B")
        assert isinstance(event, KeyStroke)
        assert event.state == KeyState.REPEATED

    def test_home_key(self):
        event = self.app._decode_kitty_csi("", "H")
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.HOME

    def test_end_key(self):
        event = self.app._decode_kitty_csi("", "F")
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.END

    def test_tilde_home(self):
        event = self.app._decode_kitty_csi("1", "~")
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.HOME

    def test_unknown_final(self):
        event = self.app._decode_kitty_csi("", "Z")
        assert event is None


# ---------------------------------------------------------------------------
# Unit tests for _parse_csi_sequence
# ---------------------------------------------------------------------------

class TestParseCsiSequence:
    """Test raw byte CSI sequence parsing."""

    def setup_method(self):
        self.app = Application()

    def test_u_terminated(self):
        consumed, event = self.app._parse_csi_sequence(b'\x1b[97u')
        assert consumed == 5
        assert isinstance(event, KeyStroke)
        assert event.key == 97

    def test_u_terminated_with_event_type(self):
        consumed, event = self.app._parse_csi_sequence(b'\x1b[97;1:3u')
        assert consumed == 9
        assert isinstance(event, KeyRelease)
        assert event.key == 97

    def test_arrow_key(self):
        consumed, event = self.app._parse_csi_sequence(b'\x1b[A')
        assert consumed == 3
        assert isinstance(event, KeyStroke)
        assert event.key == Keys.UP

    def test_arrow_release(self):
        consumed, event = self.app._parse_csi_sequence(b'\x1b[1;1:3A')
        assert consumed == 8
        assert isinstance(event, KeyRelease)
        assert event.key == Keys.UP

    def test_incomplete_returns_zero(self):
        consumed, event = self.app._parse_csi_sequence(b'\x1b[97')
        assert consumed == 0
        assert event is None

    def test_incomplete_esc_bracket(self):
        consumed, event = self.app._parse_csi_sequence(b'\x1b[')
        assert consumed == 0
        assert event is None

    def test_oversized_discarded(self):
        # Use digits (parameter bytes) — not CSI final bytes
        buf = b'\x1b[' + b'9' * 40
        consumed, event = self.app._parse_csi_sequence(buf)
        assert consumed == len(buf)
        assert event is None

    def test_trailing_data_preserved(self):
        # Parse only the first CSI sequence
        buf = b'\x1b[97uextra'
        consumed, event = self.app._parse_csi_sequence(buf)
        assert consumed == 5
        assert buf[consumed:] == b'extra'


# ---------------------------------------------------------------------------
# Unit tests for _process_kitty_buffer
# ---------------------------------------------------------------------------

class TestProcessKittyBuffer:
    """Test full buffer processing."""

    def setup_method(self):
        self.app = Application()

    def _drain_events(self):
        events = []
        while True:
            try:
                events.append(self.app.event_queue.get_nowait())
            except Empty:
                break
        return events

    def test_raw_byte_press(self):
        remaining = self.app._process_kitty_buffer(b'a')
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 1
        assert isinstance(events[0], KeyStroke)
        assert events[0].key == 97

    def test_csi_press(self):
        remaining = self.app._process_kitty_buffer(b'\x1b[97u')
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 1
        assert isinstance(events[0], KeyStroke)
        assert events[0].key == 97

    def test_csi_release(self):
        remaining = self.app._process_kitty_buffer(b'\x1b[97;1:3u')
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 1
        assert isinstance(events[0], KeyRelease)
        assert events[0].key == 97

    def test_multiple_sequences(self):
        buf = b'\x1b[97u\x1b[97;1:3u'
        remaining = self.app._process_kitty_buffer(buf)
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 2
        assert isinstance(events[0], KeyStroke)
        assert isinstance(events[1], KeyRelease)
        assert events[0].key == 97
        assert events[1].key == 97

    def test_press_repeat_release_sequence(self):
        # Two back-to-back repeats in one buffer — throttle swallows the second
        buf = b'\x1b[97u\x1b[97;1:2u\x1b[97;1:2u\x1b[97;1:3u'
        remaining = self.app._process_kitty_buffer(buf)
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 3
        assert events[0].state == KeyState.PRESSED
        assert events[1].state == KeyState.REPEATED
        assert isinstance(events[2], KeyRelease)

    def test_incomplete_csi_buffered(self):
        remaining = self.app._process_kitty_buffer(b'\x1b[97')
        assert remaining == b'\x1b[97'
        events = self._drain_events()
        assert len(events) == 0

    def test_incomplete_esc_buffered(self):
        remaining = self.app._process_kitty_buffer(b'\x1b')
        assert remaining == b'\x1b'
        events = self._drain_events()
        assert len(events) == 0

    def test_ctrl_c_raw_byte(self):
        remaining = self.app._process_kitty_buffer(b'\x03')
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 1
        assert isinstance(events[0], StopApplication)

    def test_ctrl_c_kitty_codepoint(self):
        remaining = self.app._process_kitty_buffer(b'\x1b[3u')
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 1
        assert isinstance(events[0], StopApplication)

    def test_mixed_raw_and_csi(self):
        # raw 'a', then CSI release for 'a'
        buf = b'a\x1b[97;1:3u'
        remaining = self.app._process_kitty_buffer(buf)
        assert remaining == b""
        events = self._drain_events()
        assert len(events) == 2
        assert isinstance(events[0], KeyStroke)
        assert isinstance(events[1], KeyRelease)


# ---------------------------------------------------------------------------
# PTY integration test
# ---------------------------------------------------------------------------

class TestSyntheticReleasePty:
    """PTY integration tests: synthetic KeyRelease via idle timeout.

    PTYs don't support Kitty protocol, so the Application falls back to
    blessed key monitoring with synthetic releases (idle_count >= 3 = 150ms).
    These tests verify that full press→release flow works end-to-end.
    """

    @pytest.fixture(autouse=True)
    def _skip_if_no_pyte(self):
        pytest.importorskip("pyte")

    def test_press_then_idle_produces_release(self):
        """A key press followed by 150ms idle should trigger synthetic release."""
        from pyos.testing.pty_harness import PtyHarness
        from pyos.examples.demo import ReflexGame

        with PtyHarness(ReflexGame, enable_kitty=True) as h:
            h.wait_for_text("SPACE", timeout=5)
            h.send_key(ord(" "))
            # HOLDING should appear quickly (keystroke processed)
            h.wait_for_text("HOLDING", timeout=3)
            # Synthetic release fires after ~150ms of idle → result screen
            h.wait_for_text("Off by", timeout=3)

    def test_full_game_with_synthetic_releases(self):
        """Full game: multiple press+synthetic-release cycles."""
        from pyos.testing.pty_harness import PtyHarness
        from pyos.examples.demo import ReflexGame

        with PtyHarness(ReflexGame, enable_kitty=True) as h:
            h.wait_for_text("SPACE", timeout=5)
            for _ in range(ReflexGame.MAX_ROUNDS):
                h.send_key(ord(" "))
                # Wait for synthetic release + result display
                time.sleep(0.4)
            h.wait_for_text("GAME OVER", timeout=5)

    def test_different_key_releases_previous(self):
        """Pressing a different key should release the previous one first."""
        from pyos.testing.pty_harness import PtyHarness
        from pyos.examples.demo import ReflexGame

        with PtyHarness(ReflexGame, enable_kitty=True) as h:
            h.wait_for_text("SPACE", timeout=5)
            h.send_key(ord(" "))
            h.wait_for_text("HOLDING", timeout=3)
            # Press ESC — should release space first (synthetic), then handle ESC
            h.send_key(Keys.ESC)
            # ESC goes back → should see the menu
            time.sleep(0.5)
            # Game should have processed the release
            assert not h.find_text("HOLDING")
