from __future__ import annotations

import pytest
from rich.text import Text

from textual._wait import wait_for_idle
from textual.actions import SkipAction
from textual.app import App
from textual.coordinate import Coordinate
from textual.geometry import Offset
from textual.message import Message
from textual.widgets import DataTable
from textual.widgets.data_table import (
    CellDoesNotExist,
    CellKey,
    ColumnDoesNotExist,
    ColumnKey,
    DuplicateKey,
    Row,
    RowDoesNotExist,
    RowKey,
)

ROWS = [["0/0", "0/1"], ["1/0", "1/1"], ["2/0", "2/1"]]


class DataTableApp(App):
    messages_to_record = {
        "CellHighlighted",
        "CellSelected",
        "RowHighlighted",
        "RowSelected",
        "ColumnHighlighted",
        "ColumnSelected",
        "HeaderSelected",
        "RowLabelSelected",
    }

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

    def compose(self):
        table = DataTable()
        table.focus()
        yield table

    def record_data_table_event(self, message: Message) -> None:
        name = message.__class__.__name__
        if name in self.messages_to_record:
            self.messages.append(message)

    @property
    def message_names(self) -> list[str]:
        return [message.__class__.__name__ for message in self.messages]

    async def _on_message(self, message: Message) -> None:
        await super()._on_message(message)
        self.record_data_table_event(message)


async def test_datatable_message_emission():
    app = DataTableApp()
    expected_messages = []
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)

        assert app.message_names == expected_messages

        table.add_columns("Column0", "Column1")
        table.add_rows(ROWS)

        # A CellHighlighted is emitted because there were no rows (and
        # therefore no highlighted cells), but then a row was added, and
        # so the cell at (0, 0) became highlighted.
        expected_messages.append("CellHighlighted")
        await pilot.pause()
        assert app.message_names == expected_messages

        # Pressing Enter when the cursor is on a cell emits a CellSelected
        await pilot.press("enter")
        await pilot.pause()
        expected_messages.append("CellSelected")
        assert app.message_names == expected_messages

        # Moving the cursor left and up when the cursor is at origin
        # emits no events, since the cursor doesn't move at all.
        await pilot.press("left", "up")
        assert app.message_names == expected_messages

        # ROW CURSOR
        # Switch over to the row cursor... should emit a `RowHighlighted`
        table.cursor_type = "row"
        expected_messages.append("RowHighlighted")
        await pilot.pause()
        assert app.message_names == expected_messages

        # Select the row...
        await pilot.press("enter")
        await pilot.pause()
        expected_messages.append("RowSelected")
        assert app.message_names == expected_messages

        # COLUMN CURSOR
        # Switching to the column cursor emits a `ColumnHighlighted`
        table.cursor_type = "column"
        expected_messages.append("ColumnHighlighted")
        await pilot.pause()
        assert app.message_names == expected_messages

        # Select the column...
        await pilot.press("enter")
        expected_messages.append("ColumnSelected")
        await pilot.pause()
        assert app.message_names == expected_messages

        # NONE CURSOR
        # No messages get emitted at all...
        table.cursor_type = "none"
        await pilot.press("up", "down", "left", "right", "enter")
        await pilot.pause()
        # No new messages since cursor not visible
        assert app.message_names == expected_messages

        # Edge case - if show_cursor is False, and the cursor type
        # is changed back to a visible type, then no messages should
        # be emitted since the cursor is still not visible.
        table.show_cursor = False
        table.cursor_type = "cell"
        await pilot.press("up", "down", "left", "right", "enter")
        await pilot.pause()
        # No new messages since show_cursor = False
        assert app.message_names == expected_messages

        # Now when show_cursor is set back to True, the appropriate
        # message should be emitted for highlighting the cell.
        table.show_cursor = True
        expected_messages.append("CellHighlighted")
        await pilot.pause()
        assert app.message_names == expected_messages

        # Similarly for showing the cursor again when row or column
        # cursor was active before the cursor was hidden.
        table.show_cursor = False
        table.cursor_type = "row"
        table.show_cursor = True
        expected_messages.append("RowHighlighted")
        await pilot.pause()
        assert app.message_names == expected_messages

        table.show_cursor = False
        table.cursor_type = "column"
        table.show_cursor = True
        expected_messages.append("ColumnHighlighted")
        await pilot.pause()
        assert app.message_names == expected_messages

        # Likewise, if the cursor_type is "none", and we change the
        # show_cursor to True, then no events should be raised since
        # the cursor is still not visible to the user.
        table.cursor_type = "none"
        await pilot.press("up", "down", "left", "right", "enter")
        await pilot.pause()
        assert app.message_names == expected_messages


