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

local feed = n.feed
local fn = n.call
local source = n.source
local clear = n.clear
local command = n.command
local expect = n.expect
local poke_eventloop = n.poke_eventloop
local api = n.api
local eq = t.eq
local pcall_err = t.pcall_err
local neq = t.neq
local exec_lua = n.exec_lua

describe('prompt buffer', function()
  local screen

  before_each(function()
    clear()
    screen = Screen.new(25, 10)
    command('set laststatus=0 nohidden')
  end)

  local function source_script()
    source([[
      func TextEntered(text)
        if a:text == "exit"
          stopinsert
          close
        else
          " Add the output above the current prompt.
          call prompt_appendbuf(bufnr(''), split('Command: "' . a:text . '"', '\n'))
          " Reset &modified to allow the buffer to be closed.
          set nomodified
          call timer_start(20, {id -> TimerFunc(a:text)})
        endif
      endfunc

      func TimerFunc(text)
        " Add the output above the current prompt.
        call prompt_appendbuf(bufnr(''),  split('Result: "' . a:text .'"', '\n'))
        " Reset &modified to allow the buffer to be closed.
        set nomodified
      endfunc

      func SwitchWindows()
        call timer_start(0, {-> execute("wincmd p", "")})
      endfunc

      call setline(1, "other buffer")
      set nomodified
      new
      set buftype=prompt
      call prompt_setcallback(bufnr(''), function("TextEntered"))
      eval bufnr("")->prompt_setprompt("cmd: ")
      startinsert
    ]])
    screen:expect([[
      cmd: ^                    |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
  end

  -- oldtest: Test_prompt_basic()
  it('works', function()
    source_script()
    feed('hello\n')
    screen:expect([[
      cmd: hello               |
      Command: "hello"         |
      Result: "hello"          |
      cmd: ^                    |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
    feed('exit\n')
    screen:expect([[
      ^other buffer             |
      {1:~                        }|*8
                               |
    ]])

    command('new')
    command('set buftype=prompt')
    feed('iabc<BS><BS>')
    eq('a', fn('prompt_getinput', fn('bufnr')))
    command('quit')
    eq(1, #api.nvim_list_wins())

    command('new')
    command('set buftype=prompt modified')
    eq(
      'Vim(quit):E37: No write since last change (add ! to override)',
      t.pcall_err(command, 'quit')
    )
    eq(2, #api.nvim_list_wins())
  end)

  -- oldtest: Test_prompt_editing()
  it('editing', function()
    source_script()
    feed('hello<BS><BS>')
    screen:expect([[
      cmd: hel^                 |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
    feed('<Left><Left><Left><BS>-')
    screen:expect([[
      cmd: -^hel                |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
    feed('<C-O>lz')
    screen:expect([[
      cmd: -hz^el               |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
    feed('<End>x')
    screen:expect([[
      cmd: -hzelx^              |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    -- :edit doesn't apply on prompt buffer
    eq('Vim(edit):cannot :edit a prompt buffer', t.pcall_err(api.nvim_command, 'edit'))

    feed('<C-U>exit\n')
    screen:expect([[
      ^other buffer             |
      {1:~                        }|*8
                               |
    ]])
  end)

  -- oldtest: Test_prompt_switch_windows()
  it('switch windows', function()
    source_script()
    feed('<C-O>:call SwitchWindows()<CR>')
    screen:expect([[
      cmd:                     |
      {1:~                        }|*3
      {2:[Prompt]                 }|
      ^other buffer             |
      {1:~                        }|*3
                               |
    ]])
    feed('<C-O>:call SwitchWindows()<CR>')
    screen:expect([[
      cmd: ^                    |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
    feed('<Esc>')
    screen:expect([[
      cmd:^                     |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
                               |
    ]])
  end)

  -- oldtest: Test_prompt_while_writing_to_hidden_buffer()
  it('keeps insert mode after aucmd_restbuf in callback', function()
    source_script()
    source [[
      let s:buf = nvim_create_buf(1, 1)
      call timer_start(0, {-> nvim_buf_set_lines(s:buf, -1, -1, 0, ['walrus'])})
    ]]
    poke_eventloop()
    eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
  end)

  -- oldtest: Test_prompt_appending_while_hidden()
  it('accessing hidden prompt buffer does not start insert mode', function()
    local prev_win = api.nvim_get_current_win()
    source([[
      new prompt
      set buftype=prompt
      set bufhidden=hide

      func s:TextEntered(text)
          if a:text == 'exit'
              close
          endif
          let g:entered = a:text
      endfunc
      call prompt_setcallback(bufnr(), function('s:TextEntered'))

      func DoAppend(cmd_before = '')
        exe a:cmd_before
        call appendbufline('prompt', '$', 'Test')
        return ''
      endfunc

      autocmd User SwitchTabPages tabprevious | tabnext
      func DoAutoAll(cmd_before = '')
        exe a:cmd_before
        doautoall User SwitchTabPages
        return ''
      endfunc
    ]])
    feed('asomething<CR>')
    eq('something', api.nvim_get_var('entered'))
    neq(prev_win, api.nvim_get_current_win())
    feed('exit<CR>')
    eq(prev_win, api.nvim_get_current_win())
    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
    command('call DoAppend()')
    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
    feed('i')
    eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
    command('call DoAppend()')
    eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
    command("call DoAppend('stopinsert')")
    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
    command("call DoAppend('startreplace')")
    eq({ mode = 'R', blocking = false }, api.nvim_get_mode())
    feed('<Esc>')
    command('tabnew')
    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
    command("call DoAutoAll('startinsert')")
    eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
    command("call DoAutoAll('stopinsert')")
    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
  end)

  -- oldtest: Test_prompt_leave_modify_hidden()
  it('modifying hidden buffer does not prevent prompt buffer mode change', function()
    source([[
      file hidden
      set bufhidden=hide
      enew
      new prompt
      set buftype=prompt

      inoremap <buffer> w <Cmd>wincmd w<CR>
      inoremap <buffer> q <Cmd>bwipe!<CR>
      autocmd BufLeave prompt call appendbufline('hidden', '$', 'Leave')
      autocmd BufEnter prompt call appendbufline('hidden', '$', 'Enter')
      autocmd BufWinLeave prompt call appendbufline('hidden', '$', 'Close')
    ]])
    feed('a')
    eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
    feed('w')
    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
    feed('<C-W>w')
    eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
    feed('q')
    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
    command('bwipe!')
    expect([[

      Leave
      Enter
      Leave
      Close]])
  end)

  it('can insert multiline text', function()
    source_script()
    local buf = api.nvim_get_current_buf()

    feed('line 1<s-cr>line 2<s-cr>line 3')
    screen:expect([[
      cmd: line 1              |
      line 2                   |
      line 3^                   |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    -- prompt_getinput works with multiline input
    eq('line 1\nline 2\nline 3', fn('prompt_getinput', buf))

    feed('<cr>')
    -- submitting multiline text works
    screen:expect([[
      Result: "line 1          |
      line 2                   |
      line 3"                  |
      cmd: ^                    |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    eq('', fn('prompt_getinput', buf))

    -- % prompt is not repeated with formatoptions+=r
    source([[
      bwipeout!
      set formatoptions+=r
      call prompt_setprompt(bufnr(), "% ")
      set buftype=prompt
    ]])
    feed('iline1<s-cr>line2')
    screen:expect([[
      other buffer             |
      % line1                  |
      line2^                    |
      {1:~                        }|*6
      {5:-- INSERT --}             |
    ]])

    -- ensure cursor gets placed on first line of user input.
    -- when insert mode is entered from read-only region of prompt buffer.
    local prompt_pos = api.nvim_buf_get_mark(0, ':')
    feed('<esc>')
    -- works before prompt
    api.nvim_win_set_cursor(0, { prompt_pos[1] - 1, 0 })
    screen:expect([[
      ^other buffer             |
      % line1                  |
      line2                    |
      {1:~                        }|*6
                               |
    ]])
    feed('a')
    feed('<esc>')
    screen:expect([[
      other buffer             |
      %^ line1                  |
      line2                    |
      {1:~                        }|*6
                               |
    ]])
    -- works on prompt
    api.nvim_win_set_cursor(0, { prompt_pos[1], 0 })
    screen:expect([[
      other buffer             |
      ^% line1                  |
      line2                    |
      {1:~                        }|*6
                               |
    ]])
    feed('a')
    feed('<esc>')
    screen:expect([[
      other buffer             |
      %^ line1                  |
      line2                    |
      {1:~                        }|*6
                               |
    ]])

    -- i_<Left> i_<C-Left> i_<Home> i_<End> keys on prompt-line doesn't put cursor
    -- at end of text
    feed('a<Left><C-Left>')
    screen:expect([[
      other buffer             |
      % ^line1                  |
      line2                    |
      {1:~                        }|*6
      {5:-- INSERT --}             |
    ]])

    feed('<End>')
    screen:expect([[
      other buffer             |
      % line1^                  |
      line2                    |
      {1:~                        }|*6
      {5:-- INSERT --}             |
    ]])

    feed('<Home>')
    screen:expect([[
      other buffer             |
      % ^line1                  |
      line2                    |
      {1:~                        }|*6
      {5:-- INSERT --}             |
    ]])
  end)

  it('can put (p) multiline text', function()
    source_script()
    local buf = api.nvim_get_current_buf()

    fn('setreg', 'a', 'line 1\nline 2\nline 3')
    feed('<esc>"ap')
    screen:expect([[
      cmd: ^line 1              |
      line 2                   |
      line 3                   |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
                               |
    ]])

    -- prompt_getinput works with pasted input
    eq('line 1\nline 2\nline 3', fn('prompt_getinput', buf))

    feed('i<cr>')
    screen:expect([[
      Result: "line 1          |
      line 2                   |
      line 3"                  |
      cmd: ^                    |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
  end)

  it('can put multiline text with nvim_paste', function()
    source_script()
    api.nvim_paste('line 1\nline 2\nline 3', false, -1)
    screen:expect([[
      cmd: line 1              |
      line 2                   |
      line 3^                   |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
  end)

  it('can undo current prompt', function()
    source_script()
    local buf = api.nvim_get_current_buf()

    -- text editing allowed in current prompt
    feed('tests-initial<esc>')
    feed('bimiddle-<esc>')
    screen:expect([[
      cmd: tests-middle^-initial|
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
                               |
    ]])

    feed('Fdx')
    screen:expect([[
      cmd: tests-mid^le-initial |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
                               |
    ]])

    -- can undo edits until prompt has been submitted
    feed('u')
    screen:expect([[
      cmd: tests-mid^dle-initial|
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      1 change; {MATCH:.*} |
    ]])

    -- undo is reflected in prompt_getinput
    eq('tests-middle-initial', fn('prompt_getinput', buf))

    feed('u')
    screen:expect([[
      cmd: tests-^initial       |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      1 change; {MATCH:.*} |
    ]])

    feed('i<cr><esc>')
    screen:expect([[
      cmd: tests-initial       |
      Command: "tests-initial" |
      Result: "tests-initial"  |
      cmd:^                     |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
                               |
    ]])

    -- after submit undo does nothing
    feed('u')
    screen:expect([[
      cmd: tests-initial       |
      Command: "tests-initial" |
      cmd:^                     |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      1 line {MATCH:.*} |
    ]])

    -- "S" does not clear undo
    feed('ihello<Esc>S')
    screen:expect([[
      cmd: tests-initial       |
      Command: "tests-initial" |
      cmd: ^                    |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
    feed('<Esc>u')
    screen:expect([[
      cmd: tests-initial       |
      Command: "tests-initial" |
      ^cmd: hello               |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      1 change; {MATCH:.*} |
    ]])

    -- undo cleared if prompt changes
    -- (otherwise undoing would abort it and append a new prompt, which isn't useful)
    fn('prompt_setprompt', '', 'cmd > ')
    feed('u')
    screen:expect([[
      cmd: tests-initial       |
      Command: "tests-initial" |
      c^md > hello              |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      Already at oldest change |
    ]])

    -- new prompt line appended to fix missing prompt also clears undo
    feed('A there')
    fn('setpos', "':", { 0, fn('line', '.'), 99, 0 })
    feed('<Esc>u')
    screen:expect([[
      cmd: tests-initial       |
      Command: "tests-initial" |
      cmd > hello there        |
      cmd >^                    |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      Already at oldest change |
    ]])
  end)

  it('o/O can create new lines', function()
    source_script()
    local buf = api.nvim_get_current_buf()

    feed('line 1<s-cr>line 2<s-cr>line 3')
    screen:expect([[
      cmd: line 1              |
      line 2                   |
      line 3^                   |
      {1:~                        }|
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('<esc>koafter')
    screen:expect([[
      cmd: line 1              |
      line 2                   |
      after^                    |
      line 3                   |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    -- newline created with o is reflected in prompt_getinput
    eq('line 1\nline 2\nafter\nline 3', fn('prompt_getinput', buf))

    feed('<esc>kObefore')

    screen:expect([[
      cmd: line 1              |
      before^                   |
      line 2                   |
      after                    |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    -- newline created with O is reflected in prompt_getinput
    eq('line 1\nbefore\nline 2\nafter\nline 3', fn('prompt_getinput', buf))

    feed('<cr>')
    vim.uv.sleep(20)
    eq('', fn('prompt_getinput', buf))
    screen:expect([[
      line 2                   |
      after                    |
      line 3"                  |
      cmd: ^                    |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('line 4<s-cr>line 5')
    screen:expect([[
      after                    |
      line 3"                  |
      cmd: line 4              |
      line 5^                   |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('<esc>k0oafter prompt')
    screen:expect([[
      after                    |
      line 3"                  |
      cmd: line 4              |
      after prompt^             |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('<esc>k0Oat prompt')
    screen:expect([[
      after                    |
      line 3"                  |
      cmd: at prompt^           |
      line 4                   |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('<cr>')
    vim.uv.sleep(20)
    eq('', fn('prompt_getinput', buf))
    screen:expect([[
      line 4                   |
      after prompt             |
      line 5"                  |
      cmd: ^                    |
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
  end)

  it('deleting prompt adds it back on insert', function()
    source_script()
    feed('asdf')
    screen:expect([[
      cmd: asdf^                |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('<esc>ddi')
    screen:expect([[
      cmd: ^                    |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('asdf')
    screen:expect([[
      cmd: asdf^                |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    feed('<esc>cc')
    screen:expect([[
      cmd: ^                    |
      {1:~                        }|*3
      {3:[Prompt]                 }|
      other buffer             |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])
  end)

  it("sets the ': mark", function()
    api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })
    exec_lua(function()
      local buf = vim.api.nvim_get_current_buf()
      vim.fn.prompt_setprompt(buf, 'cmd > ')
      vim.fn.prompt_setcallback(buf, function(str)
        local last_line = vim.api.nvim_buf_line_count(buf)
        vim.api.nvim_buf_set_lines(buf, last_line - 1, last_line - 1, true, vim.split(str, '\n'))
      end)
    end)

    feed('asdf')
    eq({ 1, 6 }, api.nvim_buf_get_mark(0, ':'))
    feed('<cr>')
    eq({ 3, 6 }, api.nvim_buf_get_mark(0, ':'))
    -- Multiline prompt.
    feed('<s-cr>line1<s-cr>line2<s-cr>line3<cr>')
    eq({ 11, 6 }, api.nvim_buf_get_mark(0, ':'))

    -- ': mark is only available in prompt buffer.
    api.nvim_set_option_value('buftype', '', { buf = 0 })
    eq("Invalid mark name: ':'", t.pcall_err(api.nvim_buf_get_mark, 0, ':'))

    -- mark can be moved
    api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })
    local last_line = api.nvim_buf_line_count(0)
    eq({ last_line, 6 }, api.nvim_buf_get_mark(0, ':'))
    eq(true, api.nvim_buf_set_mark(0, ':', 1, 5, {}))
    eq({ 1, 5 }, api.nvim_buf_get_mark(0, ':'))

    -- No crash from invalid col.
    eq(true, api.nvim_buf_set_mark(0, ':', fn('line', '.'), 999, {}))
    eq({ 12, 6 }, api.nvim_buf_get_mark(0, ':'))

    -- Clamps lnum to at least 1. Do in one event to repro the leak.
    exec_lua(function()
      vim.fn.setpos("':", { 0, 0, 0, 0 })
      vim.fn.prompt_setprompt('', 'bar > ')
    end)
    eq({ 1, 6 }, api.nvim_buf_get_mark(0, ':'))

    -- No ml_get error from invalid lnum.
    command('set messagesopt+=wait:0 messagesopt-=hit-enter')
    fn('setpos', "':", { 0, 999, 7, 0 })
    eq('', api.nvim_get_vvar('errmsg'))
    command('set messagesopt&')
    eq({ 13, 6 }, api.nvim_buf_get_mark(0, ':'))
  end)

  describe('prompt_getinput', function()
    it('returns current prompts text', function()
      command('new')
      local bufnr = fn('bufnr')
      api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })
      eq('', fn('prompt_getinput', bufnr))
      feed('iasdf')
      eq('asdf', fn('prompt_getinput', bufnr))
      feed('<esc>dd')
      eq('', fn('prompt_getinput', bufnr))
      feed('iasdf2')
      eq('asdf2', fn('prompt_getinput', bufnr))

      -- returns empty string when called from non prompt buffer
      api.nvim_set_option_value('buftype', '', { buf = 0 })
      eq('', fn('prompt_getinput', bufnr))
    end)
  end)

  it('programmatic (non-user) edits', function()
    api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })

    -- with nvim_buf_set_lines
    exec_lua([[
      local buf = vim.api.nvim_get_current_buf()
      vim.fn.prompt_setcallback(buf, function(text)
        vim.api.nvim_buf_set_lines(buf, -2, -2, true, vim.split(text, '\n'))
      end)
    ]])
    feed('iset_lines<cr>')
    feed('set_lines2<cr>')
    screen:expect([[
      % set_lines              |
      set_lines                |
      % set_lines2             |
      set_lines2               |
      % ^                       |
      {1:~                        }|*4
      {5:-- INSERT --}             |
    ]])

    feed('set_lines3(multi-1)<s-cr>set_lines3(multi-2)<cr>')
    screen:expect([[
      % set_lines              |
      set_lines                |
      % set_lines2             |
      set_lines2               |
      % set_lines3(multi-1)    |
      set_lines3(multi-2)      |
      set_lines3(multi-1)      |
      set_lines3(multi-2)      |
      % ^                       |
      {5:-- INSERT --}             |
    ]])
    -- with nvim_buf_set_text
    source('bwipeout!')
    api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })
    exec_lua([[
      local buf = vim.api.nvim_get_current_buf()
      vim.fn.prompt_setcallback(buf, function(text)
        local lines = vim.split(text, '\n')
        if lines[#lines] ~= '' then
          table.insert(lines, '')
        end
        vim.api.nvim_buf_set_text(buf, -1, 0, -1, 0, lines)
      end)
    ]])
    feed('set_text<cr>')
    feed('set_text2<cr>')
    screen:expect([[
      % set_text               |
      set_text                 |
      % set_text2              |
      set_text2                |
      % ^                       |
      {1:~                        }|*4
      {5:-- INSERT --}             |
    ]])

    feed('set_text3(multi-1)<s-cr>set_text3(multi-2)<cr>')
    screen:expect([[
      % set_text               |
      set_text                 |
      % set_text2              |
      set_text2                |
      % set_text3(multi-1)     |
      set_text3(multi-2)       |
      set_text3(multi-1)       |
      set_text3(multi-2)       |
      % ^                       |
      {5:-- INSERT --}             |
    ]])
  end)

  it('works correctly with empty string as prompt', function()
    api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })
    exec_lua(function()
      local buf = vim.api.nvim_get_current_buf()
      vim.fn.prompt_setprompt(buf, '')
    end)

    source('startinsert')

    -- mark correctly set
    eq({ 1, 0 }, api.nvim_buf_get_mark(0, ':'))

    feed('asdf')
    screen:expect([[
      asdf^                     |
      {1:~                        }|*8
      {5:-- INSERT --}             |
    ]])

    -- can clear all of it
    feed('<backspace><backspace><backspace><backspace>')
    screen:expect([[
      ^                         |
      {1:~                        }|*8
      {5:-- INSERT --}             |
    ]])
    feed('<cr>')

    eq({ 2, 0 }, api.nvim_buf_get_mark(0, ':'))
  end)

  it('prompt can be changed without interrupting user input', function()
    api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })
    local buf = api.nvim_get_current_buf()

    local function set_prompt(prompt, b)
      fn('prompt_setprompt', b or buf, prompt)
    end

    set_prompt('> ')

    source('startinsert')

    feed('user input')
    -- Move the cursor a bit to check cursor maintaining position
    feed('<esc>hhi')

    screen:expect([[
      > user in^put             |
      {1:~                        }|*8
      {5:-- INSERT --}             |
    ]])

    eq({ 1, 2 }, api.nvim_buf_get_mark(0, ':'))

    set_prompt('new-prompt > ')

    screen:expect([[
      new-prompt > user in^put  |
      {1:~                        }|*8
      {5:-- INSERT --}             |
    ]])

    eq({ 1, 13 }, api.nvim_buf_get_mark(0, ':'))

    set_prompt('new-prompt(status) > ')

    screen:expect([[
      new-prompt(status) > user|
       in^put                   |
      {1:~                        }|*7
      {5:-- INSERT --}             |
    ]])
    eq({ 1, 21 }, api.nvim_buf_get_mark(0, ':'))

    set_prompt('new-prompt > ')

    screen:expect([[
      new-prompt > user in^put  |
      {1:~                        }|*8
      {5:-- INSERT --}             |
    ]])
    eq({ 1, 13 }, api.nvim_buf_get_mark(0, ':'))

    -- Cursor not moved when not on the prompt line.
    feed('<CR>user input<Esc>k')
    screen:expect([[
      new-prompt > user inpu^t  |
      new-prompt > user input  |
      {1:~                        }|*7
                               |
    ]])
    set_prompt('<>< ')
    screen:expect([[
      new-prompt > user inpu^t  |
      <>< user input           |
      {1:~                        }|*7
                               |
    ]])
    -- Correct col when prompt has multi-cell chars.
    feed('i<Left><Left>')
    screen:expect([[
      new-prompt > user input  |
      <>< user inp^ut           |
      {1:~                        }|*7
      {5:-- INSERT --}             |
    ]])
    set_prompt('\t > ')
    screen:expect([[
      new-prompt > user input  |
               > user inp^ut    |
      {1:~                        }|*7
      {5:-- INSERT --}             |
    ]])
    -- Works with 'virtualedit': coladd remains sensible. Cursor is redrawn correctly.
    -- Tab size visually changes due to multiples of 'tabstop'.
    command('set virtualedit=all')
    feed('<C-O>Sa<Tab>b<C-O>3h')
    screen:expect([[
      new-prompt > user input  |
               > a  ^  b        |
      {1:~                        }|*7
      {5:-- INSERT --}             |
    ]])
    set_prompt('😊 > ')
    screen:expect([[
      new-prompt > user input  |
      😊 > a ^ b                |
      {1:~                        }|*7
      {5:-- INSERT --}             |
    ]])
    -- Minimum col should be 1. Same event to avoid corrections from the state loop.
    feed('<Esc>0')
    local colnr = exec_lua(function()
      vim.fn.prompt_setprompt('', '')
      return vim.fn.col('.')
    end)
    eq(1, colnr)
    -- Correct cursor adjustment when old ': col and old prompt length differs.
    set_prompt('foo > ')
    fn('setpos', "':", { 0, fn('line', '.'), 10, 0 })
    fn('setline', '.', '   foo > hello')
    feed('fh')
    screen:expect([[
      new-prompt > user input  |
         foo > ^hello           |
      {1:~                        }|*7
                               |
    ]])
    set_prompt('bar > ')
    screen:expect([[
      new-prompt > user input  |
      bar > ^hello              |
      {1:~                        }|*7
                               |
    ]])

    -- No crash when setting shorter prompt than curbuf's in other buffer.
    feed('ztA')
    command('set virtualedit& | new | setlocal buftype=prompt')
    set_prompt('looooooooooooooooooooooooooooooooooooooooooooong > ', '') -- curbuf
    set_prompt('foo > ')
    screen:expect([[
      loooooooooooooooooooooooo|
      ooooooooooooooooooooong >|
       ^                        |
      {1:~                        }|
      {3:[Prompt]                 }|
      foo > hello              |
      {1:~                        }|*3
      {5:-- INSERT --}             |
    ]])

    -- No prompt_setprompt crash from invalid ': col. Must happen in the same event.
    exec_lua(function()
      vim.cmd 'bwipeout!'
      vim.api.nvim_buf_set_mark(0, ':', vim.fn.line('.'), 999, {})
      vim.fn.prompt_setprompt('', 'new-prompt > ')
    end)
    screen:expect([[
      new-prompt > ^            |
      {1:~                        }|*8
      {5:-- INSERT --}             |
    ]])

    -- No leak if prompt_setprompt called for unloaded prompt buffer.
    local unloaded_buf = fn('bufadd', '')
    api.nvim_set_option_value('buftype', 'prompt', { buf = unloaded_buf })
    fn('prompt_setprompt', unloaded_buf, 'hello unloaded! > ')
    eq('hello unloaded! > ', fn('prompt_getprompt', unloaded_buf))
  end)

  it('prompt_appendbuf with multi-element list and singleline prompt', function()
    command('new')
    local buf = api.nvim_get_current_buf()
    api.nvim_set_option_value('buftype', 'prompt', { buf = buf })
    fn('prompt_setprompt', buf, 'cmd: ')

    -- Single element list: appends before prompt
    fn('prompt_appendbuf', buf, { 'line1' })
    eq({ 'line1', 'cmd: ' }, api.nvim_buf_get_lines(buf, 0, -1, false))

    -- Multi-element list: first element appended, rest inserted as new lines before prompt
    fn('prompt_appendbuf', buf, { '-append', 'line2' })
    eq({ 'line1-append', 'line2', 'cmd: ' }, api.nvim_buf_get_lines(buf, 0, -1, false))

    -- Multi-element list after multi-element list
    fn('prompt_appendbuf', buf, { '', 'line3', 'line4', 'line5', 'line6' })
    eq(
      { 'line1-append', 'line2', 'line3', 'line4', 'line5', 'line6', 'cmd: ' },
      api.nvim_buf_get_lines(buf, 0, -1, false)
    )
  end)

  it('prompt_appendbuf with multi-element list and multiline prompt', function()
    command('new')
    local buf = api.nvim_get_current_buf()
    api.nvim_set_option_value('buftype', 'prompt', { buf = buf })
    fn('prompt_setprompt', buf, 'cmd: ')
    source('startinsert')

    -- User types multiline input in the prompt
    feed('input1<s-cr>input2')
    eq({ 'cmd: input1', 'input2' }, api.nvim_buf_get_lines(buf, 0, -1, false))

    -- Single element list: appends before prompt
    fn('prompt_appendbuf', buf, { 'line1' })
    eq({ 'line1', 'cmd: input1', 'input2' }, api.nvim_buf_get_lines(buf, 0, -1, false))

    -- Multi-element list: first element appended, rest inserted as new lines before prompt
    fn('prompt_appendbuf', buf, { '-append', 'line2' })
    eq(
      { 'line1-append', 'line2', 'cmd: input1', 'input2' },
      api.nvim_buf_get_lines(buf, 0, -1, false)
    )

    -- Multi-element list after multi-element list
    fn('prompt_appendbuf', buf, { '', 'line3', 'line4', 'line5', 'line6' })
    eq(
      { 'line1-append', 'line2', 'line3', 'line4', 'line5', 'line6', 'cmd: input1', 'input2' },
      api.nvim_buf_get_lines(buf, 0, -1, false)
    )
  end)
end)
