package editor

import (
	"fmt"
	"testing"

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

// NOTE: AI Generated tests

// generateLines creates n lines of content
func generateLines(n int) []string {
	lines := make([]string, n)
	for i := range n {
		lines[i] = fmt.Sprintf("line %d", i+1)
	}
	return lines
}

func TestScrollBasic(t *testing.T) {
	t.Run("small file does not scroll", func(t *testing.T) {
		// 10 lines, viewport 24 -> no scrolling needed
		lines := generateLines(10)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 24)
		sendKeys(tm, "G") // go to bottom

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

	t.Run("scrolls down when cursor moves past bottom margin", func(t *testing.T) {
		// 50 lines, viewport 20, scrollOff 8
		// Viewport shows lines 0-18 (19 lines, -1 for status bar)
		// Safe zone: lines 8 to 10 (19-1-8=10)
		// Moving to line 11+ should trigger scroll
		lines := generateLines(50)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)

		// Move down 15 times to get to line 15
		for range 15 {
			sendKeys(tm, "j")
		}

		m := getFinalModel(t, tm)
		if m.CursorY() != 15 {
			t.Errorf("CursorY() = %d, want 15", m.CursorY())
		}
		// With scrollOff=8, viewport=19, cursor at 15 means:
		// cursor should be at position 10 from top (19-1-8=10)
		// so scrollY = 15 - 10 = 5
		if m.ScrollY() < 1 {
			t.Errorf("ScrollY() = %d, want > 0 (should have scrolled)", m.ScrollY())
		}
	})

	t.Run("scrolls up when cursor moves past top margin", func(t *testing.T) {
		// Start at line 20, move up
		lines := generateLines(50)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 20}, 80, 20)

		// First, let the model adjust (it will scroll to show cursor)
		// Then move up 15 times
		for range 15 {
			sendKeys(tm, "k")
		}

		m := getFinalModel(t, tm)
		if m.CursorY() != 5 {
			t.Errorf("CursorY() = %d, want 5", m.CursorY())
		}
		// Cursor at line 5 with scrollOff=8 means scrollY should be 0
		// (can't scroll negative, and cursor is within safe zone from top)
		if m.ScrollY() != 0 {
			t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
		}
	})

	t.Run("G jumps to bottom and scrolls", func(t *testing.T) {
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)
		sendKeys(tm, "G")

		m := getFinalModel(t, tm)
		if m.CursorY() != 99 {
			t.Errorf("CursorY() = %d, want 99", m.CursorY())
		}
		// With 100 lines and viewport 18 (height - 2 for status + command bar),
		// max scrollY = 100 - 18 = 82
		if m.ScrollY() != 82 {
			t.Errorf("ScrollY() = %d, want 82", m.ScrollY())
		}
	})

	t.Run("gg jumps to top and scrolls", func(t *testing.T) {
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 20)
		sendKeys(tm, "g", "g")

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

func TestScrollEdgeCases(t *testing.T) {
	t.Run("scrollY never goes negative", func(t *testing.T) {
		lines := generateLines(50)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)

		// Try to move up from top
		for range 5 {
			sendKeys(tm, "k")
		}

		m := getFinalModel(t, tm)
		if m.ScrollY() < 0 {
			t.Errorf("ScrollY() = %d, want >= 0", m.ScrollY())
		}
	})

	t.Run("scrollY clamped to max scroll", func(t *testing.T) {
		lines := generateLines(30)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)
		sendKeys(tm, "G")

		m := getFinalModel(t, tm)
		// 30 lines, viewport 18 (height - 2) -> maxScroll = 30 - 18 = 12
		maxScroll := 30 - 18
		if m.ScrollY() > maxScroll {
			t.Errorf("ScrollY() = %d, want <= %d", m.ScrollY(), maxScroll)
		}
	})

	t.Run("cursor stays visible after delete at bottom", func(t *testing.T) {
		lines := generateLines(30)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 29}, 80, 20)

		// Delete some lines at bottom
		sendKeys(tm, "d", "d", "d", "d")

		m := getFinalModel(t, tm)
		// Cursor should still be visible
		viewportHeight := 19
		if m.CursorY() < m.ScrollY() || m.CursorY() >= m.ScrollY()+viewportHeight {
			t.Errorf("Cursor at %d not visible in viewport [%d, %d)",
				m.CursorY(), m.ScrollY(), m.ScrollY()+viewportHeight)
		}
	})
}