async def test_empty_table_interactions():
    app = DataTableApp()
    async with app.run_test() as pilot:
        await pilot.press("enter", "up", "down", "left", "right")
        assert app.message_names == []


async def test_add_rows():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("A", "B")
        row_keys = table.add_rows(ROWS)
        # We're given a key for each row
        assert len(row_keys) == len(ROWS)
        assert len(row_keys) == len(table._data)
        assert table.row_count == len(ROWS)
        # Each key can be used to fetch a row from the DataTable
        assert all(key in table._data for key in row_keys)


async def test_add_rows_user_defined_keys():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        key_a, key_b = table.add_columns("A", "B")
        algernon_key = table.add_row(*ROWS[0], key="algernon")
        table.add_row(*ROWS[1], key="charlie")
        auto_key = table.add_row(*ROWS[2])

        assert algernon_key == "algernon"
        # We get a RowKey object back, but we can use our own string *or* this object
        # to find the row we're looking for, they're considered equivalent for lookups.
        assert isinstance(algernon_key, RowKey)

        # Ensure the data in the table is mapped as expected
        first_row = {key_a: ROWS[0][0], key_b: ROWS[0][1]}
        assert table._data[algernon_key] == first_row
        assert table._data["algernon"] == first_row

        second_row = {key_a: ROWS[1][0], key_b: ROWS[1][1]}
        assert table._data["charlie"] == second_row

        third_row = {key_a: ROWS[2][0], key_b: ROWS[2][1]}
        assert table._data[auto_key] == third_row

        first_row = Row(algernon_key, height=1)
        assert table.rows[algernon_key] == first_row
        assert table.rows["algernon"] == first_row


async def test_add_row_duplicate_key():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("A")
        table.add_row("1", key="1")
        with pytest.raises(DuplicateKey):
            table.add_row("2", key="1")  # Duplicate row key


async def test_add_column_duplicate_key():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("A", key="A")
        with pytest.raises(DuplicateKey):
            table.add_column("B", key="A")  # Duplicate column key


async def test_add_column_with_width():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        column = table.add_column("ABC", width=10, key="ABC")
        row = table.add_row("123")
        assert table.get_cell(row, column) == "123"
        assert table.columns[column].width == 10
        assert table.columns[column].render_width == 12  # 10 + (2 padding)


async def test_add_columns():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        column_keys = table.add_columns("1", "2", "3")
        assert len(column_keys) == 3
        assert len(table.columns) == 3


async def test_add_columns_user_defined_keys():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        key = table.add_column("Column", key="donut")
        assert key == "donut"
        assert key == key


async def test_clear():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        assert table.cursor_coordinate == Coordinate(0, 0)
        assert table.hover_coordinate == Coordinate(0, 0)

        # Add some data and update cursor positions
        table.add_column("Column0")
        table.add_rows([["Row0"], ["Row1"], ["Row2"]])
        table.cursor_coordinate = Coordinate(1, 0)
        table.hover_coordinate = Coordinate(2, 0)

        # Ensure the cursor positions are reset to origin on clear()
        table.clear()
        assert table.cursor_coordinate == Coordinate(0, 0)
        assert table.hover_coordinate == Coordinate(0, 0)

        # Ensure that the table has been cleared
        assert table._data == {}
        assert table.rows == {}
        assert table.row_count == 0
        assert len(table._row_locations) == 0
        assert len(table._column_locations) == 1
        assert len(table.columns) == 1

        # Clearing the columns too
        table.clear(columns=True)
        assert len(table._column_locations) == 0
        assert len(table.columns) == 0


