package editor

import (
	"testing"

	"git.gophernest.net/azpect/TextEditor/internal/action"
)

// NOTE: AI Generated tests

func TestDeleteLine(t *testing.T) {
	t.Run("test 'dd' deletes first line", func(t *testing.T) {
		lines := []string{"hello", "world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want '1'", m.LineCount())
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want '0'", m.CursorY())
		}
		if m.Line(0) != "world" {
			t.Errorf("Line(0) = %s, want 'world'", m.Line(0))
		}
	})

	t.Run("test 'dd' deletes middle line", func(t *testing.T) {
		lines := []string{"hello", "world", "testing"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want '2'", m.LineCount())
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.CursorY() != 1 {
			t.Errorf("CursorY() = %d, want '1'", m.CursorY())
		}
		if m.Line(0) != "hello" {
			t.Errorf("Line(0) = %s, want 'hello'", m.Line(0))
		}
		if m.Line(1) != "testing" {
			t.Errorf("Line(1) = %s, want 'testing'", m.Line(1))
		}
	})

	t.Run("test 'dd' deletes last line", func(t *testing.T) {
		lines := []string{"hello", "world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want '1'", m.LineCount())
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want '0'", m.CursorY())
		}
		if m.Line(0) != "hello" {
			t.Errorf("Line(0) = %s, want 'hello'", m.Line(0))
		}
	})

	t.Run("test 'dd' deletes line and preserves column", func(t *testing.T) {
		lines := []string{"hello", "world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
		sendKeys(tm, "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want '1'", m.LineCount())
		}
		if m.CursorX() != 3 {
			t.Errorf("CursorX() = %d, want '3'", m.CursorX())
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want '0'", m.CursorY())
		}
		if m.Line(0) != "world" {
			t.Errorf("Line(0) = %s, want 'world'", m.Line(0))
		}
	})

	t.Run("test '3dd' deletes three lines", func(t *testing.T) {
		lines := []string{"hello", "world", "testing", "line", "another line"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "3", "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want '2'", m.LineCount())
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.CursorY() != 1 {
			t.Errorf("CursorY() = %d, want '1'", m.CursorY())
		}
		if m.Line(0) != "hello" {
			t.Errorf("Line(0) = %s, want 'hello'", m.Line(0))
		}

		if m.Line(1) != "another line" {
			t.Errorf("Line(1) = %s, want 'another line'", m.Line(1))
		}
	})

	t.Run("test 'dd' deletes only line and preserves content", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want '1'", m.LineCount())
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want '0'", m.CursorY())
		}
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %s, want ''", m.Line(0))
		}
	})

	t.Run("test 'dd' with no lines preserves content", func(t *testing.T) {
		lines := []string{""}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want '1'", m.LineCount())
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want '0'", m.CursorY())
		}
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %s, want ''", m.Line(0))
		}
	})

	t.Run("test 'dd' clamps cursor when next line is shorter", func(t *testing.T) {
		lines := []string{"hello world", "hi"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
		sendKeys(tm, "d", "d")

		m := getFinalModel(t, tm)
		if m.CursorX() != 2 {
			t.Errorf("CursorX() = %d, want 2", m.CursorX())
		}
	})

	t.Run("test '3dd' count exceeds remaining lines", func(t *testing.T) {
		lines := []string{"hello", "world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "3", "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %q, want \"\"", m.Line(0))
		}
	})

	t.Run("test '3dd' starting near end of file", func(t *testing.T) {
		lines := []string{"hello", "world", "testing"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "3", "d", "d")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "hello" {
			t.Errorf("Line(0) = %q, want \"hello\"", m.Line(0))
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want 0", m.CursorY())
		}
	})

}

