"""Tests for mouse support: SGR parsing, event synthesis, region tracking, handlers."""

import pytest
from queue import Queue, Empty

from pyos.Application import Application, _RawMouseMotion
from pyos.Activity import Activity
from pyos.EventTypes import (
    MouseDown, MouseUp, MouseClick, MouseScroll, MouseDrag, MouseMove,
    MouseButton, ScrollChange, TextBoxChange,
)
from pyos.input_handlers import handle_scroll_list_mouse, handle_text_input_mouse
from pyos.testing.harness import MockScreen, HarnessApplication
from pyos.printers.ScrollList import ScrollList
from pyos.printers.TextInput import TextInput


# ---------------------------------------------------------------------------
# Unit tests for _decode_sgr_mouse
# ---------------------------------------------------------------------------

class TestDecodeSgrMouse:
    """Test SGR mouse Cb decoding."""

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

    def test_left_press(self):
        event = self.app._decode_sgr_mouse(0, 10, 5, False)
        assert isinstance(event, MouseDown)
        assert event.button == MouseButton.LEFT
        assert event.x == 10
        assert event.y == 5

    def test_middle_press(self):
        event = self.app._decode_sgr_mouse(1, 3, 7, False)
        assert isinstance(event, MouseDown)
        assert event.button == MouseButton.MIDDLE

    def test_right_press(self):
        event = self.app._decode_sgr_mouse(2, 0, 0, False)
        assert isinstance(event, MouseDown)
        assert event.button == MouseButton.RIGHT

    def test_left_release(self):
        event = self.app._decode_sgr_mouse(0, 10, 5, True)
        assert isinstance(event, MouseUp)
        assert event.button == MouseButton.LEFT
        assert event.x == 10
        assert event.y == 5

    def test_right_release(self):
        event = self.app._decode_sgr_mouse(2, 1, 1, True)
        assert isinstance(event, MouseUp)
        assert event.button == MouseButton.RIGHT

    def test_scroll_up(self):
        event = self.app._decode_sgr_mouse(64, 5, 10, False)
        assert isinstance(event, MouseScroll)
        assert event.direction == "up"
        assert event.x == 5
        assert event.y == 10

    def test_scroll_down(self):
        event = self.app._decode_sgr_mouse(65, 5, 10, False)
        assert isinstance(event, MouseScroll)
        assert event.direction == "down"

    def test_motion_no_button(self):
        # Cb=35: motion (32) + button bits 11 (3) = no button held
        event = self.app._decode_sgr_mouse(35, 20, 15, False)
        assert isinstance(event, MouseMove)
        assert event.x == 20
        assert event.y == 15

    def test_motion_with_left_button(self):
        # Cb=32: motion (32) + button 0 (left)
        event = self.app._decode_sgr_mouse(32, 8, 4, False)
        assert isinstance(event, _RawMouseMotion)
        assert event.button == MouseButton.LEFT
        assert event.x == 8
        assert event.y == 4

    def test_motion_with_right_button(self):
        # Cb=34: motion (32) + button 2 (right)
        event = self.app._decode_sgr_mouse(34, 1, 1, False)
        assert isinstance(event, _RawMouseMotion)
        assert event.button == MouseButton.RIGHT

    def test_modifiers_shift(self):
        # Cb=4: shift (4) + left press
        event = self.app._decode_sgr_mouse(4, 0, 0, False)
        assert isinstance(event, MouseDown)
        assert event.modifiers == 4

    def test_modifiers_ctrl(self):
        # Cb=16: ctrl (16) + left press
        event = self.app._decode_sgr_mouse(16, 0, 0, False)
        assert isinstance(event, MouseDown)
        assert event.modifiers == 16

    def test_modifiers_meta(self):
        # Cb=8: meta (8) + left press
        event = self.app._decode_sgr_mouse(8, 0, 0, False)
        assert isinstance(event, MouseDown)
        assert event.modifiers == 8


