from __future__ import annotations

import unittest

import urwid
from tests.util import SelectableText


class ListBoxCalculateVisibleTest(unittest.TestCase):
    def cvtest(
        self,
        desc: str,
        body,
        focus: int,
        offset_rows: int,
        inset_fraction,
        exp_offset_inset: int,
        exp_cur: tuple[int, int] | None,
    ) -> None:
        with self.subTest(desc):
            lbox = urwid.ListBox(urwid.SimpleListWalker(body))
            lbox.body.set_focus(focus)
            lbox.offset_rows = offset_rows
            lbox.inset_fraction = inset_fraction

            middle, top, bottom = lbox.calculate_visible((4, 5), focus=1)
            offset_inset, focus_widget, focus_pos, _ign, cursor = middle

            if cursor is not None:
                x, y = cursor
                y += offset_inset
                cursor = x, y

            self.assertEqual(exp_offset_inset, offset_inset)
            self.assertEqual(exp_cur, cursor)

    def test1_simple(self):
        T = urwid.Text

        l = [T(""), T(""), T("\n"), T("\n\n"), T("\n"), T(""), T("")]

        self.cvtest(
            "simple top position",
            l,
            3,
            0,
            (0, 1),
            0,
            None,
        )

        self.cvtest(
            "simple middle position",
            l,
            3,
            1,
            (0, 1),
            1,
            None,
        )

        self.cvtest(
            "simple bottom position",
            l,
            3,
            2,
            (0, 1),
            2,
            None,
        )

        self.cvtest(
            "straddle top edge",
            l,
            3,
            0,
            (1, 2),
            -1,
            None,
        )

        self.cvtest(
            "straddle bottom edge",
            l,
            3,
            4,
            (0, 1),
            4,
            None,
        )

        self.cvtest(
            "off bottom edge",
            l,
            3,
            5,
            (0, 1),
            4,
            None,
        )

        self.cvtest(
            "way off bottom edge",
            l,
            3,
            100,
            (0, 1),
            4,
            None,
        )

        self.cvtest(
            "gap at top",
            l,
            0,
            2,
            (0, 1),
            0,
            None,
        )

        self.cvtest(
            "gap at top and off bottom edge",
            l,
            2,
            5,
            (0, 1),
            2,
            None,
        )

        self.cvtest(
            "gap at bottom",
            l,
            6,
            1,
            (0, 1),
            4,
            None,
        )

        self.cvtest(
            "gap at bottom and straddling top edge",
            l,
            4,
            0,
            (1, 2),
            1,
            None,
        )

        self.cvtest(
            "gap at bottom cannot completely fill",
            [T(""), T(""), T("")],
            1,
            0,
            (0, 1),
            1,
            None,
        )

        self.cvtest(
            "gap at top and bottom",
            [T(""), T(""), T("")],
            1,
            2,
            (0, 1),
            1,
            None,
        )

    def test2_cursor(self):
        T, E = urwid.Text, urwid.Edit

        l1 = [T(""), T(""), T("\n"), E("", "\n\nX"), T("\n"), T(""), T("")]
        l2 = [T(""), T(""), T("\n"), E("", "YY\n\n"), T("\n"), T(""), T("")]

        l2[3].set_edit_pos(2)

        self.cvtest(
            "plain cursor in view",
            l1,
            3,
            1,
            (0, 1),
            1,
            (1, 3),
        )

        self.cvtest(
            "cursor off top",
            l2,
            3,
            0,
            (1, 3),
            0,
            (2, 0),
        )

        self.cvtest(
            "cursor further off top",
            l2,
            3,
            0,
            (2, 3),
            0,
            (2, 0),
        )

        self.cvtest(
            "cursor off bottom",
            l1,
            3,
            3,
            (0, 1),
            2,
            (1, 4),
        )

        self.cvtest(
            "cursor way off bottom",
            l1,
            3,
            100,
            (0, 1),
            2,
            (1, 4),
        )

    def test_sized(self):
        lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(str(num)) for num in range(5)]))
        self.assertEqual(5, len(lbox))

    def test_not_sized(self):
        class TestWalker(urwid.ListWalker):
            @property
            def contents(self):
                return self

            @staticmethod
            def next_position(position: int) -> tuple[urwid.Text, int]:
                return urwid.Text(str(position)), position

            @staticmethod
            def prev_position(position: int) -> tuple[urwid.Text, int]:
                return urwid.Text(str(position)), position

        lbox = urwid.ListBox(TestWalker())
        with self.assertRaises(AttributeError) as exc:
            len(lbox)

        self.assertEqual(f"{TestWalker.__name__} is not Sized", str(exc.exception))


