"""
Integration tests for the PyOS demo gallery.

Drives the full demo app through every exhibit using the Espresso-esque
UI test harness.  Read it like a guided tour of pyos capabilities.

Uses:  MockScreen + HarnessApplication from pyos.testing
Under test:  pyos/examples/demo.py (the gallery app)
"""

from pyos.Attrs import NORMAL, BOLD, DIM, UNDERLINE, REVERSE, RED, GREEN, BLUE, YELLOW, MAGENTA, CYAN, WHITE, fg, bg
import pytest

from pyos.testing import MockScreen, HarnessApplication
from pyos import Keys
from pyos.examples.demo import (
    DemoMenuActivity,
    ContactsBrowser,
    DataDashboard,
    NotepadDemo,
    TaskManagerDemo,
    CommentThreadDemo,
    AccordionShowcase,
    LayoutShowcase,
    ConstraintLayoutDemo,
    ColorShowcase,
    ReflexGame,
    PaintDemo,
)


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

def navigate_to(app, label):
    """From the DemoMenuActivity, scroll to *label* and press ENTER."""
    menu = app.current_activity()
    items = menu.items
    target = items.index(label)
    current = int(app.state("list").get("selected_index", 0))
    for _ in range(target - current):
        app.send_key(Keys.DOWN)
    app.send_key(Keys.ENTER)
    app.drain()


# ===========================================================================
# 1. Gallery navigation -- the lobby
# ===========================================================================

