from __future__ import annotations

from typing import List

import pytest
from textual.app import App
from textual.events import MouseDown, MouseUp, Paste
from textual.pilot import _get_mouse_message_arguments
from textual.widget import Widget
from textual.widgets.text_area import Selection

from textual_textarea import TextEditor


@pytest.mark.parametrize(
    "keys,text,selection,expected_text,expected_selection",
    [
        (
            ["ctrl+a"],
            "select\n foo",
            Selection(start=(1, 2), end=(1, 2)),
            "select\n foo",
            Selection(start=(0, 0), end=(1, 4)),
        ),
        (
            ["ctrl+shift+right"],
            "select\n foo",
            Selection(start=(0, 0), end=(0, 0)),
            "select\n foo",
            Selection(start=(0, 0), end=(0, 6)),
        ),
        (
            ["right"],
            "select\n foo",
            Selection(start=(0, 0), end=(0, 6)),
            "select\n foo",
            Selection(start=(0, 6), end=(0, 6)),
        ),
        (
            ["a"],
            "select\n foo",
            Selection(start=(1, 4), end=(1, 4)),
            "select\n fooa",
            Selection(start=(1, 5), end=(1, 5)),
        ),
        (
            ["a"],
            "select\n foo",
            Selection(start=(1, 0), end=(1, 4)),
            "select\na",
            Selection(start=(1, 1), end=(1, 1)),
        ),
        (
            ["enter"],
            "a\na",
            Selection(start=(1, 0), end=(1, 0)),
            "a\n\na",
            Selection(start=(2, 0), end=(2, 0)),
        ),
        (
            ["enter"],
            "a\na",
            Selection(start=(1, 1), end=(1, 1)),
            "a\na\n",
            Selection(start=(2, 0), end=(2, 0)),
        ),
        (
            ["enter", "b"],
            "a()",
            Selection(start=(0, 2), end=(0, 2)),
            "a(\n    b\n)",
            Selection(start=(1, 5), end=(1, 5)),
        ),
        (
            ["enter", "b"],
            " a()",
            Selection(start=(0, 3), end=(0, 3)),
            " a(\n    b\n )",
            Selection(start=(1, 5), end=(1, 5)),
        ),
        (
            ["delete"],
            "0\n1\n2\n3",
            Selection(start=(2, 1), end=(2, 1)),
            "0\n1\n23",
            Selection(start=(2, 1), end=(2, 1)),
        ),
        (
            ["shift+delete"],
            "0\n1\n2\n3",
            Selection(start=(2, 1), end=(2, 1)),
            "0\n1\n3",
            Selection(start=(2, 0), end=(2, 0)),
        ),
        (
            ["shift+delete"],
            "0\n1\n2\n3",
            Selection(start=(2, 0), end=(2, 1)),
            "0\n1\n\n3",
            Selection(start=(2, 0), end=(2, 0)),
        ),
        (
            ["shift+delete"],
            "0\n1\n2\n3",
            Selection(start=(3, 1), end=(3, 1)),
            "0\n1\n2",
            Selection(start=(2, 1), end=(2, 1)),
        ),
        (
            ["shift+delete"],
            "foo",
            Selection(start=(3, 1), end=(3, 1)),
            "",
            Selection(start=(0, 0), end=(0, 0)),
        ),
        (
            ["ctrl+home"],
            "foo\nbar",
            Selection(start=(1, 2), end=(1, 2)),
            "foo\nbar",
            Selection(start=(0, 0), end=(0, 0)),
        ),
        (
            ["ctrl+end"],
            "foo\nbar",
            Selection(start=(0, 1), end=(0, 1)),
            "foo\nbar",
            Selection(start=(1, 3), end=(1, 3)),
        ),
        (
            ["("],
            "foo",
            Selection(start=(0, 3), end=(0, 3)),
            "foo()",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["("],
            "foo",
            Selection(start=(0, 2), end=(0, 2)),
            "fo(o",
            Selection(start=(0, 3), end=(0, 3)),
        ),
        (
            ["("],
            "foo.",
            Selection(start=(0, 3), end=(0, 3)),
            "foo().",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["("],
            "foo-",
            Selection(start=(0, 3), end=(0, 3)),
            "foo(-",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["'"],
            "foo",
            Selection(start=(0, 3), end=(0, 3)),
            "foo'",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["'"],
            "ba  r",
            Selection(start=(0, 3), end=(0, 3)),
            "ba '' r",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["'"],
            "foo-",
            Selection(start=(0, 3), end=(0, 3)),
            "foo'-",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["'"],
            "fo--",
            Selection(start=(0, 3), end=(0, 3)),
            "fo-'-",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["'"],
            "fo-.",
            Selection(start=(0, 3), end=(0, 3)),
            "fo-''.",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["'"],
            "fo()",
            Selection(start=(0, 3), end=(0, 3)),
            "fo('')",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["tab"],
            "bar",
            Selection(start=(0, 1), end=(0, 1)),
            "b   ar",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["tab"],
            "bar",
            Selection(start=(0, 0), end=(0, 0)),
            "    bar",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["shift+tab"],
            "bar",
            Selection(start=(0, 0), end=(0, 0)),
            "bar",
            Selection(start=(0, 0), end=(0, 0)),
        ),
        (
            ["shift+tab"],
            "    bar",
            Selection(start=(0, 7), end=(0, 7)),
            "bar",
            Selection(start=(0, 3), end=(0, 3)),
        ),
        (
            ["tab"],
            "bar\n baz",
            Selection(start=(0, 2), end=(1, 1)),
            "    bar\n    baz",
            Selection(start=(0, 6), end=(1, 4)),
        ),
        (
            ["tab"],
            "bar\n baz",
            Selection(start=(0, 0), end=(1, 1)),
            "    bar\n    baz",
            Selection(start=(0, 0), end=(1, 4)),
        ),
        (
            ["shift+tab"],
            "    bar\n    baz",
            Selection(start=(0, 0), end=(1, 1)),
            "bar\nbaz",
            Selection(start=(0, 0), end=(1, 0)),
        ),
        # word deletion, using Textual's own delete_word_left/right actions
        (
            ["ctrl+backspace"],
            "foo bar",
            Selection(start=(0, 7), end=(0, 7)),
            "foo ",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["alt+backspace"],
            "foo bar",
            Selection(start=(0, 7), end=(0, 7)),
            "foo ",
            Selection(start=(0, 4), end=(0, 4)),
        ),
        (
            ["alt+delete"],
            "foo bar",
            Selection(start=(0, 0), end=(0, 0)),
            " bar",
            Selection(start=(0, 0), end=(0, 0)),
        ),
    ],
)
@pytest.mark.asyncio
async def test_keys(
    app: App,
    keys: List[str],
    text: str,
    selection: Selection,
    expected_text: str,
    expected_selection: Selection,
) -> None:
    async with app.run_test() as pilot:
        ta = app.query_one("#ta", expect_type=TextEditor)
        ta.text = text
        ta.selection = selection

        for key in keys:
            await pilot.press(key)

        assert ta.text == expected_text
        assert ta.selection == expected_selection


