" Tests for the undo tree.
" Since this script is sourced we need to explicitly break changes up in
" undo-able pieces.  Do that by setting 'undolevels'.
" Also tests :earlier and :later.

func Test_undotree()
  new

  normal! Aabc
  set ul=100
  let d = undotree()
  call assert_equal(1, d.seq_last)
  call assert_equal(1, d.seq_cur)
  call assert_equal(0, d.save_last)
  call assert_equal(0, d.save_cur)
  call assert_equal(1, len(d.entries))
  call assert_equal(1, d.entries[0].newhead)
  call assert_equal(1, d.entries[0].seq)
  call assert_true(d.entries[0].time <= d.time_cur)

  normal! Adef
  set ul=100
  let d = undotree()
  call assert_equal(2, d.seq_last)
  call assert_equal(2, d.seq_cur)
  call assert_equal(0, d.save_last)
  call assert_equal(0, d.save_cur)
  call assert_equal(2, len(d.entries))
  call assert_equal(1, d.entries[0].seq)
  call assert_equal(1, d.entries[1].newhead)
  call assert_equal(2, d.entries[1].seq)
  call assert_true(d.entries[1].time <= d.time_cur)

  undo
  set ul=100
  let d = undotree()
  call assert_equal(2, d.seq_last)
  call assert_equal(1, d.seq_cur)
  call assert_equal(0, d.save_last)
  call assert_equal(0, d.save_cur)
  call assert_equal(2, len(d.entries))
  call assert_equal(1, d.entries[0].seq)
  call assert_equal(1, d.entries[1].curhead)
  call assert_equal(1, d.entries[1].newhead)
  call assert_equal(2, d.entries[1].seq)
  call assert_true(d.entries[1].time == d.time_cur)

  normal! Aghi
  set ul=100
  let d = undotree()
  call assert_equal(3, d.seq_last)
  call assert_equal(3, d.seq_cur)
  call assert_equal(0, d.save_last)
  call assert_equal(0, d.save_cur)
  call assert_equal(2, len(d.entries))
  call assert_equal(1, d.entries[0].seq)
  call assert_equal(2, d.entries[1].alt[0].seq)
  call assert_equal(1, d.entries[1].newhead)
  call assert_equal(3, d.entries[1].seq)
  call assert_true(d.entries[1].time <= d.time_cur)

  undo
  set ul=100
  let d = undotree()
  call assert_equal(3, d.seq_last)
  call assert_equal(1, d.seq_cur)
  call assert_equal(0, d.save_last)
  call assert_equal(0, d.save_cur)
  call assert_equal(2, len(d.entries))
  call assert_equal(1, d.entries[0].seq)
  call assert_equal(2, d.entries[1].alt[0].seq)
  call assert_equal(1, d.entries[1].curhead)
  call assert_equal(1, d.entries[1].newhead)
  call assert_equal(3, d.entries[1].seq)
  call assert_true(d.entries[1].time == d.time_cur)

  w! Xtest
  let d = undotree()
  call assert_equal(1, d.save_cur)
  call assert_equal(1, d.save_last)
  call delete('Xtest')
  bwipe! Xtest
endfunc

func FillBuffer()
  for i in range(1,13)
    put=i
    " Set 'undolevels' to split undo.
    exe "setg ul=" . &g:ul
  endfor
endfunc

func Test_global_local_undolevels()
  new one
  set undolevels=5
  call FillBuffer()
  " will only undo the last 5 changes, end up with 13 - (5 + 1) = 7 lines
  earlier 10
  call assert_equal(5, &g:undolevels)
  call assert_equal(-123456, &l:undolevels)
  call assert_equal('7', getline('$'))

  new two
  setlocal undolevels=2
  call FillBuffer()
  " will only undo the last 2 changes, end up with 13 - (2 + 1) = 10 lines
  earlier 10
  call assert_equal(5, &g:undolevels)
  call assert_equal(2, &l:undolevels)
  call assert_equal('10', getline('$'))

  setlocal ul=10
  call assert_equal(5, &g:undolevels)
  call assert_equal(10, &l:undolevels)

  " Setting local value in "two" must not change local value in "one"
  wincmd p
  call assert_equal(5, &g:undolevels)
  call assert_equal(-123456, &l:undolevels)

  new three
  setglobal ul=50
  call assert_equal(50, &g:undolevels)
  call assert_equal(-123456, &l:undolevels)

  " Drop created windows
  set ul&
  new
  only!
