"""Tests for mark operations."""

import pytest

from e3po.editor import Buffer, Mark, MarkMode, Editor


class MockScreen:
    """Mock curses screen for testing."""
    def __init__(self):
        self.height = 24
        self.width = 80
        self.last_move = None  # Track cursor position

    def getmaxyx(self):
        return (self.height, self.width)

    def clear(self):
        pass

    def refresh(self):
        pass

    def addstr(self, *args):
        pass

    def move(self, row, col):
        self.last_move = (row, col)

    def getch(self):
        return -1


def make_editor(lines=None):
    """Create an editor with mock screen for testing."""
    editor = Editor()
    editor.screen = MockScreen()
    editor.height = 24
    editor.width = 80
    editor.edit_height = 20
    if lines:
        editor.buffer.lines = lines.copy()
    return editor


class TestMarkCreation:
    """Test mark creation."""

    def test_line_mark_single_line(self):
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 1
        editor.action_mark_line()

        assert editor.mark is not None
        assert editor.mark.mode == MarkMode.LINE
        assert editor.mark.start_row == 1
        assert editor.mark.end_row == 1

    def test_line_mark_extend_down(self):
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.cursor_row = 2
        editor.action_mark_line()

        assert editor.mark.start_row == 1
        assert editor.mark.end_row == 2

    def test_line_mark_extend_up(self):
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.cursor_row = 0
        editor.action_mark_line()

        assert editor.mark.start_row == 0
        assert editor.mark.end_row == 1

    def test_block_mark_creation(self):
        editor = make_editor(["line0", "line1"])
        editor.cursor_row = 0
        editor.cursor_col = 2
        editor.action_mark_block()

        assert editor.mark is not None
        assert editor.mark.mode == MarkMode.BLOCK
        assert editor.mark.start_row == 0
        assert editor.mark.start_col == 2

    def test_char_mark_creation(self):
        editor = make_editor(["hello world"])
        editor.cursor_row = 0
        editor.cursor_col = 3
        editor.action_mark_char()

        assert editor.mark is not None
        assert editor.mark.mode == MarkMode.CHAR
        assert editor.mark.start_col == 3

    def test_unmark_clears_mark(self):
        editor = make_editor(["line"])
        editor.action_mark_line()
        assert editor.mark is not None
        editor.action_unmark()
        assert editor.mark is None


class TestGetMarkedText:
    """Test extracting marked text."""

    def test_get_marked_text_line_single(self):
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 1
        editor.action_mark_line()

        text = editor.get_marked_text()
        assert text == ["line1"]

    def test_get_marked_text_line_multiple(self):
        editor = make_editor(["line0", "line1", "line2", "line3"])
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.cursor_row = 2
        editor.action_mark_line()

        text = editor.get_marked_text()
        assert text == ["line1", "line2"]

    def test_get_marked_text_block(self):
        editor = make_editor(["abcdef", "ghijkl", "mnopqr"])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=1,
            end_row=2, end_col=3,
            pending=False
        )

        text = editor.get_marked_text()
        assert text == ["bcd", "hij", "nop"]

    def test_get_marked_text_char_single_line(self):
        editor = make_editor(["hello world"])
        editor.mark = Mark(
            mode=MarkMode.CHAR,
            start_row=0, start_col=0,
            end_row=0, end_col=4,
            pending=False
        )

        text = editor.get_marked_text()
        assert text == ["hello"]


class TestMarkDelete:
    """Test deleting marked regions."""

    def test_delete_line_mark(self):
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.action_delete()

        assert editor.buffer.lines == ["line0", "line2"]
        assert editor.mark is None

    def test_delete_multiple_lines(self):
        editor = make_editor(["a", "b", "c", "d"])
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.cursor_row = 2
        editor.action_mark_line()
        editor.action_delete()

        assert editor.buffer.lines == ["a", "d"]

    def test_delete_block_mark(self):
        editor = make_editor(["abcdef", "ghijkl"])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=1,
            end_row=1, end_col=3,
            pending=False
        )
        editor.action_delete()

        assert editor.buffer.lines == ["aef", "gkl"]


