" Test for user functions.
" Also test an <expr> mapping calling a function.
" Also test that a builtin function cannot be replaced.
" Also test for regression when calling arbitrary expression.

source check.vim
source shared.vim
import './vim9.vim' as v9

func Table(title, ...)
  let ret = a:title
  let idx = 1
  while idx <= a:0
    exe "let ret = ret . a:" . idx
    let idx = idx + 1
  endwhile
  return ret
endfunc

func Compute(n1, n2, divname)
  if a:n2 == 0
    return "fail"
  endif
  exe "let g:" . a:divname . " = ". a:n1 / a:n2
  return "ok"
endfunc

func Expr1()
  silent! normal! v
  return "111"
endfunc

func Expr2()
  call search('XX', 'b')
  return "222"
endfunc

func ListItem()
  let g:counter += 1
  return g:counter . '. '
endfunc

func ListReset()
  let g:counter = 0
  return ''
endfunc

func FuncWithRef(a)
  unlet g:FuncRef
  return a:a
endfunc

func Test_user_func()
  let g:FuncRef = function("FuncWithRef")
  let g:counter = 0
  inoremap <expr> ( ListItem()
  inoremap <expr> [ ListReset()
  imap <expr> + Expr1()
  imap <expr> * Expr2()
  let g:retval = "nop"

  call assert_equal('xxx4asdf', Table("xxx", 4, "asdf"))
  call assert_equal('fail', Compute(45, 0, "retval"))
  call assert_equal('nop', g:retval)
  call assert_equal('ok', Compute(45, 5, "retval"))
  call assert_equal(9, g:retval)
  call assert_equal(333, g:FuncRef(333))

  let g:retval = "nop"
  call assert_equal('xxx4asdf', "xxx"->Table(4, "asdf"))
  call assert_equal('fail', 45->Compute(0, "retval"))
  call assert_equal('nop', g:retval)
  call assert_equal('ok', 45->Compute(5, "retval"))
  call assert_equal(9, g:retval)
  " call assert_equal(333, 333->g:FuncRef())

  enew

  normal oXX+-XX
  call assert_equal('XX111-XX', getline('.'))
  normal o---*---
  call assert_equal('---222---', getline('.'))
  normal o(one
  call assert_equal('1. one', getline('.'))
  normal o(two
  call assert_equal('2. two', getline('.'))
  normal o[(one again
  call assert_equal('1. one again', getline('.'))

  " Try to overwrite a function in the global (g:) scope
  call assert_equal(3, max([1, 2, 3]))
  call assert_fails("call extend(g:, {'max': function('min')})", 'E704:')
  call assert_equal(3, max([1, 2, 3]))

  " Try to overwrite an user defined function with a function reference
  call assert_fails("let Expr1 = function('min')", 'E705:')

  " Regression: the first line below used to throw ?E110: Missing ')'?
  " Second is here just to prove that this line is correct when not skipping
  " rhs of &&.
  call assert_equal(0, (0 && (function('tr'))(1, 2, 3)))
  call assert_equal(1, (1 && (function('tr'))(1, 2, 3)))

  delfunc Table
  delfunc Compute
  delfunc Expr1
  delfunc Expr2
  delfunc ListItem
  delfunc ListReset
  unlet g:retval g:counter
  enew!
endfunc

func Log(val, base = 10)
  return log(a:val) / log(a:base)
endfunc

func Args(mandatory, optional = v:null, ...)
  return deepcopy(a:)
endfunc

func Args2(a = 1, b = 2, c = 3)
  return deepcopy(a:)
endfunc

func MakeBadFunc()
  func s:fcn(a, b=1, c)
  endfunc
endfunc

func Test_default_arg()
  call assert_equal(1.0, Log(10))
  call assert_equal(log(10), Log(10, exp(1)))
  call assert_fails("call Log(1,2,3)", 'E118:')

  let res = Args(1)
  call assert_equal(res.mandatory, 1)
  call assert_equal(res.optional, v:null)
  call assert_equal(res['0'], 0)

  let res = Args(1,2)
  call assert_equal(res.mandatory, 1)
  call assert_equal(res.optional, 2)
  call assert_equal(res['0'], 0)

  let res = Args(1,2,3)
  call assert_equal(res.mandatory, 1)
  call assert_equal(res.optional, 2)
  call assert_equal(res['0'], 1)

  call assert_fails("call MakeBadFunc()", 'E989:')
  call assert_fails("fu F(a=1 ,) | endf", 'E1068:')

  let d = Args2(7, v:none, 9)
  call assert_equal([7, 2, 9], [d.a, d.b, d.c])

  call assert_equal("\n"
	\ .. "   function Args2(a = 1, b = 2, c = 3)\n"
	\ .. "1    return deepcopy(a:)\n"
	\ .. "   endfunction",
	\ execute('func Args2'))

  " Error in default argument expression
  let l =<< trim END
    func F1(x = y)
      return a:x * 2
    endfunc
    echo F1()
  END
  let @a = l->join("\n")
  call assert_fails("exe @a", 'E121:')
endfunc

func s:addFoo(lead)
  return a:lead .. 'foo'
endfunc

func Test_user_method()
  eval 'bar'->s:addFoo()->assert_equal('barfoo')
endfunc

func Test_method_with_linebreaks()
  let lines =<< trim END
      vim9script

      export def Scan(ll: list<number>): func(func(number))
        return (Emit: func(number)) => {
          for v in ll
            Emit(v)
          endfor
        }
      enddef

      export def Build(Cont: func(func(number))): list<number>
        var result: list<number> = []
        Cont((v) => {
            add(result, v)
        })
        return result
      enddef

      export def Noop(Cont: func(func(number))): func(func(number))
        return (Emit: func(number)) => {
          Cont(Emit)
        }
      enddef
  END
  call writefile(lines, 'Xlib.vim', 'D')

  let lines =<< trim END
      vim9script

      import "./Xlib.vim" as lib

      const x = [1, 2, 3]

      var result = lib.Scan(x)->lib.Noop()->lib.Build()
      assert_equal([1, 2, 3], result)

      result = lib.Scan(x)->lib.Noop()
              ->lib.Build()
      assert_equal([1, 2, 3], result)

      result = lib.Scan(x)
            ->lib.Noop()->lib.Build()
      assert_equal([1, 2, 3], result)

      result = lib.Scan(x)
                ->lib.Noop()
                ->lib.Build()
      assert_equal([1, 2, 3], result)
  END
  call v9.CheckScriptSuccess(lines)
endfunc

func Test_failed_call_in_try()
  try | call UnknownFunc() | catch | endtry
endfunc

" Test for listing user-defined functions
func Test_function_list()
  call assert_fails("function Xabc", 'E123:')
endfunc

" Test for <sfile>, <slnum> in a function
func Test_sfile_in_function()
  func Xfunc()
    call assert_match('..Test_sfile_in_function\[5]..Xfunc', expand('<sfile>'))
    call assert_equal('2', expand('<slnum>'))
  endfunc
  call Xfunc()
  delfunc Xfunc
endfunc

" Test trailing text after :endfunction				    {{{1
func Test_endfunction_trailing()
  call assert_false(exists('*Xtest'))

  exe "func Xtest()\necho 'hello'\nendfunc\nlet done = 'yes'"
  call assert_true(exists('*Xtest'))
  call assert_equal('yes', done)
  delfunc Xtest
  unlet done

  exe "func Xtest()\necho 'hello'\nendfunc|let done = 'yes'"
  call assert_true(exists('*Xtest'))
  call assert_equal('yes', done)
  delfunc Xtest
  unlet done

  " trailing line break
  exe "func Xtest()\necho 'hello'\nendfunc\n"
  call assert_true(exists('*Xtest'))
  delfunc Xtest

  set verbose=1
  exe "func Xtest()\necho 'hello'\nendfunc \" garbage"
  call assert_notmatch('W22:', split(execute('1messages'), "\n")[0])
  call assert_true(exists('*Xtest'))
  delfunc Xtest

  exe "func Xtest()\necho 'hello'\nendfunc garbage"
  call assert_match('W22:', split(execute('1messages'), "\n")[0])
  call assert_true(exists('*Xtest'))
  delfunc Xtest
  set verbose=0

  func Xtest(a1, a2)
    echo a:a1 .. a:a2
  endfunc
  set verbose=15
  redir @a
  call Xtest(123, repeat('x', 100))
  redir END
  call assert_match('calling Xtest(123, ''xxxxxxx.*x\.\.\.x.*xxxx'')', getreg('a'))
  delfunc Xtest
  set verbose=0

  function Foo()
    echo 'hello'
  endfunction | echo 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
  delfunc Foo
endfunc

func Test_delfunction_force()
  delfunc! Xtest
  delfunc! Xtest
  func Xtest()
    echo 'nothing'
  endfunc
  delfunc! Xtest
  delfunc! Xtest

  " Try deleting the current function
  call assert_fails('delfunc Test_delfunction_force', 'E131:')
endfunc

func Test_function_defined_line()
  CheckNotGui

  let lines =<< trim [CODE]
  " F1
  func F1()
    " F2
    func F2()
      "
      "
      "
      return
    endfunc
    " F3
    execute "func F3()\n\n\n\nreturn\nendfunc"
    " F4
    execute "func F4()\n
                \\n
                \\n
                \\n
                \return\n
                \endfunc"
  endfunc
  " F5
  execute "func F5()\n\n\n\nreturn\nendfunc"
  " F6
  execute "func F6()\n
              \\n
              \\n
              \\n
              \return\n
              \endfunc"
  call F1()
  verbose func F1
  verbose func F2
  verbose func F3
  verbose func F4
  verbose func F5
  verbose func F6
  qall!
  [CODE]

  call writefile(lines, 'Xtest.vim', 'D')
  let res = system(GetVimCommandClean() .. ' -es -X -S Xtest.vim')
  call assert_equal(0, v:shell_error)

  let m = matchstr(res, 'function F1()[^[:print:]]*[[:print:]]*')
  call assert_match(' line 2$', m)

  let m = matchstr(res, 'function F2()[^[:print:]]*[[:print:]]*')
  call assert_match(' line 4$', m)

  let m = matchstr(res, 'function F3()[^[:print:]]*[[:print:]]*')
  call assert_match(' line 11$', m)

  let m = matchstr(res, 'function F4()[^[:print:]]*[[:print:]]*')
  call assert_match(' line 13$', m)

  let m = matchstr(res, 'function F5()[^[:print:]]*[[:print:]]*')
  call assert_match(' line 21$', m)

  let m = matchstr(res, 'function F6()[^[:print:]]*[[:print:]]*')
  call assert_match(' line 23$', m)
endfunc

" Test for defining a function reference in the global scope
func Test_add_funcref_to_global_scope()
  let x = g:
  let caught_E862 = 0
  try
    func x.Xfunc()
      return 1
    endfunc
  catch /E862:/
    let caught_E862 = 1
  endtry
  call assert_equal(1, caught_E862)
endfunc

func Test_funccall_garbage_collect()
  func Func(x, ...)
    call add(a:x, a:000)
  endfunc
  call Func([], [])
  " Must not crash cause by invalid freeing
  call test_garbagecollect_now()
  call assert_true(v:true)
  delfunc Func
endfunc

" Test for script-local function
func <SID>DoLast()
  call append(line('$'), "last line")
endfunc

func s:DoNothing()
  call append(line('$'), "nothing line")
endfunc

func Test_script_local_func()
  set nocp nomore viminfo+=nviminfo
  new
  nnoremap <buffer> _x	:call <SID>DoNothing()<bar>call <SID>DoLast()<bar>delfunc <SID>DoNothing<bar>delfunc <SID>DoLast<cr>

  normal _x
  call assert_equal('nothing line', getline(2))
  call assert_equal('last line', getline(3))
  close!

  " Try to call a script local function in global scope
  let lines =<< trim [CODE]
    :call assert_fails('call s:Xfunc()', 'E81:')
    :call assert_fails('let x = call("<SID>Xfunc", [])', 'E120:')
    :call writefile(v:errors, 'Xresult')
    :qall

  [CODE]
  call writefile(lines, 'Xscript', 'D')
  if RunVim([], [], '-s Xscript')
    call assert_equal([], readfile('Xresult'))
  endif
  call delete('Xresult')
endfunc

" Test for errors in defining new functions
func Test_func_def_error()
  call assert_fails('func Xfunc abc ()', 'E124:')
  call assert_fails('func Xfunc(', 'E125:')
  call assert_fails('func xfunc()', 'E128:')

  " Try to redefine a function that is in use
  let caught_E127 = 0
  try
    func! Test_func_def_error()
    endfunc
  catch /E127:/
    let caught_E127 = 1
  endtry
  call assert_equal(1, caught_E127)

  " Try to define a function in a dict twice
  let d = {}
  let lines =<< trim END
    func d.F1()
      return 1
    endfunc
  END
  let l = join(lines, "\n") . "\n"
  exe l
  call assert_fails('exe l', 'E717:')
  call assert_fails('call feedkeys(":func d.F1()\<CR>", "xt")', 'E717:')

  " Define an autoload function with an incorrect file name
  call writefile(['func foo#Bar()', 'return 1', 'endfunc'], 'Xscript', 'D')
  call assert_fails('source Xscript', 'E746:')

  " Try to list functions using an invalid search pattern
  call assert_fails('function /\%(/', 'E53:')
endfunc

" Test for deleting a function
func Test_del_func()
  call assert_fails('delfunction Xabc', 'E117:')
  let d = {'a' : 10}
  call assert_fails('delfunc d.a', 'E718:')
  func d.fn()
    return 1
  endfunc

  " cannot delete the dict function by number
  let nr = substitute(execute('echo d'), '.*function(''\(\d\+\)'').*', '\1', '')
  call assert_fails('delfunction g:' .. nr, 'E475: Invalid argument: g:')

  delfunc d.fn
  call assert_equal({'a' : 10}, d)
endfunc

" Test for calling return outside of a function
func Test_return_outside_func()
  call writefile(['return 10'], 'Xscript', 'D')
  call assert_fails('source Xscript', 'E133:')
endfunc

" Test for errors in calling a function
func Test_func_arg_error()
  " Too many arguments
  call assert_fails("call call('min', range(1,20))", 'E118:')
  call assert_fails("call call('min', range(1,21))", 'E699:')
  call assert_fails('echo min(0,1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,0,1)',
        \ 'E740:')

  " Missing dict argument
  func Xfunc() dict
    return 1
  endfunc
  call assert_fails('call Xfunc()', 'E725:')
  delfunc Xfunc
endfunc

func Test_func_dict()
  let mydict = {'a': 'b'}
  function mydict.somefunc() dict
    return len(self)
  endfunc

  call assert_equal("{'a': 'b', 'somefunc': function('3')}", string(mydict))
  call assert_equal(2, mydict.somefunc())
  call assert_match("^\n   function \\d\\\+() dict"
  \              .. "\n1      return len(self)"
  \              .. "\n   endfunction$", execute('func mydict.somefunc'))
  call assert_fails('call mydict.nonexist()', 'E716:')
endfunc

func Test_func_range()
  new
  call setline(1, range(1, 8))
  func FuncRange() range
    echo a:firstline
    echo a:lastline
  endfunc
  3
  call assert_equal("\n3\n3", execute('call FuncRange()'))
  call assert_equal("\n4\n6", execute('4,6 call FuncRange()'))
  call assert_equal("\n   function FuncRange() range"
  \              .. "\n1      echo a:firstline"
  \              .. "\n2      echo a:lastline"
  \              .. "\n   endfunction",
  \                 execute('function FuncRange'))

  bwipe!
endfunc

" Test for memory allocation failure when defining a new function
func Test_funcdef_alloc_failure()
  new
  let lines =<< trim END
    func Xtestfunc()
      return 321
    endfunc
  END
  call setline(1, lines)
  call test_alloc_fail(GetAllocId('get_func'), 0, 0)
  call assert_fails('source', 'E342:')
  call assert_false(exists('*Xtestfunc'))
  call assert_fails('delfunc Xtestfunc', 'E117:')
  %d _
  let lines =<< trim END
    def g:Xvim9func(): number
      return 456
    enddef
  END
  call setline(1, lines)
  call test_alloc_fail(GetAllocId('get_func'), 0, 0)
  call assert_fails('source', 'E342:')
  call assert_false(exists('*Xvim9func'))
  "call test_alloc_fail(GetAllocId('get_func'), 0, 0)
  "call assert_fails('source', 'E342:')
  "call assert_false(exists('*Xtestfunc'))
  "call assert_fails('delfunc Xtestfunc', 'E117:')
  bw!
endfunc

func AddDefer(arg1, ...)
  call extend(g:deferred, [a:arg1])
  if a:0 == 1
    call extend(g:deferred, [a:1])
  endif
endfunc

func WithDeferTwo()
  call extend(g:deferred, ['in Two'])
  for nr in range(3)
    defer AddDefer('Two' .. nr)
  endfor
  call extend(g:deferred, ['end Two'])
endfunc

func WithDeferOne()
  call extend(g:deferred, ['in One'])
  call writefile(['text'], 'Xfuncdefer')
  defer delete('Xfuncdefer')
  defer AddDefer('One')
  call WithDeferTwo()
  call extend(g:deferred, ['end One'])
endfunc

func WithPartialDefer()
  call extend(g:deferred, ['in Partial'])
  let Part = funcref('AddDefer', ['arg1'])
  defer Part("arg2")
  call extend(g:deferred, ['end Partial'])
endfunc

func Test_defer()
  let g:deferred = []
  call WithDeferOne()

  call assert_equal(['in One', 'in Two', 'end Two', 'Two2', 'Two1', 'Two0', 'end One', 'One'], g:deferred)
  unlet g:deferred

  call assert_equal('', glob('Xfuncdefer'))

  call assert_fails('defer delete("Xfuncdefer")->Another()', 'E488:')
  call assert_fails('defer delete("Xfuncdefer").member', 'E488:')

  let g:deferred = []
  call WithPartialDefer()
  call assert_equal(['in Partial', 'end Partial', 'arg1', 'arg2'], g:deferred)
  unlet g:deferred

  let Part = funcref('AddDefer', ['arg1'], {})
  call assert_fails('defer Part("arg2")', 'E1300:')
endfunc

func DeferLevelTwo()
  call writefile(['text'], 'XDeleteTwo', 'D')
  throw 'someerror'
endfunc

def DeferLevelOne()
  call writefile(['text'], 'XDeleteOne', 'D')
  call g:DeferLevelTwo()
enddef

func Test_defer_throw()
  let caught = 'no'
  try
    call DeferLevelOne()
  catch /someerror/
    let caught = 'yes'
  endtry
  call assert_equal('yes', caught)
  call assert_false(filereadable('XDeleteOne'))
  call assert_false(filereadable('XDeleteTwo'))
endfunc

func Test_defer_quitall()
  let lines =<< trim END
      vim9script
      func DeferLevelTwo()
        call writefile(['text'], 'XQuitallTwo', 'D')
        qa!
      endfunc

      def DeferLevelOne()
        call writefile(['text'], 'XQuitallOne', 'D')
        call DeferLevelTwo()
      enddef

      DeferLevelOne()
  END
  call writefile(lines, 'XdeferQuitall', 'D')
  let res = system(GetVimCommand() .. ' -X -S XdeferQuitall')
  call assert_equal(0, v:shell_error)
  call assert_false(filereadable('XQuitallOne'))
  call assert_false(filereadable('XQuitallTwo'))
endfunc

func Test_defer_quitall_in_expr_func()
  let lines =<< trim END
      def DefIndex(idx: number, val: string): bool
        call writefile([idx .. ': ' .. val], 'Xentry' .. idx, 'D')
        if val == 'b'
          qa!
        endif
        return val == 'c'
      enddef

      def Test_defer_in_funcref()
        assert_equal(2, indexof(['a', 'b', 'c'], funcref('g:DefIndex')))
      enddef
      call Test_defer_in_funcref()
  END
  call writefile(lines, 'XdeferQuitallExpr', 'D')
  let res = system(GetVimCommand() .. ' -X -S XdeferQuitallExpr')
  call assert_equal(0, v:shell_error)
  call assert_false(filereadable('Xentry0'))
  call assert_false(filereadable('Xentry1'))
  call assert_false(filereadable('Xentry2'))
endfunc

func FuncIndex(idx, val)
  call writefile([a:idx .. ': ' .. a:val], 'Xentry' .. a:idx, 'D')
  return a:val == 'c'
endfunc

def DefIndex(idx: number, val: string): bool
  call writefile([idx .. ': ' .. val], 'Xentry' .. idx, 'D')
  return val == 'c'
enddef

def DefIndexXtra(xtra: string, idx: number, val: string): bool
  call writefile([idx .. ': ' .. val], 'Xentry' .. idx, 'D')
  return val == 'c'
enddef

def Test_defer_in_funcref()
  assert_equal(2, indexof(['a', 'b', 'c'], function('g:FuncIndex')))
  assert_false(filereadable('Xentry0'))
  assert_false(filereadable('Xentry1'))
  assert_false(filereadable('Xentry2'))

  assert_equal(2, indexof(['a', 'b', 'c'], g:DefIndex))
  assert_false(filereadable('Xentry0'))
  assert_false(filereadable('Xentry1'))
  assert_false(filereadable('Xentry2'))

  assert_equal(2, indexof(['a', 'b', 'c'], function('g:DefIndex')))
  assert_false(filereadable('Xentry0'))
  assert_false(filereadable('Xentry1'))
  assert_false(filereadable('Xentry2'))

  assert_equal(2, indexof(['a', 'b', 'c'], funcref(g:DefIndex)))
  assert_false(filereadable('Xentry0'))
  assert_false(filereadable('Xentry1'))
  assert_false(filereadable('Xentry2'))

  assert_equal(2, indexof(['a', 'b', 'c'], function(g:DefIndexXtra, ['xtra'])))
  assert_false(filereadable('Xentry0'))
  assert_false(filereadable('Xentry1'))
  assert_false(filereadable('Xentry2'))

  assert_equal(2, indexof(['a', 'b', 'c'], funcref(g:DefIndexXtra, ['xtra'])))
  assert_false(filereadable('Xentry0'))
  assert_false(filereadable('Xentry1'))
  assert_false(filereadable('Xentry2'))
enddef

func Test_defer_wrong_arguments()
  call assert_fails('defer delete()', 'E119:')
  call assert_fails('defer FuncIndex(1)', 'E119:')
  call assert_fails('defer delete(1, 2, 3)', 'E118:')
  call assert_fails('defer FuncIndex(1, 2, 3)', 'E118:')

  let lines =<< trim END
      def DeferFunc0()
        defer delete()
      enddef
      defcompile
  END
  call v9.CheckScriptFailure(lines, 'E119:')
  let lines =<< trim END
      def DeferFunc3()
        defer delete(1, 2, 3)
      enddef
      defcompile
  END
  call v9.CheckScriptFailure(lines, 'E118:')
  let lines =<< trim END
      def DeferFunc2()
        defer delete(1, 2)
      enddef
      defcompile
  END
  call v9.CheckScriptFailure(lines, 'E1013: Argument 1: type mismatch, expected string but got number')

  def g:FuncOneArg(arg: string)
    echo arg
  enddef

  let lines =<< trim END
      def DeferUserFunc0()
        defer g:FuncOneArg()
      enddef
      defcompile
  END
  call v9.CheckScriptFailure(lines, 'E119:')
  let lines =<< trim END
      def DeferUserFunc2()
        defer g:FuncOneArg(1, 2)
      enddef
      defcompile
  END
  call v9.CheckScriptFailure(lines, 'E118:')
  let lines =<< trim END
      def DeferUserFunc1()
        defer g:FuncOneArg(1)
      enddef
      defcompile
  END
  call v9.CheckScriptFailure(lines, 'E1013: Argument 1: type mismatch, expected string but got number')
endfunc


" vim: shiftwidth=2 sts=2 expandtab