func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
	t.Run("test 'dl' deletes current character from start", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "l")

		m := getFinalModel(t, tm)
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.Line(0) != "ello" {
			t.Errorf("Line(0) = %s, want 'ello'", m.Line(0))
		}
	})

	t.Run("test 'dl' deletes current character from middle", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
		sendKeys(tm, "d", "l")

		m := getFinalModel(t, tm)
		if m.CursorX() != 2 {
			t.Errorf("CursorX() = %d, want '2'", m.CursorX())
		}
		if m.Line(0) != "helo" {
			t.Errorf("Line(0) = %s, want 'helo'", m.Line(0))
		}
	})

	t.Run("test 'dl' deletes current character from end", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
		sendKeys(tm, "d", "l")

		m := getFinalModel(t, tm)
		if m.CursorX() != 4 {
			t.Errorf("CursorX() = %d, want '4'", m.CursorX())
		}
		if m.Line(0) != "hell" {
			t.Errorf("Line(0) = %s, want 'hell'", m.Line(0))
		}
	})

	t.Run("test 'dh' does nothing on first char", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "h")

		m := getFinalModel(t, tm)
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want '0'", m.CursorX())
		}
		if m.Line(0) != "hello" {
			t.Errorf("Line(0) = %s, want 'hello'", m.Line(0))
		}
	})

	t.Run("test 'dh' deletes character to the left from middle", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
		sendKeys(tm, "d", "h")

		m := getFinalModel(t, tm)
		// dh deletes char to the left (position 1, 'e'), cursor moves to that position
		if m.CursorX() != 1 {
			t.Errorf("CursorX() = %d, want 1", m.CursorX())
		}
		if m.Line(0) != "hllo" {
			t.Errorf("Line(0) = %q, want 'hllo'", m.Line(0))
		}
	})

	t.Run("test 'dh' deletes character to the left from end", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
		sendKeys(tm, "d", "h")

		m := getFinalModel(t, tm)
		// dh deletes char to the left (position 3, 'l'), cursor moves to that position
		if m.CursorX() != 3 {
			t.Errorf("CursorX() = %d, want 3", m.CursorX())
		}
		if m.Line(0) != "helo" {
			t.Errorf("Line(0) = %q, want 'helo'", m.Line(0))
		}
	})

	t.Run("test '2dl' deletes two characters", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "2", "d", "l")

		m := getFinalModel(t, tm)
		if m.Line(0) != "llo" {
			t.Errorf("Line(0) = %q, want 'llo'", m.Line(0))
		}
	})

	t.Run("test 'd2l' deletes two characters", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "2", "l")

		m := getFinalModel(t, tm)
		if m.Line(0) != "llo" {
			t.Errorf("Line(0) = %q, want 'llo'", m.Line(0))
		}
	})

	t.Run("test '2dh' deletes two characters backwards", func(t *testing.T) {
		lines := []string{"hello"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
		sendKeys(tm, "2", "d", "h")

		m := getFinalModel(t, tm)
		if m.Line(0) != "heo" {
			t.Errorf("Line(0) = %q, want 'heo'", m.Line(0))
		}
	})
}

func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
	t.Run("test 'dj' deletes current and next line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "d", "j")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 4" {
			t.Errorf("Line(1) = %q, want 'line 4'", m.Line(1))
		}
		if m.CursorY() != 1 {
			t.Errorf("CursorY() = %d, want 1", m.CursorY())
		}
	})

	t.Run("test 'dj' from first line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "j")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "line 3" {
			t.Errorf("Line(0) = %q, want 'line 3'", m.Line(0))
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want 0", m.CursorY())
		}
	})

	// Note: In vim, dj from last line does nothing. Our implementation treats all
	// linewise motions consistently - they operate on at least the current line.
	t.Run("test 'dj' from last line deletes current line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
		sendKeys(tm, "d", "j")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 2" {
			t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1))
		}
	})

	t.Run("test 'd2j' deletes current and next two lines", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "d", "2", "j")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 5" {
			t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1))
		}
	})

	t.Run("test '2dj' deletes current and next two lines", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "2", "d", "j")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 5" {
			t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1))
		}
	})

	t.Run("test 'dk' deletes current and previous line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
		sendKeys(tm, "d", "k")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 4" {
			t.Errorf("Line(1) = %q, want 'line 4'", m.Line(1))
		}
		if m.CursorY() != 1 {
			t.Errorf("CursorY() = %d, want 1", m.CursorY())
		}
	})

	// Note: In vim, dk from first line does nothing. Our implementation treats all
	// linewise motions consistently - they operate on at least the current line.
	t.Run("test 'dk' from first line deletes current line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "k")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 2" {
			t.Errorf("Line(0) = %q, want 'line 2'", m.Line(0))
		}
		if m.Line(1) != "line 3" {
			t.Errorf("Line(1) = %q, want 'line 3'", m.Line(1))
		}
	})

	t.Run("test 'dk' from second line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "d", "k")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "line 3" {
			t.Errorf("Line(0) = %q, want 'line 3'", m.Line(0))
		}
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want 0", m.CursorY())
		}
	})

	t.Run("test 'd2k' deletes current and previous two lines", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 3})
		sendKeys(tm, "d", "2", "k")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 5" {
			t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1))
		}
	})

	t.Run("test '2dk' deletes current and previous two lines", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 3})
		sendKeys(tm, "2", "d", "k")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 5" {
			t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1))
		}
	})

	t.Run("test 'dj' with count exceeding remaining lines", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "d", "5", "j")

		m := getFinalModel(t, tm)
		// Should delete from line 1 to end
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
	})

	t.Run("test 'dk' with count exceeding previous lines", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
		sendKeys(tm, "d", "5", "k")

		m := getFinalModel(t, tm)
		// Should delete from line 0 to line 1
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "line 3" {
			t.Errorf("Line(0) = %q, want 'line 3'", m.Line(0))
		}
	})
}

