import pytest

from textual.app import App, ComposeResult
from textual.geometry import Offset
from textual.widgets import TextEditor
from textual.widgets.text_editor import Document, Selection

TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
"""


class TextEditorApp(App):
    def compose(self) -> ComposeResult:
        text_editor = TextEditor()
        text_editor.load_text(TEXT)
        yield text_editor


async def test_mouse_click():
    """When you click the TextEditor, the cursor moves to the expected location."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        await pilot.click(TextEditor, Offset(x=5, y=2))
        assert text_editor.selection == Selection.cursor((2, 2))


async def test_mouse_click_clamp_from_right():
    """When you click to the right of the document bounds, the cursor is clamped
    to within the document bounds."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        await pilot.click(TextEditor, Offset(x=8, y=20))
        assert text_editor.selection == Selection.cursor((4, 0))


async def test_mouse_click_gutter_clamp():
    """When you click the gutter, it selects the start of the line."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        await pilot.click(TextEditor, Offset(x=0, y=3))
        assert text_editor.selection == Selection.cursor((3, 0))


async def test_cursor_movement_basic():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("01234567\n012345\n0123456789")

        await pilot.press("right")
        assert text_editor.selection == Selection.cursor((0, 1))

        await pilot.press("down")
        assert text_editor.selection == Selection.cursor((1, 1))

        await pilot.press("left")
        assert text_editor.selection == Selection.cursor((1, 0))

        await pilot.press("up")
        assert text_editor.selection == Selection.cursor((0, 0))


async def test_cursor_selection_right():
    """When you press shift+right the selection is updated correctly."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        await pilot.press(*["shift+right"] * 3)
        assert text_editor.selection == Selection((0, 0), (0, 3))


async def test_cursor_selection_right_to_previous_line():
    """When you press shift+right resulting in the cursor moving to the next line,
    the selection is updated correctly."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.selection = Selection.cursor((0, 15))
        await pilot.press(*["shift+right"] * 4)
        assert text_editor.selection == Selection((0, 15), (1, 2))


async def test_cursor_selection_left():
    """When you press shift+left the selection is updated correctly."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.selection = Selection.cursor((2, 5))
        await pilot.press(*["shift+left"] * 3)
        assert text_editor.selection == Selection((2, 5), (2, 2))


async def test_cursor_selection_left_to_previous_line():
    """When you press shift+left resulting in the cursor moving back to the previous line,
    the selection is updated correctly."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.selection = Selection.cursor((2, 2))
        await pilot.press(*["shift+left"] * 3)

        # The cursor jumps up to the end of the line above.
        end_of_previous_line = len(TEXT.splitlines()[1])
        assert text_editor.selection == Selection((2, 2), (1, end_of_previous_line))


async def test_cursor_selection_up():
    """When you press shift+up the selection is updated correctly."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.move_cursor((2, 3))

        await pilot.press("shift+up")
        assert text_editor.selection == Selection((2, 3), (1, 3))


async def test_cursor_selection_up_when_cursor_on_first_line():
    """When you press shift+up the on the first line, it selects to the start."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.move_cursor((0, 4))

        await pilot.press("shift+up")
        assert text_editor.selection == Selection((0, 4), (0, 0))
        await pilot.press("shift+up")
        assert text_editor.selection == Selection((0, 4), (0, 0))


async def test_cursor_selection_down():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.move_cursor((2, 5))

        await pilot.press("shift+down")
        assert text_editor.selection == Selection((2, 5), (3, 5))


async def test_cursor_selection_down_when_cursor_on_last_line():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("ABCDEF\nGHIJK")
        text_editor.move_cursor((1, 2))

        await pilot.press("shift+down")
        assert text_editor.selection == Selection((1, 2), (1, 5))
        await pilot.press("shift+down")
        assert text_editor.selection == Selection((1, 2), (1, 5))


async def test_cursor_word_right():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("ABC DEF\nGHIJK")

        await pilot.press("ctrl+right")

        assert text_editor.selection == Selection.cursor((0, 3))