async def test_column_labels() -> None:
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("1", "2", "3")
        actual_labels = [col.label.plain for col in table.columns.values()]
        expected_labels = ["1", "2", "3"]
        assert actual_labels == expected_labels


async def test_initial_column_widths() -> None:
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        foo, bar = table.add_columns("foo", "bar")

        assert table.columns[foo].width == 3
        assert table.columns[bar].width == 3
        table.add_row("Hello", "World!")
        await wait_for_idle()
        assert table.columns[foo].content_width == 5
        assert table.columns[bar].content_width == 6

        table.add_row("Hello World!!!", "fo")
        await wait_for_idle()
        assert table.columns[foo].content_width == 14
        assert table.columns[bar].content_width == 6


async def test_get_cell_returns_value_at_cell():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("Column1", key="C1")
        table.add_row("TargetValue", key="R1")
        assert table.get_cell("R1", "C1") == "TargetValue"


async def test_get_cell_invalid_row_key():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("Column1", key="C1")
        table.add_row("TargetValue", key="R1")
        with pytest.raises(CellDoesNotExist):
            table.get_cell("INVALID_ROW", "C1")


async def test_get_cell_invalid_column_key():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("Column1", key="C1")
        table.add_row("TargetValue", key="R1")
        with pytest.raises(CellDoesNotExist):
            table.get_cell("R1", "INVALID_COLUMN")


async def test_get_cell_at_returns_value_at_cell():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("A", "B")
        table.add_rows(ROWS)
        assert table.get_cell_at(Coordinate(0, 0)) == "0/0"


async def test_get_cell_at_exception():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("A", "B")
        table.add_rows(ROWS)
        with pytest.raises(CellDoesNotExist):
            table.get_cell_at(Coordinate(9999, 0))


async def test_get_row():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        a, b, c = table.add_columns("A", "B", "C")
        first_row = table.add_row(2, 4, 1)
        second_row = table.add_row(3, 2, 1)
        assert table.get_row(first_row) == [2, 4, 1]
        assert table.get_row(second_row) == [3, 2, 1]

        # Even if row positions change, keys should always refer to same rows.
        table.sort(b)
        assert table.get_row(first_row) == [2, 4, 1]
        assert table.get_row(second_row) == [3, 2, 1]


async def test_get_row_invalid_row_key():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        with pytest.raises(RowDoesNotExist):
            table.get_row("INVALID")


async def test_get_row_at():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        a, b, c = table.add_columns("A", "B", "C")
        table.add_row(2, 4, 1)
        table.add_row(3, 2, 1)
        assert table.get_row_at(0) == [2, 4, 1]
        assert table.get_row_at(1) == [3, 2, 1]

        # If we sort, then the rows present at the indices *do* change!
        table.sort(b)

        # Since we sorted on column "B", the rows at indices 0 and 1 are swapped.
        assert table.get_row_at(0) == [3, 2, 1]
        assert table.get_row_at(1) == [2, 4, 1]


@pytest.mark.parametrize("index", (-1, 2))
async def test_get_row_at_invalid_index(index):
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("A", "B", "C")
        table.add_row(2, 4, 1)
        table.add_row(3, 2, 1)
        with pytest.raises(RowDoesNotExist):
            table.get_row_at(index)


async def test_get_column():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        a, b = table.add_columns("A", "B")
        table.add_rows(ROWS)
        cells = table.get_column(a)
        assert next(cells) == ROWS[0][0]
        assert next(cells) == ROWS[1][0]
        assert next(cells) == ROWS[2][0]
        with pytest.raises(StopIteration):
            next(cells)