func TestDeleteOperatorWithWordMotion(t *testing.T) {
	// --- dw tests (delete to start of next word) ---
	t.Run("test 'dw' deletes word from start of word", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "w")

		m := getFinalModel(t, tm)
		if m.Line(0) != "world" {
			t.Errorf("Line(0) = %q, want \"world\"", m.Line(0))
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want 0", m.CursorX())
		}
	})

	t.Run("test 'dw' deletes from middle of word to start of next word", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
		sendKeys(tm, "d", "w")

		m := getFinalModel(t, tm)
		if m.Line(0) != "heworld" {
			t.Errorf("Line(0) = %q, want \"heworld\"", m.Line(0))
		}
		if m.CursorX() != 2 {
			t.Errorf("CursorX() = %d, want 2", m.CursorX())
		}
	})

	t.Run("test 'dw' at last word deletes to end of line", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
		sendKeys(tm, "d", "w")

		m := getFinalModel(t, tm)
		if m.Line(0) != "hello " {
			t.Errorf("Line(0) = %q, want \"hello \"", m.Line(0))
		}
	})

	t.Run("test '2dw' deletes two words", func(t *testing.T) {
		lines := []string{"one two three four"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "2", "d", "w")

		m := getFinalModel(t, tm)
		if m.Line(0) != "three four" {
			t.Errorf("Line(0) = %q, want \"three four\"", m.Line(0))
		}
	})

	t.Run("test 'd2w' deletes two words", func(t *testing.T) {
		lines := []string{"one two three four"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "2", "w")

		m := getFinalModel(t, tm)
		if m.Line(0) != "three four" {
			t.Errorf("Line(0) = %q, want \"three four\"", m.Line(0))
		}
	})

	t.Run("test 'dw' with punctuation", func(t *testing.T) {
		lines := []string{"hello, world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "w")

		m := getFinalModel(t, tm)
		// 'w' motion stops at punctuation, so it should delete "hello"
		if m.Line(0) != ", world" {
			t.Errorf("Line(0) = %q, want \", world\"", m.Line(0))
		}
	})

	// --- de tests (delete to end of word, inclusive) ---
	t.Run("test 'de' deletes to end of word from start (inclusive)", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "e")

		m := getFinalModel(t, tm)
		// 'e' is inclusive - deletes "hello" (cols 0-4 inclusive), leaves " world"
		if m.Line(0) != " world" {
			t.Errorf("Line(0) = %q, want \" world\"", m.Line(0))
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want 0", m.CursorX())
		}
	})

	t.Run("test 'de' deletes to end of word from middle (inclusive)", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
		sendKeys(tm, "d", "e")

		m := getFinalModel(t, tm)
		// From 'l' (col 2) to 'o' (col 4) inclusive → deletes "llo"
		if m.Line(0) != "he world" {
			t.Errorf("Line(0) = %q, want \"he world\"", m.Line(0))
		}
	})

	t.Run("test 'de' at end of word jumps to next word end", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
		sendKeys(tm, "d", "e")

		m := getFinalModel(t, tm)
		// From 'o' of "hello" (col 4) to 'd' of "world" (col 10) inclusive
		if m.Line(0) != "hell" {
			t.Errorf("Line(0) = %q, want \"hell\"", m.Line(0))
		}
	})

	t.Run("test '2de' deletes to end of second word (inclusive)", func(t *testing.T) {
		lines := []string{"one two three four"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "2", "d", "e")

		m := getFinalModel(t, tm)
		// Deletes "one" and " two" (to end of second word inclusive)
		if m.Line(0) != " three four" {
			t.Errorf("Line(0) = %q, want \" three four\"", m.Line(0))
		}
	})

	// --- db tests (delete backward word) ---
	t.Run("test 'db' does nothing at start of line", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "b")

		m := getFinalModel(t, tm)
		if m.Line(0) != "hello world" {
			t.Errorf("Line(0) = %q, want \"hello world\"", m.Line(0))
		}
	})

	t.Run("test 'db' deletes back to start of current word", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0})
		sendKeys(tm, "d", "b")

		m := getFinalModel(t, tm)
		// cursor at 'r' of "world", db should delete back to start of "world"
		if m.Line(0) != "hello rld" {
			t.Errorf("Line(0) = %q, want \"hello rld\"", m.Line(0))
		}
	})

	t.Run("test 'db' at start of word deletes previous word", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
		sendKeys(tm, "d", "b")

		m := getFinalModel(t, tm)
		// cursor at 'w', db should delete "hello " back
		if m.Line(0) != "world" {
			t.Errorf("Line(0) = %q, want \"world\"", m.Line(0))
		}
	})

	t.Run("test '2db' deletes two words backward", func(t *testing.T) {
		lines := []string{"one two three four"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 14, Line: 0})
		sendKeys(tm, "2", "d", "b")

		m := getFinalModel(t, tm)
		// cursor at 'f' of "four", 2db should delete "two three "
		if m.Line(0) != "one four" {
			t.Errorf("Line(0) = %q, want \"one four\"", m.Line(0))
		}
	})

	t.Run("test 'db' at end of line", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
		sendKeys(tm, "d", "b")

		m := getFinalModel(t, tm)
		// cursor at 'd' (last char), db should delete back to start of "world"
		if m.Line(0) != "hello d" {
			t.Errorf("Line(0) = %q, want \"hello d\"", m.Line(0))
		}
	})
}

