"""
Validation tests for the MockScreen / TestApplication test harness.
"""

from pyos import Keys
from pyos.Attrs import NORMAL, BOLD, DIM, UNDERLINE, REVERSE
import time
import pytest

from pyos.testing import MockScreen, HarnessApplication
from pyos.Activity import Activity
from pyos.EventTypes import KeyStroke, ScrollChange, TextBoxSubmit, TextBoxChange
from pyos.input_handlers import handle_scroll_list_input, handle_text_box_input
from pyos.printers.TopBar import TopBar
from pyos.printers.BottomBar import BottomBar
from pyos.printers.ScrollList import ScrollList
from pyos.printers.TextInput import TextInput


# ---------------------------------------------------------------------------
# Test subject: a minimal Activity with TopBar + ScrollList + BottomBar
# ---------------------------------------------------------------------------

class SimpleListActivity(Activity):
    def __init__(self, items=None):
        super().__init__()
        self.items = items or ["Alpha", "Bravo", "Charlie", "Delta", "Echo"]

    def on_start(self):
        self.display_state = {
            "top_bar": TopBar.display_state(items={"title": "Test App"}),
            "scroll_list": ScrollList.display_state(
                screen=self.screen,
                items=self.items,
                selected_index=0,
                focused=True,
            ),
            "bottom_bar": BottomBar.display_state(items={"status": "Ready"}),
        }
        self.application.subscribe(KeyStroke, self, self.on_key)

    def on_key(self, event):
        if event.key == 27:  # ESC
            self.application.pop_activity()
            return
        handle_scroll_list_input("scroll_list", self.display_state["scroll_list"], event, self.event_queue)
        self.refresh_screen()


class SimpleTextInputActivity(Activity):
    def __init__(self):
        super().__init__()

    def on_start(self):
        self.display_state = {
            "text_input": TextInput.display_state(label="Name", text="", focused=True),
        }
        self.application.subscribe(KeyStroke, self, self.on_key)

    def on_key(self, event):
        handle_text_box_input("text_input", self.display_state["text_input"], event, self.event_queue)
        self.refresh_screen()


# ===========================================================================
# MockScreen tests
# ===========================================================================

class TestMockScreen:
    def test_addstr_writes_to_buffer(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "Hello", BOLD)
        assert screen.get_row_text(0) == "Hello"

    def test_addstr_with_offset(self):
        screen = MockScreen(10, 40)
        screen.addstr(2, 5, "World")
        assert "World" in screen.get_row_text(2)
        # Characters before offset remain spaces
        assert screen.buffer[2][4] == " "

    def test_addstr_truncates_at_edge(self):
        screen = MockScreen(5, 10)
        screen.addstr(0, 7, "ABCDEF")
        row = "".join(screen.buffer[0])
        assert row == "       ABC"

    def test_clear_resets_buffer(self):
        screen = MockScreen(5, 10)
        screen.addstr(0, 0, "data")
        screen.clear()
        assert screen.get_row_text(0) == ""

    def test_find_text(self):
        screen = MockScreen(10, 40)
        screen.addstr(3, 0, "needle in a haystack")
        assert screen.find_text("needle")
        assert not screen.find_text("missing")

    def test_attr_buffer(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "Bold", BOLD)
        assert screen.get_attr_at(0, 0) == BOLD
        assert screen.get_attr_at(0, 3) == BOLD
        # Unwritten cells have attr 0
        assert screen.get_attr_at(0, 10) == 0

    def test_clrtoeol(self):
        screen = MockScreen(5, 20)
        screen.addstr(0, 0, "Hello World")
        screen.move(0, 5)
        screen.clrtoeol()
        assert screen.get_row_text(0) == "Hello"

    def test_refresh_counter(self):
        screen = MockScreen()
        assert screen.refresh_count == 0
        screen.refresh()
        screen.refresh()
        assert screen.refresh_count == 2

    def test_assert_text_on_screen(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "expected text")
        screen.assert_text_on_screen("expected")
        with pytest.raises(AssertionError):
            screen.assert_text_on_screen("not here")

    def test_assert_text_not_on_screen(self):
        screen = MockScreen(10, 40)
        screen.assert_text_not_on_screen("ghost")
        screen.addstr(0, 0, "ghost")
        with pytest.raises(AssertionError):
            screen.assert_text_not_on_screen("ghost")


# ===========================================================================
# MockScreen compound helper tests
# ===========================================================================

