"""
UI integration tests for printer components:
ContextMenuList, ThreadList, MultilineText, Table, Accordion, Spacer, HorizontalBar.

Uses the Espresso-esque MockScreen + HarnessApplication test harness.
"""

import pytest

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

from pyos.testing import MockScreen, HarnessApplication
from pyos.Activity import Activity
from pyos.EventTypes import KeyStroke, ScrollChange
from pyos.input_handlers import handle_scroll_list_input
from pyos.printers.ContextMenuList import ContextMenuList
from pyos.printers.ThreadList import ThreadList
from pyos.printers.MultilineText import MultilineText
from pyos.printers.Table import Table
from pyos.printers.Accordion import Accordion
from pyos.printers.Spacer import Spacer
from pyos.printers.HorizontalBar import HorizontalBar
from pyos.printers.TopBar import TopBar
from pyos.printers.ScrollList import ScrollList


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

def simple_renderer(item, screen):
    """Single-line item renderer for ContextMenuList / ThreadList."""
    return [str(item)]


def multiline_renderer(item, screen):
    """Two-line item renderer — title + subtitle."""
    return [str(item), f"  detail of {item}"]


# ---------------------------------------------------------------------------
# Test activities
# ---------------------------------------------------------------------------

class ContextMenuActivity(Activity):
    """Activity wrapping a ContextMenuList with optional inline menu."""

    def __init__(self, items=None, menu_items=None, renderer=None):
        super().__init__()
        self._items = ["Apple", "Banana", "Cherry", "Date", "Elderberry"] if items is None else items
        self._menu_items = [{"text": "Open"}, {"text": "Delete"}] if menu_items is None else menu_items
        self._renderer = renderer or simple_renderer

    def on_start(self):
        self.display_state = {
            "list": ContextMenuList.display_state(
                screen=self.screen,
                items=self._items,
                item_renderer=self._renderer,
                menu={"selected_index": 0, "items": self._menu_items},
                selected_index=0,
                focused=True,
            ),
        }
        self.application.subscribe(KeyStroke, self, self.on_key)

    def on_key(self, event):
        if event.key == 27:
            self.application.pop_activity()
            return
        if event.key == ord("\n") or event.key == 10:
            ctx = self.display_state["list"]
            if ctx["selected_item"] is None:
                ctx["selected_item"] = ctx["items"][ctx["selected_index"]]
            else:
                ctx["selected_item"] = None
            self.refresh_screen()
            return
        handle_scroll_list_input("list", self.display_state["list"], event, self.event_queue)
        self.refresh_screen()


class ThreadListActivity(Activity):
    """Activity wrapping a ThreadList with tree-indented items."""

    def __init__(self, items=None, renderer=None):
        super().__init__()
        self._items = [
            ("Root", 0),
            ("Child A", 1),
            ("Grandchild A1", 2),
            ("Child B", 1),
            ("Grandchild B1", 2),
            ("Great-grandchild B1a", 3),
        ] if items is None else items
        self._renderer = renderer or simple_renderer

    def on_start(self):
        self.display_state = {
            "threads": ThreadList.display_state(
                screen=self.screen,
                items=self._items,
                item_renderer=self._renderer,
                selected_index=0,
                focused=True,
            ),
        }
        self.application.subscribe(KeyStroke, self, self.on_key)

    def on_key(self, event):
        handle_scroll_list_input("threads", self.display_state["threads"], event, self.event_queue)
        self.refresh_screen()


class MultilineTextActivity(Activity):
    """Activity wrapping a MultilineText."""

    def __init__(self, lines=None):
        super().__init__()
        self._lines = ["Line one", "Line two", "Line three"] if lines is None else lines

    def on_start(self):
        self.display_state = {
            "text": MultilineText.display_state(lines=self._lines),
        }


class TableActivity(Activity):
    """Activity wrapping a Table."""

    def __init__(self, headers=None, rows=None, **kwargs):
        super().__init__()
        self._headers = ["Name", "Age", "City"] if headers is None else headers
        self._rows = [
            ["Alice", "30", "NYC"],
            ["Bob", "25", "LA"],
            ["Charlie", "35", "Chicago"],
        ] if rows is None else rows
        self._kwargs = kwargs

    def on_start(self):
        self.display_state = {
            "table": Table.display_state(
                screen=self.screen,
                headers=self._headers,
                rows=self._rows,
                **self._kwargs,
            ),
        }