# ---------------------------------------------------------------------------
# Tests for _parse_sgr_mouse (full sequence parsing)
# ---------------------------------------------------------------------------

class TestParseSgrMouseSequence:
    """Test parsing of complete SGR mouse sequences from raw bytes."""

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

    def test_left_click_press(self):
        # ESC [ < 0 ; 1 ; 1 M  — left press at (1,1) -> (0,0) 0-based
        buf = b'\x1b[<0;1;1M'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert consumed == len(buf)
        assert isinstance(event, MouseDown)
        assert event.button == MouseButton.LEFT
        assert event.x == 0
        assert event.y == 0

    def test_left_click_release(self):
        # ESC [ < 0 ; 1 ; 1 m  — left release
        buf = b'\x1b[<0;1;1m'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert consumed == len(buf)
        assert isinstance(event, MouseUp)
        assert event.button == MouseButton.LEFT

    def test_large_coordinates(self):
        buf = b'\x1b[<0;120;50M'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert consumed == len(buf)
        assert isinstance(event, MouseDown)
        assert event.x == 119
        assert event.y == 49

    def test_scroll_up_sequence(self):
        buf = b'\x1b[<64;10;20M'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert consumed == len(buf)
        assert isinstance(event, MouseScroll)
        assert event.direction == "up"

    def test_scroll_down_sequence(self):
        buf = b'\x1b[<65;10;20M'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert isinstance(event, MouseScroll)
        assert event.direction == "down"

    def test_incomplete_sequence_returns_zero(self):
        buf = b'\x1b[<0;1;'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert consumed == 0
        assert event is None

    def test_extra_data_after_sequence(self):
        buf = b'\x1b[<0;5;10Mextra'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert consumed == len(b'\x1b[<0;5;10M')
        assert isinstance(event, MouseDown)
        assert event.x == 4
        assert event.y == 9

    def test_bad_params(self):
        buf = b'\x1b[<abc;1;1M'
        consumed, event = self.app._parse_sgr_mouse(buf)
        assert consumed > 0
        assert event is None

    def test_csi_routes_mouse(self):
        """_parse_csi_sequence should route to _parse_sgr_mouse for mouse."""
        buf = b'\x1b[<0;1;1M'
        consumed, event = self.app._parse_csi_sequence(buf)
        assert consumed == len(buf)
        assert isinstance(event, MouseDown)


# ---------------------------------------------------------------------------
# Tests for _synthesize_mouse_events
# ---------------------------------------------------------------------------