class TestMockScreenCompound:
    def test_find_text_at_returns_position(self):
        screen = MockScreen(10, 40)
        screen.addstr(3, 5, "target")
        assert screen.find_text_at("target") == (3, 5)

    def test_find_text_at_returns_none_when_missing(self):
        screen = MockScreen(10, 40)
        assert screen.find_text_at("missing") is None

    def test_find_text_at_returns_first_occurrence(self):
        screen = MockScreen(10, 40)
        screen.addstr(1, 0, "dup")
        screen.addstr(5, 0, "dup")
        assert screen.find_text_at("dup") == (1, 0)

    def test_assert_text_has_attr_passes(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "Bold", BOLD)
        screen.assert_text_has_attr("Bold", BOLD)

    def test_assert_text_has_attr_fails_missing_text(self):
        screen = MockScreen(10, 40)
        with pytest.raises(AssertionError, match="Expected to find"):
            screen.assert_text_has_attr("missing", BOLD)

    def test_assert_text_has_attr_fails_wrong_attr(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "Normal", 0)
        with pytest.raises(AssertionError, match="has attr"):
            screen.assert_text_has_attr("Normal", BOLD)

    def test_assert_text_not_has_attr_passes(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "Normal", 0)
        screen.assert_text_not_has_attr("Normal", BOLD)

    def test_assert_text_not_has_attr_fails(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "Bold", BOLD)
        with pytest.raises(AssertionError, match="NOT to have"):
            screen.assert_text_not_has_attr("Bold", BOLD)

    def test_get_text_with_attr(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "normal", 0)
        screen.addstr(1, 0, "bold1", BOLD)
        screen.addstr(1, 10, "bold2", BOLD)
        screen.addstr(2, 0, "also normal", 0)
        segments = screen.get_text_with_attr(BOLD)
        assert segments == ["bold1", "bold2"]

    def test_get_text_with_attr_empty(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "normal", 0)
        assert screen.get_text_with_attr(BOLD) == []


# ===========================================================================
# Activity rendering tests
# ===========================================================================

class TestActivityRendering:
    def test_simple_activity_renders(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)

        mock_screen.assert_text_on_screen("Test App")
        mock_screen.assert_text_on_screen("Alpha")
        mock_screen.assert_text_on_screen("Ready")

    def test_scroll_down_changes_selection(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)

        # Initially "Alpha" is selected (highlighted)
        mock_screen.assert_text_has_attr("Alpha", REVERSE)

        # Press KEY_DOWN — selection moves to "Bravo"
        app.send_key(Keys.DOWN)

        mock_screen.assert_text_has_attr("Bravo", REVERSE)
        mock_screen.assert_text_not_has_attr("Alpha", REVERSE)

    def test_multiple_scrolls(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)

        app.send_keys(Keys.DOWN, Keys.DOWN, Keys.DOWN)

        mock_screen.assert_text_has_attr("Delta", REVERSE)

    def test_esc_pops_activity(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)
        assert app.current_activity() is activity

        app.send_key(27)  # ESC
        app.drain()

        assert app.current_activity() is None

    def test_stacked_activities(self, app, mock_screen):
        first = SimpleListActivity(items=["First"])
        second = SimpleListActivity(items=["Second"])

        app.start_activity(first)
        mock_screen.assert_text_on_screen("First")

        app.start_activity(second)
        mock_screen.assert_text_on_screen("Second")

        assert app.current_activity() is second

        # Pop second — first resumes
        app.send_key(27)  # ESC
        app.drain()

        assert app.current_activity() is first
        mock_screen.assert_text_on_screen("First")


# ===========================================================================
# TextInput tests
# ===========================================================================

class TestTextInput:
    def test_type_text(self, app, mock_screen):
        activity = SimpleTextInputActivity()
        app.start_activity(activity)

        mock_screen.assert_text_on_screen("Name")

        app.send_text("hello")
        mock_screen.assert_text_on_screen("hello")
        assert app.input_text("text_input") == "hello"

    def test_backspace(self, app, mock_screen):
        activity = SimpleTextInputActivity()
        app.start_activity(activity)

        app.send_text("abc")
        mock_screen.assert_text_on_screen("abc")

        app.send_key(127)  # backspace
        mock_screen.assert_text_not_on_screen("abc")
        mock_screen.assert_text_on_screen("ab")
        assert app.input_text("text_input") == "ab"

    def test_text_state_updated(self, app, mock_screen):
        activity = SimpleTextInputActivity()
        app.start_activity(activity)

        app.send_text("test")
        assert app.input_text("text_input") == "test"


# ===========================================================================
# Display-state shortcut tests
# ===========================================================================