class AccordionActivity(Activity):
    """Activity wrapping an Accordion containing a ScrollList."""

    def __init__(self, title="Section", items=None, collapsed=False, disabled=False):
        super().__init__()
        self._title = title
        self._items = ["Item 1", "Item 2", "Item 3"] if items is None else items
        self._collapsed = collapsed
        self._disabled = disabled

    def on_start(self):
        child = ScrollList.display_state(
            screen=self.screen,
            items=self._items,
            selected_index=0,
            focused=True,
        )
        self.display_state = {
            "accordion": Accordion.display_state(
                title=self._title,
                child=child,
                collapsed=self._collapsed,
                disabled=self._disabled,
            ),
        }
        self.application.subscribe(KeyStroke, self, self.on_key)

    def on_key(self, event):
        ctx = self.display_state["accordion"]
        if event.key == ord("t"):
            Accordion.set_state(ctx, collapsed=not ctx["collapsed"])
            self.refresh_screen()
            return
        if not ctx["collapsed"] and not ctx["disabled"]:
            handle_scroll_list_input("accordion_child", ctx["child"], event, self.event_queue)
            self.refresh_screen()


class SpacerActivity(Activity):
    """Activity with content pushed to bottom via Spacer."""

    def __init__(self):
        super().__init__()

    def on_start(self):
        self.display_state = {
            "top": TopBar.display_state(items={"title": "Top"}),
            "spacer": Spacer.display_state(flex=1),
            "bottom": MultilineText.display_state(lines=["Bottom Line"]),
        }


class HorizontalBarActivity(Activity):
    """Activity with a HorizontalBar separator."""

    def __init__(self):
        super().__init__()

    def on_start(self):
        self.display_state = {
            "above": MultilineText.display_state(lines=["Above"]),
            "bar": HorizontalBar.display_state(),
            "below": MultilineText.display_state(lines=["Below"]),
        }


class CompositeActivity(Activity):
    """Activity composing multiple components together."""

    def __init__(self):
        super().__init__()

    def on_start(self):
        self.display_state = {
            "top": TopBar.display_state(items={"title": "Dashboard"}),
            "bar1": HorizontalBar.display_state(),
            "table": Table.display_state(
                screen=self.screen,
                headers=["Key", "Value"],
                rows=[["host", "localhost"], ["port", "8080"]],
            ),
            "bar2": HorizontalBar.display_state(),
            "text": MultilineText.display_state(lines=["Status: OK"]),
            "spacer": Spacer.display_state(),
        }


# ===========================================================================
# ContextMenuList tests
# ===========================================================================