class ListBoxChangeFocusTest(unittest.TestCase):
    def cftest(
        self,
        desc: str,
        body,
        pos: int,
        offset_inset: int,
        coming_from,
        cursor: tuple[int, int] | None,
        snap_rows,
        exp_offset_rows: int,
        exp_inset_fraction,
        exp_cur: tuple[int, int] | None,
    ):
        with self.subTest(desc):
            lbox = urwid.ListBox(urwid.SimpleListWalker(body))

            lbox.change_focus((4, 5), pos, offset_inset, coming_from, cursor, snap_rows)

            exp = exp_offset_rows, exp_inset_fraction
            act = lbox.offset_rows, lbox.inset_fraction

            cursor = None
            focus_widget, focus_pos = lbox.body.get_focus()
            if focus_widget.selectable():
                if hasattr(focus_widget, "get_cursor_coords"):
                    cursor = focus_widget.get_cursor_coords((4,))

            self.assertEqual(exp, act)
            self.assertEqual(exp_cur, cursor)

    def test1unselectable(self):
        T = urwid.Text
        l = [T("\n"), T("\n\n"), T("\n\n"), T("\n\n"), T("\n")]

        self.cftest(
            "simple unselectable",
            l,
            2,
            0,
            None,
            None,
            None,
            0,
            (0, 1),
            None,
        )

        self.cftest(
            "unselectable",
            l,
            2,
            1,
            None,
            None,
            None,
            1,
            (0, 1),
            None,
        )

        self.cftest(
            "unselectable off top",
            l,
            2,
            -2,
            None,
            None,
            None,
            0,
            (2, 3),
            None,
        )

        self.cftest(
            "unselectable off bottom",
            l,
            3,
            2,
            None,
            None,
            None,
            2,
            (0, 1),
            None,
        )

    def test2selectable(self):
        T, S = urwid.Text, SelectableText
        l = [T("\n"), T("\n\n"), S("\n\n"), T("\n\n"), T("\n")]

        self.cftest(
            "simple selectable",
            l,
            2,
            0,
            None,
            None,
            None,
            0,
            (0, 1),
            None,
        )

        self.cftest(
            "selectable",
            l,
            2,
            1,
            None,
            None,
            None,
            1,
            (0, 1),
            None,
        )

        self.cftest(
            "selectable at top",
            l,
            2,
            0,
            "below",
            None,
            None,
            0,
            (0, 1),
            None,
        )

        self.cftest(
            "selectable at bottom",
            l,
            2,
            2,
            "above",
            None,
            None,
            2,
            (0, 1),
            None,
        )

        self.cftest(
            "selectable off top snap",
            l,
            2,
            -1,
            "below",
            None,
            None,
            0,
            (0, 1),
            None,
        )

        self.cftest(
            "selectable off bottom snap",
            l,
            2,
            3,
            "above",
            None,
            None,
            2,
            (0, 1),
            None,
        )

        self.cftest(
            "selectable off top no snap",
            l,
            2,
            -1,
            "above",
            None,
            None,
            0,
            (1, 3),
            None,
        )

        self.cftest(
            "selectable off bottom no snap",
            l,
            2,
            3,
            "below",
            None,
            None,
            3,
            (0, 1),
            None,
        )

    def test3large_selectable(self):
        T, S = urwid.Text, SelectableText
        l = [T("\n"), S("\n\n\n\n\n\n"), T("\n")]
        self.cftest(
            "large selectable no snap",
            l,
            1,
            -1,
            None,
            None,
            None,
            0,
            (1, 7),
            None,
        )

        self.cftest(
            "large selectable snap up",
            l,
            1,
            -2,
            "below",
            None,
            None,
            0,
            (0, 1),
            None,
        )

        self.cftest(
            "large selectable snap up2",
            l,
            1,
            -2,
            "below",
            None,
            2,
            0,
            (0, 1),
            None,
        )

        self.cftest(
            "large selectable almost snap up",
            l,
            1,
            -2,
            "below",
            None,
            1,
            0,
            (2, 7),
            None,
        )

        self.cftest(
            "large selectable snap down",
            l,
            1,
            0,
            "above",
            None,
            None,
            0,
            (2, 7),
            None,
        )

        self.cftest(
            "large selectable snap down2",
            l,
            1,
            0,
            "above",
            None,
            2,
            0,
            (2, 7),
            None,
        )

        self.cftest(
            "large selectable almost snap down",
            l,
            1,
            0,
            "above",
            None,
            1,
            0,
            (0, 1),
            None,
        )

        m = [T("\n\n\n\n"), S("\n\n\n\n\n"), T("\n\n\n\n")]
        self.cftest(
            "large selectable outside view down",
            m,
            1,
            4,
            "above",
            None,
            None,
            0,
            (0, 1),
            None,
        )

        self.cftest(
            "large selectable outside view up",
            m,
            1,
            -5,
            "below",
            None,
            None,
            0,
            (1, 6),
            None,
        )

    def test4cursor(self):
        T, E = urwid.Text, urwid.Edit
        # ...

    def test5set_focus_valign(self):
        T, E = urwid.Text, urwid.Edit
        lbox = urwid.ListBox(urwid.SimpleFocusListWalker([T(""), T("")]))
        lbox.set_focus_valign("middle")
        # TODO: actually test the result