class TestSynthesizeMouseEvents:
    """Test high-level mouse event synthesis."""

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

    def test_click_synthesis(self):
        """Down + up at same position produces MouseDown, MouseUp, MouseClick."""
        down = MouseDown(MouseButton.LEFT, 10, 5)
        events = self.app._synthesize_mouse_events(down)
        assert len(events) == 1
        assert isinstance(events[0], MouseDown)

        up = MouseUp(MouseButton.LEFT, 10, 5)
        events = self.app._synthesize_mouse_events(up)
        assert len(events) == 2
        assert isinstance(events[0], MouseUp)
        assert isinstance(events[1], MouseClick)
        assert events[1].x == 10
        assert events[1].y == 5

    def test_no_click_on_different_positions(self):
        """Down at one position, up at another — no MouseClick."""
        self.app._synthesize_mouse_events(MouseDown(MouseButton.LEFT, 10, 5))
        events = self.app._synthesize_mouse_events(MouseUp(MouseButton.LEFT, 20, 5))
        types = [type(e) for e in events]
        assert MouseClick not in types

    def test_drag_synthesis(self):
        """Down + motion + up produces drag events."""
        self.app._synthesize_mouse_events(MouseDown(MouseButton.LEFT, 10, 5))

        motion = _RawMouseMotion(MouseButton.LEFT, 15, 5)
        events = self.app._synthesize_mouse_events(motion)
        assert len(events) == 1
        assert isinstance(events[0], MouseDrag)
        assert events[0].start_x == 10
        assert events[0].start_y == 5
        assert events[0].x == 15

        up_events = self.app._synthesize_mouse_events(MouseUp(MouseButton.LEFT, 20, 5))
        # Should have final drag + up, but no click (was dragging)
        types = [type(e) for e in up_events]
        assert MouseDrag in types
        assert MouseUp in types
        assert MouseClick not in types

    def test_scroll_passthrough(self):
        """MouseScroll events pass through unchanged."""
        scroll = MouseScroll("up", 5, 10)
        events = self.app._synthesize_mouse_events(scroll)
        assert len(events) == 1
        assert events[0] is scroll

    def test_move_passthrough(self):
        """MouseMove events pass through unchanged."""
        move = MouseMove(5, 10)
        events = self.app._synthesize_mouse_events(move)
        assert len(events) == 1
        assert events[0] is move

    def test_state_cleanup_after_click(self):
        """After up, drag state and last_down are cleared."""
        self.app._synthesize_mouse_events(MouseDown(MouseButton.LEFT, 0, 0))
        self.app._synthesize_mouse_events(MouseUp(MouseButton.LEFT, 0, 0))
        assert self.app._mouse_drag_state is None
        assert self.app._mouse_last_down is None

    def test_motion_wrong_button_ignored(self):
        """Motion with different button than the down event is ignored."""
        self.app._synthesize_mouse_events(MouseDown(MouseButton.LEFT, 0, 0))
        motion = _RawMouseMotion(MouseButton.RIGHT, 5, 5)
        events = self.app._synthesize_mouse_events(motion)
        assert len(events) == 0


# ---------------------------------------------------------------------------
# Tests for region tracking
# ---------------------------------------------------------------------------

class TestMouseRegionTracking:
    """Test _region_map population, region_at_y, delegate_mouse_to_region."""

    def _make_app(self, rows=24, cols=80):
        screen = MockScreen(rows=rows, cols=cols)
        app = HarnessApplication(screen=screen)
        app.setup()
        return app, screen

    def test_region_map_populated(self):
        """generate_line_printers populates _region_map."""
        app, screen = self._make_app()

        class TestActivity(Activity):
            def on_start(self):
                self.display_state = {
                    "top": {
                        "layout": {"height": 3},
                        "line_generator": lambda ctx, h: [lambda s, y: None] * 3,
                    },
                    "bottom": {
                        "layout": {"height": 2},
                        "line_generator": lambda ctx, h: [lambda s, y: None] * 2,
                    },
                }

        app.start_activity(TestActivity())
        act = app.current_activity()
        assert "top" in act._region_map
        assert "bottom" in act._region_map
        assert act._region_map["top"] == (0, 3)
        assert act._region_map["bottom"] == (3, 5)
        app.teardown()

    def test_region_at_y(self):
        app, screen = self._make_app()

        class TestActivity(Activity):
            def on_start(self):
                self.display_state = {
                    "a": {
                        "layout": {"height": 5},
                        "line_generator": lambda ctx, h: [lambda s, y: None] * 5,
                    },
                    "b": {
                        "layout": {"height": 3},
                        "line_generator": lambda ctx, h: [lambda s, y: None] * 3,
                    },
                }

        app.start_activity(TestActivity())
        act = app.current_activity()
        assert act.region_at_y(0) == "a"
        assert act.region_at_y(4) == "a"
        assert act.region_at_y(5) == "b"
        assert act.region_at_y(7) == "b"
        assert act.region_at_y(8) is None
        app.teardown()

    def test_delegate_mouse_to_region(self):
        app, screen = self._make_app()
        handler_calls = []

        def mock_handler(ui_element, ctx, event, local_y, event_queue):
            handler_calls.append((ui_element, local_y))

        class TestActivity(Activity):
            def on_start(self):
                self.display_state = {
                    "header": {
                        "layout": {"height": 2},
                        "line_generator": lambda ctx, h: [lambda s, y: None] * 2,
                    },
                    "list": {
                        "layout": {"height": 5},
                        "line_generator": lambda ctx, h: [lambda s, y: None] * 5,
                        "mouse_handler": mock_handler,
                    },
                }

        app.start_activity(TestActivity())
        act = app.current_activity()
        event = MouseClick(MouseButton.LEFT, 10, 4)  # row 4 -> local_y=2 in "list"
        result = act.delegate_mouse_to_region(event)
        assert result is True
        assert handler_calls == [("list", 2)]

        # Click in header (no mouse_handler) — returns False
        event2 = MouseClick(MouseButton.LEFT, 10, 0)
        result2 = act.delegate_mouse_to_region(event2)
        assert result2 is False
        app.teardown()

    def test_delegate_mouse_returns_false_for_unmapped_y(self):
        app, screen = self._make_app()

        class TestActivity(Activity):
            def on_start(self):
                self.display_state = {
                    "a": {
                        "layout": {"height": 2},
                        "line_generator": lambda ctx, h: [lambda s, y: None] * 2,
                    },
                }

        app.start_activity(TestActivity())
        act = app.current_activity()
        event = MouseClick(MouseButton.LEFT, 0, 20)
        assert act.delegate_mouse_to_region(event) is False
        app.teardown()