async def test_cursor_word_right_select():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("ABC DEF\nGHIJK")

        await pilot.press("ctrl+shift+right")

        assert text_editor.selection == Selection((0, 0), (0, 3))


async def test_cursor_word_left():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("ABC DEF\nGHIJK")
        text_editor.move_cursor((0, 7))

        await pilot.press("ctrl+left")

        assert text_editor.selection == Selection.cursor((0, 4))


async def test_cursor_word_left_select():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("ABC DEF\nGHIJK")
        text_editor.move_cursor((0, 7))

        await pilot.press("ctrl+shift+left")

        assert text_editor.selection == Selection((0, 7), (0, 4))


@pytest.mark.parametrize("key", ["end", "ctrl+e"])
async def test_cursor_to_line_end(key):
    """You can use the keyboard to jump the cursor to the end of the current line."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.selection = Selection.cursor((2, 2))
        await pilot.press(key)
        eol_index = len(TEXT.splitlines()[2])
        assert text_editor.cursor_location == (2, eol_index)
        assert text_editor.selection.is_empty


@pytest.mark.parametrize("key", ["home", "ctrl+a"])
async def test_cursor_to_line_home_basic_behaviour(key):
    """You can use the keyboard to jump the cursor to the start of the current line."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.selection = Selection.cursor((2, 2))
        await pilot.press(key)
        assert text_editor.cursor_location == (2, 0)
        assert text_editor.selection.is_empty


@pytest.mark.parametrize(
    "cursor_start,cursor_destination",
    [
        ((0, 0), (0, 4)),
        ((0, 2), (0, 0)),
        ((0, 4), (0, 0)),
        ((0, 5), (0, 4)),
        ((0, 9), (0, 4)),
        ((0, 15), (0, 4)),
    ],
)
async def test_cursor_line_home_smart_home(cursor_start, cursor_destination):
    """If the line begins with whitespace, pressing home firstly goes
    to the start of the (non-whitespace) content. Pressing it again takes you to column
    0. If you press it again, it goes back to the first non-whitespace column."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("    hello world")
        text_editor.move_cursor(cursor_start)
        await pilot.press("home")
        assert text_editor.selection == Selection.cursor(cursor_destination)


async def test_cursor_page_down():
    """Pagedown moves the cursor down 1 page, retaining column index."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("XXX\n" * 200)
        text_editor.selection = Selection.cursor((0, 1))
        await pilot.press("pagedown")
        assert text_editor.selection == Selection.cursor((app.console.height - 1, 1))


async def test_cursor_page_up():
    """Pageup moves the cursor up 1 page, retaining column index."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_text("XXX\n" * 200)
        text_editor.selection = Selection.cursor((100, 1))
        await pilot.press("pageup")
        assert text_editor.selection == Selection.cursor(
            (100 - app.console.height + 1, 1)
        )


async def test_cursor_vertical_movement_visual_alignment_snapping():
    """When you move the cursor vertically, it should stay vertically
    aligned even when double-width characters are used."""
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.load_document(Document("こんにちは\n012345"))
        text_editor.move_cursor((1, 3), record_width=True)

        # The '3' is aligned with ん at (0, 1)
        # こんにちは
        # 012345
        # Pressing `up` takes us from (1, 3) to (0, 1) because record_width=True.
        await pilot.press("up")
        assert text_editor.selection == Selection.cursor((0, 1))

        # Pressing `down` takes us from (0, 1) to (1, 3)
        await pilot.press("down")
        assert text_editor.selection == Selection.cursor((1, 3))


async def test_select_line_binding():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)
        text_editor.move_cursor((2, 2))

        await pilot.press("f6")

        assert text_editor.selection == Selection((2, 0), (2, 56))


async def test_select_all_binding():
    app = TextEditorApp()
    async with app.run_test() as pilot:
        text_editor = app.query_one(TextEditor)

        await pilot.press("f7")

        assert text_editor.selection == Selection((0, 0), (4, 0))
