text editor inspired vim and yi
import ViE.State.Config
import ViE.State.Layout
import ViE.Workspace
import ViE.Command.Impl
import ViE.App
import ViE.Buffer.Content
import ViE.Basic
import Test.Utils

set_option maxRecDepth 2048

namespace Test.Workspace

open Test.Utils
open ViE

def addWorkspace (state : EditorState) (ws : WorkspaceState) : EditorState :=
  state.updateCurrentWorkgroup fun wg =>
    { wg with workspaces := wg.workspaces.push ws }

def switchWorkspace (state : EditorState) (idx : Nat) : EditorState :=
  state.updateCurrentWorkgroup fun wg =>
    if idx < wg.workspaces.size then
      { wg with currentWorkspace := idx }
    else
      wg

def test : IO Unit := do
  IO.println "Starting Workspace Test..."

  let s0 := { ViE.initialState with windowHeight := 40, windowWidth := 120 }
  let wg0 := s0.getCurrentWorkgroup
  assertEqual "Initial workgroups size" 10 s0.workgroups.size
  assertEqual "Initial workgroup count" 1 wg0.workspaces.size
  assertEqual "Initial workspace index" 0 wg0.currentWorkspace
  assertEqual "Initial workspace name" "ViE" s0.getCurrentWorkspace.name

  let ws1 := makeWorkspaceState "Project A" (some "/tmp/project-a") 10
  let s1 := addWorkspace s0 ws1
  let wg1 := s1.getCurrentWorkgroup
  assertEqual "Workgroup has 2 workspaces" 2 wg1.workspaces.size

  let s2 := switchWorkspace s1 1
  assertEqual "Switched to workspace 1" "Project A" s2.getCurrentWorkspace.name
  assertEqual "Active buffer comes from workspace 1" 10 (s2.getActiveBuffer.id)

  let resolved := s2.getCurrentWorkspace.resolvePath "file.txt"
  assertEqual "ResolvePath uses workspace root" "/tmp/project-a/file.txt" resolved

  let resolvedAbs := s2.getCurrentWorkspace.resolvePath "/abs/file.txt"
  assertEqual "ResolvePath keeps absolute path" "/abs/file.txt" resolvedAbs

  IO.println "Starting Workspace Command Test..."
  let s3 ← ViE.Command.cmdWs ["new", "Project B", "/tmp/project-b"] s2
  assertEqual "Workspace created and switched" "Project B" s3.getCurrentWorkspace.name
  assertEqual "Workspace rootPath set" (some "/tmp/project-b") s3.getCurrentWorkspace.rootPath
  assertEqual "Active buffer id is new workspace buffer" 11 s3.getActiveBuffer.id

  let s4 ← ViE.Command.cmdWs ["rename", "Project B2"] s3
  assertEqual "Workspace rename applied" "Project B2" s4.getCurrentWorkspace.name

  let s5 ← ViE.Command.cmdWs ["new", "Project C"] s4
  assertEqual "Workspace C created" "Project C" s5.getCurrentWorkspace.name

  let s6 ← ViE.Command.cmdWs ["prev"] s5
  assertEqual "Workspace prev switches back" "Project B2" s6.getCurrentWorkspace.name

  let s7 ← ViE.Command.cmdWs ["0"] s6
  assertEqual "Workspace switch by index" "ViE" s7.getCurrentWorkspace.name

  let s8 ← ViE.Command.cmdWs ["list"] s7
  assertEqual "Workspace list opens explorer buffer" (some "explorer://workgroup") s8.getActiveBuffer.filename
  let ws8 := s8.getCurrentWorkspace
  assertEqual "Workspace explorer opens in floating window" true (ws8.isFloatingWindow ws8.activeWindowId)
  let wsListText := s8.getActiveBuffer.table.toString
  assertEqual "Workspace list contains New Workspace entry" true (wsListText.contains "[New Workspace]")
  assertEqual "Workspace list contains Rename Workspace entry" true (wsListText.contains "[Rename Workspace]")
  assertEqual "Workspace list contains Project B2" true (wsListText.contains "Project B2")
  assertEqual "Workspace list marks current workspace" true (wsListText.contains "*")

  let s8a := s8.updateActiveView fun v => { v with cursor := {row := ⟨2⟩, col := 0} }
  let s8b ← ViE.Feature.handleExplorerEnter s8a
  assertEqual "New workspace opens floating input" true s8b.floatingOverlay.isSome
  assertEqual "New workspace floating command prefix" (some "ws new ") s8b.floatingInputCommand

  let s8c := s8.updateActiveView fun v => { v with cursor := {row := ⟨3⟩, col := 0} }
  let s8d ← ViE.Feature.handleExplorerEnter s8c
  assertEqual "Rename workspace opens floating input" true s8d.floatingOverlay.isSome
  assertEqual "Rename workspace floating command prefix" (some "ws rename ") s8d.floatingInputCommand

  let s8e ← ViE.Command.cmdWorkspace ["rename", ""] s8
  assertEqual "Rename workspace rejects empty" "Workspace name cannot be empty" s8e.message

  let s8f ← ViE.Command.cmdWorkspace ["rename", "Project B2"] s8
  assertEqual "Rename workspace rejects duplicate" true (s8f.message.contains "already exists")

  let s9 ← ViE.Command.cmdWs ["open", "/tmp/project-d"] s8
  assertEqual "Workspace open uses path name" "project-d" s9.getCurrentWorkspace.name
  assertEqual "Workspace open rootPath set" (some "/tmp/project-d") s9.getCurrentWorkspace.rootPath

  let s10 ← ViE.Command.cmdWs ["open", "--name", "Project E", "/tmp/project-e"] s9
  assertEqual "Workspace open --name prefix" "Project E" s10.getCurrentWorkspace.name
  assertEqual "Workspace open --name prefix rootPath" (some "/tmp/project-e") s10.getCurrentWorkspace.rootPath

  let s11 ← ViE.Command.cmdWs ["open", "/tmp/project-f", "--name", "Project F"] s10
  assertEqual "Workspace open --name suffix" "Project F" s11.getCurrentWorkspace.name
  assertEqual "Workspace open --name suffix rootPath" (some "/tmp/project-f") s11.getCurrentWorkspace.rootPath

  let s12 ← ViE.Command.cmdWs ["close"] s11
  assertEqual "Workspace close switches to previous" "Project E" s12.getCurrentWorkspace.name

  IO.println "Starting Workspace Explorer Preview Test..."
  let bufP1 : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 1 (some "/tmp/a.txt") #[stringToLine "A"]
  let bufP2 : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 2 (some "/tmp/b.txt") #[stringToLine "B"]
  let wsP : WorkspaceState := {
    name := "WS-P"
    rootPath := none
    buffers := [bufP1, bufP2]
    nextBufferId := 3
    layout := .window 0 { initialView with bufferId := 1 }
    activeWindowId := 0
    nextWindowId := 1
  }
  let s12a := addWorkspace s12 wsP
  let wgP := s12a.getCurrentWorkgroup
  let idxP := if wgP.workspaces.size == 0 then 0 else wgP.workspaces.size - 1
  let s12b := switchWorkspace s12a idxP
  let s12c ← ViE.Feature.openWorkspaceExplorer s12b
  let explorerOpt := s12c.explorers.find? (fun (id, _) => id == s12c.getActiveBuffer.id)
  match explorerOpt with
  | none => throw (IO.userError "Workspace explorer not registered")
  | some (_, explorer) =>
    assertEqual "Workspace explorer preview window exists" true explorer.previewWindowId.isSome
    let previewWinId := explorer.previewWindowId.get!
    let wsPrev := s12c.getCurrentWorkspace
    assertEqual "Workspace explorer preview window is floating" true (wsPrev.isFloatingWindow previewWinId)
    let pairSideBySide :=
      match s12c.getFloatingWindowBounds wsPrev.activeWindowId, s12c.getFloatingWindowBounds previewWinId with
      | some (et, el, eh, ew), some (pt, pl, ph, pw) =>
          et == pt && eh == ph && ((el + ew <= pl) || (pl + pw <= el))
      | _, _ => false
    assertEqual "Workspace explorer/preview floating pair is side-by-side" true pairSideBySide
    let previewView := wsPrev.layout.findView previewWinId |>.getD initialView
    let previewBuf := wsPrev.buffers.find? (fun b => b.id == previewView.bufferId) |>.getD initialBuffer
    let previewText := previewBuf.table.toString
    assertEqual "Workspace preview includes a.txt" true (previewText.contains "/tmp/a.txt")
    assertEqual "Workspace preview includes b.txt" true (previewText.contains "/tmp/b.txt")

  IO.println "Starting Workspace Restore Test..."
  let bufA : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 0 none #[stringToLine "WS-A"]
  let bufB : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 10 none #[stringToLine "WS-B"]
  let wsA : WorkspaceState := {
    name := "WS-A"
    rootPath := none
    buffers := [bufA]
    nextBufferId := 1
    layout := .window 0 { initialView with bufferId := 0 }
    activeWindowId := 0
    nextWindowId := 1
  }
  let wsB : WorkspaceState := {
    name := "WS-B"
    rootPath := none
    buffers := [bufB]
    nextBufferId := 11
    layout := .window 0 { initialView with bufferId := 10 }
    activeWindowId := 0
    nextWindowId := 1
  }
  let s12a := s12.updateCurrentWorkgroup fun wg =>
    { wg with workspaces := #[wsA, wsB], currentWorkspace := 0 }
  let s12b ← ViE.Feature.openWorkspaceExplorer s12a
  let s12c := s12b.updateActiveView fun v => { v with cursor := {row := ⟨5⟩, col := 0} }
  let s12d ← ViE.Feature.handleExplorerEnter s12c
  assertEqual "Workspace selection switches current workspace" "WS-B" s12d.getCurrentWorkspace.name
  assertEqual "Workspace selection restores active buffer" "WS-B" s12d.getActiveBuffer.table.toString

  IO.println "Starting Workspace Restore Layout Test..."
  let bufC : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 20 none #[stringToLine "WS-C1"]
  let bufD : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 21 none #[stringToLine "WS-C2"]
  let layoutC : Layout :=
    .hsplit
      (.window 0 { initialView with bufferId := 20 })
      (.window 1 { initialView with bufferId := 21, cursor := ⟨⟨0⟩, ⟨2⟩⟩ })
      0.3
  let wsC : WorkspaceState := {
    name := "WS-C"
    rootPath := none
    buffers := [bufC, bufD]
    nextBufferId := 22
    layout := layoutC
    activeWindowId := 1
    nextWindowId := 2
  }
  let s12e := s12.updateCurrentWorkgroup fun wg =>
    { wg with workspaces := #[wsA, wsC], currentWorkspace := 0 }
  let s12f ← ViE.Feature.openWorkspaceExplorer s12e
  let s12g := s12f.updateActiveView fun v => { v with cursor := {row := ⟨5⟩, col := 0} }
  let s12h ← ViE.Feature.handleExplorerEnter s12g
  assertEqual "Workspace layout restore (active buffer)" "WS-C2" s12h.getActiveBuffer.table.toString
  let ratio := match s12h.getCurrentWorkspace.layout with
    | .hsplit _ _ r => r
    | _ => 0.0
  assertEqual "Workspace layout restore (ratio)" 0.3 ratio
  let cursor := s12h.getCursor
  assertEqual "Workspace layout restore (cursor row)" 0 cursor.row.val
  assertEqual "Workspace layout restore (cursor col)" 2 cursor.col.val

  IO.println "Starting Workspace Restore VSplit Test..."
  let bufE : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 30 none #[stringToLine "WS-D1"]
  let bufF : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 31 none #[stringToLine "WS-D2"]
  let layoutD : Layout :=
    .vsplit
      (.window 0 { initialView with bufferId := 30, cursor := ⟨⟨1⟩, ⟨0⟩⟩, scrollRow := ⟨1⟩, scrollCol := ⟨2⟩ })
      (.window 1 { initialView with bufferId := 31, cursor := ⟨⟨2⟩, ⟨3⟩⟩, scrollRow := ⟨0⟩, scrollCol := ⟨1⟩ })
      0.7
  let wsD : WorkspaceState := {
    name := "WS-D"
    rootPath := none
    buffers := [bufE, bufF]
    nextBufferId := 32
    layout := layoutD
    activeWindowId := 1
    nextWindowId := 2
  }
  let s12i := s12.updateCurrentWorkgroup fun wg =>
    { wg with workspaces := #[wsA, wsC, wsD], currentWorkspace := 0 }
  let s12j ← ViE.Feature.openWorkspaceExplorer s12i
  let s12k := s12j.updateActiveView fun v => { v with cursor := {row := ⟨6⟩, col := 0} }
  let s12l ← ViE.Feature.handleExplorerEnter s12k
  assertEqual "Workspace vsplit restore (active buffer)" "WS-D2" s12l.getActiveBuffer.table.toString
  let ratio2 := match s12l.getCurrentWorkspace.layout with
    | .vsplit _ _ r => r
    | _ => 0.0
  assertEqual "Workspace vsplit restore (ratio)" 0.7 ratio2
  let cursor2 := s12l.getCursor
  assertEqual "Workspace vsplit restore (cursor row)" 2 cursor2.row.val
  assertEqual "Workspace vsplit restore (cursor col)" 3 cursor2.col.val
  let (sRow, sCol) := s12l.getScroll
  assertEqual "Workspace vsplit restore (scroll row)" 0 sRow.val
  assertEqual "Workspace vsplit restore (scroll col)" 1 sCol.val

  IO.println "Starting Workspace Multi-View Restore Test..."
  let bufG : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 40 none #[stringToLine "WS-E1"]
  let bufH : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 41 none #[stringToLine "WS-E2"]
  let bufI : FileBuffer := ViE.Buffer.fileBufferFromTextBuffer 42 none #[stringToLine "WS-E3"]
  let layoutE : Layout :=
    .hsplit
      (.vsplit
        (.window 0 { initialView with bufferId := 40, cursor := ⟨⟨0⟩, ⟨1⟩⟩, scrollRow := ⟨0⟩, scrollCol := ⟨0⟩ })
        (.window 1 { initialView with bufferId := 41, cursor := ⟨⟨2⟩, ⟨2⟩⟩, scrollRow := ⟨1⟩, scrollCol := ⟨1⟩ })
        0.4)
      (.window 2 { initialView with bufferId := 42, cursor := ⟨⟨3⟩, ⟨0⟩⟩, scrollRow := ⟨2⟩, scrollCol := ⟨2⟩ })
      0.6
  let wsE : WorkspaceState := {
    name := "WS-E"
    rootPath := none
    buffers := [bufG, bufH, bufI]
    nextBufferId := 43
    layout := layoutE
    activeWindowId := 1
    nextWindowId := 3
  }
  let s12m := s12.updateCurrentWorkgroup fun wg =>
    { wg with workspaces := #[wsA, wsC, wsD, wsE], currentWorkspace := 0 }
  let s12n ← ViE.Feature.openWorkspaceExplorer s12m
  let s12o := s12n.updateActiveView fun v => { v with cursor := {row := ⟨7⟩, col := 0} }
  let s12p ← ViE.Feature.handleExplorerEnter s12o
  assertEqual "Multi-view restore (active buffer)" "WS-E2" s12p.getActiveBuffer.table.toString
  let (sr2, sc2) := s12p.getScroll
  assertEqual "Multi-view restore (scroll row)" 1 sr2.val
  assertEqual "Multi-view restore (scroll col)" 1 sc2.val
  let cursor3 := s12p.getCursor
  assertEqual "Multi-view restore (cursor row)" 2 cursor3.row.val
  assertEqual "Multi-view restore (cursor col)" 2 cursor3.col.val
  assertEqual "Multi-view restore (layout hsplit ratio)" 0.6 (match s12p.getCurrentWorkspace.layout with | .hsplit _ _ r => r | _ => 0.0)
  let subRatio := match s12p.getCurrentWorkspace.layout with
    | .hsplit left _ _ =>
        match left with
        | .vsplit _ _ r => r
        | _ => 0.0
    | _ => 0.0
  assertEqual "Multi-view restore (layout vsplit ratio)" 0.4 subRatio

  IO.println "Starting Workspace Startup Target Test..."
  let base := "Test/test_paths"

  let dirOnly := s!"{base}/dir0"
  let (wsDirOnly, fileDirOnly) ← ViE.resolveStartupTarget (some dirOnly)
  assertEqual "Directory arg sets workspace" (some dirOnly) wsDirOnly
  assertEqual "Directory arg has no file" none fileDirOnly

  let fileNested := s!"{base}/dir0/dir1/dir2/file0.txt"
  let (wsFileNested, fileFileNested) ← ViE.resolveStartupTarget (some fileNested)
  assertEqual "File arg sets workspace to parent dir" (some s!"{base}/dir0/dir1/dir2") wsFileNested
  assertEqual "File arg keeps filename" (some fileNested) fileFileNested

  let fileTop := s!"{base}/file0.txt"
  let (wsFileTop, fileFileTop) ← ViE.resolveStartupTarget (some fileTop)
  assertEqual "Top-level file sets workspace to base" (some base) wsFileTop
  assertEqual "Top-level file keeps filename" (some fileTop) fileFileTop

  IO.println "Starting Explorer Path Resolution Test..."
  let s13 := s12.updateCurrentWorkspace fun ws =>
    { ws with rootPath := some "Test", name := "Test" }
  let s14 ← ViE.Feature.openExplorer s13 "Test"
  assertEqual "Explorer opens workspace root without duplication" (some "explorer://Test") s14.getActiveBuffer.filename

  IO.println "WorkspaceTest passed!"

end Test.Workspace