# ---------------------------------------------------------------------------
# Tests for handle_scroll_list_mouse
# ---------------------------------------------------------------------------

class TestScrollListMouseHandler:
    """Test handle_scroll_list_mouse."""

    def _make_ctx(self, items, selected=0):
        ctx = {
            "items": list(items),
            "selected_index": selected,
            "_rendered_start": 0,
        }
        return ctx

    def test_click_to_select(self):
        q = Queue()
        ctx = self._make_ctx(["a", "b", "c", "d", "e"])
        event = MouseClick(MouseButton.LEFT, 5, 10)
        handle_scroll_list_mouse("mylist", ctx, event, 2, q)
        assert ctx["selected_index"] == 2
        ev = q.get_nowait()
        assert isinstance(ev, ScrollChange)
        assert ev.ui_element == "mylist"

    def test_click_with_rendered_start(self):
        q = Queue()
        ctx = self._make_ctx(["a", "b", "c", "d", "e"], selected=3)
        ctx["_rendered_start"] = 2
        event = MouseClick(MouseButton.LEFT, 5, 10)
        handle_scroll_list_mouse("mylist", ctx, event, 1, q)
        # clicked_index = 2 + 1 = 3
        assert ctx["selected_index"] == 3

    def test_click_beyond_list_ignored(self):
        q = Queue()
        ctx = self._make_ctx(["a", "b"])
        event = MouseClick(MouseButton.LEFT, 5, 10)
        handle_scroll_list_mouse("mylist", ctx, event, 5, q)
        # No change — index 5 is out of range
        assert ctx["selected_index"] == 0
        assert q.empty()

    def test_scroll_up(self):
        q = Queue()
        ctx = self._make_ctx(["a", "b", "c"], selected=2)
        event = MouseScroll("up", 5, 10)
        handle_scroll_list_mouse("mylist", ctx, event, 0, q)
        assert ctx["selected_index"] == 1

    def test_scroll_down(self):
        q = Queue()
        ctx = self._make_ctx(["a", "b", "c"], selected=0)
        event = MouseScroll("down", 5, 10)
        handle_scroll_list_mouse("mylist", ctx, event, 0, q)
        assert ctx["selected_index"] == 1


# ---------------------------------------------------------------------------
# Tests for handle_text_input_mouse
# ---------------------------------------------------------------------------