class TestDisplayStateShortcuts:
    def test_state_returns_context(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)

        ctx = app.state("scroll_list")
        assert "items" in ctx
        assert "selected_index" in ctx

    def test_selected_item(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)

        assert app.selected_item("scroll_list") == "Alpha"
        app.send_key(Keys.DOWN)
        assert app.selected_item("scroll_list") == "Bravo"

    def test_input_text_shortcut(self, app, mock_screen):
        activity = SimpleTextInputActivity()
        app.start_activity(activity)

        assert app.input_text("text_input") == ""
        app.send_text("hi")
        assert app.input_text("text_input") == "hi"


# ===========================================================================
# wait_for tests
# ===========================================================================

class TestWaitFor:
    def test_wait_for_text_immediate(self, app, mock_screen):
        """Text already on screen — returns immediately."""
        activity = SimpleListActivity()
        app.start_activity(activity)
        app.wait_for_text("Alpha", timeout=1.0)

    def test_wait_for_text_after_delay(self, app, mock_screen):
        """Background thread posts update after a short delay."""
        activity = SimpleTextInputActivity()
        app.start_activity(activity)

        def delayed_type():
            time.sleep(0.1)
            app.main_thread.submit_async(lambda: _type_and_refresh())

        def _type_and_refresh():
            ctx = activity.display_state["text_input"]
            ctx["text"] = "delayed"
            ctx["cursor_position"] = len("delayed")
            activity.refresh_screen()

        app.background_thread.submit_async(delayed_type)
        app.wait_for_text("delayed", timeout=2.0)

    def test_wait_for_text_timeout(self, app, mock_screen):
        """Times out when text never appears."""
        activity = SimpleListActivity()
        app.start_activity(activity)
        with pytest.raises(TimeoutError, match="never_here"):
            app.wait_for_text("never_here", timeout=0.15)

    def test_wait_for_predicate(self, app, mock_screen):
        """Generic wait_for with a predicate."""
        activity = SimpleListActivity()
        app.start_activity(activity)

        def delayed_scroll():
            time.sleep(0.1)
            app.main_thread.submit_async(lambda: _scroll())

        def _scroll():
            ctx = activity.display_state["scroll_list"]
            ctx["selected_index"] = 2
            activity.refresh_screen()

        app.background_thread.submit_async(delayed_scroll)
        app.wait_for(lambda: app.selected_item("scroll_list") == "Charlie", timeout=2.0)

    def test_wait_for_timeout(self, app, mock_screen):
        """Generic wait_for times out when predicate stays falsy."""
        activity = SimpleListActivity()
        app.start_activity(activity)
        with pytest.raises(TimeoutError):
            app.wait_for(lambda: False, timeout=0.15)


# ===========================================================================
# Bitmask attr comparison tests
# ===========================================================================

class TestMockScreenBitmask:
    def test_row_has_attr_combined(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "combo", BOLD | REVERSE)
        assert screen.row_has_attr(0, BOLD)
        assert screen.row_has_attr(0, REVERSE)
        assert screen.row_has_attr(0, BOLD | REVERSE)

    def test_row_has_attr_missing(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "bold", BOLD)
        assert not screen.row_has_attr(0, REVERSE)

    def test_assert_text_has_attr_combined(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "combo", BOLD | REVERSE)
        screen.assert_text_has_attr("combo", BOLD)
        screen.assert_text_has_attr("combo", REVERSE)
        screen.assert_text_has_attr("combo", BOLD | REVERSE)

    def test_assert_text_has_attr_combined_fails(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "bold", BOLD)
        with pytest.raises(AssertionError):
            screen.assert_text_has_attr("bold", BOLD | REVERSE)

    def test_assert_text_not_has_attr_combined(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "bold", BOLD)
        screen.assert_text_not_has_attr("bold", REVERSE)

    def test_assert_text_not_has_attr_combined_fails(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "combo", BOLD | REVERSE)
        with pytest.raises(AssertionError):
            screen.assert_text_not_has_attr("combo", BOLD)

    def test_get_text_with_attr_combined(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "both", BOLD | REVERSE)
        screen.addstr(1, 0, "just_bold", BOLD)
        # Searching for A_BOLD matches both lines
        segments = screen.get_text_with_attr(BOLD)
        assert "both" in segments
        assert "just_bold" in segments

    def test_get_text_with_attr_combined_exact(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "both", BOLD | REVERSE)
        screen.addstr(1, 0, "just_bold", BOLD)
        # Searching for combined only matches the combined line
        segments = screen.get_text_with_attr(BOLD | REVERSE)
        assert "both" in segments
        assert "just_bold" not in segments


# ===========================================================================
# count_text tests
# ===========================================================================