async def test_get_column_invalid_key():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        with pytest.raises(ColumnDoesNotExist):
            list(table.get_column("INVALID"))


async def test_get_column_at():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("A", "B")
        table.add_rows(ROWS)

        first_column = list(table.get_column_at(0))
        assert first_column == [ROWS[0][0], ROWS[1][0], ROWS[2][0]]

        second_column = list(table.get_column_at(1))
        assert second_column == [ROWS[0][1], ROWS[1][1], ROWS[2][1]]


@pytest.mark.parametrize("index", [-1, 5])
async def test_get_column_at_invalid_index(index):
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        with pytest.raises(ColumnDoesNotExist):
            list(table.get_column_at(index))


async def test_update_cell_cell_exists():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("A", key="A")
        table.add_row("1", key="1")
        table.update_cell("1", "A", "NEW_VALUE")
        assert table.get_cell("1", "A") == "NEW_VALUE"


async def test_update_cell_cell_doesnt_exist():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("A", key="A")
        table.add_row("1", key="1")
        with pytest.raises(CellDoesNotExist):
            table.update_cell("INVALID", "CELL", "Value")


async def test_update_cell_at_coordinate_exists():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        column_0, column_1 = table.add_columns("A", "B")
        row_0, *_ = table.add_rows(ROWS)

        table.update_cell_at(Coordinate(0, 1), "newvalue")
        assert table.get_cell(row_0, column_1) == "newvalue"


async def test_update_cell_at_coordinate_doesnt_exist():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("A", "B")
        table.add_rows(ROWS)
        with pytest.raises(CellDoesNotExist):
            table.update_cell_at(Coordinate(999, 999), "newvalue")


@pytest.mark.parametrize(
    "label,new_value,new_content_width",
    [
        # Shorter than initial cell value, larger than label => width remains same
        ("A", "BB", 3),
        # Larger than cell value, shorter than label => width remains that of label
        ("1234567", "1234", 7),
        # Shorter than cell value, shorter than label => width remains same
        ("12345", "123", 5),
        # Larger than cell value, larger than label => width updates to new cell value
        ("12345", "123456789", 9),
    ],
)
async def test_update_cell_at_column_width(label, new_value, new_content_width):
    # Initial cell values are length 3. Let's update cell content and ensure
    # that the width of the column is correct given the new cell content widths
    # and the label of the column the cell is in.
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        key, _ = table.add_columns(label, "Column2")
        table.add_rows(ROWS)
        first_column = table.columns.get(key)

        table.update_cell_at(Coordinate(0, 0), new_value, update_width=True)
        await wait_for_idle()
        assert first_column.content_width == new_content_width
        assert first_column.render_width == new_content_width + 2


async def test_coordinate_to_cell_key():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        column_key, _ = table.add_columns("Column0", "Column1")
        row_key = table.add_row("A", "B")

        cell_key = table.coordinate_to_cell_key(Coordinate(0, 0))
        assert cell_key == CellKey(row_key, column_key)


async def test_coordinate_to_cell_key_invalid_coordinate():
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        with pytest.raises(CellDoesNotExist):
            table.coordinate_to_cell_key(Coordinate(9999, 9999))


async def test_datatable_click_cell_cursor():
    """When the cell cursor is used, and we click, we emit a CellHighlighted
    *and* a CellSelected message for the cell that was clicked.
    Regression test for https://github.com/Textualize/textual/issues/1723"""
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        column_key = table.add_column("ABC")
        table.add_row("123")
        row_key = table.add_row("456")
        await pilot.click(offset=Offset(1, 2))
        # There's two CellHighlighted events since a cell is highlighted on initial load,
        # then when we click, another cell is highlighted (and selected).
        assert app.message_names == [
            "CellHighlighted",
            "CellHighlighted",
            "CellSelected",
        ]
        cell_highlighted_event: DataTable.CellHighlighted = app.messages[1]
        assert cell_highlighted_event.value == "456"
        assert cell_highlighted_event.cell_key == CellKey(row_key, column_key)
        assert cell_highlighted_event.coordinate == Coordinate(1, 0)

        cell_selected_event: DataTable.CellSelected = app.messages[2]
        assert cell_selected_event.value == "456"
        assert cell_selected_event.cell_key == CellKey(row_key, column_key)
        assert cell_selected_event.coordinate == Coordinate(1, 0)