class ListBoxRenderTest(unittest.TestCase):
    def ltest(
        self,
        desc: str,
        body,
        focus: int,
        offset_inset_rows: int,
        exp_text: list[bytes],
        exp_cur: tuple[int, int] | None,
    ) -> None:
        with self.subTest(desc):
            exp_text = [t.encode("iso8859-1") for t in exp_text]
            lbox = urwid.ListBox(urwid.SimpleListWalker(body))
            lbox.body.set_focus(focus)
            lbox.shift_focus((4, 10), offset_inset_rows)
            canvas = lbox.render((4, 5), focus=1)

            self.assertEqual(exp_text, canvas.text)
            self.assertEqual(exp_cur, canvas.cursor)

    def test1_simple(self):
        T = urwid.Text

        self.ltest(
            "simple one text item render",
            [T("1\n2")],
            0,
            0,
            ["1   ", "2   ", "    ", "    ", "    "],
            None,
        )

        self.ltest(
            "simple multi text item render off bottom",
            [T("1"), T("2"), T("3\n4"), T("5"), T("6")],
            2,
            2,
            ["1   ", "2   ", "3   ", "4   ", "5   "],
            None,
        )

        self.ltest(
            "simple multi text item render off top",
            [T("1"), T("2"), T("3\n4"), T("5"), T("6")],
            2,
            1,
            ["2   ", "3   ", "4   ", "5   ", "6   "],
            None,
        )

    def test2_trim(self):
        T = urwid.Text

        self.ltest(
            "trim unfocused bottom",
            [T("1\n2"), T("3\n4"), T("5\n6")],
            1,
            2,
            ["1   ", "2   ", "3   ", "4   ", "5   "],
            None,
        )

        self.ltest(
            "trim unfocused top",
            [T("1\n2"), T("3\n4"), T("5\n6")],
            1,
            1,
            ["2   ", "3   ", "4   ", "5   ", "6   "],
            None,
        )

        self.ltest(
            "trim none full focus",
            [T("1\n2\n3\n4\n5")],
            0,
            0,
            ["1   ", "2   ", "3   ", "4   ", "5   "],
            None,
        )

        self.ltest(
            "trim focus bottom",
            [T("1\n2\n3\n4\n5\n6")],
            0,
            0,
            ["1   ", "2   ", "3   ", "4   ", "5   "],
            None,
        )

        self.ltest(
            "trim focus top",
            [T("1\n2\n3\n4\n5\n6")],
            0,
            -1,
            ["2   ", "3   ", "4   ", "5   ", "6   "],
            None,
        )

        self.ltest(
            "trim focus top and bottom",
            [T("1\n2\n3\n4\n5\n6\n7")],
            0,
            -1,
            ["2   ", "3   ", "4   ", "5   ", "6   "],
            None,
        )

    def test3_shift(self):
        T, E = urwid.Text, urwid.Edit

        self.ltest(
            "shift up one fit",
            [T("1\n2"), T("3"), T("4"), T("5"), T("6")],
            4,
            5,
            ["2   ", "3   ", "4   ", "5   ", "6   "],
            None,
        )

        e = E("", "ab\nc", 1)
        e.set_edit_pos(2)
        self.ltest(
            "shift down one cursor over edge",
            [e, T("3"), T("4"), T("5\n6")],
            0,
            -1,
            ["ab  ", "c   ", "3   ", "4   ", "5   "],
            (2, 0),
        )

        self.ltest(
            "shift up one cursor over edge",
            [T("1\n2"), T("3"), T("4"), E("", "d\ne")],
            3,
            4,
            ["2   ", "3   ", "4   ", "d   ", "e   "],
            (1, 4),
        )

        self.ltest(
            "shift none cursor top focus over edge",
            [E("", "ab\n"), T("3"), T("4"), T("5\n6")],
            0,
            -1,
            ["    ", "3   ", "4   ", "5   ", "6   "],
            (0, 0),
        )

        e = E("", "abc\nd")
        e.set_edit_pos(3)
        self.ltest(
            "shift none cursor bottom focus over edge",
            [T("1\n2"), T("3"), T("4"), e],
            3,
            4,
            ["1   ", "2   ", "3   ", "4   ", "abc "],
            (3, 4),
        )

    def test4_really_large_contents(self):
        T, E = urwid.Text, urwid.Edit
        self.ltest(
            "really large edit",
            [T("hello" * 100)],
            0,
            0,
            ["hell", "ohel", "lohe", "lloh", "ello"],
            None,
        )

        self.ltest(
            "really large edit",
            [E("", "hello" * 100)],
            0,
            0,
            ["hell", "ohel", "lohe", "lloh", "llo "],
            (3, 4),
        )