class TestMockScreenCountText:
    def test_single_occurrence(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "hello world")
        assert screen.count_text("hello") == 1

    def test_multiple_occurrences_different_rows(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "item")
        screen.addstr(1, 0, "item")
        screen.addstr(2, 0, "item")
        assert screen.count_text("item") == 3

    def test_multiple_occurrences_same_row(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "ab ab ab")
        assert screen.count_text("ab") == 3

    def test_zero_occurrences(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "hello")
        assert screen.count_text("missing") == 0

    def test_empty_screen(self):
        screen = MockScreen(5, 10)
        assert screen.count_text("anything") == 0


# ===========================================================================
# resize tests
# ===========================================================================

class TestMockScreenResize:
    def test_grow(self):
        screen = MockScreen(5, 10)
        screen.addstr(0, 0, "hello")
        screen.resize(10, 20)
        assert screen.rows == 10
        assert screen.cols == 20
        assert screen.getmaxyx() == (10, 20)
        assert screen.get_row_text(0) == "hello"

    def test_shrink(self):
        screen = MockScreen(10, 40)
        screen.addstr(0, 0, "long text here")
        screen.resize(5, 8)
        assert screen.rows == 5
        assert screen.cols == 8
        assert screen.get_row_text(0) == "long tex"

    def test_cursor_clamped(self):
        screen = MockScreen(10, 40)
        screen.move(8, 35)
        screen.resize(5, 10)
        assert screen.cursor_y == 4
        assert screen.cursor_x == 9

    def test_cursor_preserved_when_fits(self):
        screen = MockScreen(10, 40)
        screen.move(3, 5)
        screen.resize(20, 80)
        assert screen.cursor_y == 3
        assert screen.cursor_x == 5

    def test_attr_preserved(self):
        screen = MockScreen(5, 10)
        screen.addstr(0, 0, "bold", BOLD)
        screen.resize(10, 20)
        assert screen.get_attr_at(0, 0) == BOLD
        assert screen.get_attr_at(0, 3) == BOLD


# ===========================================================================
# HarnessApplication convenience method tests
# ===========================================================================

class TestHarnessConvenience:
    def test_assert_text_on_screen_proxy(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)
        app.assert_text_on_screen("Alpha")
        with pytest.raises(AssertionError):
            app.assert_text_on_screen("missing")

    def test_assert_text_not_on_screen_proxy(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)
        app.assert_text_not_on_screen("missing")
        with pytest.raises(AssertionError):
            app.assert_text_not_on_screen("Alpha")

    def test_activity_stack_depth(self, app, mock_screen):
        assert app.activity_stack_depth() == 0
        first = SimpleListActivity()
        app.start_activity(first)
        assert app.activity_stack_depth() == 1
        second = SimpleListActivity()
        app.start_activity(second)
        assert app.activity_stack_depth() == 2

    def test_activity_stack_types(self, app, mock_screen):
        app.start_activity(SimpleListActivity())
        app.start_activity(SimpleTextInputActivity())
        assert app.activity_stack_types() == ["SimpleListActivity", "SimpleTextInputActivity"]

    def test_focused_element_none_when_empty(self, app, mock_screen):
        assert app.focused_element() is None

    def test_focused_element_returns_focus(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)
        # SimpleListActivity doesn't set self.focus, so it should be None
        assert app.focused_element() is None

    def test_replace_activity(self, app, mock_screen):
        first = SimpleListActivity(items=["First"])
        app.start_activity(first)
        assert app.activity_stack_depth() == 1

        second = SimpleListActivity(items=["Second"])
        app.replace_activity(second)
        assert app.activity_stack_depth() == 1
        assert app.current_activity() is second
        mock_screen.assert_text_on_screen("Second")

    def test_resize(self, app, mock_screen):
        activity = SimpleListActivity()
        app.start_activity(activity)
        app.resize(40, 120)
        assert mock_screen.rows == 40
        assert mock_screen.cols == 120
        # Activity still renders after resize
        mock_screen.assert_text_on_screen("Alpha")

    def test_teardown_error_none_on_success(self, mock_screen):
        application = HarnessApplication(mock_screen)
        application.setup()
        application.teardown()
        assert application.teardown_error is None


# ===========================================================================
# make_app factory fixture tests
# ===========================================================================

