local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')

local clear = n.clear
local feed = n.feed
local eq = t.eq
local expect = n.expect
local eval = n.eval
local fn = n.fn
local insert = n.insert
local write_file = t.write_file
local exc_exec = n.exc_exec
local command = n.command

describe('mappings with <Cmd>', function()
  local screen
  local tmpfile = 'X_ex_cmds_cmd_map'

  local function cmdmap(lhs, rhs)
    command('noremap ' .. lhs .. ' <Cmd>' .. rhs .. '<cr>')
    command('noremap! ' .. lhs .. ' <Cmd>' .. rhs .. '<cr>')
  end

  before_each(function()
    clear()
    screen = Screen.new(65, 8)
    screen:set_default_attr_ids({
      [1] = { bold = true, foreground = Screen.colors.Blue1 },
      [2] = { foreground = Screen.colors.Grey100, background = Screen.colors.Red },
      [3] = { bold = true, foreground = Screen.colors.SeaGreen4 },
      [4] = { bold = true },
      [5] = { foreground = Screen.colors.Black, background = Screen.colors.LightGrey },
      [6] = { foreground = Screen.colors.Blue1 },
      [7] = { bold = true, reverse = true },
      [8] = { background = Screen.colors.WebGray },
      [9] = { background = Screen.colors.LightMagenta },
      [10] = { foreground = Screen.colors.Red },
    })

    cmdmap('<F3>', 'let m = mode(1)')
    cmdmap('<F4>', 'normal! ww')
    cmdmap('<F5>', 'normal! "ay')
    cmdmap('<F6>', 'throw "very error"')
    command([[
        function! TextObj()
            if mode() !=# "v"
                normal! v
            end
            call cursor(1,3)
            normal! o
            call cursor(2,4)
        endfunction]])
    cmdmap('<F7>', 'call TextObj()')
    insert([[
        some short lines
        of test text]])
    feed('gg')
    cmdmap('<F8>', 'startinsert')
    cmdmap('<F9>', 'stopinsert')
    command('abbr foo <Cmd>let g:y = 17<cr>bar')
  end)

  after_each(function()
    os.remove(tmpfile)
  end)

  it('can be displayed', function()
    command('map <F3>')
    screen:expect([[
      ^some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
         {6:<F3>}        {6:*} {6:<Cmd>}let m = mode(1){6:<CR>}                        |
    ]])
  end)

  it('handles invalid mappings', function()
    command('let x = 0')
    command('noremap <F3> <Cmd><Cmd>let x = 1<cr>')
    feed('<F3>')
    screen:expect([[
      ^some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {2:E1136: <Cmd> mapping must end with <CR> before second <Cmd>}      |
    ]])

    command('noremap <F3> <Cmd>let x = 3')
    feed('<F3>')
    screen:expect([[
      ^some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {2:E1255: <Cmd> mapping must end with <CR>}                          |
    ]])
    eq(0, eval('x'))
  end)

  it('allows special keys and modifiers', function()
    command('noremap <F3> <Cmd>normal! <Down><CR>')
    feed('<F3>')
    screen:expect([[
      some short lines                                                 |
      ^of test text                                                     |
      {1:~                                                                }|*5
                                                                       |
    ]])

    command('noremap <F3> <Cmd>normal! <C-Right><CR>')
    feed('<F3>')
    screen:expect([[
      some short lines                                                 |
      of ^test text                                                     |
      {1:~                                                                }|*5
                                                                       |
    ]])
  end)

  it('handles string containing K_SPECIAL (0x80) bytes correctly', function()
    command([[noremap <F3> <Cmd>let g:str = 'foo…bar'<CR>]])
    feed('<F3>')
    eq('foo…bar', eval('g:str'))
    local str = eval([["foo\<D-…>bar"]])
    command([[noremap <F3> <Cmd>let g:str = ']] .. str .. [['<CR>]])
    feed('<F3>')
    eq(str, eval('g:str'))
    command([[noremap <F3> <Cmd>let g:str = 'foo<D-…>bar'<CR>]])
    feed('<F3>')
    eq(str, eval('g:str'))
  end)

  it('works in various modes and sees correct `mode()` value', function()
    -- normal mode
    feed('<F3>')
    eq('n', eval('m'))

    -- visual mode
    feed('v<F3>')
    eq('v', eval('m'))
    -- didn't leave visual mode
    eq('v', eval('mode(1)'))
    feed('<esc>')
    eq('n', eval('mode(1)'))

    -- visual mapping in select mode
    feed('gh<F3>')
    eq('v', eval('m'))
    -- didn't leave select mode
    eq('s', eval('mode(1)'))
    feed('<esc>')
    eq('n', eval('mode(1)'))

    -- select mode mapping
    command('snoremap <F3> <Cmd>let m = mode(1)<cr>')
    feed('gh<F3>')
    eq('s', eval('m'))
    -- didn't leave select mode
    eq('s', eval('mode(1)'))
    feed('<esc>')
    eq('n', eval('mode(1)'))

    -- operator-pending mode
    feed('d<F3>')
    eq('no', eval('m'))
    -- did leave operator-pending mode
    eq('n', eval('mode(1)'))

    --insert mode
    feed('i<F3>')
    eq('i', eval('m'))
    eq('i', eval('mode(1)'))

    -- replace mode
    feed('<Ins><F3>')
    eq('R', eval('m'))
    eq('R', eval('mode(1)'))
    feed('<esc>')
    eq('n', eval('mode(1)'))

    -- virtual replace mode
    feed('gR<F3>')
    eq('Rv', eval('m'))
    eq('Rv', eval('mode(1)'))
    feed('<esc>')
    eq('n', eval('mode(1)'))

    -- langmap works, but is not distinguished in mode(1)
    feed(':set iminsert=1<cr>i<F3>')
    eq('i', eval('m'))
    eq('i', eval('mode(1)'))
    feed('<esc>')
    eq('n', eval('mode(1)'))

    feed(':<F3>')
    eq('c', eval('m'))
    eq('c', eval('mode(1)'))
    feed('<esc>')
    eq('n', eval('mode(1)'))

    -- terminal mode
    command('tnoremap <F3> <Cmd>let m = mode(1)<cr>')
    command('split | terminal')
    feed('i')
    eq('t', eval('mode(1)'))
    feed('<F3>')
    eq('t', eval('m'))
    eq('t', eval('mode(1)'))
  end)

  it('works in normal mode', function()
    cmdmap('<F2>', 'let s = [mode(1), v:count, v:register]')

    -- check v:count and v:register works
    feed('<F2>')
    eq({ 'n', 0, '"' }, eval('s'))
    feed('7<F2>')
    eq({ 'n', 7, '"' }, eval('s'))
    feed('"e<F2>')
    eq({ 'n', 0, 'e' }, eval('s'))
    feed('5"k<F2>')
    eq({ 'n', 5, 'k' }, eval('s'))
    feed('"+2<F2>')
    eq({ 'n', 2, '+' }, eval('s'))

    -- text object enters visual mode
    feed('<F7>')
    screen:expect([[
      so{5:me short lines}                                                 |
      {5:of }^test text                                                     |
      {1:~                                                                }|*5
      {4:-- VISUAL --}                                                     |
    ]])
    feed('<esc>')

    -- startinsert
    feed('<F8>')
    eq('i', eval('mode(1)'))
    feed('<esc>')

    eq('n', eval('mode(1)'))
    cmdmap(',a', 'call feedkeys("aalpha") \\| let g:a = getline(2)')
    cmdmap(',b', 'call feedkeys("abeta", "x") \\| let g:b = getline(2)')

    feed(',a<F3>')
    screen:expect([[
      some short lines                                                 |
      of alpha^test text                                                |
      {1:~                                                                }|*5
      {4:-- INSERT --}                                                     |
    ]])
    -- feedkeys were not executed immediately
    eq({ 'n', 'of test text' }, eval('[m,a]'))
    eq('i', eval('mode(1)'))
    feed('<esc>')

    feed(',b<F3>')
    screen:expect([[
      some short lines                                                 |
      of alphabet^atest text                                            |
      {1:~                                                                }|*5
                                                                       |
    ]])
    -- feedkeys(..., 'x') was executed immediately, but insert mode gets aborted
    eq({ 'n', 'of alphabetatest text' }, eval('[m,b]'))
    eq('n', eval('mode(1)'))
  end)

  it('works in :normal command', function()
    command('noremap ,x <Cmd>call append(1, "xx")\\| call append(1, "aa")<cr>')
    command('noremap ,f <Cmd>nosuchcommand<cr>')
    command('noremap ,e <Cmd>throw "very error"\\| call append(1, "yy")<cr>')
    command('noremap ,m <Cmd>echoerr "The message."\\| call append(1, "zz")<cr>')
    command(
      'noremap ,w <Cmd>for i in range(5)\\|if i==1\\|echoerr "Err"\\|endif\\|call append(1, i)\\|endfor<cr>'
    )

    feed(':normal ,x<cr>')
    screen:expect([[
      ^some short lines                                                 |
      aa                                                               |
      xx                                                               |
      of test text                                                     |
      {1:~                                                                }|*3
      :normal ,x                                                       |
    ]])

    eq('Vim:E492: Not an editor command: nosuchcommand', exc_exec('normal ,f'))
    eq('very error', exc_exec('normal ,e'))
    eq('Vim(echoerr):The message.', exc_exec('normal ,m'))
    feed('w')
    screen:expect([[
      some ^short lines                                                 |
      aa                                                               |
      xx                                                               |
      of test text                                                     |
      {1:~                                                                }|*3
      :normal ,x                                                       |
    ]])

    command(':%d')
    eq('Vim(echoerr):Err', exc_exec('normal ,w'))
    screen:expect([[
      ^                                                                 |
      0                                                                |
      {1:~                                                                }|*5
      --No lines in buffer--                                           |
    ]])

    command(':%d')
    feed(':normal ,w<cr>')
    screen:expect([[
      ^                                                                 |
      4                                                                |
      3                                                                |
      2                                                                |
      1                                                                |
      0                                                                |
      {1:~                                                                }|
      {2:Err}                                                              |
    ]])
  end)

  it('works in visual mode', function()
    -- can extend visual mode
    feed('v<F4>')
    screen:expect([[
      {5:some short }^lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- VISUAL --}                                                     |
    ]])
    eq('v', fn.mode(1))

    -- can invoke operator, ending visual mode
    feed('<F5>')
    eq('n', fn.mode(1))
    eq({ 'some short l' }, fn.getreg('a', 1, 1))

    -- error doesn't interrupt visual mode
    feed('ggvw<F6>')
    screen:expect([[
      {5:some }short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*2
      {7:                                                                 }|
      {2:Error detected while processing :}                                |
      {2:E605: Exception not caught: very error}                           |
      {3:Press ENTER or type command to continue}^                          |
    ]])
    feed('<cr>')
    eq('E605: Exception not caught: very error', eval('v:errmsg'))
    -- still in visual mode, <cr> was consumed by the error prompt
    screen:expect([[
      {5:some }^short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- VISUAL --}                                                     |
    ]])
    eq('v', fn.mode(1))
    feed('<F7>')
    screen:expect([[
      so{5:me short lines}                                                 |
      {5:of }^test text                                                     |
      {1:~                                                                }|*5
      {4:-- VISUAL --}                                                     |
    ]])
    eq('v', fn.mode(1))

    -- startinsert gives "-- (insert) VISUAL --" mode
    feed('<F8>')
    screen:expect([[
      so{5:me short lines}                                                 |
      {5:of }^test text                                                     |
      {1:~                                                                }|*5
      {4:-- (insert) VISUAL --}                                            |
    ]])
    eq('v', eval('mode(1)'))
    feed('<esc>')
    eq('i', eval('mode(1)'))
  end)

  it('works in select mode', function()
    command('snoremap <F1> <cmd>throw "very error"<cr>')
    command('snoremap <F2> <cmd>normal! <c-g>"by<cr>')
    -- can extend select mode
    feed('gh<F4>')
    screen:expect([[
      {5:some short }^lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- SELECT --}                                                     |
    ]])
    eq('s', fn.mode(1))

    -- visual mapping in select mode restart select mode after operator
    feed('<F5>')
    eq('s', fn.mode(1))
    eq({ 'some short l' }, fn.getreg('a', 1, 1))

    -- select mode mapping works, and does not restart select mode
    feed('<F2>')
    eq('n', fn.mode(1))
    eq({ 'some short l' }, fn.getreg('b', 1, 1))

    -- error doesn't interrupt temporary visual mode
    feed('<esc>ggvw<c-g><F6>')
    screen:expect([[
      {5:some }short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*2
      {7:                                                                 }|
      {2:Error detected while processing :}                                |
      {2:E605: Exception not caught: very error}                           |
      {3:Press ENTER or type command to continue}^                          |
    ]])
    feed('<cr>')
    eq('E605: Exception not caught: very error', eval('v:errmsg'))
    -- still in visual mode, <cr> was consumed by the error prompt
    screen:expect([[
      {5:some }^short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- VISUAL --}                                                     |
    ]])
    -- quirk: restoration of select mode is not performed
    eq('v', fn.mode(1))

    -- error doesn't interrupt select mode
    feed('<esc>ggvw<c-g><F1>')
    screen:expect([[
      {5:some }short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*2
      {7:                                                                 }|
      {2:Error detected while processing :}                                |
      {2:E605: Exception not caught: very error}                           |
      {3:Press ENTER or type command to continue}^                          |
    ]])
    feed('<cr>')
    eq('E605: Exception not caught: very error', eval('v:errmsg'))
    -- still in select mode, <cr> was consumed by the error prompt
    screen:expect([[
      {5:some }^short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- SELECT --}                                                     |
    ]])
    -- quirk: restoration of select mode is not performed
    eq('s', fn.mode(1))

    feed('<F7>')
    screen:expect([[
      so{5:me short lines}                                                 |
      {5:of }^test text                                                     |
      {1:~                                                                }|*5
      {4:-- SELECT --}                                                     |
    ]])
    eq('s', fn.mode(1))

    -- startinsert gives "-- SELECT (insert) --" mode
    feed('<F8>')
    screen:expect([[
      so{5:me short lines}                                                 |
      {5:of }^test text                                                     |
      {1:~                                                                }|*5
      {4:-- (insert) SELECT --}                                            |
    ]])
    eq('s', eval('mode(1)'))
    feed('<esc>')
    eq('i', eval('mode(1)'))
  end)

  it('works in operator-pending mode', function()
    feed('d<F4>')
    expect([[
        lines
        of test text]])
    eq({ 'some short ' }, fn.getreg('"', 1, 1))
    feed('.')
    expect([[
        test text]])
    eq({ 'lines', 'of ' }, fn.getreg('"', 1, 1))
    feed('uu')
    expect([[
        some short lines
        of test text]])

    -- error aborts operator-pending, operator not performed
    feed('d<F6>')
    screen:expect([[
      some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*2
      {7:                                                                 }|
      {2:Error detected while processing :}                                |
      {2:E605: Exception not caught: very error}                           |
      {3:Press ENTER or type command to continue}^                          |
    ]])
    feed('<cr>')
    eq('E605: Exception not caught: very error', eval('v:errmsg'))
    expect([[
        some short lines
        of test text]])

    feed('"bd<F7>')
    expect([[
        soest text]])
    eq({ 'me short lines', 'of t' }, fn.getreg('b', 1, 1))

    -- startinsert aborts operator
    feed('d<F8>')
    eq('i', eval('mode(1)'))
    expect([[
        soest text]])
  end)

  it('works in insert mode', function()
    -- works the same as <c-o>w<c-o>w
    feed('iindeed <F4>little ')
    screen:expect([[
      indeed some short little ^lines                                   |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- INSERT --}                                                     |
    ]])

    feed('<F6>')
    screen:expect([[
      indeed some short little lines                                   |
      of test text                                                     |
      {1:~                                                                }|*2
      {7:                                                                 }|
      {2:Error detected while processing :}                                |
      {2:E605: Exception not caught: very error}                           |
      {3:Press ENTER or type command to continue}^                          |
    ]])

    feed('<cr>')
    eq('E605: Exception not caught: very error', eval('v:errmsg'))
    -- still in insert
    screen:expect([[
      indeed some short little ^lines                                   |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- INSERT --}                                                     |
    ]])
    eq('i', eval('mode(1)'))

    -- When entering visual mode from InsertEnter autocmd, an async event, or
    -- a <cmd> mapping, vim ends up in undocumented "INSERT VISUAL" mode. If a
    -- vim patch decides to disable this mode, this test is expected to fail.
    feed('<F7>stuff ')
    screen:expect([[
      in{5:deed some short little lines}                                   |
      {5:of stuff }^test text                                               |
      {1:~                                                                }|*5
      {4:-- INSERT VISUAL --}                                              |
    ]])
    expect([[
      indeed some short little lines
      of stuff test text]])

    feed('<F5>')
    eq({ 'deed some short little lines', 'of stuff t' }, fn.getreg('a', 1, 1))

    -- still in insert
    screen:expect([[
      in^deed some short little lines                                   |
      of stuff test text                                               |
      {1:~                                                                }|*5
      {4:-- INSERT --}                                                     |
    ]])
    eq('i', eval('mode(1)'))

    -- also works as part of abbreviation
    feed('<space>foo ')
    screen:expect([[
      in bar ^deed some short little lines                              |
      of stuff test text                                               |
      {1:~                                                                }|*5
      {4:-- INSERT --}                                                     |
    ]])
    eq(17, eval('g:y'))

    -- :startinsert does nothing
    feed('<F8>')
    eq('i', eval('mode(1)'))

    -- :stopinsert works
    feed('<F9>')
    eq('n', eval('mode(1)'))
  end)

  it('works in insert completion (Ctrl-X) mode', function()
    feed('os<c-x><c-n>')
    screen:expect([[
      some short lines                                                 |
      some^                                                             |
      {8:some           }                                                  |
      {9:short          }{1:                                                  }|
      {1:~                                                                }|*3
      {4:-- Keyword Local completion (^N^P) }{3:match 1 of 2}                  |
    ]])

    feed('<f3>')
    eq('ic', eval('m'))

    -- ensure a redraw, this would have moved the cursor
    -- instead if CTRL-X mode was left.
    feed('<up>')
    screen:expect([[
      some short lines                                                 |
      some^                                                             |
      {9:some           }                                                  |
      {9:short          }{1:                                                  }|
      {1:~                                                                }|*3
      {4:-- Keyword Local completion (^N^P) }{10:Back at original}              |
    ]])
  end)

  it('works in cmdline mode', function()
    feed(':text<F3>')
    eq('c', eval('m'))
    -- didn't leave cmdline mode
    eq('c', eval('mode(1)'))
    feed('<cr>')
    eq('n', eval('mode(1)'))
    screen:expect([[
      ^some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {2:E492: Not an editor command: text}                                |
    ]])

    feed(':echo 2<F6>')
    screen:expect([[
      some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|
      {7:                                                                 }|
      :echo 2                                                          |
      {2:Error detected while processing :}                                |
      {2:E605: Exception not caught: very error}                           |
      :echo 2^                                                          |
    ]])
    eq('E605: Exception not caught: very error', eval('v:errmsg'))
    -- didn't leave cmdline mode
    eq('c', eval('mode(1)'))
    feed('+2<cr>')
    screen:expect([[
      some short lines                                                 |
      of test text                                                     |
      {7:                                                                 }|
      :echo 2                                                          |
      {2:Error detected while processing :}                                |
      {2:E605: Exception not caught: very error}                           |
      4                                                                |
      {3:Press ENTER or type command to continue}^                          |
    ]])
    -- however, message scrolling may cause extra CR prompt
    -- This is consistent with output from async events.
    feed('<cr>')
    screen:expect([[
      ^some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
                                                                       |
    ]])
    eq('n', eval('mode(1)'))

    feed(':let g:x = 3<F4>')
    screen:expect([[
      some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      :let g:x = 3^                                                     |
    ]])
    feed('+2<cr>')
    -- cursor was moved in the background
    screen:expect([[
      some short ^lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      :let g:x = 3+2                                                   |
    ]])
    eq(5, eval('g:x'))

    feed(':let g:y = 7<F8>')
    screen:expect([[
      some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      :let g:y = 7^                                                     |
    ]])
    eq('c', eval('mode(1)'))
    feed('+2<cr>')
    -- startinsert takes effect after leaving cmdline mode
    screen:expect([[
      some short ^lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      {4:-- INSERT --}                                                     |
    ]])
    eq('i', eval('mode(1)'))
    eq(9, eval('g:y'))
  end)

  it("doesn't crash when invoking cmdline mode recursively #8859", function()
    cmdmap('<F2>', 'norm! :foo')
    feed(':bar')
    screen:expect([[
      some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      :bar^                                                             |
    ]])

    feed('<f2>x')
    screen:expect([[
      some short lines                                                 |
      of test text                                                     |
      {1:~                                                                }|*5
      :barx^                                                            |
    ]])
  end)

  it('works with <SID> mappings', function()
    command('new!')
    write_file(
      tmpfile,
      [[
      map <f2> <Cmd>call <SID>do_it()<Cr>
      function! s:do_it()
        let g:x = 10
      endfunction
    ]]
    )
    command('source ' .. tmpfile)
    feed('<f2>')
    eq('', eval('v:errmsg'))
    eq(10, eval('g:x'))
  end)
end)