class ListBoxKeypressTest(unittest.TestCase):
    def ktest(
        self,
        desc: str,
        key,
        body,
        focus: int,
        offset_inset: int,
        exp_focus: int,
        exp_offset_inset: int,
        exp_cur: tuple[int, int] | None,
        lbox=None,
    ) -> tuple[str | None, urwid.ListBox]:

        if lbox is None:
            lbox = urwid.ListBox(urwid.SimpleListWalker(body))
            lbox.body.set_focus(focus)
            lbox.shift_focus((4, 10), offset_inset)

        ret_key = lbox.keypress((4, 5), key)
        middle, top, bottom = lbox.calculate_visible((4, 5), focus=1)
        offset_inset, focus_widget, focus_pos, _ign, cursor = middle

        if cursor is not None:
            x, y = cursor
            y += offset_inset
            cursor = x, y

        exp = exp_focus, exp_offset_inset
        act = focus_pos, offset_inset
        self.assertEqual(exp, act)
        self.assertEqual(exp_cur, cursor)
        return ret_key, lbox

    def test1_up(self):
        T, S, E = urwid.Text, SelectableText, urwid.Edit

        self.ktest(
            "direct selectable both visible",
            "up",
            [S(""), S("")],
            1,
            1,
            0,
            0,
            None,
        )

        self.ktest(
            "selectable skip one all visible",
            "up",
            [S(""), T(""), S("")],
            2,
            2,
            0,
            0,
            None,
        )

        key, lbox = self.ktest(
            "nothing above no scroll",
            "up",
            [S("")],
            0,
            0,
            0,
            0,
            None,
        )
        self.assertEqual("up", key)

        key, lbox = self.ktest(
            "unselectable above no scroll",
            "up",
            [T(""), T(""), S("")],
            2,
            2,
            2,
            2,
            None,
        )
        self.assertEqual("up", key)

        self.ktest(
            "unselectable above scroll 1",
            "up",
            [T(""), S(""), T("\n\n\n")],
            1,
            0,
            1,
            1,
            None,
        )

        self.ktest(
            "selectable above scroll 1",
            "up",
            [S(""), S(""), T("\n\n\n")],
            1,
            0,
            0,
            0,
            None,
        )

        self.ktest(
            "selectable above too far",
            "up",
            [S(""), T(""), S(""), T("\n\n\n")],
            2,
            0,
            2,
            1,
            None,
        )

        self.ktest(
            "selectable above skip 1 scroll 1",
            "up",
            [S(""), T(""), S(""), T("\n\n\n")],
            2,
            1,
            0,
            0,
            None,
        )

        self.ktest(
            "tall selectable above scroll 2",
            "up",
            [S(""), S("\n"), S(""), T("\n\n\n")],
            2,
            0,
            1,
            0,
            None,
        )

        self.ktest(
            "very tall selectable above scroll 5",
            "up",
            [S(""), S("\n\n\n\n"), S(""), T("\n\n\n\n")],
            2,
            0,
            1,
            0,
            None,
        )

        self.ktest(
            "very tall selected scroll within 1",
            "up",
            [S(""), S("\n\n\n\n\n")],
            1,
            -1,
            1,
            0,
            None,
        )

        self.ktest(
            "edit above pass cursor",
            "up",
            [E("", "abc"), E("", "de")],
            1,
            1,
            0,
            0,
            (2, 0),
        )

        key, lbox = self.ktest(
            "edit too far above pass cursor A",
            "up",
            [E("", "abc"), T("\n\n\n\n"), E("", "de")],
            2,
            4,
            1,
            0,
            None,
        )

        self.ktest(
            "edit too far above pass cursor B",
            "up",
            None,
            None,
            None,
            0,
            0,
            (2, 0),
            lbox,
        )

        self.ktest(
            "within focus cursor made not visible",
            "up",
            [T("\n\n\n"), E("hi\n", "ab")],
            1,
            3,
            0,
            0,
            None,
        )

        self.ktest(
            "within focus cursor made not visible (2)",
            "up",
            [T("\n\n\n\n"), E("hi\n", "ab")],
            1,
            3,
            0,
            -1,
            None,
        )

        self.ktest(
            "force focus unselectable",
            "up",
            [T("\n\n\n\n"), S("")],
            1,
            4,
            0,
            0,
            None,
        )

        self.ktest(
            "pathological cursor widget",
            "up",
            [T("\n"), E("\n\n\n\n\n", "a")],
            1,
            4,
            0,
            -1,
            None,
        )

        self.ktest(
            "unselectable to unselectable",
            "up",
            [T(""), T(""), T(""), T(""), T(""), T(""), T("")],
            2,
            0,
            1,
            0,
            None,
        )

        self.ktest(
            "unselectable over edge to same",
            "up",
            [T(""), T("12\n34"), T(""), T(""), T(""), T("")],
            1,
            -1,
            1,
            0,
            None,
        )

        key, lbox = self.ktest(
            "edit short between pass cursor A",
            "up",
            [E("", "abcd"), E("", "a"), E("", "def")],
            2,
            2,
            1,
            1,
            (1, 1),
        )

        self.ktest(
            "edit short between pass cursor B",
            "up",
            None,
            None,
            None,
            0,
            0,
            (3, 0),
            lbox,
        )

        e = E("", "\n\n\n\n\n")
        e.set_edit_pos(1)
        key, lbox = self.ktest(
            "edit cursor force scroll",
            "up",
            [e],
            0,
            -1,
            0,
            0,
            (0, 0),
        )
        self.assertEqual(0, lbox.inset_fraction[0])

    def test2_down(self):
        T, S, E = urwid.Text, SelectableText, urwid.Edit

        self.ktest(
            "direct selectable both visible",
            "down",
            [S(""), S("")],
            0,
            0,
            1,
            1,
            None,
        )

        self.ktest(
            "selectable skip one all visible",
            "down",
            [S(""), T(""), S("")],
            0,
            0,
            2,
            2,
            None,
        )

        key, lbox = self.ktest(
            "nothing below no scroll",
            "down",
            [S("")],
            0,
            0,
            0,
            0,
            None,
        )
        self.assertEqual("down", key)

        key, lbox = self.ktest(
            "unselectable below no scroll",
            "down",
            [S(""), T(""), T("")],
            0,
            0,
            0,
            0,
            None,
        )
        self.assertEqual("down", key)

        self.ktest(
            "unselectable below scroll 1",
            "down",
            [T("\n\n\n"), S(""), T("")],
            1,
            4,
            1,
            3,
            None,
        )

        self.ktest(
            "selectable below scroll 1",
            "down",
            [T("\n\n\n"), S(""), S("")],
            1,
            4,
            2,
            4,
            None,
        )

        self.ktest(
            "selectable below too far",
            "down",
            [T("\n\n\n"), S(""), T(""), S("")],
            1,
            4,
            1,
            3,
            None,
        )

        self.ktest(
            "selectable below skip 1 scroll 1",
            "down",
            [T("\n\n\n"), S(""), T(""), S("")],
            1,
            3,
            3,
            4,
            None,
        )

        self.ktest(
            "tall selectable below scroll 2",
            "down",
            [T("\n\n\n"), S(""), S("\n"), S("")],
            1,
            4,
            2,
            3,
            None,
        )

        self.ktest(
            "very tall selectable below scroll 5",
            "down",
            [T("\n\n\n\n"), S(""), S("\n\n\n\n"), S("")],
            1,
            4,
            2,
            0,
            None,
        )

        self.ktest(
            "very tall selected scroll within 1",
            "down",
            [S("\n\n\n\n\n"), S("")],
            0,
            0,
            0,
            -1,
            None,
        )

        self.ktest(
            "edit below pass cursor",
            "down",
            [E("", "de"), E("", "abc")],
            0,
            0,
            1,
            1,
            (2, 1),
        )

        key, lbox = self.ktest(
            "edit too far below pass cursor A",
            "down",
            [E("", "de"), T("\n\n\n\n"), E("", "abc")],
            0,
            0,
            1,
            0,
            None,
        )

        self.ktest(
            "edit too far below pass cursor B",
            "down",
            None,
            None,
            None,
            2,
            4,
            (2, 4),
            lbox,
        )

        odd_e = E("", "hi\nab")
        odd_e.set_edit_pos(2)
        # disble cursor movement in odd_e object
        odd_e.move_cursor_to_coords = lambda s, c, xy: 0
        self.ktest(
            "within focus cursor made not visible",
            "down",
            [odd_e, T("\n\n\n\n")],
            0,
            0,
            1,
            1,
            None,
        )

        self.ktest(
            "within focus cursor made not visible (2)",
            "down",
            [
                odd_e,
                T("\n\n\n\n"),
            ],
            0,
            0,
            1,
            1,
            None,
        )

        self.ktest(
            "force focus unselectable",
            "down",
            [S(""), T("\n\n\n\n")],
            0,
            0,
            1,
            0,
            None,
        )

        odd_e.set_edit_text("hi\n\n\n\n\n")
        self.ktest(
            "pathological cursor widget",
            "down",
            [odd_e, T("\n")],
            0,
            0,
            1,
            4,
            None,
        )

        self.ktest(
            "unselectable to unselectable",
            "down",
            [T(""), T(""), T(""), T(""), T(""), T(""), T("")],
            4,
            4,
            5,
            4,
            None,
        )

        self.ktest(
            "unselectable over edge to same",
            "down",
            [T(""), T(""), T(""), T(""), T("12\n34"), T("")],
            4,
            4,
            4,
            3,
            None,
        )

        key, lbox = self.ktest(
            "edit short between pass cursor A",
            "down",
            [E("", "abc"), E("", "a"), E("", "defg")],
            0,
            0,
            1,
            1,
            (1, 1),
        )

        self.ktest(
            "edit short between pass cursor B",
            "down",
            None,
            None,
            None,
            2,
            2,
            (3, 2),
            lbox,
        )

        e = E("", "\n\n\n\n\n")
        e.set_edit_pos(4)
        key, lbox = self.ktest(
            "edit cursor force scroll",
            "down",
            [e],
            0,
            0,
            0,
            -1,
            (0, 4),
        )
        self.assertEqual(1, lbox.inset_fraction[0])

    def test3_page_up(self):
        T, S, E = urwid.Text, SelectableText, urwid.Edit

        self.ktest(
            "unselectable aligned to aligned",
            "page up",
            [T(""), T("\n"), T("\n\n"), T(""), T("\n"), T("\n\n")],
            3,
            0,
            1,
            0,
            None,
        )

        self.ktest(
            "unselectable unaligned to aligned",
            "page up",
            [T(""), T("\n"), T("\n"), T("\n"), T("\n"), T("\n\n")],
            3,
            -1,
            1,
            0,
            None,
        )

        self.ktest(
            "selectable to unselectable",
            "page up",
            [T(""), T("\n"), T("\n"), T("\n"), S("\n"), T("\n\n")],
            4,
            1,
            1,
            -1,
            None,
        )

        self.ktest(
            "selectable to cut off selectable",
            "page up",
            [S("\n\n"), T("\n"), T("\n"), S("\n"), T("\n\n")],
            3,
            1,
            0,
            -1,
            None,
        )

        self.ktest(
            "seletable to selectable",
            "page up",
            [T("\n\n"), S("\n"), T("\n"), S("\n"), T("\n\n")],
            3,
            1,
            1,
            1,
            None,
        )

        self.ktest(
            "within very long selectable",
            "page up",
            [S(""), S("\n\n\n\n\n\n\n\n"), T("\n")],
            1,
            -6,
            1,
            -1,
            None,
        )

        e = E("", "\n\nab\n\n\n\n\ncd\n")
        e.set_edit_pos(11)
        self.ktest(
            "within very long cursor widget",
            "page up",
            [S(""), e, T("\n")],
            1,
            -6,
            1,
            -2,
            (2, 0),
        )

        self.ktest(
            "pathological cursor widget",
            "page up",
            [T(""), E("\n\n\n\n\n\n\n\n", "ab"), T("")],
            1,
            -5,
            0,
            0,
            None,
        )

        e = E("", "\nab\n\n\n\n\ncd\n")
        e.set_edit_pos(10)
        self.ktest(
            "very long cursor widget snap",
            "page up",
            [T(""), e, T("\n")],
            1,
            -5,
            1,
            0,
            (2, 1),
        )

        self.ktest(
            "slight scroll selectable",
            "page up",
            [T("\n"), S("\n"), T(""), S(""), T("\n\n\n"), S("")],
            5,
            4,
            3,
            0,
            None,
        )

        self.ktest(
            "scroll into snap region",
            "page up",
            [T("\n"), S("\n"), T(""), T(""), T("\n\n\n"), S("")],
            5,
            4,
            1,
            0,
            None,
        )

        self.ktest(
            "mid scroll short",
            "page up",
            [T("\n"), T(""), T(""), S(""), T(""), T("\n"), S(""), T("\n")],
            6,
            2,
            3,
            1,
            None,
        )

        self.ktest(
            "mid scroll long",
            "page up",
            [T("\n"), S(""), T(""), S(""), T(""), T("\n"), S(""), T("\n")],
            6,
            2,
            1,
            0,
            None,
        )

        self.ktest(
            "mid scroll perfect",
            "page up",
            [T("\n"), S(""), S(""), S(""), T(""), T("\n"), S(""), T("\n")],
            6,
            2,
            2,
            0,
            None,
        )

        self.ktest(
            "cursor move up fail short",
            "page up",
            [T("\n"), T("\n"), E("", "\nab"), T(""), T("")],
            2,
            1,
            2,
            4,
            (0, 4),
        )

        self.ktest(
            "cursor force fail short",
            "page up",
            [T("\n"), T("\n"), E("\n", "ab"), T(""), T("")],
            2,
            1,
            0,
            0,
            None,
        )

        odd_e = E("", "hi\nab")
        odd_e.set_edit_pos(2)
        # disble cursor movement in odd_e object
        odd_e.move_cursor_to_coords = lambda s, c, xy: 0
        self.ktest(
            "cursor force fail long",
            "page up",
            [odd_e, T("\n"), T("\n"), T("\n"), S(""), T("\n")],
            4,
            2,
            1,
            -1,
            None,
        )

        self.ktest(
            "prefer not cut off",
            "page up",
            [S("\n"), T("\n"), S(""), T("\n\n"), S(""), T("\n")],
            4,
            2,
            2,
            1,
            None,
        )

        self.ktest(
            "allow cut off",
            "page up",
            [S("\n"), T("\n"), T(""), T("\n\n"), S(""), T("\n")],
            4,
            2,
            0,
            -1,
            None,
        )

        self.ktest(
            "at top fail",
            "page up",
            [T("\n\n"), T("\n"), T("\n\n\n")],
            0,
            0,
            0,
            0,
            None,
        )

        self.ktest(
            "all visible fail",
            "page up",
            [T("a"), T("\n")],
            0,
            0,
            0,
            0,
            None,
        )

        self.ktest(
            "current ok fail",
            "page up",
            [T("\n\n"), S("hi")],
            1,
            3,
            1,
            3,
            None,
        )

        self.ktest(
            "all visible choose top selectable",
            "page up",
            [T(""), S("a"), S("b"), S("c")],
            3,
            3,
            1,
            1,
            None,
        )

        self.ktest(
            "bring in edge choose top",
            "page up",
            [S("b"), T("-"), S("-"), T("c"), S("d"), T("-")],
            4,
            3,
            0,
            0,
            None,
        )

        self.ktest(
            "bring in edge choose top selectable",
            "page up",
            [T("b"), S("-"), S("-"), T("c"), S("d"), T("-")],
            4,
            3,
            1,
            1,
            None,
        )

    def test4_page_down(self):
        T, S, E = urwid.Text, SelectableText, urwid.Edit

        self.ktest(
            "unselectable aligned to aligned",
            "page down",
            [T("\n\n"), T("\n"), T(""), T("\n\n"), T("\n"), T("")],
            2,
            4,
            4,
            3,
            None,
        )

        self.ktest(
            "unselectable unaligned to aligned",
            "page down",
            [T("\n\n"), T("\n"), T("\n"), T("\n"), T("\n"), T("")],
            2,
            4,
            4,
            3,
            None,
        )

        self.ktest(
            "selectable to unselectable",
            "page down",
            [T("\n\n"), S("\n"), T("\n"), T("\n"), T("\n"), T("")],
            1,
            2,
            4,
            4,
            None,
        )

        self.ktest(
            "selectable to cut off selectable",
            "page down",
            [T("\n\n"), S("\n"), T("\n"), T("\n"), S("\n\n")],
            1,
            2,
            4,
            3,
            None,
        )

        self.ktest(
            "seletable to selectable",
            "page down",
            [T("\n\n"), S("\n"), T("\n"), S("\n"), T("\n\n")],
            1,
            1,
            3,
            2,
            None,
        )

        self.ktest(
            "within very long selectable",
            "page down",
            [T("\n"), S("\n\n\n\n\n\n\n\n"), S("")],
            1,
            2,
            1,
            -3,
            None,
        )

        e = E("", "\nab\n\n\n\n\ncd\n\n")
        e.set_edit_pos(2)
        self.ktest(
            "within very long cursor widget",
            "page down",
            [T("\n"), e, S("")],
            1,
            2,
            1,
            -2,
            (1, 4),
        )

        odd_e = E("", "ab\n\n\n\n\n\n\n\n\n")
        odd_e.set_edit_pos(1)
        # disble cursor movement in odd_e object
        odd_e.move_cursor_to_coords = lambda s, c, xy: 0
        self.ktest(
            "pathological cursor widget",
            "page down",
            [T(""), odd_e, T("")],
            1,
            1,
            2,
            4,
            None,
        )

        e = E("", "\nab\n\n\n\n\ncd\n")
        e.set_edit_pos(2)
        self.ktest(
            "very long cursor widget snap",
            "page down",
            [T("\n"), e, T("")],
            1,
            2,
            1,
            -3,
            (1, 3),
        )

        self.ktest(
            "slight scroll selectable",
            "page down",
            [S(""), T("\n\n\n"), S(""), T(""), S("\n"), T("\n")],
            0,
            0,
            2,
            4,
            None,
        )

        self.ktest(
            "scroll into snap region",
            "page down",
            [S(""), T("\n\n\n"), T(""), T(""), S("\n"), T("\n")],
            0,
            0,
            4,
            3,
            None,
        )

        self.ktest(
            "mid scroll short",
            "page down",
            [T("\n"), S(""), T("\n"), T(""), S(""), T(""), T(""), T("\n")],
            1,
            2,
            4,
            3,
            None,
        )

        self.ktest(
            "mid scroll long",
            "page down",
            [T("\n"), S(""), T("\n"), T(""), S(""), T(""), S(""), T("\n")],
            1,
            2,
            6,
            4,
            None,
        )

        self.ktest(
            "mid scroll perfect",
            "page down",
            [T("\n"), S(""), T("\n"), T(""), S(""), S(""), S(""), T("\n")],
            1,
            2,
            5,
            4,
            None,
        )

        e = E("", "hi\nab")
        e.set_edit_pos(1)
        self.ktest(
            "cursor move up fail short",
            "page down",
            [T(""), T(""), e, T("\n"), T("\n")],
            2,
            1,
            2,
            -1,
            (1, 0),
        )

        odd_e = E("", "hi\nab")
        odd_e.set_edit_pos(1)
        # disble cursor movement in odd_e object
        odd_e.move_cursor_to_coords = lambda s, c, xy: 0
        self.ktest(
            "cursor force fail short",
            "page down",
            [T(""), T(""), odd_e, T("\n"), T("\n")],
            2,
            2,
            4,
            3,
            None,
        )

        self.ktest(
            "cursor force fail long",
            "page down",
            [T("\n"), S(""), T("\n"), T("\n"), T("\n"), E("hi\n", "ab")],
            1,
            2,
            4,
            4,
            None,
        )

        self.ktest(
            "prefer not cut off",
            "page down",
            [T("\n"), S(""), T("\n\n"), S(""), T("\n"), S("\n")],
            1,
            2,
            3,
            3,
            None,
        )

        self.ktest(
            "allow cut off",
            "page down",
            [T("\n"), S(""), T("\n\n"), T(""), T("\n"), S("\n")],
            1,
            2,
            5,
            4,
            None,
        )

        self.ktest(
            "at bottom fail",
            "page down",
            [T("\n\n"), T("\n"), T("\n\n\n")],
            2,
            1,
            2,
            1,
            None,
        )

        self.ktest(
            "all visible fail",
            "page down",
            [T("a"), T("\n")],
            1,
            1,
            1,
            1,
            None,
        )

        self.ktest(
            "current ok fail",
            "page down",
            [S("hi"), T("\n\n")],
            0,
            0,
            0,
            0,
            None,
        )

        self.ktest(
            "all visible choose last selectable",
            "page down",
            [S("a"), S("b"), S("c"), T("")],
            0,
            0,
            2,
            2,
            None,
        )

        self.ktest(
            "bring in edge choose last",
            "page down",
            [T("-"), S("d"), T("c"), S("-"), T("-"), S("b")],
            1,
            1,
            5,
            4,
            None,
        )

        self.ktest(
            "bring in edge choose last selectable",
            "page down",
            [T("-"), S("d"), T("c"), S("-"), S("-"), T("b")],
            1,
            1,
            4,
            3,
            None,
        )