endfunc

func BackOne(expected)
  call feedkeys('g-', 'xt')
  call assert_equal(a:expected, getline(1))
endfunc

func Test_undo_del_chars()
  " Setup a buffer without creating undo entries
  new
  set ul=-1
  call setline(1, ['123-456'])
  set ul=100
  1
  call test_settime(100)

  " Delete three characters and undo with g-
  call feedkeys('x', 'xt')
  call feedkeys('x', 'xt')
  call feedkeys('x', 'xt')
  call assert_equal('-456', getline(1))
  call BackOne('3-456')
  call BackOne('23-456')
  call BackOne('123-456')
  call assert_fails("BackOne('123-456')")

  :" Delete three other characters and go back in time with g-
  call feedkeys('$x', 'xt')
  call feedkeys('x', 'xt')
  call feedkeys('x', 'xt')
  call assert_equal('123-', getline(1))
  call test_settime(101)

  call BackOne('123-4')
  call BackOne('123-45')
  " skips '123-456' because it's older
  call BackOne('-456')
  call BackOne('3-456')
  call BackOne('23-456')
  call BackOne('123-456')
  call assert_fails("BackOne('123-456')")
  normal 10g+
  call assert_equal('123-', getline(1))

  :" Jump two seconds and go some seconds forward and backward
  call test_settime(103)
  call feedkeys("Aa\<Esc>", 'xt')
  call feedkeys("Ab\<Esc>", 'xt')
  call feedkeys("Ac\<Esc>", 'xt')
  call assert_equal('123-abc', getline(1))
  earlier 1s
  call assert_equal('123-', getline(1))
  earlier 3s
  call assert_equal('123-456', getline(1))
  later 1s
  call assert_equal('123-', getline(1))
  later 1h
  call assert_equal('123-abc', getline(1))

  close!
endfunc

func Test_undolist()
  new
  set ul=100

  let a = execute('undolist')
  call assert_equal("\nNothing to undo", a)

  " 1 leaf (2 changes).
  call feedkeys('achange1', 'xt')
  call feedkeys('achange2', 'xt')
  let a = execute('undolist')
  call assert_match("^\nnumber changes  when  *saved\n *2  *2 .*$", a)

  " 2 leaves.
  call feedkeys('u', 'xt')
  call feedkeys('achange3\<Esc>', 'xt')
  let a = execute('undolist')
  call assert_match("^\nnumber changes  when  *saved\n *2  *2  *.*\n *3  *2 .*$", a)
  close!
endfunc

func Test_U_command()
  new
  set ul=100
  call feedkeys("achange1\<Esc>", 'xt')
  call feedkeys("achange2\<Esc>", 'xt')
  norm! U
  call assert_equal('', getline(1))
  norm! U
  call assert_equal('change1change2', getline(1))
  close!
endfunc

func Test_undojoin()
  new
  call feedkeys("Goaaaa\<Esc>", 'xt')
  call feedkeys("obbbb\<Esc>", 'xt')
  call assert_equal(['aaaa', 'bbbb'], getline(2, '$'))
  call feedkeys("u", 'xt')
  call assert_equal(['aaaa'], getline(2, '$'))
  call feedkeys("obbbb\<Esc>", 'xt')
  undojoin
  " Note: next change must not be as if typed
  call feedkeys("occcc\<Esc>", 'x')
  call assert_equal(['aaaa', 'bbbb', 'cccc'], getline(2, '$'))
  call feedkeys("u", 'xt')
  call assert_equal(['aaaa'], getline(2, '$'))
  bwipe!
endfunc

func Test_undojoin_redo()
  new
  call setline(1, ['first line', 'second line'])
  call feedkeys("ixx\<Esc>", 'xt')
  call feedkeys(":undojoin | redo\<CR>", 'xt')
  call assert_equal('xxfirst line', getline(1))
  call assert_equal('second line', getline(2))
  bwipe!
endfunc

" undojoin not allowed after undo
func Test_undojoin_after_undo()
  new
  call feedkeys("ixx\<Esc>u", 'xt')
  call assert_fails(':undojoin', 'E790:')
  bwipe!
endfunc