@pytest.mark.parametrize(
    "starting_selection,expected_clipboard,expected_paste_loc",
    [
        (Selection((0, 5), (1, 5)), "56789\n01234", (1, 5)),
        (Selection((0, 0), (1, 0)), "0123456789\n", (1, 0)),
    ],
)
@pytest.mark.asyncio
async def test_copy_paste(
    app_all_clipboards: App,
    starting_selection: Selection,
    expected_clipboard: str,
    expected_paste_loc: tuple[int, int],
) -> None:
    original_text = "0123456789\n0123456789\n0123456789"

    def eq(a: str, b: str) -> bool:
        return a.replace("\r\n", "\n") == b.replace("\r\n", "\n")

    async with app_all_clipboards.run_test() as pilot:
        ta = app_all_clipboards.query_one("#ta", expect_type=TextEditor)
        while ta.text_input is None:
            await pilot.pause(0.1)
        ti = ta.text_input
        assert ti is not None
        ta.text = original_text
        ta.selection = starting_selection

        await pilot.press("ctrl+c")
        await pilot.pause()
        assert eq(ti.clipboard, expected_clipboard)
        assert ta.selection == starting_selection
        assert ta.text == original_text

        await pilot.press("ctrl+u")
        await pilot.pause()
        assert eq(ti.clipboard, expected_clipboard)
        assert ta.selection == Selection(starting_selection.end, starting_selection.end)
        assert ta.text == original_text

        await pilot.press("ctrl+a")
        assert ta.selection == Selection(
            (0, 0),
            (len(original_text.splitlines()) - 1, len(original_text.splitlines()[-1])),
        )
        assert eq(ti.clipboard, expected_clipboard)
        assert ta.text == original_text

        await pilot.press("ctrl+u")
        await pilot.pause()
        assert ta.selection == Selection(expected_paste_loc, expected_paste_loc)
        assert eq(ti.clipboard, expected_clipboard)
        assert ta.text == expected_clipboard

        await pilot.press("ctrl+a")
        await pilot.press("ctrl+x")
        await pilot.pause()
        assert ta.selection == Selection((0, 0), (0, 0))
        assert eq(ti.clipboard, expected_clipboard)
        assert ta.text == ""

        await pilot.press("ctrl+v")
        await pilot.pause()
        assert eq(ti.clipboard, expected_clipboard)
        assert ta.text == expected_clipboard
        assert ta.selection == Selection(expected_paste_loc, expected_paste_loc)