class TestMarkCopy:
    """Test copying marked regions."""

    def test_copy_line_inserts_after_cursor(self):
        editor = make_editor(["line0", "line1", "line2"])
        # Mark line1
        editor.cursor_row = 1
        editor.action_mark_line()
        # Move cursor to line2 and copy
        editor.cursor_row = 2
        editor.action_copy()

        assert editor.buffer.lines == ["line0", "line1", "line2", "line1"]

    def test_copy_creates_new_mark_at_destination(self):
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 0
        editor.action_mark_line()
        editor.cursor_row = 2
        editor.action_copy()

        # New mark should be at the copied location
        assert editor.mark.start_row == 3
        assert editor.mark.end_row == 3


class TestMarkShift:
    """Test shifting marks left/right."""

    def test_shift_line_left(self):
        editor = make_editor(["    indented", "    also"])
        editor.cursor_row = 0
        editor.action_mark_line()
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.action_shift_mark_left()

        assert editor.buffer.lines == ["   indented", "   also"]

    def test_shift_line_right(self):
        editor = make_editor(["text", "more"])
        editor.cursor_row = 0
        editor.action_mark_line()
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.action_shift_mark_right()

        assert editor.buffer.lines == [" text", " more"]

    def test_shift_requires_mark(self):
        editor = make_editor(["text"])
        editor.action_shift_mark_left()
        assert "No mark" in editor.message


class TestWordMark:
    """Test word marking."""

    def test_mark_word(self):
        editor = make_editor(["hello world"])
        editor.cursor_row = 0
        editor.cursor_col = 1  # Inside "hello"
        editor.action_mark_word()

        assert editor.mark is not None
        assert editor.mark.mode == MarkMode.CHAR
        assert editor.mark.start_col == 0
        assert editor.mark.end_col == 4

    def test_mark_word_with_underscore(self):
        editor = make_editor(["foo_bar baz"])
        editor.cursor_row = 0
        editor.cursor_col = 2  # Inside "foo_bar"
        editor.action_mark_word()

        assert editor.mark.start_col == 0
        assert editor.mark.end_col == 6

    def test_mark_word_not_on_word(self):
        editor = make_editor(["hello world"])
        editor.cursor_row = 0
        editor.cursor_col = 5  # On space
        editor.action_mark_word()

        assert "No word" in editor.message


class TestMarkFill:
    """Test fill mark operation (Alt-F)."""

    def test_fill_line_mark(self):
        editor = make_editor(["hello", "world", "test"])
        editor.cursor_row = 0
        editor.action_mark_line()
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.do_fill_mark('#')

        assert editor.buffer.lines[0] == "#####"
        assert editor.buffer.lines[1] == "#####"
        assert editor.buffer.lines[2] == "test"  # Unchanged

    def test_fill_block_mark(self):
        editor = make_editor(["abcdef", "ghijkl", "mnopqr"])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=1,
            end_row=2, end_col=3,
            pending=False
        )
        editor.do_fill_mark('X')

        assert editor.buffer.lines[0] == "aXXXef"
        assert editor.buffer.lines[1] == "gXXXkl"
        assert editor.buffer.lines[2] == "mXXXqr"

    def test_fill_requires_mark(self):
        editor = make_editor(["test"])
        editor.action_fill_mark()
        assert "No mark" in editor.message

    def test_fill_sets_pending(self):
        editor = make_editor(["test"])
        editor.cursor_row = 0
        editor.action_mark_line()
        editor.action_fill_mark()

        assert editor.fill_pending == True
        assert "Fill with" in editor.message

    def test_fill_block_pads_short_lines(self):
        editor = make_editor(["ab", "c", ""])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=1,
            end_row=2, end_col=3,
            pending=False
        )
        editor.do_fill_mark('*')

        assert editor.buffer.lines[0] == "a***"
        assert editor.buffer.lines[1] == "c***"
        assert editor.buffer.lines[2] == " ***"