" undojoin is a noop when no change yet, or when 'undolevels' is negative
func Test_undojoin_noop()
  new
  call feedkeys(":undojoin\<CR>", 'xt')
  call assert_equal([''], getline(1, '$'))
  setlocal undolevels=-1
  call feedkeys("ixx\<Esc>u", 'xt')
  call feedkeys(":undojoin\<CR>", 'xt')
  call assert_equal(['xx'], getline(1, '$'))
  bwipe!
endfunc

func Test_undo_write()
  call delete('Xtest')
  split Xtest
  call feedkeys("ione one one\<Esc>", 'xt')
  w!
  call feedkeys("otwo\<Esc>", 'xt')
  call feedkeys("otwo\<Esc>", 'xt')
  w
  call feedkeys("othree\<Esc>", 'xt')
  call assert_equal(['one one one', 'two', 'two', 'three'], getline(1, '$'))
  earlier 1f
  call assert_equal(['one one one', 'two', 'two'], getline(1, '$'))
  earlier 1f
  call assert_equal(['one one one'], getline(1, '$'))
  earlier 1f
  call assert_equal([''], getline(1, '$'))
  later 1f
  call assert_equal(['one one one'], getline(1, '$'))
  later 1f
  call assert_equal(['one one one', 'two', 'two'], getline(1, '$'))
  later 1f
  call assert_equal(['one one one', 'two', 'two', 'three'], getline(1, '$'))

  close!
  call delete('Xtest')
  bwipe! Xtest
endfunc

func Test_insert_expr()
  new
  " calling setline() triggers undo sync
  call feedkeys("oa\<Esc>", 'xt')
  call feedkeys("ob\<Esc>", 'xt')
  set ul=100
  call feedkeys("o1\<Esc>a2\<C-R>=setline('.','1234')\<CR>\<CR>\<Esc>", 'x')
  call assert_equal(['a', 'b', '120', '34'], getline(2, '$'))
  call feedkeys("u", 'x')
  call assert_equal(['a', 'b', '12'], getline(2, '$'))
  call feedkeys("u", 'x')
  call assert_equal(['a', 'b'], getline(2, '$'))

  call feedkeys("oc\<Esc>", 'xt')
  set ul=100
  call feedkeys("o1\<Esc>a2\<C-R>=setline('.','1234')\<CR>\<CR>\<Esc>", 'x')
  call assert_equal(['a', 'b', 'c', '120', '34'], getline(2, '$'))
  call feedkeys("u", 'x')
  call assert_equal(['a', 'b', 'c', '12'], getline(2, '$'))

  call feedkeys("od\<Esc>", 'xt')
  set ul=100
  call feedkeys("o1\<Esc>a2\<C-R>=string(123)\<CR>\<Esc>", 'x')
  call assert_equal(['a', 'b', 'c', '12', 'd', '12123'], getline(2, '$'))
  call feedkeys("u", 'x')
  call assert_equal(['a', 'b', 'c', '12', 'd'], getline(2, '$'))

  close!
endfunc

func Test_undofile_earlier()
  " Issue #1254
  " create undofile with timestamps older than Vim startup time.
  let t0 = localtime() - 43200
  call test_settime(t0)
  new Xfile
  call feedkeys("ione\<Esc>", 'xt')
  set ul=100
  call test_settime(t0 + 1)
  call feedkeys("otwo\<Esc>", 'xt')
  set ul=100
  call test_settime(t0 + 2)
  call feedkeys("othree\<Esc>", 'xt')
  set ul=100
  w
  wundo Xundofile
  bwipe!
  " restore normal timestamps.
  call test_settime(0)
  new Xfile
  rundo Xundofile
  earlier 1d
  call assert_equal('', getline(1))
  bwipe!
  call delete('Xfile')
  call delete('Xundofile')
endfunc

func Test_wundo_errors()
  new
  call setline(1, 'hello')
  call assert_fails('wundo! Xdoesnotexist/Xundofile', 'E828:')
  bwipe!
endfunc

" Check that reading a truncated undo file doesn't hang.
func Test_undofile_truncated()
  new
  call setline(1, 'hello')
  set ul=100
  wundo Xundofile
  let contents = readfile('Xundofile', 'B')

  " try several sizes
  for size in range(20, 500, 33)
    call writefile(contents[0:size], 'Xundofile')
    call assert_fails('rundo Xundofile', 'E825:')
  endfor

  bwipe!
  call delete('Xundofile')
endfunc