@pytest.mark.asyncio
async def test_undo_redo(app: App) -> None:
    async with app.run_test() as pilot:
        ta = app.query_one("#ta", expect_type=TextEditor)
        ti = ta.text_input
        assert ti
        assert ti.has_focus

        for char in "foo":
            await pilot.press(char)
        await pilot.pause(0.6)

        await pilot.press("enter")
        for char in "bar":
            await pilot.press(char)
        await pilot.pause(0.6)

        await pilot.press("ctrl+z")
        assert ta.text == "foo\n"
        assert ta.selection == Selection((1, 0), (1, 0))

        await pilot.press("ctrl+z")
        assert ta.text == "foo"
        assert ta.selection == Selection((0, 3), (0, 3))

        await pilot.press("ctrl+z")
        assert ta.text == ""
        assert ta.selection == Selection((0, 0), (0, 0))

        await pilot.press("ctrl+y")
        assert ta.text == "foo"
        assert ta.selection == Selection((0, 3), (0, 3))

        await pilot.press("z")
        assert ta.text == "fooz"


@pytest.mark.parametrize(
    "start_text,insert_text,selection,expected_text",
    [
        (
            "select ",
            '"main"."drivers"."driverId"',
            Selection((0, 7), (0, 7)),
            'select "main"."drivers"."driverId"',
        ),
        (
            "select , foo",
            '"main"."drivers"."driverId"',
            Selection((0, 7), (0, 7)),
            'select "main"."drivers"."driverId", foo',
        ),
        (
            "aaa\naaa\naaa\naaa",
            "bb",
            Selection((2, 2), (2, 2)),
            "aaa\naaa\naabba\naaa",
        ),
        (
            "aaa\naaa\naaa\naaa",
            "bb",
            Selection((2, 2), (1, 1)),
            "aaa\nabba\naaa",
        ),
        (
            "01234",
            "\nabc\n",
            Selection((0, 2), (0, 2)),
            "01\nabc\n234",
        ),
    ],
)
@pytest.mark.asyncio
async def test_insert_text(
    app: App,
    start_text: str,
    insert_text: str,
    selection: Selection,
    expected_text: str,
) -> None:
    async with app.run_test() as pilot:
        ta = app.query_one("#ta", expect_type=TextEditor)
        ta.text = start_text
        ta.selection = selection
        await pilot.pause()

        ta.insert_text_at_selection(insert_text)
        await pilot.pause()

        assert ta.text == expected_text