class ZeroHeightContentsTest(unittest.TestCase):
    def test_listbox_pile(self):
        lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Pile([])]))
        size = (2, 5)
        canvas = lb.render(size, focus=True)
        self.assertEqual([b"  ", b"  ", b"  ", b"  ", b"  "], canvas.text)

    def test_listbox_text_pile_page_down(self):
        lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("above"), urwid.Pile([])]))
        size = (7, 5)
        lb.keypress(size, "page down")
        self.assertEqual(lb.focus_position, 0)
        lb.keypress(size, "page down")  # second one caused ListBox failure
        self.assertEqual(lb.focus_position, 0)
        canvas = lb.render(size, focus=True)
        self.assertEqual([b"above  ", b"       ", b"       ", b"       ", b"       "], canvas.text)

    def test_listbox_text_pile_page_up(self):
        lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Pile([]), urwid.Text("below")]))
        size = (7, 5)
        lb.set_focus(1)
        lb.keypress(size, "page up")
        self.assertEqual(lb.focus_position, 1)
        lb.keypress(size, "page up")  # second one caused pile failure
        self.assertEqual(lb.focus_position, 1)
        canvas = lb.render(size, focus=True)
        self.assertEqual([b"below  ", b"       ", b"       ", b"       ", b"       "], canvas.text)

    def test_listbox_text_pile_down(self):
        sp = urwid.Pile([])
        sp.selectable = lambda: True  # abuse our Pile
        lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("above"), sp]))
        size = (7, 5)
        lb.keypress(size, "down")
        self.assertEqual(lb.focus_position, 0)
        lb.keypress(size, "down")
        self.assertEqual(lb.focus_position, 0)
        canvas = lb.render(size, focus=True)
        self.assertEqual([b"above  ", b"       ", b"       ", b"       ", b"       "], canvas.text)

    def test_listbox_text_pile_up(self):
        sp = urwid.Pile([])
        sp.selectable = lambda: True  # abuse our Pile
        lb = urwid.ListBox(urwid.SimpleListWalker([sp, urwid.Text("below")]))
        size = (7, 5)
        lb.set_focus(1)
        lb.keypress(size, "up")
        self.assertEqual(lb.focus_position, 1)
        lb.keypress(size, "up")
        self.assertEqual(lb.focus_position, 1)
        canvas = lb.render(size, focus=True)
        self.assertEqual([b"below  ", b"       ", b"       ", b"       ", b"       "], canvas.text)