async def test_click_row_cursor():
    """When the row cursor is used, and we click, we emit a RowHighlighted
    *and* a RowSelected message for the row that was clicked."""
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        table.cursor_type = "row"
        table.add_column("ABC")
        table.add_row("123")
        row_key = table.add_row("456")
        await pilot.click(offset=Offset(1, 2))
        assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"]

        row_highlighted: DataTable.RowHighlighted = app.messages[1]

        assert row_highlighted.row_key == row_key
        assert row_highlighted.cursor_row == 1

        row_selected: DataTable.RowSelected = app.messages[2]
        assert row_selected.row_key == row_key
        assert row_highlighted.cursor_row == 1


async def test_click_column_cursor():
    """When the column cursor is used, and we click, we emit a ColumnHighlighted
    *and* a ColumnSelected message for the column that was clicked."""
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        table.cursor_type = "column"
        column_key = table.add_column("ABC")
        table.add_row("123")
        table.add_row("456")
        await pilot.click(offset=Offset(1, 2))
        assert app.message_names == [
            "ColumnHighlighted",
            "ColumnHighlighted",
            "ColumnSelected",
        ]
        column_highlighted: DataTable.ColumnHighlighted = app.messages[1]
        assert column_highlighted.column_key == column_key
        assert column_highlighted.cursor_column == 0

        column_selected: DataTable.ColumnSelected = app.messages[2]
        assert column_selected.column_key == column_key
        assert column_highlighted.cursor_column == 0


async def test_hover_coordinate():
    """Ensure that the hover_coordinate reactive is updated as expected."""
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        table.add_column("ABC")
        table.add_row("123")
        table.add_row("456")
        assert table.hover_coordinate == Coordinate(0, 0)
        await pilot.hover(DataTable, offset=Offset(2, 2))
        assert table.hover_coordinate == Coordinate(1, 0)


async def test_header_selected():
    """Ensure that a HeaderSelected event gets posted when we click
    on the header in the DataTable."""
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        column_key = table.add_column("number")
        table.add_row(3)

        click_location = Offset(3, 0)  # Click the header
        await pilot.click(DataTable, offset=click_location)
        message: DataTable.HeaderSelected = app.messages[-1]
        assert message.label == Text("number")
        assert message.column_index == 0
        assert message.column_key == column_key

        # Now hide the header and click in the exact same place - no additional message emitted.
        table.show_header = False
        await pilot.click(DataTable, offset=click_location)
        await pilot.pause()
        assert app.message_names.count("HeaderSelected") == 1


async def test_row_label_selected():
    """Ensure that the DataTable sends a RowLabelSelected event when
    the user clicks on a row label."""
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        table.add_column("number")
        row_key = table.add_row(3, label="A")
        await pilot.pause()
        await pilot.click(DataTable, offset=Offset(1, 1))
        message: DataTable.RowLabelSelected = app.messages[-1]
        assert message.label == Text("A")
        assert message.row_index == 0
        assert message.row_key == row_key

        # Now hide the row label and click in the same place - no additional message emitted.
        table.show_row_labels = False
        await pilot.click(DataTable, offset=Offset(1, 1))
        assert app.message_names.count("RowLabelSelected") == 1