class TestMarkMove:
    """Test move mark operation (Alt-M)."""

    def test_move_line_mark(self):
        editor = make_editor(["a", "b", "c", "d"])
        # Mark line "b"
        editor.cursor_row = 1
        editor.action_mark_line()
        # Move cursor to line "d" and move mark there
        editor.cursor_row = 3
        editor.action_move()

        # "b" should be removed and inserted after "d"
        assert editor.buffer.lines == ["a", "c", "d", "b"]

    def test_move_multiple_lines(self):
        editor = make_editor(["a", "b", "c", "d", "e"])
        # Mark lines "b" and "c"
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.cursor_row = 2
        editor.action_mark_line()
        # Move to after "e"
        editor.cursor_row = 4
        editor.action_move()

        assert editor.buffer.lines == ["a", "d", "e", "b", "c"]

    def test_move_creates_new_mark_at_destination(self):
        editor = make_editor(["a", "b", "c"])
        editor.cursor_row = 0
        editor.action_mark_line()
        editor.cursor_row = 2
        editor.action_move()

        # New mark should be at destination
        assert editor.mark is not None
        assert editor.mark.start_row == 2

    def test_move_requires_mark(self):
        editor = make_editor(["test"])
        editor.action_move()
        assert "No mark" in editor.message

    def test_move_block_mark(self):
        editor = make_editor(["abcdef", "ghijkl"])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=0,
            end_row=0, end_col=1,
            pending=False
        )
        editor.cursor_row = 1
        editor.cursor_col = 4
        editor.action_move()

        # "ab" moved from row 0 to row 1 col 4
        assert editor.buffer.lines[0] == "cdef"
        assert "ab" in editor.buffer.lines[1]


class TestMarkOverlay:
    """Test overlay mark operation (Alt-O)."""

    def test_overlay_block(self):
        editor = make_editor(["abcdef", "ghijkl", "123456"])
        # Mark "ab" on first line
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=0,
            end_row=0, end_col=1,
            pending=False
        )
        # Overlay at row 2, col 2
        editor.cursor_row = 2
        editor.cursor_col = 2
        editor.action_overlay()

        # "ab" should replace "34" in row 2
        assert editor.buffer.lines[2] == "12ab56"

    def test_overlay_multi_row_block(self):
        editor = make_editor(["ab", "cd", "123456", "789012"])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=0,
            end_row=1, end_col=1,
            pending=False
        )
        editor.cursor_row = 2
        editor.cursor_col = 2
        editor.action_overlay()

        assert editor.buffer.lines[2] == "12ab56"
        assert editor.buffer.lines[3] == "78cd12"

    def test_overlay_requires_block_mark(self):
        editor = make_editor(["test"])
        editor.cursor_row = 0
        editor.action_mark_line()
        editor.action_overlay()

        assert "block mark" in editor.message.lower()

    def test_overlay_requires_mark(self):
        editor = make_editor(["test"])
        editor.action_overlay()
        assert "No mark" in editor.message


class TestGotoMark:
    """Test goto mark start/end (Alt-Y, Alt-E)."""

    def test_goto_mark_start_line(self):
        editor = make_editor(["a", "b", "c", "d", "e"])
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.cursor_row = 3
        editor.action_mark_line()
        # Now mark spans rows 1-3, cursor at row 3
        editor.cursor_row = 4  # Move cursor away

        editor.action_goto_mark_start()
        assert editor.cursor_row == 1

    def test_goto_mark_end_line(self):
        editor = make_editor(["a", "b", "c", "d", "e"])
        editor.cursor_row = 1
        editor.action_mark_line()
        editor.cursor_row = 3
        editor.action_mark_line()
        editor.cursor_row = 0  # Move cursor away

        editor.action_goto_mark_end()
        assert editor.cursor_row == 3

    def test_goto_mark_start_block(self):
        editor = make_editor(["abcdef"])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=2,
            end_row=0, end_col=4,
            pending=False
        )
        editor.cursor_col = 0

        editor.action_goto_mark_start()
        assert editor.cursor_row == 0
        assert editor.cursor_col == 2

    def test_goto_mark_end_block(self):
        editor = make_editor(["abcdef"])
        editor.mark = Mark(
            mode=MarkMode.BLOCK,
            start_row=0, start_col=2,
            end_row=0, end_col=4,
            pending=False
        )
        editor.cursor_col = 0

        editor.action_goto_mark_end()
        assert editor.cursor_row == 0
        assert editor.cursor_col == 4

    def test_goto_mark_start_requires_mark(self):
        editor = make_editor(["test"])
        editor.action_goto_mark_start()
        assert "No mark" in editor.message

    def test_goto_mark_end_requires_mark(self):
        editor = make_editor(["test"])
        editor.action_goto_mark_end()
        assert "not complete" in editor.message.lower()