func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
	// --- d0 tests (delete to line start) ---
	t.Run("test 'd0' does nothing at start of line", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "0")

		m := getFinalModel(t, tm)
		if m.Line(0) != "hello world" {
			t.Errorf("Line(0) = %q, want \"hello world\"", m.Line(0))
		}
	})

	t.Run("test 'd0' deletes from cursor to start of line", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
		sendKeys(tm, "d", "0")

		m := getFinalModel(t, tm)
		if m.Line(0) != "world" {
			t.Errorf("Line(0) = %q, want \"world\"", m.Line(0))
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want 0", m.CursorX())
		}
	})

	t.Run("test 'd0' from end of line", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
		sendKeys(tm, "d", "0")

		m := getFinalModel(t, tm)
		if m.Line(0) != "d" {
			t.Errorf("Line(0) = %q, want \"d\"", m.Line(0))
		}
	})

	t.Run("test 'd0' from middle of line", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
		sendKeys(tm, "d", "0")

		m := getFinalModel(t, tm)
		if m.Line(0) != "lo world" {
			t.Errorf("Line(0) = %q, want \"lo world\"", m.Line(0))
		}
	})

	// --- d$ tests (delete to line end) ---
	t.Run("test 'd$' deletes to end of line from start", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "$")

		m := getFinalModel(t, tm)
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %q, want \"\"", m.Line(0))
		}
	})

	t.Run("test 'd$' deletes to end of line from middle", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
		sendKeys(tm, "d", "$")

		m := getFinalModel(t, tm)
		if m.Line(0) != "hello " {
			t.Errorf("Line(0) = %q, want \"hello \"", m.Line(0))
		}
	})

	t.Run("test 'd$' at end of line deletes last char", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
		sendKeys(tm, "d", "$")

		m := getFinalModel(t, tm)
		if m.Line(0) != "hello worl" {
			t.Errorf("Line(0) = %q, want \"hello worl\"", m.Line(0))
		}
	})

	t.Run("test 'd$' does not affect other lines", func(t *testing.T) {
		lines := []string{"hello world", "second line"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
		sendKeys(tm, "d", "$")

		m := getFinalModel(t, tm)
		if m.Line(0) != "hello " {
			t.Errorf("Line(0) = %q, want \"hello \"", m.Line(0))
		}
		if m.Line(1) != "second line" {
			t.Errorf("Line(1) = %q, want \"second line\"", m.Line(1))
		}
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
	})

	t.Run("test 'd$' on empty line does nothing", func(t *testing.T) {
		lines := []string{"", "second line"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "$")

		m := getFinalModel(t, tm)
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %q, want \"\"", m.Line(0))
		}
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
	})

	// --- d_ tests (delete to first non-whitespace) ---
	t.Run("test 'd_' from start deletes leading whitespace", func(t *testing.T) {
		lines := []string{"   hello world"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "_")

		m := getFinalModel(t, tm)
		// From col 0 to first non-whitespace (col 3, 'h') - deletes leading spaces
		if m.Line(0) != "hello world" {
			t.Errorf("Line(0) = %q, want \"hello world\"", m.Line(0))
		}
		if m.CursorX() != 0 {
			t.Errorf("CursorX() = %d, want 0", m.CursorX())
		}
	})

	t.Run("test 'd_' from middle whitespace deletes to first non-blank", func(t *testing.T) {
		lines := []string{"   hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
		sendKeys(tm, "d", "_")

		m := getFinalModel(t, tm)
		// From col 1 to first non-whitespace (col 3, 'h') - deletes cols 1-2
		if m.Line(0) != " hello world" {
			t.Errorf("Line(0) = %q, want \" hello world\"", m.Line(0))
		}
		if m.CursorX() != 1 {
			t.Errorf("CursorX() = %d, want 1", m.CursorX())
		}
	})

	t.Run("test 'd_' from after first char deletes back to first non-whitespace", func(t *testing.T) {
		lines := []string{"   hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
		sendKeys(tm, "d", "_")

		m := getFinalModel(t, tm)
		// From col 6 ('l') back to col 3 ('h')
		// Should delete "hel" leaving "   lo world"
		if m.Line(0) != "   lo world" {
			t.Errorf("Line(0) = %q, want \"   lo world\"", m.Line(0))
		}
	})

	t.Run("test 'd_' on line with no leading whitespace", func(t *testing.T) {
		lines := []string{"hello world"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
		sendKeys(tm, "d", "_")

		m := getFinalModel(t, tm)
		// From col 5 (' ') back to col 0 ('h')
		// Should delete "hello" leaving " world"
		if m.Line(0) != " world" {
			t.Errorf("Line(0) = %q, want \" world\"", m.Line(0))
		}
	})
}

func TestDeleteOperatorWithJumpMotion(t *testing.T) {
	t.Run("test 'dG' deletes from cursor to end of file", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
		sendKeys(tm, "d", "G")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 2" {
			t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1))
		}
	})

	t.Run("test 'dG' from first line deletes everything", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "G")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %q, want ''", m.Line(0))
		}
	})

	t.Run("test 'dG' from last line deletes only last line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
		sendKeys(tm, "d", "G")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 1" {
			t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
		}
		if m.Line(1) != "line 2" {
			t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1))
		}
	})

	t.Run("test 'dG' positions cursor correctly", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
		sendKeys(tm, "d", "G")

		m := getFinalModel(t, tm)
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want 0", m.CursorY())
		}
	})

	t.Run("test 'dgg' deletes from cursor to start of file", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
		sendKeys(tm, "d", "g", "g")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 4" {
			t.Errorf("Line(0) = %q, want 'line 4'", m.Line(0))
		}
		if m.Line(1) != "line 5" {
			t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1))
		}
	})

	t.Run("test 'dgg' from last line deletes everything", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
		sendKeys(tm, "d", "g", "g")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %q, want ''", m.Line(0))
		}
	})

	t.Run("test 'dgg' from first line deletes only first line", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "g", "g")

		m := getFinalModel(t, tm)
		if m.LineCount() != 2 {
			t.Errorf("LineCount() = %d, want 2", m.LineCount())
		}
		if m.Line(0) != "line 2" {
			t.Errorf("Line(0) = %q, want 'line 2'", m.Line(0))
		}
		if m.Line(1) != "line 3" {
			t.Errorf("Line(1) = %q, want 'line 3'", m.Line(1))
		}
	})

	t.Run("test 'dgg' positions cursor at start", func(t *testing.T) {
		lines := []string{"line 1", "line 2", "line 3", "line 4"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 2})
		sendKeys(tm, "d", "g", "g")

		m := getFinalModel(t, tm)
		if m.CursorY() != 0 {
			t.Errorf("CursorY() = %d, want 0", m.CursorY())
		}
	})

	t.Run("test 'dG' on single line file deletes the line", func(t *testing.T) {
		lines := []string{"only line"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "G")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %q, want ''", m.Line(0))
		}
	})

	t.Run("test 'dgg' on single line file deletes the line", func(t *testing.T) {
		lines := []string{"only line"}
		tm := newTestModelWithLines(t, lines)
		sendKeys(tm, "d", "g", "g")

		m := getFinalModel(t, tm)
		if m.LineCount() != 1 {
			t.Errorf("LineCount() = %d, want 1", m.LineCount())
		}
		if m.Line(0) != "" {
			t.Errorf("Line(0) = %q, want ''", m.Line(0))
		}
	})

	t.Run("test 'dG' clamps cursor when file shrinks", func(t *testing.T) {
		lines := []string{"short", "this is a longer line", "line 3", "line 4"}
		tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
		sendKeys(tm, "d", "G")

		m := getFinalModel(t, tm)
		// Cursor was at col 10, but "short" only has 5 chars
		if m.CursorX() > len("short") {
			t.Errorf("CursorX() = %d, should be clamped to line length %d", m.CursorX(), len("short"))
		}
	})
}