// Tests use terminal 80x30: viewportH=28, half-scroll=14, scrollOff=8, safe zone relY 8-19.

func TestHalfPageScrollDown(t *testing.T) {
	t.Run("ctrl+d scrolls viewport down by half", func(t *testing.T) {
		// cursor at line 15 (relY=15, in safe zone), scrollY starts at 0
		// After ctrl+d: newScrollY=14, newCursorY=14+15=29
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
		sendKeys(tm, "ctrl+d")

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

	t.Run("ctrl+d preserves cursor relative position in viewport", func(t *testing.T) {
		// relY=15 before and after
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
		sendKeys(tm, "ctrl+d")

		m := getFinalModel(t, tm)
		relY := m.CursorY() - m.ScrollY()
		if relY != 15 {
			t.Errorf("relative position = %d, want 15", relY)
		}
	})

	t.Run("ctrl+d clamps cursor to scrollOff when cursor is near top", func(t *testing.T) {
		// cursor at line 0 (relY=0 < scrollOff=8), clamp to scrollOff
		// After ctrl+d: newScrollY=14, newCursorY=14+8=22
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 30)
		sendKeys(tm, "ctrl+d")

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

	t.Run("ctrl+d at end of file does not scroll past max", func(t *testing.T) {
		// 40 lines, viewport 28: maxScroll=12
		// AdjustScroll puts cursor 35 at scrollY=12 (clamped), relY=23
		// After ctrl+d: newScrollY clamped to 12, relY=23>19 clamped to 19, newCursorY=31
		lines := generateLines(40)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 35}, 80, 30)
		sendKeys(tm, "ctrl+d")

		m := getFinalModel(t, tm)
		maxScroll := 40 - 28
		if m.ScrollY() > maxScroll {
			t.Errorf("ScrollY() = %d, want <= %d", m.ScrollY(), maxScroll)
		}
		if m.CursorY() != 31 {
			t.Errorf("CursorY() = %d, want 31", m.CursorY())
		}
	})

	t.Run("ctrl+d on file smaller than viewport does not crash", func(t *testing.T) {
		// 20 lines < viewport 28: maxScroll=0, scrollY stays 0
		// relY=0 < scrollOff, clamp to 8; newCursorY=0+8=8
		lines := generateLines(20)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 30)
		sendKeys(tm, "ctrl+d")

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

	t.Run("ctrl+d clamps cursor x to new line length", func(t *testing.T) {
		// Lines 0-14: "hello world" (11 chars), lines 15-99: "hi" (2 chars)
		// cursor at line 5, cursorX=10
		// ctrl+d: relY=5<8 clamp to 8; newScrollY=14; newCursorY=22 (a "hi" line)
		// ClampCursorX: 10 >= 2, so cursorX=2
		lines := make([]string, 100)
		for i := range 15 {
			lines[i] = "hello world"
		}
		for i := 15; i < 100; i++ {
			lines[i] = "hi"
		}
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 10, Line: 5}, 80, 30)
		sendKeys(tm, "ctrl+d")

		m := getFinalModel(t, tm)
		if m.CursorY() != 22 {
			t.Errorf("CursorY() = %d, want 22", m.CursorY())
		}
		if m.CursorX() > len(m.Line(m.CursorY())) {
			t.Errorf("CursorX() = %d exceeds line length %d", m.CursorX(), len(m.Line(m.CursorY())))
		}
	})

	t.Run("multiple ctrl+d presses scroll incrementally", func(t *testing.T) {
		// cursor at line 15, scrollY=0, relY=15
		// ctrl+d #1: scrollY=14, cursorY=29, relY=15
		// ctrl+d #2: scrollY=28, cursorY=43, relY=15
		lines := generateLines(200)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
		sendKeys(tm, "ctrl+d", "ctrl+d")

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

func TestHalfPageScrollUp(t *testing.T) {
	t.Run("ctrl+u scrolls viewport up by half", func(t *testing.T) {
		// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
		// After ctrl+u: newScrollY=17, newCursorY=17+19=36
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
		sendKeys(tm, "ctrl+u")

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

	t.Run("ctrl+u preserves cursor relative position in viewport", func(t *testing.T) {
		// relY=19 before and after
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
		sendKeys(tm, "ctrl+u")

		m := getFinalModel(t, tm)
		relY := m.CursorY() - m.ScrollY()
		if relY != 19 {
			t.Errorf("relative position = %d, want 19", relY)
		}
	})

	t.Run("ctrl+u at top of file does not make scrollY negative", func(t *testing.T) {
		// cursor at line 10, scrollY=0, relY=10 (in safe zone)
		// ctrl+u: newScrollY=max(0,-14)=0, relY=10 preserved, cursorY=10
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 10}, 80, 30)
		sendKeys(tm, "ctrl+u")

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

	t.Run("ctrl+u clamps cursor to scrollOff when cursor is near top of viewport", func(t *testing.T) {
		// cursor at line 5, scrollY=0, relY=5 < scrollOff=8
		// ctrl+u: newScrollY=0; relY clamp to 8; newCursorY=8
		lines := generateLines(100)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 5}, 80, 30)
		sendKeys(tm, "ctrl+u")

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

	t.Run("multiple ctrl+u presses scroll incrementally", func(t *testing.T) {
		// cursor at line 80: AdjustScroll -> scrollY=61, relY=19
		// ctrl+u #1: newScrollY=47, cursorY=66
		// ctrl+u #2: newScrollY=33, cursorY=52
		lines := generateLines(200)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 80}, 80, 30)
		sendKeys(tm, "ctrl+u", "ctrl+u")

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

func TestHalfPageScrollRoundTrip(t *testing.T) {
	t.Run("ctrl+d then ctrl+u returns cursor to original position", func(t *testing.T) {
		// cursor at line 15, scrollY=0, relY=15
		// ctrl+d: scrollY=14, cursorY=29, relY=15
		// ctrl+u: newScrollY=max(0,14-14)=0, cursorY=0+15=15
		lines := generateLines(200)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
		sendKeys(tm, "ctrl+d", "ctrl+u")

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

	t.Run("ctrl+u then ctrl+d returns cursor to original position", func(t *testing.T) {
		// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
		// ctrl+u: scrollY=17, cursorY=36, relY=19
		// ctrl+d: scrollY=31, cursorY=50, relY=19
		lines := generateLines(200)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
		sendKeys(tm, "ctrl+u", "ctrl+d")

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

	t.Run("alternating ctrl+d and ctrl+u maintains scroll stability", func(t *testing.T) {
		lines := generateLines(200)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
		sendKeys(tm, "ctrl+d", "ctrl+u", "ctrl+d", "ctrl+u")

		m := getFinalModel(t, tm)
		if m.ScrollY() != 0 {
			t.Errorf("ScrollY() = %d, want 0 after 2 round trips", m.ScrollY())
		}
		if m.CursorY() != 15 {
			t.Errorf("CursorY() = %d, want 15 after 2 round trips", m.CursorY())
		}
	})
}

func TestScrollWithCount(t *testing.T) {
	t.Run("5j scrolls appropriately", func(t *testing.T) {
		lines := generateLines(50)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 5}, 80, 20)

		sendKeys(tm, "1", "0", "j") // move down 10 lines

		m := getFinalModel(t, tm)
		if m.CursorY() != 15 {
			t.Errorf("CursorY() = %d, want 15", m.CursorY())
		}
		// Should have scrolled since we moved past the safe zone
		if m.ScrollY() == 0 {
			t.Errorf("ScrollY() = %d, want > 0", m.ScrollY())
		}
	})

	t.Run("5k scrolls appropriately", func(t *testing.T) {
		lines := generateLines(50)
		tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 25}, 80, 20)

		sendKeys(tm, "1", "5", "k") // move up 15 lines

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