class TestContextMenuList:
    def test_renders_items(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        mock_screen.assert_text_on_screen("Apple")
        mock_screen.assert_text_on_screen("Banana")
        mock_screen.assert_text_on_screen("Cherry")

    def test_first_item_highlighted(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        mock_screen.assert_text_has_attr("Apple", REVERSE)

    def test_scroll_down(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        app.send_key(Keys.DOWN)
        mock_screen.assert_text_has_attr("Banana", REVERSE)
        mock_screen.assert_text_not_has_attr("Apple", REVERSE)

    def test_scroll_multiple(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        app.send_keys(Keys.DOWN, Keys.DOWN, Keys.DOWN)
        mock_screen.assert_text_has_attr("Date", REVERSE)

    def test_scroll_up_at_top_stays(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        app.send_key(Keys.UP)
        mock_screen.assert_text_has_attr("Apple", REVERSE)

    def test_scroll_down_at_bottom_stays(self, app, mock_screen):
        items = ["One", "Two", "Three"]
        app.start_activity(ContextMenuActivity(items=items))
        app.send_keys(Keys.DOWN, Keys.DOWN, Keys.DOWN, Keys.DOWN)
        ctx = app.state("list")
        assert ctx["selected_index"] == 2

    def test_open_context_menu(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        app.send_key(10)  # ENTER to open menu
        mock_screen.assert_text_on_screen("[Open]")
        mock_screen.assert_text_on_screen("[Delete]")

    def test_context_menu_navigate_right(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        app.send_key(10)  # open menu
        # Initially first menu item selected
        mock_screen.assert_text_has_attr("[Open]", REVERSE)
        app.send_key(Keys.RIGHT)
        mock_screen.assert_text_has_attr("[Delete]", REVERSE)

    def test_context_menu_navigate_wraps(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        app.send_key(10)  # open menu
        # RIGHT past last wraps to first
        app.send_key(Keys.RIGHT)  # -> Delete
        app.send_key(Keys.RIGHT)  # -> wraps to Open
        mock_screen.assert_text_has_attr("[Open]", REVERSE)

    def test_close_context_menu(self, app, mock_screen):
        app.start_activity(ContextMenuActivity())
        app.send_key(10)  # open menu
        mock_screen.assert_text_on_screen("[Open]")
        app.send_key(10)  # close menu
        mock_screen.assert_text_not_on_screen("[Open]")

    def test_empty_items(self, app, mock_screen):
        app.start_activity(ContextMenuActivity(items=[]))
        # Should render without crashing; no items visible
        mock_screen.assert_text_not_on_screen("Apple")

    def test_single_item(self, app, mock_screen):
        app.start_activity(ContextMenuActivity(items=["Solo"]))
        mock_screen.assert_text_has_attr("Solo", REVERSE)
        app.send_key(Keys.DOWN)
        # Still on the same item
        mock_screen.assert_text_has_attr("Solo", REVERSE)

    def test_multiline_renderer(self, app, mock_screen):
        app.start_activity(ContextMenuActivity(
            items=["Task A", "Task B"],
            renderer=multiline_renderer,
        ))
        mock_screen.assert_text_on_screen("Task A")
        mock_screen.assert_text_on_screen("detail of Task A")
        mock_screen.assert_text_on_screen("Task B")
        mock_screen.assert_text_on_screen("detail of Task B")

    def test_multiline_renderer_selection_tracks_lines(self, app, mock_screen):
        """With 2-line items, line_ids maps each line back to its item."""
        act = ContextMenuActivity(
            items=["Task A", "Task B"],
            renderer=multiline_renderer,
        )
        app.start_activity(act)
        ctx = app.state("list")
        # After rendering, line_ids should be populated
        assert len(ctx["line_ids"]) == 4  # 2 items x 2 lines each

    def test_many_items_scroll_window(self, make_app):
        """With many items on a small screen, only a window is visible."""
        app, screen = make_app(rows=8, cols=40)
        items = [f"Item {i}" for i in range(50)]
        app.start_activity(ContextMenuActivity(items=items))
        # Not all items fit
        assert not screen.find_text("Item 49")
        # Scroll down many times
        for _ in range(49):
            app.send_key(Keys.DOWN)
        screen.assert_text_on_screen("Item 49")

    def test_context_menu_with_no_menu_items(self, app, mock_screen):
        """Opening a menu with empty items list doesn't crash."""
        app.start_activity(ContextMenuActivity(menu_items=[]))
        app.send_key(10)  # open menu
        # Menu area appears but with no items
        ctx = app.state("list")
        assert ctx["selected_item"] is not None


# ===========================================================================
# ThreadList tests
# ===========================================================================

class TestThreadList:
    def test_renders_tree(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        mock_screen.assert_text_on_screen("Root")
        mock_screen.assert_text_on_screen("Child A")

    def test_indentation_depth_0(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        pos = mock_screen.find_text_at("Root")
        assert pos is not None
        assert pos[1] == 0  # no indent

    def test_indentation_depth_1(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        pos = mock_screen.find_text_at("Child A")
        assert pos is not None
        assert pos[1] == 4  # 4 spaces indent

    def test_indentation_depth_2(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        pos = mock_screen.find_text_at("Grandchild A1")
        assert pos is not None
        assert pos[1] == 8  # 8 spaces indent

    def test_indentation_depth_3(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        pos = mock_screen.find_text_at("Great-grandchild B1a")
        assert pos is not None
        assert pos[1] == 12  # 12 spaces indent

    def test_selection_highlighted(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        mock_screen.assert_text_has_attr("Root", REVERSE)

    def test_scroll_through_tree(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        app.send_key(Keys.DOWN)
        # Now on "    Child A"
        row = mock_screen.find_text_row("Child A")
        assert mock_screen.row_has_attr(row, REVERSE)

    def test_scroll_to_deep_node(self, app, mock_screen):
        app.start_activity(ThreadListActivity())
        # Go to Great-grandchild B1a (index 5)
        for _ in range(5):
            app.send_key(Keys.DOWN)
        row = mock_screen.find_text_row("Great-grandchild B1a")
        assert mock_screen.row_has_attr(row, REVERSE)

    def test_empty_thread_list(self, app, mock_screen):
        app.start_activity(ThreadListActivity(items=[]))
        # No crash, nothing rendered
        mock_screen.assert_text_not_on_screen("Root")

    def test_single_node(self, app, mock_screen):
        app.start_activity(ThreadListActivity(items=[("Lone Node", 0)]))
        mock_screen.assert_text_on_screen("Lone Node")
        mock_screen.assert_text_has_attr("Lone Node", REVERSE)

    def test_line_ids_populated(self, app, mock_screen):
        act = ThreadListActivity()
        app.start_activity(act)
        ctx = app.state("threads")
        # line_ids maps each rendered line to its node
        assert ctx["line_ids"][0] == "Root"
        assert ctx["line_ids"][1] == "Child A"

    def test_deep_nesting_many_levels(self, make_app):
        """Deeply nested tree still renders with increasing indentation."""
        app, screen = make_app(rows=24, cols=120)
        items = [(f"Level {d}", d) for d in range(10)]
        app.start_activity(ThreadListActivity(items=items))
        # Level 9 should be at indent 36 (9*4)
        pos = screen.find_text_at("Level 9")
        assert pos is not None
        assert pos[1] == 36

    def test_wide_text_at_depth_truncated(self, make_app):
        """Text + indent exceeding screen width gets clipped."""
        app, screen = make_app(rows=10, cols=30)
        long_text = "A" * 40
        items = [(long_text, 2)]  # 8 spaces indent + 40 chars
        app.start_activity(ThreadListActivity(items=items))
        row_text = screen.get_row_text(0)
        assert len(row_text) <= 30


# ===========================================================================
# MultilineText tests
# ===========================================================================

class TestMultilineText:
    def test_renders_lines(self, app, mock_screen):
        app.start_activity(MultilineTextActivity())
        mock_screen.assert_text_on_screen("Line one")
        mock_screen.assert_text_on_screen("Line two")
        mock_screen.assert_text_on_screen("Line three")

    def test_empty_lines(self, app, mock_screen):
        app.start_activity(MultilineTextActivity(lines=[]))
        # No crash, blank screen
        mock_screen.assert_text_not_on_screen("Line")

    def test_single_line(self, app, mock_screen):
        app.start_activity(MultilineTextActivity(lines=["Only line"]))
        mock_screen.assert_text_on_screen("Only line")

    def test_long_line_truncated(self, make_app):
        app, screen = make_app(rows=5, cols=20)
        app.start_activity(MultilineTextActivity(lines=["A" * 100]))
        row_text = screen.get_row_text(0)
        assert len(row_text) <= 20
        assert row_text == "A" * 20

    def test_newline_in_text_replaced(self, app, mock_screen):
        """Embedded newlines in a single line entry get replaced with space."""
        app.start_activity(MultilineTextActivity(lines=["foo\nbar"]))
        mock_screen.assert_text_on_screen("foo bar")
        mock_screen.assert_text_not_on_screen("foo\nbar")

    def test_carriage_return_stripped(self, app, mock_screen):
        app.start_activity(MultilineTextActivity(lines=["hello\rworld"]))
        mock_screen.assert_text_on_screen("helloworld")

    def test_many_lines(self, make_app):
        """More lines than screen height — only visible ones render."""
        app, screen = make_app(rows=5, cols=40)
        lines = [f"Line {i}" for i in range(100)]
        app.start_activity(MultilineTextActivity(lines=lines))
        # First lines visible, later ones not
        screen.assert_text_on_screen("Line 0")
        # Can't fit all 100 lines in 5 rows
        assert not screen.find_text("Line 99")

    def test_renders_with_normal_attr(self, app, mock_screen):
        app.start_activity(MultilineTextActivity(lines=["Normal text"]))
        # MultilineText uses print_line which uses A_NORMAL
        mock_screen.assert_text_not_has_attr("Normal text", BOLD)
        mock_screen.assert_text_not_has_attr("Normal text", REVERSE)


# ===========================================================================
# Table tests
# ===========================================================================

class TestTable:
    def test_renders_headers(self, app, mock_screen):
        app.start_activity(TableActivity())
        mock_screen.assert_text_on_screen("Name")
        mock_screen.assert_text_on_screen("Age")
        mock_screen.assert_text_on_screen("City")

    def test_headers_are_bold(self, app, mock_screen):
        app.start_activity(TableActivity())
        mock_screen.assert_text_has_attr("Name", BOLD)

    def test_renders_rows(self, app, mock_screen):
        app.start_activity(TableActivity())
        mock_screen.assert_text_on_screen("Alice")
        mock_screen.assert_text_on_screen("Bob")
        mock_screen.assert_text_on_screen("Charlie")

    def test_header_delimiter(self, app, mock_screen):
        app.start_activity(TableActivity())
        # Default delimiter is "-" repeated
        assert mock_screen.find_text("---")

    def test_no_header_delimiter(self, app, mock_screen):
        app.start_activity(TableActivity(show_header_delimiter=False))
        mock_screen.assert_text_on_screen("Alice")
        # Header row still renders
        mock_screen.assert_text_on_screen("Name")

    def test_column_delimiter(self, app, mock_screen):
        app.start_activity(TableActivity(column_delimiter=" | "))
        mock_screen.assert_text_on_screen("|")

    def test_custom_delimiter_char(self, app, mock_screen):
        app.start_activity(TableActivity(header_delimiter_char="="))
        assert mock_screen.find_text("===")

    def test_right_justify(self, app, mock_screen):
        app.start_activity(TableActivity(
            headers=["Num"],
            rows=[["1"], ["200"]],
            column_justify=["right"],
        ))
        mock_screen.assert_text_on_screen("1")
        mock_screen.assert_text_on_screen("200")

    def test_center_justify(self, app, mock_screen):
        app.start_activity(TableActivity(
            headers=["Title"],
            rows=[["Hi"]],
            column_justify=["center"],
        ))
        mock_screen.assert_text_on_screen("Hi")

    def test_empty_rows(self, app, mock_screen):
        app.start_activity(TableActivity(rows=[]))
        mock_screen.assert_text_on_screen("Name")  # headers still render
        mock_screen.assert_text_not_on_screen("Alice")

    def test_empty_headers(self, app, mock_screen):
        """Empty headers means no columns, table returns empty."""
        app.start_activity(TableActivity(headers=[], rows=[]))
        # No crash; nothing visible
        mock_screen.assert_text_not_on_screen("Name")

    def test_row_with_fewer_cells_than_headers(self, app, mock_screen):
        """Rows with missing cells padded with empty strings."""
        app.start_activity(TableActivity(
            headers=["A", "B", "C"],
            rows=[["only_a"]],
        ))
        mock_screen.assert_text_on_screen("only_a")

    def test_row_with_none_cells(self, app, mock_screen):
        """None cells converted to empty string."""
        app.start_activity(TableActivity(
            headers=["A", "B"],
            rows=[[None, "val"]],
        ))
        mock_screen.assert_text_on_screen("val")

    def test_narrow_screen_truncates(self, make_app):
        """On a very narrow screen, columns shrink and text gets truncated."""
        app, screen = make_app(rows=10, cols=25)
        app.start_activity(TableActivity(
            headers=["LongHeaderName", "AnotherLongOne"],
            rows=[["SomeLongValue", "AnotherLongValue"]],
        ))
        # Should render without crash; content is clipped
        all_text = screen.get_all_text()
        assert len(all_text) > 0

    def test_many_rows_scrolling(self, make_app):
        """More rows than screen height — only a window is visible."""
        app, screen = make_app(rows=8, cols=60)
        rows = [[f"Name{i}", str(i), f"City{i}"] for i in range(50)]
        app.start_activity(TableActivity(rows=rows))
        screen.assert_text_on_screen("Name0")
        assert not screen.find_text("Name49")

    def test_selected_row_when_focused(self, app, mock_screen):
        """When focused=True, selected row is highlighted."""
        act = TableActivity()
        app.start_activity(act)
        ctx = app.state("table")
        ctx["focused"] = True
        ctx["selected_index"] = 1
        act.refresh_screen()
        mock_screen.assert_text_has_attr("Bob", REVERSE)

    def test_row_attrs_bold(self, app, mock_screen):
        """row_attrs can make specific rows bold."""
        act = TableActivity()
        app.start_activity(act)
        ctx = app.state("table")
        ctx["row_attrs"] = [False, True, False]  # Bob's row is bold
        act.refresh_screen()
        mock_screen.assert_text_has_attr("Bob", BOLD)

    def test_column_min_widths(self, app, mock_screen):
        """min_widths respected even with short content."""
        app.start_activity(TableActivity(
            headers=["X", "Y"],
            rows=[["a", "b"]],
            column_min_widths=[10, 10],
        ))
        mock_screen.assert_text_on_screen("a")
        mock_screen.assert_text_on_screen("b")

    def test_single_column(self, app, mock_screen):
        app.start_activity(TableActivity(
            headers=["Only"],
            rows=[["val1"], ["val2"]],
        ))
        mock_screen.assert_text_on_screen("Only")
        mock_screen.assert_text_on_screen("val1")
        mock_screen.assert_text_on_screen("val2")

    def test_padding_zero(self, app, mock_screen):
        app.start_activity(TableActivity(
            headers=["A", "B"],
            rows=[["x", "y"]],
            padding=0,
        ))
        mock_screen.assert_text_on_screen("x")
        mock_screen.assert_text_on_screen("y")


# ===========================================================================
# Accordion tests
# ===========================================================================

class TestAccordion:
    def test_expanded_renders_title_and_child(self, app, mock_screen):
        app.start_activity(AccordionActivity(title="My Section"))
        mock_screen.assert_text_on_screen("My Section")
        mock_screen.assert_text_on_screen("collapse")
        mock_screen.assert_text_on_screen("Item 1")
        mock_screen.assert_text_on_screen("Item 2")

    def test_collapsed_shows_expand_hint(self, app, mock_screen):
        app.start_activity(AccordionActivity(title="Collapsed", collapsed=True))
        mock_screen.assert_text_on_screen("Collapsed")
        mock_screen.assert_text_on_screen("expand")
        # Child items NOT visible
        mock_screen.assert_text_not_on_screen("Item 1")

    def test_disabled_shows_empty(self, app, mock_screen):
        app.start_activity(AccordionActivity(title="Disabled", disabled=True))
        mock_screen.assert_text_on_screen("Disabled")
        mock_screen.assert_text_on_screen("[empty]")
        mock_screen.assert_text_not_on_screen("Item 1")

    def test_disabled_has_dim_attr(self, app, mock_screen):
        app.start_activity(AccordionActivity(title="Disabled", disabled=True))
        mock_screen.assert_text_has_attr("Disabled", DIM)

    def test_expanded_title_is_bold(self, app, mock_screen):
        app.start_activity(AccordionActivity(title="Bold Title"))
        mock_screen.assert_text_has_attr("Bold Title", BOLD)

    def test_toggle_collapse_expand(self, app, mock_screen):
        app.start_activity(AccordionActivity(title="Toggle"))
        mock_screen.assert_text_on_screen("Item 1")

        app.send_key(ord("t"))  # collapse
        mock_screen.assert_text_on_screen("expand")
        mock_screen.assert_text_not_on_screen("Item 1")

        app.send_key(ord("t"))  # expand
        mock_screen.assert_text_on_screen("collapse")
        mock_screen.assert_text_on_screen("Item 1")

    def test_child_scroll_when_expanded(self, app, mock_screen):
        app.start_activity(AccordionActivity(items=["A", "B", "C"]))
        mock_screen.assert_text_has_attr("A", REVERSE)
        app.send_key(Keys.DOWN)
        mock_screen.assert_text_has_attr("B", REVERSE)

    def test_child_not_interactive_when_collapsed(self, app, mock_screen):
        app.start_activity(AccordionActivity(collapsed=True, items=["A", "B"]))
        # Pressing down shouldn't crash or change anything meaningful
        app.send_key(Keys.DOWN)
        mock_screen.assert_text_not_on_screen("A")

    def test_layout_changes_on_toggle(self, app, mock_screen):
        act = AccordionActivity()
        app.start_activity(act)
        ctx = app.state("accordion")
        assert "flex" in ctx["layout"]

        app.send_key(ord("t"))  # collapse
        ctx = app.state("accordion")
        assert ctx["layout"] == {"height": 1}

        app.send_key(ord("t"))  # expand
        ctx = app.state("accordion")
        assert "flex" in ctx["layout"]

    def test_empty_child_items(self, app, mock_screen):
        app.start_activity(AccordionActivity(items=[]))
        mock_screen.assert_text_on_screen("Section")
        mock_screen.assert_text_on_screen("collapse")

    def test_focused_title_has_reverse_attr(self, app, mock_screen):
        act = AccordionActivity(title="Focus Me")
        app.start_activity(act)
        ctx = app.state("accordion")
        ctx["focused"] = True
        act.refresh_screen()
        mock_screen.assert_text_has_attr("Focus Me", REVERSE)


# ===========================================================================
# Spacer tests
# ===========================================================================

class TestSpacer:
    def test_pushes_content_to_bottom(self, make_app):
        """Spacer absorbs flex space, pushing subsequent content down."""
        app, screen = make_app(rows=10, cols=40)
        app.start_activity(SpacerActivity())
        # "Top" is near row 0
        top_row = screen.find_text_row("Top")
        assert top_row == 0
        # "Bottom Line" is near the bottom
        bottom_row = screen.find_text_row("Bottom Line")
        assert bottom_row >= 6  # should be pushed toward the bottom

    def test_spacer_with_zero_flex(self, make_app):
        """Spacer with flex=0 takes no space."""
        app, screen = make_app(rows=10, cols=40)

        class ZeroFlexSpacerActivity(Activity):
            def on_start(self):
                self.display_state = {
                    "line1": MultilineText.display_state(lines=["First"]),
                    "spacer": Spacer.display_state(flex=0, min_height=0),
                    "line2": MultilineText.display_state(lines=["Second"]),
                }

        app.start_activity(ZeroFlexSpacerActivity())
        first_row = screen.find_text_row("First")
        second_row = screen.find_text_row("Second")
        # With flex=0 spacer, content should be adjacent
        assert second_row - first_row <= 2

    def test_multiple_spacers_share_space(self, make_app):
        """Two spacers with equal flex split available space."""
        app, screen = make_app(rows=12, cols=40)

        class DualSpacerActivity(Activity):
            def on_start(self):
                self.display_state = {
                    "line1": MultilineText.display_state(lines=["Top"]),
                    "spacer1": Spacer.display_state(flex=1),
                    "line2": MultilineText.display_state(lines=["Middle"]),
                    "spacer2": Spacer.display_state(flex=1),
                    "line3": MultilineText.display_state(lines=["Bottom"]),
                }

        app.start_activity(DualSpacerActivity())
        top = screen.find_text_row("Top")
        mid = screen.find_text_row("Middle")
        bot = screen.find_text_row("Bottom")
        # Middle should be roughly centered, spacers split the gap
        gap1 = mid - top
        gap2 = bot - mid
        assert abs(gap1 - gap2) <= 1

    def test_spacer_max_height(self, make_app):
        """Spacer with max_height doesn't grow beyond it."""
        app, screen = make_app(rows=20, cols=40)

        class CappedSpacerActivity(Activity):
            def on_start(self):
                self.display_state = {
                    "line1": MultilineText.display_state(lines=["Above"]),
                    "spacer": Spacer.display_state(flex=1, max_height=2),
                    "line2": MultilineText.display_state(lines=["Below"]),
                }

        app.start_activity(CappedSpacerActivity())
        above = screen.find_text_row("Above")
        below = screen.find_text_row("Below")
        # gap should be at most max_height (2) + 1 for the lines themselves
        assert below - above <= 3


# ===========================================================================
# HorizontalBar tests
# ===========================================================================

class TestHorizontalBar:
    def test_renders_dashes(self, app, mock_screen):
        app.start_activity(HorizontalBarActivity())
        assert mock_screen.find_text("----")

    def test_full_width(self, make_app):
        """Bar fills the entire screen width."""
        app, screen = make_app(rows=5, cols=40)
        app.start_activity(HorizontalBarActivity())
        bar_row = -1
        for r in range(screen.rows):
            line = "".join(screen.buffer[r])
            if line == "-" * 40:
                bar_row = r
                break
        assert bar_row >= 0, "Expected a full-width dash line"

    def test_separates_content(self, app, mock_screen):
        app.start_activity(HorizontalBarActivity())
        above_row = mock_screen.find_text_row("Above")
        bar_row = mock_screen.find_text_row("----")
        below_row = mock_screen.find_text_row("Below")
        assert above_row < bar_row < below_row

    def test_fixed_height_1(self, app, mock_screen):
        """HorizontalBar layout is always height=1."""
        act = HorizontalBarActivity()
        app.start_activity(act)
        ctx = app.state("bar")
        assert ctx["layout"] == {"height": 1}

    def test_wide_screen(self, make_app):
        """Bar stretches on a wide screen."""
        app, screen = make_app(rows=5, cols=200)
        app.start_activity(HorizontalBarActivity())
        for r in range(screen.rows):
            line = "".join(screen.buffer[r])
            if line == "-" * 200:
                return
        pytest.fail("Expected a 200-char dash line")

    def test_narrow_screen(self, make_app):
        """Bar works even on a very narrow screen."""
        app, screen = make_app(rows=5, cols=3)
        app.start_activity(HorizontalBarActivity())
        assert screen.find_text("---")


# ===========================================================================
# Composite / stress tests
# ===========================================================================

class TestComposite:
    def test_all_components_render(self, app, mock_screen):
        app.start_activity(CompositeActivity())
        mock_screen.assert_text_on_screen("Dashboard")
        mock_screen.assert_text_on_screen("Key")
        mock_screen.assert_text_on_screen("Value")
        mock_screen.assert_text_on_screen("localhost")
        mock_screen.assert_text_on_screen("8080")
        mock_screen.assert_text_on_screen("Status: OK")
        assert mock_screen.find_text("---")

    def test_composite_small_screen(self, make_app):
        """All components squeezed into a tiny screen."""
        app, screen = make_app(rows=6, cols=30)
        app.start_activity(CompositeActivity())
        # Should not crash; some content visible
        screen.assert_text_on_screen("Dashboard")

    def test_composite_large_screen(self, make_app):
        """All components on a generous screen."""
        app, screen = make_app(rows=50, cols=200)
        app.start_activity(CompositeActivity())
        screen.assert_text_on_screen("Dashboard")
        screen.assert_text_on_screen("localhost")
        screen.assert_text_on_screen("Status: OK")

    def test_resize_during_render(self, make_app):
        """Resize the screen after initial render and re-render."""
        app, screen = make_app(rows=24, cols=80)
        app.start_activity(CompositeActivity())
        screen.assert_text_on_screen("Dashboard")

        app.resize(10, 40)
        screen.assert_text_on_screen("Dashboard")

        app.resize(50, 200)
        screen.assert_text_on_screen("Dashboard")
        screen.assert_text_on_screen("localhost")


class TestContextMenuListStress:
    def test_rapid_scroll_1000_items(self, make_app):
        """Rapid scrolling through a large list doesn't crash or corrupt state."""
        app, screen = make_app(rows=20, cols=60)
        items = [f"Entry-{i:04d}" for i in range(1000)]
        app.start_activity(ContextMenuActivity(items=items))
        # Scroll down 500 times
        for _ in range(500):
            app.send_key(Keys.DOWN)
        ctx = app.state("list")
        assert ctx["selected_index"] == 500
        screen.assert_text_on_screen("Entry-0500")
        # Scroll back up 250
        for _ in range(250):
            app.send_key(Keys.UP)
        assert ctx["selected_index"] == 250
        screen.assert_text_on_screen("Entry-0250")

    def test_open_close_menu_rapidly(self, app, mock_screen):
        """Open and close the context menu many times."""
        app.start_activity(ContextMenuActivity())
        for _ in range(20):
            app.send_key(10)  # toggle open
            app.send_key(10)  # toggle close
        # Should be back to normal, no crash
        mock_screen.assert_text_on_screen("Apple")
        ctx = app.state("list")
        assert ctx["selected_item"] is None

    def test_menu_navigate_while_scrolling(self, app, mock_screen):
        """Open menu, navigate, close, scroll, reopen on different item."""
        app.start_activity(ContextMenuActivity())
        app.send_key(10)  # open on Apple
        app.send_key(Keys.RIGHT)  # -> Delete
        app.send_key(10)  # close
        app.send_key(Keys.DOWN)  # -> Banana
        app.send_key(Keys.DOWN)  # -> Cherry
        app.send_key(10)  # open on Cherry
        mock_screen.assert_text_has_attr("Cherry", REVERSE)
        mock_screen.assert_text_on_screen("[Open]")


class TestTableStress:
    def test_100_rows_10_columns(self, make_app):
        """Large table renders without error."""
        app, screen = make_app(rows=30, cols=120)
        headers = [f"Col{i}" for i in range(10)]
        rows = [[f"R{r}C{c}" for c in range(10)] for r in range(100)]
        app.start_activity(TableActivity(headers=headers, rows=rows))
        screen.assert_text_on_screen("Col0")
        screen.assert_text_on_screen("R0C0")

    def test_table_extremely_narrow(self, make_app):
        """Table on a screen narrower than columns handles gracefully."""
        app, screen = make_app(rows=10, cols=15)
        headers = ["Header1", "Header2", "Header3"]
        rows = [["val1", "val2", "val3"]]
        app.start_activity(TableActivity(headers=headers, rows=rows))
        # Should not crash
        all_text = screen.get_all_text()
        assert len(all_text) > 0

    def test_table_with_unicode_content(self, app, mock_screen):
        """Table cells with unicode characters."""
        app.start_activity(TableActivity(
            headers=["Name", "Symbol"],
            rows=[["Alpha", "A"], ["Beta", "B"]],
        ))
        mock_screen.assert_text_on_screen("Alpha")
        mock_screen.assert_text_on_screen("Beta")

    def test_table_cells_with_varying_lengths(self, app, mock_screen):
        """Columns auto-size to content width."""
        app.start_activity(TableActivity(
            headers=["Short", "A Very Long Header Name"],
            rows=[["x", "y"], ["hello world", "z"]],
        ))
        mock_screen.assert_text_on_screen("Short")
        mock_screen.assert_text_on_screen("A Very Long Header Name")
        mock_screen.assert_text_on_screen("hello world")


class TestAccordionStress:
    def test_rapid_toggle(self, app, mock_screen):
        """Toggle accordion 50 times."""
        app.start_activity(AccordionActivity())
        for _ in range(50):
            app.send_key(ord("t"))
        # 50 toggles = even number = back to original expanded
        mock_screen.assert_text_on_screen("Item 1")

    def test_toggle_odd_times_collapses(self, app, mock_screen):
        app.start_activity(AccordionActivity())
        for _ in range(51):
            app.send_key(ord("t"))
        mock_screen.assert_text_not_on_screen("Item 1")
        mock_screen.assert_text_on_screen("expand")

    def test_scroll_child_then_collapse_and_expand(self, app, mock_screen):
        """Scroll child, collapse, expand — child state preserved."""
        items = ["A", "B", "C", "D", "E"]
        app.start_activity(AccordionActivity(items=items))
        app.send_key(Keys.DOWN)
        app.send_key(Keys.DOWN)
        ctx = app.state("accordion")["child"]
        assert ctx["selected_index"] == 2

        app.send_key(ord("t"))  # collapse
        app.send_key(ord("t"))  # expand
        # selected_index should be preserved
        assert ctx["selected_index"] == 2

    def test_nested_accordion_concept(self, app, mock_screen):
        """An accordion whose child is another accordion-like structure."""
        inner_child = ScrollList.display_state(
            screen=MockScreen(24, 80),
            items=["Inner 1", "Inner 2"],
            selected_index=0,
            focused=True,
        )
        inner_acc = Accordion.display_state(
            title="Inner",
            child=inner_child,
        )

        class NestedAccordionActivity(Activity):
            def on_start(self):
                outer_child = ScrollList.display_state(
                    screen=self.screen,
                    items=["Outer Item"],
                    selected_index=0,
                    focused=False,
                )
                self.display_state = {
                    "outer": Accordion.display_state(
                        title="Outer",
                        child=outer_child,
                    ),
                }

        app.start_activity(NestedAccordionActivity())
        mock_screen.assert_text_on_screen("Outer")
        mock_screen.assert_text_on_screen("Outer Item")