class TestMakeAppFixture:
    def test_default_dimensions(self, make_app):
        app, screen = make_app()
        assert screen.rows == 24
        assert screen.cols == 80

    def test_custom_dimensions(self, make_app):
        app, screen = make_app(rows=40, cols=120)
        assert screen.rows == 40
        assert screen.cols == 120

    def test_app_is_functional(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        activity = SimpleListActivity()
        app.start_activity(activity)
        screen.assert_text_on_screen("Alpha")

    def test_multiple_apps(self, make_app):
        app1, screen1 = make_app(rows=10, cols=40)
        app2, screen2 = make_app(rows=20, cols=60)
        assert screen1.rows == 10
        assert screen2.rows == 20


# ===========================================================================
# shutdown_signal tests
# ===========================================================================

class TestShutdownSignal:
    def test_shutdown_signal_exists(self, app, mock_screen):
        """setup() creates a shutdown_signal that is not done."""
        assert app.shutdown_signal is not None
        assert not app.shutdown_signal.done()

    def test_shutdown_signal_accessible_from_activity(self, app, mock_screen):
        """Activities can safely poll shutdown_signal.done()."""
        activity = SimpleListActivity()
        app.start_activity(activity)
        assert not activity.application.shutdown_signal.done()


# ===========================================================================
# StopApplication / flush_stop_events tests
# ===========================================================================

class TestStopEventHandling:
    def test_pop_last_activity_does_not_crash(self, app, mock_screen):
        """Popping the last activity puts StopApplication in event_queue.
        _process_event_queue should handle it gracefully."""
        activity = SimpleListActivity()
        app.start_activity(activity)
        app.send_key(27)  # ESC pops the only activity
        app.drain()
        # Should not raise — StopApplication is silently consumed
        assert app.current_activity() is None

    def test_flush_stop_events_returns_stop(self, app, mock_screen):
        """flush_stop_events() returns StopApplication events."""
        activity = SimpleListActivity()
        app.start_activity(activity)

        # Manually pop so StopApplication is queued but _process_event_queue
        # hasn't run yet
        app._pop_activity()
        app.drain()

        stops = app.flush_stop_events()
        assert len(stops) == 1

    def test_flush_stop_events_empty_when_none(self, app, mock_screen):
        """flush_stop_events() returns empty list when no stop events."""
        activity = SimpleListActivity()
        app.start_activity(activity)
        assert app.flush_stop_events() == []

    def test_flush_stop_events_preserves_other_events(self, app, mock_screen):
        """flush_stop_events() keeps non-StopApplication events in the queue."""
        from pyos.EventTypes import StopApplication
        activity = SimpleTextInputActivity()
        app.start_activity(activity)

        # Manually inject a TextBoxChange and a StopApplication
        app.event_queue.put(TextBoxChange(ui_element="text_input"))
        app.event_queue.put(StopApplication())

        stops = app.flush_stop_events()
        assert len(stops) == 1
        # The TextBoxChange should still be in the queue
        remaining = app.event_queue.get_nowait()
        assert isinstance(remaining, TextBoxChange)


# ===========================================================================
# TextBoxChange event tests
# ===========================================================================

class TextBoxChangeTracker(Activity):
    """Activity that tracks TextBoxChange events."""

    def __init__(self):
        super().__init__()
        self.change_events = []

    def on_start(self):
        self.display_state = {
            "text_input": TextInput.display_state(label="Name", text="", focused=True),
        }
        self.application.subscribe(KeyStroke, self, self.on_key)
        self.application.subscribe(TextBoxChange, self, self.on_change)

    def on_key(self, event):
        handle_text_box_input("text_input", self.display_state["text_input"], event, self.event_queue)
        self.refresh_screen()

    def on_change(self, event):
        self.change_events.append(event)


class TestTextBoxChange:
    def test_typing_fires_change_events(self, app, mock_screen):
        """Each typed character should fire a TextBoxChange."""
        activity = TextBoxChangeTracker()
        app.start_activity(activity)

        app.send_text("abc")
        assert len(activity.change_events) == 3
        assert all(isinstance(e, TextBoxChange) for e in activity.change_events)

    def test_change_event_has_ui_element(self, app, mock_screen):
        """TextBoxChange events carry the ui_element name."""
        activity = TextBoxChangeTracker()
        app.start_activity(activity)

        app.send_text("x")
        assert activity.change_events[0].ui_element == "text_input"

    def test_backspace_fires_change_event(self, app, mock_screen):
        """Backspace should also fire a TextBoxChange."""
        activity = TextBoxChangeTracker()
        app.start_activity(activity)

        app.send_text("ab")
        activity.change_events.clear()

        app.send_key(127)  # backspace
        assert len(activity.change_events) == 1
        assert activity.change_events[0].ui_element == "text_input"

    def test_backspace_on_empty_no_change(self, app, mock_screen):
        """Backspace on empty text should not fire TextBoxChange."""
        activity = TextBoxChangeTracker()
        app.start_activity(activity)

        app.send_key(127)  # backspace on empty
        assert len(activity.change_events) == 0