class PageDownAboveTopTest(unittest.TestCase):
    def test_page_down_does_not_raise(self):
        # Two-row rows so the four-item list does not fit the viewport at once.
        lb = urwid.ListBox(
            urwid.SimpleListWalker(
                [
                    urwid.Text("a0\nb0"),
                    SelectableText("a1\nb1"),
                    urwid.Text("a2\nb2"),
                    urwid.Text("a3\nb3"),
                ]
            )
        )
        size = (7, 4)
        lb.set_focus(0)
        lb.render(size, focus=True)
        lb.keypress(size, "page down")  # used to raise ListBoxError
        self.assertEqual(lb.focus_position, 3)
        canvas = lb.render(size, focus=True)
        self.assertEqual([b"a2     ", b"b2     ", b"a3     ", b"b3     "], canvas.text)
        # a second page down at the bottom is a no-op, not a crash
        lb.keypress(size, "page down")
        self.assertEqual(lb.focus_position, 3)


class ListBoxSetBodyTest(unittest.TestCase):
    def test_signal_connected(self):
        lb = urwid.ListBox([])
        lb.body = urwid.SimpleListWalker([])
        self.assertEqual(
            lb.body._urwid_signals["modified"][0][1],
            lb._invalidate,
            "outdated canvas cache reuse after ListWalker's contents modified",
        )


class TestListWalkerFromIterable(unittest.TestCase):
    def test_01_simple_list_walker(self):
        walker = urwid.SimpleListWalker(str(num) for num in range(5))
        self.assertEqual(5, len(walker))

    def test_02_simple_focus_list_walker(self):
        walker = urwid.SimpleFocusListWalker(str(num) for num in range(5))
        self.assertEqual(5, len(walker))