@pytest.mark.asyncio
async def test_toggle_comment(app: App) -> None:
    async with app.run_test() as pilot:
        ta = app.query_one("#ta", expect_type=TextEditor)
        ta.text = "one\ntwo\n\nthree"
        ta.selection = Selection((0, 0), (0, 0))
        await pilot.pause()

        await pilot.press("ctrl+underscore")
        assert ta.text == "# one\ntwo\n\nthree"

        await pilot.press("down")
        await pilot.press("ctrl+underscore")
        assert ta.text == "# one\n# two\n\nthree"

        await pilot.press("ctrl+a")
        await pilot.press("ctrl+underscore")
        assert ta.text == "# # one\n# # two\n\n# three"

        await pilot.press("ctrl+underscore")
        assert ta.text == "# one\n# two\n\nthree"

        await pilot.press("up")
        await pilot.press("up")
        await pilot.press("ctrl+underscore")
        assert ta.text == "# one\ntwo\n\nthree"

        await pilot.press("shift+down")
        await pilot.press("shift+down")
        await pilot.press("ctrl+underscore")
        assert ta.text == "# one\n# two\n\n# three"

        await pilot.press("ctrl+a")
        await pilot.press("ctrl+underscore")
        assert ta.text == "one\ntwo\n\nthree"


async def _click(app: App, widget: Widget, offset: tuple[int, int]) -> None:
    """
    Post the mouse events for a single click, and let Textual count the click
    chain itself. `pilot.click(times=n)` sets `chain` on the Click event directly,
    so it can't show how consecutive clicks at different locations interact.
    """
    kwargs = _get_mouse_message_arguments(widget, offset, button=1)
    app.post_message(MouseDown(**kwargs))
    app.post_message(MouseUp(**kwargs))


@pytest.mark.asyncio
async def test_click_chain_selection(app: App) -> None:
    """
    A single click moves the cursor; repeated clicks at the same location expand
    the selection to the word, then the line, then the whole document.
    """
    async with app.run_test() as pilot:
        ta = app.query_one("#ta", expect_type=TextEditor)
        ta.text = "select foo\nfrom bar"
        ta.selection = Selection((0, 0), (0, 0))
        await pilot.pause()
        text_input = ta.text_input
        assert text_input is not None

        # the text starts after a four-character gutter; click inside "foo"
        offset = (12, 0)

        await _click(app, text_input, offset)
        await pilot.pause()
        assert ta.selection == Selection((0, 8), (0, 8))

        await _click(app, text_input, offset)
        await pilot.pause()
        assert ta.selection == Selection((0, 7), (0, 10))

        await _click(app, text_input, offset)
        await pilot.pause()
        assert ta.selection == Selection((0, 0), (1, 0))

        await _click(app, text_input, offset)
        await pilot.pause()
        assert ta.selection == Selection((0, 0), (1, 8))


@pytest.mark.asyncio
async def test_click_chain_resets_at_a_new_location(app: App) -> None:
    """
    Double-clicking a word, then double-clicking a different word, selects the
    second word: the second pair of clicks starts a new chain instead of
    continuing the first one.
    """
    async with app.run_test() as pilot:
        ta = app.query_one("#ta", expect_type=TextEditor)
        ta.text = "select foo\nfrom barbarbar"
        ta.selection = Selection((0, 0), (0, 0))
        await pilot.pause()
        text_input = ta.text_input
        assert text_input is not None

        # double click "foo"
        for _ in range(2):
            await _click(app, text_input, (12, 0))
        await pilot.pause()
        assert ta.selection == Selection((0, 7), (0, 10))

        # double click "barbarbar" on the next line
        for _ in range(2):
            await _click(app, text_input, (12, 1))
        await pilot.pause()
        assert ta.selection == Selection((1, 5), (1, 14))


@pytest.mark.asyncio
async def test_paste_scrolls_cursor_into_view(app: App) -> None:
    """
    Pasting a line wider than the viewport leaves the cursor on screen.

    TextArea.replace() scrolled the cursor into view on Textual 6.4, but stopped
    doing so in 7.x, so on_paste has to ask for the scroll itself.
    """
    async with app.run_test(size=(80, 24)) as pilot:
        ta = app.query_one("#ta", expect_type=TextEditor)
        text_input = ta.text_input
        assert text_input is not None
        ta.text = ""
        await pilot.pause()

        text_input.post_message(Paste("x" * 200))
        await pilot.pause()

        assert ta.selection == Selection((0, 200), (0, 200))
        assert text_input.scroll_offset.x > 0