func Test_rundo_errors()
  call assert_fails('rundo XfileDoesNotExist', 'E822:')

  call writefile(['abc'], 'Xundofile')
  call assert_fails('rundo Xundofile', 'E823:')

  call delete('Xundofile')
endfunc

" Test for undo working properly when executing commands from a register.
" Also test this in an empty buffer.
func Test_cmd_in_reg_undo()
  enew!
  let @a = "Ox\<Esc>jAy\<Esc>kdd"
  edit +/^$ test_undo.vim
  normal @au
  call assert_equal(0, &modified)
  return
  new
  normal @au
  call assert_equal(0, &modified)
  only!
  let @a = ''
endfunc

" This used to cause an illegal memory access
func Test_undo_append()
  new
  call feedkeys("axx\<Esc>v", 'xt')
  undo
  norm o
  quit
endfunc

func Test_undo_0()
  new
  set ul=100
  normal i1
  undo
  normal i2
  undo
  normal i3

  undo 0
  let d = undotree()
  call assert_equal('', getline(1))
  call assert_equal(0, d.seq_cur)

  redo
  let d = undotree()
  call assert_equal('3', getline(1))
  call assert_equal(3, d.seq_cur)

  undo 2
  undo 0
  let d = undotree()
  call assert_equal('', getline(1))
  call assert_equal(0, d.seq_cur)

  redo
  let d = undotree()
  call assert_equal('2', getline(1))
  call assert_equal(2, d.seq_cur)

  undo 1
  undo 0
  let d = undotree()
  call assert_equal('', getline(1))
  call assert_equal(0, d.seq_cur)

  redo
  let d = undotree()
  call assert_equal('1', getline(1))
  call assert_equal(1, d.seq_cur)

  bwipe!
endfunc

" undo or redo are noop if there is nothing to undo or redo
func Test_undo_redo_noop()
  new
  call assert_fails('undo 2', 'E830:')

  message clear
  undo
  let messages = split(execute('message'), "\n")
  call assert_equal('Already at oldest change', messages[-1])

  message clear
  redo
  let messages = split(execute('message'), "\n")
  call assert_equal('Already at newest change', messages[-1])

  bwipe!
endfunc

func Test_redo_empty_line()
  new
  exe "norm\x16r\x160"
  exe "norm."
  bwipe!
endfunc

funct Test_undofile()
  " Test undofile() without setting 'undodir'.
  if has('persistent_undo')
    call assert_equal(fnamemodify('.Xundofoo.un~', ':p'), undofile('Xundofoo'))
  else
    call assert_equal('', undofile('Xundofoo'))
  endif
  call assert_equal('', undofile(''))

  " Test undofile() with 'undodir' set to to an existing directory.
  call mkdir('Xundodir')
  set undodir=Xundodir
  let cwd = getcwd()
  if has('win32')
    " Replace windows drive such as C:... into C%...
    let cwd = substitute(cwd, '^\([a-zA-Z]\):', '\1%', 'g')
  endif
  let cwd = substitute(cwd . '/Xundofoo', '/', '%', 'g')
  if has('persistent_undo')
    call assert_equal('Xundodir/' . cwd, undofile('Xundofoo'))
  else
    call assert_equal('', undofile('Xundofoo'))
  endif
  call assert_equal('', undofile(''))
  call delete('Xundodir', 'd')

  " Test undofile() with 'undodir' set to a non-existing directory.
  call assert_equal('', 'Xundofoo'->undofile())

  if !has('win32') && isdirectory('/tmp')
    set undodir=/tmp
    if has('osx')
      call assert_equal('/tmp/%private%tmp%file', undofile('///tmp/file'))
    else
      call assert_equal('/tmp/%tmp%file', undofile('///tmp/file'))
    endif
  endif

  set undodir&
endfunc