class TestGalleryNavigation:
    """Start at the menu, scroll around, peek into an exhibit, come back."""

    def test_gallery_renders_on_launch(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        mock_screen.assert_text_on_screen("PyOS Gallery")
        mock_screen.assert_text_on_screen("Contacts Browser")
        mock_screen.assert_text_on_screen("Data Dashboard")
        mock_screen.assert_text_on_screen("Task Manager")

    def test_welcome_text_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        mock_screen.assert_text_on_screen("Welcome to the PyOS Component Gallery!")

    def test_first_item_is_selected(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        mock_screen.assert_text_has_attr("Contacts Browser", REVERSE)

    def test_scroll_through_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        app.send_key(Keys.DOWN)
        mock_screen.assert_text_has_attr("Data Dashboard", REVERSE)
        app.send_key(Keys.DOWN)
        mock_screen.assert_text_has_attr("Notepad", REVERSE)
        app.send_key(Keys.DOWN)
        mock_screen.assert_text_has_attr("Task Manager", REVERSE)

    def test_enter_exhibit_and_esc_back(self, app, mock_screen):
        """Open Contacts Browser, verify we arrived, ESC back to gallery."""
        app.start_activity(DemoMenuActivity())
        assert app.activity_stack_depth() == 1

        app.send_key(Keys.ENTER)  # opens "Contacts Browser" (first item)
        app.drain()
        assert app.activity_stack_depth() == 2
        mock_screen.assert_text_on_screen("Contacts Browser")

        app.send_key(Keys.ESC)
        app.drain()
        assert app.activity_stack_depth() == 1
        mock_screen.assert_text_on_screen("PyOS Gallery")

    def test_selection_counter_updates(self, app, mock_screen):
        """The bottom bar shows the selection position after scrolling."""
        app.start_activity(DemoMenuActivity())
        app.send_key(Keys.DOWN)
        app.send_key(Keys.DOWN)
        # After two downs we're at index 2 -> "3/13"
        mock_screen.assert_text_on_screen("3/13")


# ===========================================================================
# 2. Contacts Browser -- search + scroll list with TAB focus cycling
# ===========================================================================

class TestContactsBrowser:
    """Test the filterable contacts list with TAB focus cycling."""

    def test_renders_contacts(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        mock_screen.assert_text_on_screen("Alice Johnson")
        mock_screen.assert_text_on_screen("Search")

    def test_contact_count_shown(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        mock_screen.assert_text_on_screen("20 contacts")

    def test_filter_contacts_by_typing(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        app.send_text("eng")
        # Should filter to Engineering dept contacts
        mock_screen.assert_text_on_screen("Engineering")
        # Non-engineering contacts should not be visible
        mock_screen.assert_text_not_on_screen("Frank Hughes")

    def test_filter_updates_count(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        app.send_text("alice")
        mock_screen.assert_text_on_screen("1 contact")

    def test_tab_switches_focus_to_results(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        # Initially search is focused
        ctx = app.state("search")
        assert ctx.get("focused") is True
        results_ctx = app.state("results")
        assert results_ctx.get("focused") is False

        # TAB switches to results
        app.send_key(Keys.TAB)
        ctx = app.state("search")
        assert ctx.get("focused") is False
        results_ctx = app.state("results")
        assert results_ctx.get("focused") is True

    def test_tab_cycles_back_to_search(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        app.send_key(Keys.TAB)  # -> results
        app.send_key(Keys.TAB)  # -> search
        ctx = app.state("search")
        assert ctx.get("focused") is True

    def test_navigate_results_after_tab(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        app.send_key(Keys.TAB)  # focus results
        app.send_key(Keys.DOWN)
        app.send_key(Keys.DOWN)
        results_ctx = app.state("results")
        assert results_ctx["selected_index"] == 2

    def test_esc_returns_to_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Contacts Browser")
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 3. Data Dashboard -- table with summary
# ===========================================================================

class TestDataDashboard:
    """Open the dashboard and verify table + summary rendering."""

    def test_summary_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Data Dashboard")
        mock_screen.assert_text_on_screen("Projects:")
        mock_screen.assert_text_on_screen("Active:")
        mock_screen.assert_text_on_screen("Open bugs:")

    def test_headers_visible_and_bold(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Data Dashboard")
        mock_screen.assert_text_on_screen("Project")
        mock_screen.assert_text_on_screen("Status")
        mock_screen.assert_text_on_screen("Coverage")
        mock_screen.assert_text_has_attr("Coverage", BOLD)

    def test_first_row_highlighted(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Data Dashboard")
        mock_screen.assert_text_has_attr("auth-service", REVERSE)

    def test_scroll_rows(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Data Dashboard")
        for _ in range(4):
            app.send_key(Keys.DOWN)
        mock_screen.assert_text_on_screen("5/12")
        mock_screen.assert_text_on_screen("api-gateway")

    def test_delimiter_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Data Dashboard")
        assert mock_screen.find_text("---")

    def test_esc_returns_to_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Data Dashboard")
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 4. Notepad -- text input, live stats, submission log
# ===========================================================================

class TestNotepadDemo:
    """Test the notepad with live stats and submission log."""

    def test_input_label_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Notepad")
        mock_screen.assert_text_on_screen("Note")

    def test_initial_stats(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Notepad")
        mock_screen.assert_text_on_screen("Chars: 0")
        mock_screen.assert_text_on_screen("Words: 0")

    def test_type_and_stats_update(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Notepad")
        app.send_text("hello world")
        mock_screen.assert_text_on_screen("Chars: 11")
        mock_screen.assert_text_on_screen("Words: 2")

    def test_submit_clears_input_and_logs(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Notepad")
        app.send_text("first note")
        app.send_key(Keys.ENTER)
        mock_screen.assert_text_on_screen("[1] first note")
        mock_screen.assert_text_on_screen("1 note submitted")
        # Input should be cleared
        mock_screen.assert_text_on_screen("Chars: 0")

    def test_multiple_submissions(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Notepad")
        app.send_text("alpha")
        app.send_key(Keys.ENTER)
        app.send_text("beta")
        app.send_key(Keys.ENTER)
        mock_screen.assert_text_on_screen("2 notes submitted")
        mock_screen.assert_text_on_screen("[1] alpha")
        mock_screen.assert_text_on_screen("[2] beta")

    def test_backspace_updates_stats(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Notepad")
        app.send_text("abc")
        mock_screen.assert_text_on_screen("Chars: 3")
        app.send_key(Keys.BACKSPACE)
        mock_screen.assert_text_on_screen("Chars: 2")

    def test_esc_returns_to_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Notepad")
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 5. Task Manager -- context menus + state mutations
# ===========================================================================

class TestTaskManagerDemo:
    """Test the task list with context menus for complete/delete/view."""

    def test_tasks_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        mock_screen.assert_text_on_screen("Set up CI pipeline")
        mock_screen.assert_text_on_screen("[HIGH]")

    def test_initial_counts(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        mock_screen.assert_text_on_screen("Total: 12")
        mock_screen.assert_text_on_screen("Done: 0")
        mock_screen.assert_text_on_screen("Pending: 12")

    def test_unchecked_boxes_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        mock_screen.assert_text_on_screen("[ ]")

    def test_open_context_menu(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(ord(' '))
        mock_screen.assert_text_on_screen("[Complete]")
        mock_screen.assert_text_on_screen("[Delete]")
        mock_screen.assert_text_on_screen("[View]")

    def test_complete_task(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(ord(' '))  # open menu
        # "Complete" should be first item
        app.send_key(Keys.ENTER)  # pick Complete
        mock_screen.assert_text_on_screen("[x]")
        mock_screen.assert_text_on_screen("Done: 1")
        mock_screen.assert_text_on_screen("Pending: 11")

    def test_toggle_complete_back(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        # Complete then uncomplete
        app.send_key(ord(' '))
        app.send_key(Keys.ENTER)  # complete
        mock_screen.assert_text_on_screen("completed:")
        app.send_key(ord(' '))
        app.send_key(Keys.ENTER)  # uncomplete
        mock_screen.assert_text_on_screen("reopened:")
        mock_screen.assert_text_on_screen("Done: 0")

    def test_delete_task(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(ord(' '))          # open menu
        app.send_key(Keys.RIGHT)  # -> Delete
        app.send_key(Keys.ENTER)        # pick Delete
        mock_screen.assert_text_on_screen("Total: 11")
        mock_screen.assert_text_on_screen("Deleted:")

    def test_view_task_detail(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(ord(' '))          # open menu
        app.send_key(Keys.RIGHT)  # -> Delete
        app.send_key(Keys.RIGHT)  # -> View
        app.send_key(Keys.ENTER)        # pick View
        app.drain()
        mock_screen.assert_text_on_screen("Task Detail")
        mock_screen.assert_text_on_screen("Set up CI pipeline")
        mock_screen.assert_text_on_screen("HIGH")

    def test_view_detail_and_return(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(ord(' '))
        app.send_key(Keys.RIGHT)
        app.send_key(Keys.RIGHT)
        app.send_key(Keys.ENTER)
        app.drain()
        assert app.activity_stack_depth() == 3
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("Task Manager")

    def test_navigate_menu_with_arrows(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(ord(' '))
        mock_screen.assert_text_has_attr("[Complete]", REVERSE)
        app.send_key(Keys.RIGHT)
        mock_screen.assert_text_has_attr("[Delete]", REVERSE)
        app.send_key(Keys.RIGHT)
        mock_screen.assert_text_has_attr("[View]", REVERSE)

    def test_esc_closes_menu_first(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(ord(' '))
        mock_screen.assert_text_on_screen("[Complete]")
        app.send_key(Keys.ESC)
        mock_screen.assert_text_not_on_screen("[Complete]")
        mock_screen.assert_text_on_screen("Task Manager")

    def test_esc_returns_to_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Task Manager")
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 6. Comment Thread -- hierarchical tree with multi-line items
# ===========================================================================

class TestCommentThreadDemo:
    """Test the comment thread with nested replies."""

    def test_comments_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Comment Thread")
        mock_screen.assert_text_on_screen("@alice")
        mock_screen.assert_text_on_screen("layout engine")

    def test_replies_visible(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Comment Thread")
        mock_screen.assert_text_on_screen("@bob")
        mock_screen.assert_text_on_screen("flex constraints")

    def test_nested_replies_indented(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Comment Thread")
        # bob is depth 1, should be indented
        pos = mock_screen.find_text_at("@bob")
        assert pos is not None
        _, col = pos
        assert col >= 4  # depth 1 -> 4 spaces indent

    def test_deeper_nesting_more_indented(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Comment Thread")
        # carol is depth 2, should be more indented than bob
        pos_carol = mock_screen.find_text_at("@carol")
        assert pos_carol is not None
        _, col = pos_carol
        assert col >= 8  # depth 2 -> 8 spaces indent

    def test_scroll_through_comments(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Comment Thread")
        for _ in range(4):
            app.send_key(Keys.DOWN)
        # Should have scrolled through several comment lines
        tree = app.state("thread")
        assert tree["selected_index"] == 4

    def test_esc_returns_to_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Comment Thread")
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 7. Accordion Showcase -- collapsible sections
# ===========================================================================

class TestAccordionShowcase:
    """Test the accordion component with multiple sections."""

    def test_sections_visible(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        screen.assert_text_on_screen("About PyOS")
        screen.assert_text_on_screen("Features")
        screen.assert_text_on_screen("Keyboard Shortcuts")

    def test_about_expanded_by_default(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        screen.assert_text_on_screen("Python TUI framework")
        screen.assert_text_on_screen("collapse")

    def test_shortcuts_collapsed_by_default(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        ctx = app.state("shortcuts")
        assert ctx.get("collapsed") is True
        screen.assert_text_on_screen("expand")

    def test_empty_section_disabled(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        ctx = app.state("empty")
        assert ctx.get("disabled") is True

    def test_toggle_collapse_with_enter(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        # Focus is on "about" which is expanded
        ctx = app.state("about")
        assert ctx.get("collapsed") is False
        # Press Enter to collapse it
        app.send_key(Keys.ENTER)
        ctx = app.state("about")
        assert ctx.get("collapsed") is True

    def test_tab_cycles_through_sections(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        # Start focused on "about"
        assert app.state("about").get("focused") is True
        # TAB to features
        app.send_key(Keys.TAB)
        assert app.state("features").get("focused") is True
        assert app.state("about").get("focused") is False
        # TAB to shortcuts
        app.send_key(Keys.TAB)
        assert app.state("shortcuts").get("focused") is True
        # TAB wraps back to about
        app.send_key(Keys.TAB)
        assert app.state("about").get("focused") is True

    def test_expand_shortcuts_with_space(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        # Navigate to shortcuts (collapsed)
        app.send_key(Keys.TAB)  # -> features
        app.send_key(Keys.TAB)  # -> shortcuts
        app.send_key(ord(' '))  # toggle expand
        ctx = app.state("shortcuts")
        assert ctx.get("collapsed") is False

    def test_left_collapses_expanded_section(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        # "about" is focused and expanded
        ctx = app.state("about")
        assert ctx.get("collapsed") is False
        app.send_key(Keys.LEFT)
        assert ctx.get("collapsed") is True

    def test_right_expands_collapsed_section(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        # Collapse first, then expand with RIGHT
        app.send_key(Keys.LEFT)
        ctx = app.state("about")
        assert ctx.get("collapsed") is True
        app.send_key(Keys.RIGHT)
        assert ctx.get("collapsed") is False

    def test_left_noop_when_already_collapsed(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        # Navigate to shortcuts (already collapsed)
        app.send_key(Keys.TAB)  # -> features
        app.send_key(Keys.TAB)  # -> shortcuts
        ctx = app.state("shortcuts")
        assert ctx.get("collapsed") is True
        app.send_key(Keys.LEFT)  # should be a no-op
        assert ctx.get("collapsed") is True

    def test_right_noop_when_already_expanded(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        ctx = app.state("about")
        assert ctx.get("collapsed") is False
        app.send_key(Keys.RIGHT)  # should be a no-op
        assert ctx.get("collapsed") is False

    def test_esc_returns_to_gallery(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Accordion Showcase")
        app.send_key(Keys.ESC)
        app.drain()
        screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 8. Assertion demos -- intentionally wrong assertions to show failure messages
# ===========================================================================

class TestAssertionFailureDemos:
    """
    These tests intentionally trigger assertion failures to show what the
    harness error messages look like.
    """

    def test_wrong_text_gives_helpful_message(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        with pytest.raises(AssertionError, match="Expected to find"):
            mock_screen.assert_text_on_screen("This text is not on screen")

    def test_wrong_attr_gives_helpful_message(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        with pytest.raises(AssertionError, match="has attr"):
            mock_screen.assert_text_has_attr("PyOS Gallery", DIM)

    def test_unexpected_text_still_present(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        with pytest.raises(AssertionError, match="NOT be on screen"):
            mock_screen.assert_text_not_on_screen("PyOS Gallery")

    def test_attr_present_when_expected_absent(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        with pytest.raises(AssertionError, match="NOT to have"):
            mock_screen.assert_text_not_has_attr("Contacts Browser", REVERSE)


# ===========================================================================
# 9. Speed run -- blast through every screen
# ===========================================================================

class TestSpeedRun:
    """
    The ultimate integration test: start at the gallery, visit every single
    exhibit, do something interesting in each, ESC back, and confirm the
    gallery is intact at the end.
    """

    def test_full_gallery_speed_run(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        screen.assert_text_on_screen("PyOS Gallery")

        # --- Contacts Browser ---
        navigate_to(app, "Contacts Browser")
        screen.assert_text_on_screen("Alice Johnson")
        app.send_text("eng")
        screen.assert_text_on_screen("Engineering")
        app.send_key(Keys.TAB)  # switch to results
        app.send_key(Keys.DOWN)
        app.send_key(Keys.ESC)
        app.drain()

        # --- Data Dashboard ---
        navigate_to(app, "Data Dashboard")
        screen.assert_text_has_attr("Coverage", BOLD)
        for _ in range(5):
            app.send_key(Keys.DOWN)
        screen.assert_text_on_screen("6/12")
        app.send_key(Keys.ESC)
        app.drain()

        # --- Notepad ---
        navigate_to(app, "Notepad")
        app.send_text("speed run!")
        screen.assert_text_on_screen("Chars: 10")
        screen.assert_text_on_screen("Words: 2")
        app.send_key(Keys.ENTER)
        screen.assert_text_on_screen("1 note submitted")
        app.send_key(Keys.ESC)
        app.drain()

        # --- Task Manager ---
        navigate_to(app, "Task Manager")
        screen.assert_text_on_screen("Set up CI pipeline")
        app.send_key(ord(' '))
        screen.assert_text_on_screen("[Complete]")
        app.send_key(Keys.ENTER)  # complete first task
        screen.assert_text_on_screen("[x]")
        app.send_key(Keys.ESC)
        app.drain()

        # --- Comment Thread ---
        navigate_to(app, "Comment Thread")
        screen.assert_text_on_screen("@alice")
        for _ in range(3):
            app.send_key(Keys.DOWN)
        app.send_key(Keys.ESC)
        app.drain()

        # --- Accordion Showcase ---
        navigate_to(app, "Accordion Showcase")
        screen.assert_text_on_screen("About PyOS")
        app.send_key(Keys.ENTER)  # toggle collapse
        app.send_key(Keys.TAB)    # cycle focus
        app.send_key(Keys.ESC)
        app.drain()

        # --- Layout Lab ---
        navigate_to(app, "Layout Lab")
        screen.assert_text_on_screen("Layout Lab")
        screen.assert_text_on_screen("Panel A")
        app.send_key(ord("2"))  # 50/50
        screen.assert_text_on_screen("A flex=1")
        app.send_key(Keys.ESC)
        app.drain()

        # --- Constraint Layout ---
        navigate_to(app, "Constraint Layout")
        screen.assert_text_on_screen("Constraint Layout")
        screen.assert_text_on_screen("Length")
        app.send_key(Keys.ESC)
        app.drain()

        # --- Color Showcase ---
        navigate_to(app, "Color Showcase")
        screen.assert_text_on_screen("Color Showcase")
        screen.assert_text_has_attr("Bold Red", BOLD | RED)
        screen.assert_text_has_attr("On Red", bg("red"))
        app.send_key(Keys.ESC)
        app.drain()

        # --- Back at the gallery, intact ---
        screen.assert_text_on_screen("PyOS Gallery")
        assert app.activity_stack_depth() == 1


# ===========================================================================
# 10. Layout Lab -- verify preset switching
# ===========================================================================

class TestLayoutShowcase:
    """Open the Layout Lab and toggle a couple of presets."""

    def test_default_panels_visible(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Layout Lab")
        screen.assert_text_on_screen("Panel A")
        screen.assert_text_on_screen("Panel B")
        screen.assert_text_on_screen("Layout Lab")

    def test_collapse_b_preset(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Layout Lab")
        app.send_key(ord("6"))  # Collapse B
        screen.assert_text_on_screen("Collapse B")

    def test_fifty_fifty_preset(self, make_app):
        app, screen = make_app(rows=30, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Layout Lab")
        app.send_key(ord("2"))  # 50/50 flex split
        screen.assert_text_on_screen("A flex=1 m=1 | B flex=1 m=1")


# ===========================================================================
# 11. Constraint Layout -- just make sure it renders
# ===========================================================================

class TestConstraintLayoutDemo:
    def test_renders_all_regions(self, make_app):
        app, screen = make_app(rows=40, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Constraint Layout")
        screen.assert_text_on_screen("Constraint Layout")
        screen.assert_text_on_screen("Length")
        screen.assert_text_on_screen("Percentage")
        screen.assert_text_on_screen("Fill")
        screen.assert_text_on_screen("Min")

    def test_esc_returns(self, make_app):
        app, screen = make_app(rows=40, cols=100)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Constraint Layout")
        app.send_key(Keys.ESC)
        app.drain()
        screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 12. Color Showcase -- named colors, fg/bg helpers, decorated combos
# ===========================================================================

class TestColorShowcase:
    """Test the color showcase demonstrating the Style color system."""

    def test_title_visible(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        screen.assert_text_on_screen("Color Showcase")

    def test_section_headers_visible(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        screen.assert_text_on_screen("Named Colors")
        screen.assert_text_on_screen("Decorated Colors")
        screen.assert_text_on_screen("Background Colors")
        screen.assert_text_on_screen("Combined Styles")

    def test_section_headers_bold(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        screen.assert_text_has_attr("Named Colors", BOLD)
        screen.assert_text_has_attr("Decorated Colors", BOLD)
        screen.assert_text_has_attr("Background Colors", BOLD)
        screen.assert_text_has_attr("Combined Styles", BOLD)

    def test_named_foreground_colors(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        screen.assert_text_has_attr("Red", RED)
        screen.assert_text_has_attr("Green", GREEN)
        screen.assert_text_has_attr("Blue", BLUE)
        screen.assert_text_has_attr("Yellow", YELLOW)
        screen.assert_text_has_attr("Magenta", MAGENTA)
        screen.assert_text_has_attr("Cyan", CYAN)
        screen.assert_text_has_attr("White", WHITE)

    def test_decorated_colors(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        # Each decorated swatch has both the decoration and the color
        screen.assert_text_has_attr("Bold Red", BOLD | RED)
        screen.assert_text_has_attr("Bold Red", BOLD)
        screen.assert_text_has_attr("Bold Red", RED)
        screen.assert_text_has_attr("Dim Green", DIM | GREEN)
        screen.assert_text_has_attr("Underline Blue", UNDERLINE | BLUE)
        screen.assert_text_has_attr("Reverse Yellow", REVERSE | YELLOW)

    def test_background_colors(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        screen.assert_text_has_attr("On Red", bg("red"))
        screen.assert_text_has_attr("On Blue", bg("blue"))

    def test_combined_fg_and_bg(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        screen.assert_text_has_attr("White on Red", WHITE)
        screen.assert_text_has_attr("White on Red", bg("red"))
        screen.assert_text_has_attr("Bold Cyan on Blue", BOLD)
        screen.assert_text_has_attr("Bold Cyan on Blue", CYAN)
        screen.assert_text_has_attr("Bold Cyan on Blue", bg("blue"))

    def test_no_wrong_color(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        # Red text should NOT have GREEN attr
        screen.assert_text_not_has_attr("Red", GREEN)
        screen.assert_text_not_has_attr("Green", RED)

    def test_esc_returns_to_gallery(self, make_app):
        app, screen = make_app(rows=30, cols=80)
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Color Showcase")
        app.send_key(Keys.ESC)
        app.drain()
        screen.assert_text_on_screen("PyOS Gallery")


# ===========================================================================
# 11. Reflex Game -- KeyRelease timing game
# ===========================================================================

class TestReflexGame:
    """Test the Reflex Game demo that uses KeyRelease events."""

    def test_initial_screen(self, app, mock_screen):
        app.start_activity(ReflexGame())
        mock_screen.assert_text_on_screen("Reflex Tester")
        mock_screen.assert_text_on_screen("Hold SPACE")

    def test_press_space_starts_holding(self, app, mock_screen):
        app.start_activity(ReflexGame())
        app.send_key(ord(" "))
        mock_screen.assert_text_on_screen("HOLDING")

    def test_release_space_shows_result(self, app, mock_screen):
        game = ReflexGame()
        app.start_activity(game)
        app.send_key(ord(" "))
        mock_screen.assert_text_on_screen("HOLDING")
        app.send_key_release(ord(" "))
        mock_screen.assert_text_on_screen("Off by")

    def test_round_counter_increments(self, app, mock_screen):
        game = ReflexGame()
        app.start_activity(game)
        # Round 1
        app.send_key(ord(" "))
        mock_screen.assert_text_on_screen("Round 1/5")
        app.send_key_release(ord(" "))
        # Advance to next round
        app.send_key(ord(" "))
        mock_screen.assert_text_on_screen("Round 2/5")

    def test_full_game_shows_game_over(self, app, mock_screen):
        game = ReflexGame()
        app.start_activity(game)
        for _ in range(game.MAX_ROUNDS):
            app.send_key(ord(" "))
            app.send_key_release(ord(" "))
        mock_screen.assert_text_on_screen("GAME OVER")
        mock_screen.assert_text_on_screen("Average error")

    def test_play_again_after_game_over(self, app, mock_screen):
        game = ReflexGame()
        app.start_activity(game)
        for _ in range(game.MAX_ROUNDS):
            app.send_key(ord(" "))
            app.send_key_release(ord(" "))
        mock_screen.assert_text_on_screen("GAME OVER")
        app.send_key(Keys.ENTER)
        mock_screen.assert_text_on_screen("Reflex Tester")
        mock_screen.assert_text_not_on_screen("GAME OVER")

    def test_esc_returns_to_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Reflex Game")
        mock_screen.assert_text_on_screen("Reflex Tester")
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("PyOS Gallery")

    def test_non_space_release_ignored(self, app, mock_screen):
        game = ReflexGame()
        app.start_activity(game)
        app.send_key(ord(" "))
        mock_screen.assert_text_on_screen("HOLDING")
        # Releasing a different key should not stop the timer
        app.send_key_release(ord("a"))
        mock_screen.assert_text_on_screen("HOLDING")


# ===========================================================================
# 13. Paint -- click/drag to draw, scroll to cycle colors
# ===========================================================================

class TestPaintDemo:
    """Test the paint canvas with mouse drawing and color selection."""

    def test_initial_screen(self, app, mock_screen):
        app.start_activity(PaintDemo())
        mock_screen.assert_text_on_screen("Paint")
        mock_screen.assert_text_on_screen("Color: white")

    def test_initial_color_is_white(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        assert paint._color_index == 1
        mock_screen.assert_text_on_screen("Color: white")

    def test_number_key_selects_color(self, app, mock_screen):
        app.start_activity(PaintDemo())
        app.send_key(ord("3"))
        mock_screen.assert_text_on_screen("Color: green")

    def test_number_key_selects_red(self, app, mock_screen):
        app.start_activity(PaintDemo())
        app.send_key(ord("2"))
        mock_screen.assert_text_on_screen("Color: red")

    def test_scroll_up_cycles_color_forward(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Start at 1 (white), scroll up goes to 2 (red)
        app.send_mouse_scroll(10, 10, direction="up")
        assert paint._color_index == 2
        mock_screen.assert_text_on_screen("Color: red")

    def test_scroll_down_cycles_color_backward(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Start at 1 (white), scroll down goes to 0 (black)
        app.send_mouse_scroll(10, 10, direction="down")
        assert paint._color_index == 0
        mock_screen.assert_text_on_screen("Color: black")

    def test_scroll_wraps_around(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Select color 0 (black), then scroll down wraps to 9
        app.send_key(ord("0"))
        app.send_mouse_scroll(10, 10, direction="down")
        assert paint._color_index == 9
        mock_screen.assert_text_on_screen("Color: bright green")

    def test_click_paints_pixel(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Select red (index 2)
        app.send_key(ord("2"))
        # Click on canvas area (row 4+ is canvas, after top bar and 3-row palette)
        app.send_mouse_click(5, 5)
        # Canvas row 1 (y=5 - 4 offset), col 5 should be painted
        assert paint._canvas[1][5] == 2

    def test_click_multiple_pixels(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        app.send_key(ord("4"))  # blue
        app.send_mouse_click(0, 4)
        app.send_mouse_click(1, 4)
        app.send_mouse_click(2, 4)
        assert paint._canvas[0][0] == 4
        assert paint._canvas[0][1] == 4
        assert paint._canvas[0][2] == 4

    def test_right_click_erases(self, app, mock_screen):
        from pyos.EventTypes import MouseButton
        paint = PaintDemo()
        app.start_activity(paint)
        # Paint a pixel first
        app.send_key(ord("1"))  # white
        app.send_mouse_click(5, 5)
        assert paint._canvas[1][5] == 1
        # Right-click erases it
        app.send_mouse_click(5, 5, button=MouseButton.RIGHT)
        assert paint._canvas[1][5] == 0

    def test_drag_paints_trail(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        app.send_key(ord("3"))  # green
        app.send_mouse_drag(5, 5, 8, 5)
        # The drag endpoint should be painted
        assert paint._canvas[1][8] == 3

    def test_clear_canvas(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Paint some pixels
        app.send_key(ord("2"))
        app.send_mouse_click(5, 5)
        app.send_mouse_click(10, 6)
        assert paint._canvas[1][5] == 2
        assert paint._canvas[2][10] == 2
        # Clear
        app.send_key(ord("c"))
        assert paint._canvas[1][5] == 0
        assert paint._canvas[2][10] == 0

    def test_click_outside_canvas_ignored(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Row 0 is the top bar — clicking there should not crash or paint
        app.send_mouse_click(5, 0)
        # All canvas cells should still be 0
        assert all(cell == 0 for row in paint._canvas for cell in row)

    def test_click_palette_selects_color(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Palette spans rows 1-3. Each swatch is 4 chars wide starting at x=1.
        # Color 4 (blue) starts at x = 1 + 4*4 = 17
        app.send_mouse_click(17, 2)
        assert paint._color_index == 4
        mock_screen.assert_text_on_screen("Color: blue")

    def test_click_palette_first_color(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Color 0 (black/eraser) starts at x=1
        app.send_mouse_click(1, 2)
        assert paint._color_index == 0
        mock_screen.assert_text_on_screen("Color: black")

    def test_click_palette_last_color(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Color 9 (bright green) starts at x = 1 + 9*4 = 37
        app.send_mouse_click(37, 2)
        assert paint._color_index == 9
        mock_screen.assert_text_on_screen("Color: bright green")

    def test_click_palette_top_row(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Clicking the top row of the palette (y=1) should also work
        app.send_mouse_click(17, 1)
        assert paint._color_index == 4

    def test_click_palette_bottom_row(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Clicking the bottom row of the palette (y=3) should also work
        app.send_mouse_click(17, 3)
        assert paint._color_index == 4

    def test_click_palette_then_draw(self, app, mock_screen):
        paint = PaintDemo()
        app.start_activity(paint)
        # Click magenta in palette (index 6, x = 1 + 6*4 = 25)
        app.send_mouse_click(25, 2)
        assert paint._color_index == 6
        # Now draw with it
        app.send_mouse_click(10, 7)
        assert paint._canvas[3][10] == 6

    def test_esc_returns_to_gallery(self, app, mock_screen):
        app.start_activity(DemoMenuActivity())
        navigate_to(app, "Paint")
        mock_screen.assert_text_on_screen("Paint")
        app.send_key(Keys.ESC)
        app.drain()
        mock_screen.assert_text_on_screen("PyOS Gallery")