async def test_sort_coordinate_and_key_access():
    """Ensure that, after sorting, that coordinates and cell keys
    can still be used to retrieve the correct cell."""
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        column = table.add_column("number")
        row_three = table.add_row(3)
        row_one = table.add_row(1)
        row_two = table.add_row(2)

        # Items inserted in correct initial positions (before sort)
        assert table.get_cell_at(Coordinate(0, 0)) == 3
        assert table.get_cell_at(Coordinate(1, 0)) == 1
        assert table.get_cell_at(Coordinate(2, 0)) == 2

        table.sort(column)

        # The keys still refer to the same cells...
        assert table.get_cell(row_one, column) == 1
        assert table.get_cell(row_two, column) == 2
        assert table.get_cell(row_three, column) == 3

        # ...even though the values under the coordinates have changed...
        assert table.get_cell_at(Coordinate(0, 0)) == 1
        assert table.get_cell_at(Coordinate(1, 0)) == 2
        assert table.get_cell_at(Coordinate(2, 0)) == 3

        assert table.ordered_rows[0].key == row_one
        assert table.ordered_rows[1].key == row_two
        assert table.ordered_rows[2].key == row_three


async def test_sort_reverse_coordinate_and_key_access():
    """Ensure that, after sorting, that coordinates and cell keys
    can still be used to retrieve the correct cell."""
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        column = table.add_column("number")
        row_three = table.add_row(3)
        row_one = table.add_row(1)
        row_two = table.add_row(2)

        # Items inserted in correct initial positions (before sort)
        assert table.get_cell_at(Coordinate(0, 0)) == 3
        assert table.get_cell_at(Coordinate(1, 0)) == 1
        assert table.get_cell_at(Coordinate(2, 0)) == 2

        table.sort(column, reverse=True)

        # The keys still refer to the same cells...
        assert table.get_cell(row_one, column) == 1
        assert table.get_cell(row_two, column) == 2
        assert table.get_cell(row_three, column) == 3

        # ...even though the values under the coordinates have changed...
        assert table.get_cell_at(Coordinate(0, 0)) == 3
        assert table.get_cell_at(Coordinate(1, 0)) == 2
        assert table.get_cell_at(Coordinate(2, 0)) == 1

        assert table.ordered_rows[0].key == row_three
        assert table.ordered_rows[1].key == row_two
        assert table.ordered_rows[2].key == row_one


async def test_cell_cursor_highlight_events():
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        column_one_key, column_two_key = table.add_columns("A", "B")
        _ = table.add_row(0, 1)
        row_two_key = table.add_row(2, 3)

        # Since initial position is (0, 0), cursor doesn't move so no event posted
        table.action_cursor_up()
        table.action_cursor_left()

        await pilot.pause()
        assert table.app.message_names == [
            "CellHighlighted"
        ]  # Initial highlight on load

        # Move the cursor one cell down, and check the highlighted event posted
        table.action_cursor_down()
        await pilot.pause()
        assert len(table.app.messages) == 2
        latest_message: DataTable.CellHighlighted = table.app.messages[-1]
        assert isinstance(latest_message, DataTable.CellHighlighted)
        assert latest_message.value == 2
        assert latest_message.coordinate == Coordinate(1, 0)
        assert latest_message.cell_key == CellKey(row_two_key, column_one_key)

        # Now move the cursor to the right, and check highlighted event posted
        table.action_cursor_right()
        await pilot.pause()
        assert len(table.app.messages) == 3
        latest_message = table.app.messages[-1]
        assert latest_message.coordinate == Coordinate(1, 1)
        assert latest_message.cell_key == CellKey(row_two_key, column_two_key)


async def test_row_cursor_highlight_events():
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        table.cursor_type = "row"
        table.add_columns("A", "B")
        row_one_key = table.add_row(0, 1)
        row_two_key = table.add_row(2, 3)

        # Since initial position is row_index=0, the following actions do nothing.
        with pytest.raises(SkipAction):
            table.action_cursor_up()
            table.action_cursor_left()
            table.action_cursor_right()

        await pilot.pause()
        assert table.app.message_names == ["RowHighlighted"]  # Initial highlight

        # Move the row cursor from row 0 to row 1, check the highlighted event posted
        table.action_cursor_down()
        await pilot.pause()
        assert len(table.app.messages) == 2
        latest_message: DataTable.RowHighlighted = table.app.messages[-1]
        assert isinstance(latest_message, DataTable.RowHighlighted)
        assert latest_message.row_key == row_two_key
        assert latest_message.cursor_row == 1

        # Move the row cursor back up to row 0, check the highlighted event posted
        table.action_cursor_up()
        await pilot.pause()
        assert len(table.app.messages) == 3
        latest_message = table.app.messages[-1]
        assert latest_message.row_key == row_one_key
        assert latest_message.cursor_row == 0