" Tests for the undo file
" Explicitly break changes up in undo-able pieces by setting 'undolevels'.
func Test_undofile_2()
  set undolevels=100 undofile
  edit Xtestfile
  call append(0, 'this is one line')
  call cursor(1, 1)

  " first a simple one-line change.
  set undolevels=100
  s/one/ONE/
  set undolevels=100
  write
  bwipe!
  edit Xtestfile
  undo
  call assert_equal('this is one line', getline(1))

  " change in original file fails check
  set noundofile
  edit! Xtestfile
  s/line/Line/
  write
  set undofile
  bwipe!
  edit Xtestfile
  undo
  call assert_equal('this is ONE Line', getline(1))

  " add 10 lines, delete 6 lines, undo 3
  set undofile
  call setbufline(0, 1, ['one', 'two', 'three', 'four', 'five', 'six',
	      \ 'seven', 'eight', 'nine', 'ten'])
  set undolevels=100
  normal 3Gdd
  set undolevels=100
  normal dd
  set undolevels=100
  normal dd
  set undolevels=100
  normal dd
  set undolevels=100
  normal dd
  set undolevels=100
  normal dd
  set undolevels=100
  write
  bwipe!
  edit Xtestfile
  normal uuu
  call assert_equal(['one', 'two', 'six', 'seven', 'eight', 'nine', 'ten'],
	      \ getline(1, '$'))

  " Test that reading the undofiles when setting undofile works
  set noundofile undolevels=0
  exe "normal i\n"
  undo
  edit! Xtestfile
  set undofile undolevels=100
  normal uuuuuu
  call assert_equal(['one', 'two', 'three', 'four', 'five', 'six', 'seven',
	      \ 'eight', 'nine', 'ten'], getline(1, '$'))

  bwipe!
  call delete('Xtestfile')
  let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
  call delete(ufile)
  set undofile& undolevels&
endfunc

" Test 'undofile' using a file encrypted with 'zip' crypt method
func Test_undofile_cryptmethod_zip()
  edit Xtestfile
  set undofile cryptmethod=zip
  call append(0, ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
  call cursor(5, 1)

  set undolevels=100
  normal kkkdd
  set undolevels=100
  normal dd
  set undolevels=100
  normal dd
  set undolevels=100
  " encrypt the file using key 'foobar'
  call feedkeys("foobar\nfoobar\n")
  X
  write!
  bwipe!

  call feedkeys("foobar\n")
  edit Xtestfile
  set key=
  normal uu
  call assert_equal(['monday', 'wednesday', 'thursday', 'friday', ''],
                    \ getline(1, '$'))

  bwipe!
  call delete('Xtestfile')
  let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
  call delete(ufile)
  set undofile& undolevels& cryptmethod&
endfunc

" Test 'undofile' using a file encrypted with 'blowfish' crypt method
func Test_undofile_cryptmethod_blowfish()
  edit Xtestfile
  set undofile cryptmethod=blowfish
  call append(0, ['jan', 'feb', 'mar', 'apr', 'jun'])
  call cursor(5, 1)

  set undolevels=100
  exe 'normal kk0ifoo '
  set undolevels=100
  normal dd
  set undolevels=100
  exe 'normal ibar '
  set undolevels=100
  " encrypt the file using key 'foobar'
  call feedkeys("foobar\nfoobar\n")
  X
  write!
  bwipe!

  call feedkeys("foobar\n")
  edit Xtestfile
  set key=
  call search('bar')
  call assert_equal('bar apr', getline('.'))
  undo
  call assert_equal('apr', getline('.'))
  undo
  call assert_equal('foo mar', getline('.'))
  undo
  call assert_equal('mar', getline('.'))

  bwipe!
  call delete('Xtestfile')
  let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
  call delete(ufile)
  set undofile& undolevels& cryptmethod&
endfunc

" Test 'undofile' using a file encrypted with 'blowfish2' crypt method
func Test_undofile_cryptmethod_blowfish2()
  edit Xtestfile
  set undofile cryptmethod=blowfish2
  call append(0, ['jan', 'feb', 'mar', 'apr', 'jun'])
  call cursor(5, 1)

  set undolevels=100
  exe 'normal kk0ifoo '
  set undolevels=100
  normal dd
  set undolevels=100
  exe 'normal ibar '
  set undolevels=100
  " encrypt the file using key 'foo2bar'
  call feedkeys("foo2bar\nfoo2bar\n")
  X
  write!
  bwipe!

  call feedkeys("foo2bar\n")
  edit Xtestfile
  set key=
  call search('bar')
  call assert_equal('bar apr', getline('.'))
  normal u
  call assert_equal('apr', getline('.'))
  normal u
  call assert_equal('foo mar', getline('.'))
  normal u
  call assert_equal('mar', getline('.'))

  bwipe!
  call delete('Xtestfile')
  let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
  call delete(ufile)
  set undofile& undolevels& cryptmethod&
endfunc

" vim: shiftwidth=2 sts=2 expandtab