class TestSplitJoinLine:
    """Test split and join line operations (Alt-S, Alt-J)."""

    def test_split_line_middle(self):
        editor = make_editor(["hello world"])
        editor.cursor_row = 0
        editor.cursor_col = 5
        editor.action_split_line()

        assert editor.buffer.lines == ["hello", " world"]

    def test_split_line_start(self):
        editor = make_editor(["hello"])
        editor.cursor_row = 0
        editor.cursor_col = 0
        editor.action_split_line()

        assert editor.buffer.lines == ["", "hello"]

    def test_split_line_end(self):
        editor = make_editor(["hello"])
        editor.cursor_row = 0
        editor.cursor_col = 5
        editor.action_split_line()

        assert editor.buffer.lines == ["hello", ""]

    def test_join_line(self):
        editor = make_editor(["hello", "world"])
        editor.cursor_row = 0
        editor.action_join_line()

        assert editor.buffer.lines == ["helloworld"]

    def test_join_line_last_line_noop(self):
        editor = make_editor(["only line"])
        editor.cursor_row = 0
        editor.action_join_line()

        # Should be unchanged - no next line to join with
        assert editor.buffer.lines == ["only line"]

    def test_split_creates_undo(self):
        editor = make_editor(["hello world"])
        editor.cursor_row = 0
        editor.cursor_col = 5
        editor.action_split_line()

        assert len(editor.buffer.undo_stack) > 0

    def test_join_creates_undo(self):
        editor = make_editor(["hello", "world"])
        editor.cursor_row = 0
        editor.action_join_line()

        assert len(editor.buffer.undo_stack) > 0


class TestCursorDisplayPosition:
    """Test that cursor screen position matches buffer position."""

    def _get_cursor_screen_pos(self, editor):
        """Calculate cursor screen position (same logic as draw())."""
        cursor_screen_row = editor.cursor_row - editor.top_row
        cursor_screen_col = editor.cursor_col - editor.left_col
        return (cursor_screen_row, cursor_screen_col)

    def test_cursor_at_origin(self):
        """Cursor at row 0, col 0 should display at screen row 0, col 0."""
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 0
        editor.cursor_col = 0
        editor.top_row = 0
        editor.left_col = 0

        assert self._get_cursor_screen_pos(editor) == (0, 0)

    def test_cursor_row_matches_display(self):
        """Cursor row should match display row (no offset)."""
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 2
        editor.cursor_col = 3
        editor.top_row = 0
        editor.left_col = 0

        # Screen row should equal buffer row (when top_row=0)
        assert self._get_cursor_screen_pos(editor) == (2, 3)

    def test_cursor_with_scroll(self):
        """Cursor position accounts for scroll offset."""
        editor = make_editor(["line" + str(i) for i in range(20)])
        editor.cursor_row = 10
        editor.cursor_col = 5
        editor.top_row = 5
        editor.left_col = 0

        # Screen row = buffer row - top_row
        assert self._get_cursor_screen_pos(editor) == (5, 5)

    def test_block_mark_at_cursor_position(self):
        """Block mark should highlight the character under the cursor."""
        editor = make_editor(["line0", "line1", "line2"])
        editor.cursor_row = 1
        editor.cursor_col = 2
        editor.top_row = 0
        editor.left_col = 0
        editor.action_mark_block()

        # Cursor screen position
        screen_pos = self._get_cursor_screen_pos(editor)
        assert screen_pos == (1, 2)

        # Mark should be at cursor position
        assert editor.mark.start_row == 1
        assert editor.mark.start_col == 2

        # get_marked_cols should return the cursor column for cursor row
        marked = editor.get_marked_cols(1)
        assert 2 in marked

        # The marked row should match cursor screen row
        # (mark is at buffer row 1, cursor screen row is 1)
        assert editor.mark.start_row == editor.cursor_row