async def test_column_cursor_highlight_events():
    app = DataTableApp()
    async with app.run_test() as pilot:
        table = app.query_one(DataTable)
        table.cursor_type = "column"
        column_one_key, column_two_key = table.add_columns("A", "B")
        table.add_row(0, 1)
        table.add_row(2, 3)

        # Since initial position is column_index=0, the following actions do nothing.
        with pytest.raises(SkipAction):
            table.action_cursor_left()
            table.action_cursor_up()
            table.action_cursor_down()

        await pilot.pause()
        assert table.app.message_names == ["ColumnHighlighted"]  # Initial highlight

        # Move the column cursor from column 0 to column 1,
        # check the highlighted event posted
        table.action_cursor_right()
        await pilot.pause()
        assert len(table.app.messages) == 2
        latest_message: DataTable.ColumnHighlighted = table.app.messages[-1]
        assert isinstance(latest_message, DataTable.ColumnHighlighted)
        assert latest_message.column_key == column_two_key
        assert latest_message.cursor_column == 1

        # Move the column cursor left, back to column 0,
        # check the highlighted event posted again.
        table.action_cursor_left()
        await pilot.pause()
        assert len(table.app.messages) == 3
        latest_message = table.app.messages[-1]
        assert latest_message.column_key == column_one_key
        assert latest_message.cursor_column == 0


async def test_reuse_row_key_after_clear():
    """Regression test for https://github.com/Textualize/textual/issues/1806"""
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_columns("A", "B")
        table.add_row(0, 1, key="ROW1")
        table.add_row(2, 3, key="ROW2")
        table.clear()
        table.add_row(4, 5, key="ROW1")  # Reusing the same keys as above
        table.add_row(7, 8, key="ROW2")
        assert table.get_row("ROW1") == [4, 5]
        assert table.get_row("ROW2") == [7, 8]


async def test_reuse_column_key_after_clear():
    """Regression test for https://github.com/Textualize/textual/issues/1806"""
    app = DataTableApp()
    async with app.run_test():
        table = app.query_one(DataTable)
        table.add_column("A", key="COLUMN1")
        table.add_column("B", key="COLUMN2")
        table.clear(columns=True)
        table.add_column("C", key="COLUMN1")  # Reusing the same keys as above
        table.add_column("D", key="COLUMN2")
        table.add_row(1, 2)
        assert list(table.get_column("COLUMN1")) == [1]
        assert list(table.get_column("COLUMN2")) == [2]


def test_key_equals_equivalent_string():
    text = "Hello"
    key = RowKey(text)
    assert key == text
    assert hash(key) == hash(text)


def test_key_doesnt_match_non_equal_string():
    key = ColumnKey("123")
    text = "laksjdlaskjd"
    assert key != text
    assert hash(key) != hash(text)


def test_key_equals_self():
    row_key = RowKey()
    column_key = ColumnKey()
    assert row_key == row_key
    assert column_key == column_key
    assert row_key != column_key


def test_key_string_lookup():
    # Indirectly covered by other tests, but let's explicitly document
    # in tests how we intend for the keys to work for cache lookups.
    dictionary = {
        "foo": "bar",
        RowKey("hello"): "world",
    }
    assert dictionary["foo"] == "bar"
    assert dictionary[RowKey("foo")] == "bar"
    assert dictionary["hello"] == "world"
    assert dictionary[RowKey("hello")] == "world"