class TestTextInputMouseHandler:
    """Test handle_text_input_mouse."""

    def _make_ctx(self, label="Name", text="hello"):
        return {
            "label": label,
            "text": text,
            "cursor_index": 0,
            "view_offset": 0,
        }

    def test_click_positions_cursor(self):
        q = Queue()
        ctx = self._make_ctx(label="Name", text="hello world")
        # Prefix: "Name: [" = 7 chars, so x=10 -> text_col = 10-7+0 = 3
        event = MouseClick(MouseButton.LEFT, 10, 0)
        handle_text_input_mouse("input", ctx, event, 0, q)
        assert ctx["cursor_index"] == 3

    def test_click_before_text_area(self):
        q = Queue()
        ctx = self._make_ctx(label="Name", text="hello")
        # x=0 -> text_col = max(0, 0-7+0) = 0
        event = MouseClick(MouseButton.LEFT, 0, 0)
        handle_text_input_mouse("input", ctx, event, 0, q)
        assert ctx["cursor_index"] == 0

    def test_click_beyond_text_clamps(self):
        q = Queue()
        ctx = self._make_ctx(label="X", text="hi")
        # Prefix: "X: [" = 4 chars, x=50 -> text_col = min(50-4+0, 2) = 2
        event = MouseClick(MouseButton.LEFT, 50, 0)
        handle_text_input_mouse("input", ctx, event, 0, q)
        assert ctx["cursor_index"] == 2

    def test_click_with_view_offset(self):
        q = Queue()
        ctx = self._make_ctx(label="Name", text="abcdefghijklmnop")
        ctx["view_offset"] = 5
        # Prefix: "Name: [" = 7, x=10 -> text_col = 10-7+5 = 8
        event = MouseClick(MouseButton.LEFT, 10, 0)
        handle_text_input_mouse("input", ctx, event, 0, q)
        assert ctx["cursor_index"] == 8

    def test_click_emits_textbox_change(self):
        q = Queue()
        ctx = self._make_ctx()
        event = MouseClick(MouseButton.LEFT, 10, 0)
        handle_text_input_mouse("input", ctx, event, 0, q)
        ev = q.get_nowait()
        assert isinstance(ev, TextBoxChange)


# ---------------------------------------------------------------------------
# Tests for harness mouse helpers
# ---------------------------------------------------------------------------

class TestHarnessMouseHelpers:
    """Test that HarnessApplication mouse helpers dispatch correctly."""

    def _make_app(self, rows=24, cols=80):
        screen = MockScreen(rows=rows, cols=cols)
        app = HarnessApplication(screen=screen)
        app.setup()
        return app, screen

    def test_send_mouse_click_dispatches(self):
        app, screen = self._make_app()
        received = []

        class TestActivity(Activity):
            def on_start(self):
                self.display_state = {}
                self.application.subscribe(MouseClick, self, lambda e: received.append(e))
                self.application.subscribe(MouseDown, self, lambda e: received.append(e))
                self.application.subscribe(MouseUp, self, lambda e: received.append(e))

        app.start_activity(TestActivity())
        app.send_mouse_click(10, 5)
        types = [type(e) for e in received]
        assert MouseDown in types
        assert MouseUp in types
        assert MouseClick in types
        app.teardown()

    def test_send_mouse_scroll_dispatches(self):
        app, screen = self._make_app()
        received = []

        class TestActivity(Activity):
            def on_start(self):
                self.display_state = {}
                self.application.subscribe(MouseScroll, self, lambda e: received.append(e))

        app.start_activity(TestActivity())
        app.send_mouse_scroll(5, 10, "down")
        assert len(received) == 1
        assert received[0].direction == "down"
        app.teardown()

    def test_send_mouse_drag_dispatches(self):
        app, screen = self._make_app()
        received = []

        class TestActivity(Activity):
            def on_start(self):
                self.display_state = {}
                self.application.subscribe(MouseDrag, self, lambda e: received.append(e))

        app.start_activity(TestActivity())
        app.send_mouse_drag(10, 5, 20, 10)
        assert len(received) == 1
        assert received[0].start_x == 10
        assert received[0].start_y == 5
        assert received[0].x == 20
        assert received[0].y == 10
        app.teardown()
