text editor inspired vim and yi
import ViE.State.Config
import ViE.Config
import ViE.Command.Impl
import ViE.Key.Map
import ViE.State.Edit

namespace Test.CursorReproduction

open ViE

def assert (msg : String) (cond : Bool) : IO Unit := do
  if cond then
    IO.println s!"[PASS] {msg}"
  else
    IO.println s!"[FAIL] {msg}"
    throw (IO.userError s!"Assertion failed: {msg}")

-- Helper to construct a full Config
def makeTestConfig : Config := {
  settings := ViE.defaultConfig
  commands := ViE.Command.defaultCommandMap
  bindings := ViE.Key.makeKeyMap ViE.Command.defaultCommandMap
}

-- Run a sequence of keys
def runKeys (startState : EditorState) (keys : List Key) : IO EditorState := do
  let config := makeTestConfig
  let mut s := startState
  for k in keys do
    s ← ViE.update config s k
  return s

def test : IO Unit := do
  IO.println "Starting Cursor Reproduction Test..."
  let s := ViE.initialState

  -- Scenario: Insert, Undo, Insert
  let s1 := s.insertChar 'a'
  let s1 := s1.commitEdit -- Force separate undo group
  let s2 := s1.insertChar 'b'
  -- Text: 'ab', Cursor: (0, 2)

  let s3 := s2.undo
  -- Text: 'a', Cursor should be (0, 1)
  let cursor := s3.getCursor
  if cursor.col.val != 1 then
     IO.println s!"[FAIL] Undo cursor mismatch. Expected 1, got {cursor.col.val}"
     assert "Undo cursor" false
  else
     IO.println "[PASS] Undo cursor correct"

  -- Scenario: Wide character insert moves cursor by display width
  let s := ViE.initialState
  let wide := Char.ofNat 0x3042 -- Hiragana A (wide)
  let s := s.insertChar wide
  let cursor := s.getCursor
  if cursor.col.val != 2 then
     IO.println s!"[FAIL] Wide char cursor mismatch. Expected 2, got {cursor.col.val}"
     assert "Wide char cursor" false
  else
     IO.println "[PASS] Wide char cursor correct"

  let s4 := s3.insertChar 'c'
  -- Text: 'ac'
  let text := getLineFromBuffer s4.getActiveBuffer 0 |>.getD ""
  if text != "ac" then
      IO.println s!"[FAIL] Insert after undo failed. Expected 'ac', got '{text}'"
      assert "Insert after undo" false
  else
      IO.println "[PASS] Insert after undo works"

  -- Scenario: Paste, Undo, Paste (checking grouping too, but focusing on cursor)
  -- Reset
  let s := ViE.initialState
  let s := s.insertChar 'x'
  let s := s.yankCurrentLine -- Yank 'x\n'
  let _s := s.pasteBelow -- Paste 'x\n' -> 'x\nx\n' ??
  -- PasteBelow pastes line.

  -- Scenario: x command
  let s := ViE.initialState
  let s := s.insertChar 'h'
  let s := s.insertChar 'e'
  let s := s.insertChar 'y'
  -- "hey"
  let s := (s.moveCursorLeft).moveCursorLeft -- At 'h' (0,0)? No: 3 -> 2 -> 1. 'e'
  -- "hey" cursor at 2 ('y'). Left -> 1 ('e'). Left -> 0 ('h').
  -- Actually moveCursorLeft from (0,3) -> (0,2) 'y'.
  -- Let's use setCursor
  let s := s.setCursor (Point.make 0 1) -- 'e'
  let s := s.deleteCharAfterCursor -- Should delete 'e' -> "hy"
  let text := getLineFromBuffer s.getActiveBuffer 0 |>.getD ""
  if text != "hy" then
     IO.println s!"[FAIL] 'x' command (deleteCharAfterCursor) failed. Expected 'hy', got '{text}'"
     assert "x command" false
  else
     IO.println "[PASS] 'x' command works"

  -- Scenario: Insert mode transition + single-char delete keeps line count and cursor in sync.
  let s := ViE.initialState
  let s ← runKeys s [Key.char 'i', Key.enter]
  let c1 := s.getCursor
  assert "Enter in insert creates second line" (s.getActiveBuffer.lineCount == 2)
  assert "Enter in insert moves cursor to next line" (c1.row.val == 1 && c1.col.val == 0)

  let s ← runKeys s [Key.esc]
  let c2 := s.getCursor
  assert "Esc from insert keeps valid cursor row" (c2.row.val < s.getActiveBuffer.lineCount)
  assert "Esc from insert keeps row/col at second line head" (c2.row.val == 1 && c2.col.val == 0)

  -- Re-enter insert and delete one char (the previous newline) via Backspace.
  let s ← runKeys s [Key.char 'i', Key.backspace]
  let c3 := s.getCursor
  assert "Backspace at BOL joins lines" (s.getActiveBuffer.lineCount == 1)
  assert "Backspace at BOL moves cursor to joined line end" (c3.row.val == 0 && c3.col.val == 0)

  -- In normal mode x at empty line is a no-op and must keep cursor valid.
  let s ← runKeys s [Key.esc, Key.char 'x']
  let c4 := s.getCursor
  assert "x on empty line keeps line count" (s.getActiveBuffer.lineCount == 1)
  assert "x on empty line keeps cursor in range" (c4.row.val < s.getActiveBuffer.lineCount)

  IO.println "Cursor Reproduction Test Finished"

end Test.CursorReproduction