-- TUI acceptance tests.
-- Uses :terminal as a way to send keys and assert screen state.
--
-- "bracketed paste" terminal feature:
-- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode

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

local eq = t.eq
local feed_data = tt.feed_data
local clear = n.clear
local command = n.command
local exec = n.exec
local exec_lua = n.exec_lua
local testprg = n.testprg
local retry = t.retry
local nvim_prog = n.nvim_prog
local nvim_set = n.nvim_set
local ok = t.ok
local read_file = t.read_file
local fn = n.fn
local api = n.api
local is_os = t.is_os
local new_pipename = n.new_pipename
local set_session = n.set_session
local write_file = t.write_file
local eval = n.eval
local assert_log = t.assert_log

local testlog = 'Xtest-tui-log'

-- Using this to have 'notermguicolors' in Nvim instances without starting a timer
-- that causes delay on exit with ASAN/TSAN.
local env_notermguicolors = { COLORTERM = 'xterm-256color' }

describe('TUI', function()
  it('exit status 1 and error message with server --listen error #34365', function()
    clear()
    local addr_in_use = api.nvim_get_vvar('servername')
    local screen = tt.setup_child_nvim(
      { '--listen', addr_in_use, '-u', 'NONE', '-i', 'NONE' },
      { extra_rows = 10, cols = 60, env = { NVIM_LOG_FILE = testlog } }
    )
    finally(function()
      os.remove(testlog)
    end)

    screen:expect({ any = vim.pesc('[Process exited 1]') })

    -- When the address is very long, the error message may be only partly visible.
    if #addr_in_use <= 600 then
      screen:expect({
        any = vim.pesc(
          ('%s: Failed to --listen: address already in use:'):format(
            fn.fnamemodify(nvim_prog, ':t')
          )
        ),
        unchanged = true,
      })
    end

    -- Always assert the log for the error message.
    assert_log(
      vim.pesc('Failed to start server: address already in use: ' .. addr_in_use),
      testlog,
      100
    )
  end)

  it('suspending does not crash or hang', function()
    clear()
    local screen = tt.setup_child_nvim({ '--clean' }, { env = env_notermguicolors })
    local s0 = [[
      ^                                                  |
      ~                                                 |*3
      {2:[No Name]                       0,0-1          All}|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen:expect(s0)
    feed_data(':')
    local s1 = [[
                                                        |
      ~                                                 |*3
      {2:[No Name]                       0,0-1          All}|
      :^                                                 |
      {5:-- TERMINAL --}                                    |
    ]]
    screen:expect(s1)
    feed_data('suspend\r')
    if is_os('win') then -- no-op on Windows
      screen:expect([[
        ^                                                  |
        ~                                                 |*3
        {2:[No Name]                       0,0-1          All}|
        :suspend                                          |
        {5:-- TERMINAL --}                                    |
      ]])
    else -- resuming works on other platforms
      screen:expect([[
                                                          |*5
        ^[Process suspended]                               |
        {5:-- TERMINAL --}                                    |
      ]])
      n.feed('<Space>')
      screen:expect(s0)
    end
    feed_data(':')
    screen:expect(s1)
  end)
end)

describe('TUI :detach', function()
  it('does not stop server', function()
    local job_opts = { env = t.shallowcopy(env_notermguicolors) }

    n.clear()
    finally(function()
      n.check_close()
    end)

    local child_server = new_pipename()
    local screen = tt.setup_child_nvim({
      '--listen',
      child_server,
      '-u',
      'NONE',
      '-i',
      'NONE',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      nvim_set .. ' laststatus=2 background=dark',
    }, job_opts)
    tt.override_screen_expect_for_conpty(screen)

    tt.feed_data('iHello, World')
    screen:expect([[
      Hello, World^                                      |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])

    local child_session = n.connect(child_server)
    finally(function()
      -- Avoid a dangling process after :detach.
      child_session:request('nvim_command', 'qall!')
    end)
    local status, child_uis = child_session:request('nvim_list_uis')
    assert(status)
    eq(1, #child_uis)

    eq(
      { false, { 0, 'Vim(detach):E477: No ! allowed: detach!' } },
      { child_session:request('nvim_command', 'detach!') }
    )
    eq(
      { false, { 0, 'Vim(detach):E481: No range allowed: 1detach' } },
      { child_session:request('nvim_command', '1detach') }
    )
    eq(
      { false, { 0, 'Vim(detach):E488: Trailing characters: foo: detach foo' } },
      { child_session:request('nvim_command', 'detach foo') }
    )

    tt.feed_data('\027\027:detach\013')
    -- Note: "Process exited" message is misleading; tt.setup_child_nvim() sees the foreground
    -- process (client) exited, and doesn't know the server is still running?
    screen:expect {
      any = [[Process exited 0]],
    }

    child_uis --[[@type any[] ]] = ({ child_session:request('nvim_list_uis') })[2]
    eq(0, #child_uis)

    -- NOTE: The tt.setup_child_nvim() screen just wraps :terminal, it's not connected to the child.
    -- To use it again, we need to detach the old one.
    screen:detach()

    -- Edit some text on the headless server.
    status = (child_session:request('nvim_input', 'ddiWe did it, pooky.<Esc><Esc>'))
    assert(status)

    -- Test reattach by connecting a new TUI.
    local screen_reattached = tt.setup_child_nvim({
      '--remote-ui',
      '--server',
      child_server,
    }, job_opts)

    screen_reattached:expect([[
      We did it, pooky^.                                 |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)
end)

describe('TUI :restart', function()
  before_each(n.clear)
  after_each(n.check_close)

  --- Asserts that the server at `addr` has (or has not) restarted since `starttime`.
  ---@param starttime integer v:starttime of the server before restart
  ---@param sess table Session
  ---@param addr string
  ---@return integer starttime
  ---@return table session
  local function assert_restarted(starttime, sess, addr)
    sess:close()
    local new_starttime
    -- Retry: old server may still be alive (connect succeeds but yields stale starttime),
    -- or on Windows the --listen address is restored async.
    retry(nil, 5000, function()
      sess = n.connect(addr)
      local _, t = sess:request('nvim_eval', 'v:starttime')
      ok(t > starttime, ('v:starttime (%d) > old starttime (%d)'):format(t, starttime), t)
      new_starttime = t
    end)
    return new_starttime, sess
  end

  it('validation', function()
    eq('Vim(restart):E481: No range allowed: :1restart', t.pcall_err(n.command, ':1restart'))
  end)

  it('ZR', function()
    -- Just exercise ZR, don't need to test all :restart functionality here.
    local server_pipe = new_pipename()
    local server_session
    finally(function()
      if server_session then
        server_session:close()
      end
    end)
    local screen = tt.setup_child_nvim({
      '--clean',
      '--listen',
      server_pipe,
      '--cmd',
      'set notermguicolors',
    }, {
      env = vim.tbl_extend('force', env_notermguicolors, {
        -- Ignore logs, because assert_restarted may log "connection refused" while it retries.
        NVIM_LOG_FILE = testlog,
      }),
    })
    finally(function()
      os.remove(testlog)
    end)
    screen:expect({ any = '%[No Name%]' })

    server_session = n.connect(server_pipe)
    local _, starttime = server_session:request('nvim_eval', 'v:starttime')

    -- ZR on modified buffer fails with E37.
    tt.feed_data('ifoo\027')
    tt.feed_data('ZR')
    screen:expect({ any = 'E37:' })

    -- [count]ZR discards unsaved changes.
    tt.feed_data('1ZR')
    screen:expect({ any = vim.pesc('[No Name]') })
    starttime, server_session = assert_restarted(starttime, server_session, server_pipe)

    -- The server is now detached and needs to be quit explicitly.
    tt.feed_data(':qall!\r')
  end)

  it('works', function()
    -- Log exit events + v:exitreason.
    local eventlog = t.tmpname()
    local initfile = t.tmpname() .. '.lua'
    local init = [[
      local f = %q
      vim.api.nvim_create_autocmd({ 'QuitPre', 'ExitPre', 'VimLeavePre', 'VimLeave' }, {
        callback = function(ev)
          vim.fn.writefile({ ('%%s:%%s'):format(ev.event, vim.v.exitreason) }, f, 'a')
        end,
      })
    ]]
    write_file(initfile, init:format(eventlog))
    finally(function()
      os.remove(eventlog)
      os.remove(initfile)
    end)

    local function assert_exitreason(expected)
      local default = 'QuitPre:restart\nExitPre:restart\nVimLeavePre:restart\nVimLeave:restart\n'
      eq(expected or default, t.read_file(eventlog))
      os.remove(eventlog)
    end

    local server_pipe = new_pipename()
    local screen = tt.setup_child_nvim({
      '--clean',
      '-u',
      initfile,
      '--listen',
      server_pipe,
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set laststatus=2 background=dark noruler noshowcmd',
    }, { env = { COLORTERM = 'truecolor' } })
    screen:set_option('rgb', true)

    -- 'termguicolors' support should be detected properly after :restart.
    -- The value of has("gui_running") should be 0 before and after :restart.
    local function assert_termguicolors_and_no_gui_running()
      tt.feed_data(':echo "&termguicolors: " .. &termguicolors\013')
      screen:expect({ any = '&termguicolors: 1' })
      tt.feed_data(':echo "GUI Running: " .. has("gui_running")\013')
      screen:expect({ any = 'GUI Running: 0' })
    end

    local s0 = [[
      ^                                                  |
      {1:~}{18:                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen:expect(s0)
    assert_termguicolors_and_no_gui_running()

    local server_session = n.connect(server_pipe)
    local _, server_pid = server_session:request('nvim_call_function', 'getpid', {})
    local function assert_new_pid()
      server_session:close()
      -- On Windows, --listen address is restored async (after old server exits).
      if is_os('win') then
        retry(nil, 5000, function()
          server_session = n.connect(server_pipe)
        end)
      else
        server_session = n.connect(server_pipe)
      end
      local _, new_pid = server_session:request('nvim_call_function', 'getpid', {})
      t.neq(server_pid, new_pid)
      server_pid = new_pid
    end

    local s1 = [[
                                                        |
      ^Hello1                                            |
      {1:~}{18:                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]

    tt.feed_data(':set nomodified\013')
    -- Command is run on new server.
    tt.feed_data(":restart put ='Hello1'\013")
    screen:expect(s1)
    assert_new_pid()
    assert_exitreason()
    assert_termguicolors_and_no_gui_running()

    -- Complex command following +cmd.
    tt.feed_data(":restart +qall! put ='Hello2' | put ='World2'\013")
    screen:expect([[
                                                        |
      Hello2                                            |
      ^World2                                            |
      {1:~}{18:                                                 }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    assert_new_pid()
    assert_exitreason()
    assert_termguicolors_and_no_gui_running()

    -- Check ":restart" on an unmodified buffer.
    tt.feed_data(':set nomodified\013')
    tt.feed_data(':restart\013')
    screen:expect(s0)
    assert_new_pid()
    assert_exitreason()
    assert_termguicolors_and_no_gui_running()

    -- Check ":restart +qall!" on an unmodified buffer.
    tt.feed_data(':restart +qall!\013')
    screen:expect(s0)
    assert_new_pid()
    assert_exitreason()
    assert_termguicolors_and_no_gui_running()

    -- Check ":restart +echo" cannot restart server.
    -- Check the full screen state to ensure this doesn't pollute the current UI.
    tt.feed_data(':restart +echo\013')
    screen:expect([[
      ^                                                  |
      {1:~}{18:                                                 }|*3
      {3:[No Name]                                         }|
      {9:restart failed: +cmd did not quit the server}      |
      {5:-- TERMINAL --}                                    |
    ]])

    tt.feed_data('ithis will be removed\027')
    screen:expect({ any = vim.pesc('this will be remove^d') })

    -- Check ":confirm restart" on a modified buffer.
    tt.feed_data(':confirm restart\013')
    screen:expect({ any = vim.pesc('Save changes to "Untitled"?') })

    -- Cancel the operation (abandons restart).
    tt.feed_data('C\013')
    screen:expect({ any = vim.pesc('[No Name]') })
    -- Failed/cancelled restarts still fire QuitPre/ExitPre (but not VimLeave[Pre]).
    assert_exitreason('QuitPre:restart\nExitPre:restart\n')

    -- Check :restart respects 'confirm' option.
    tt.feed_data(':set confirm\013')
    tt.feed_data(':restart\013')
    screen:expect({ any = vim.pesc('Save changes to "Untitled"?') })
    tt.feed_data('C\013')
    screen:expect({ any = vim.pesc('[No Name]') })
    tt.feed_data(':set noconfirm\013')
    -- Failed/cancelled restarts still fire QuitPre/ExitPre (but not VimLeave[Pre]).
    assert_exitreason('QuitPre:restart\nExitPre:restart\n')

    -- Check ":confirm restart <cmd>" on a modified buffer.
    tt.feed_data(":confirm restart put ='Hello3'\013")
    screen:expect({ any = vim.pesc('Save changes to "Untitled"?') })
    tt.feed_data('N\013')
    screen:expect({ any = '%^Hello3' })
    assert_new_pid()
    assert_exitreason()
    assert_termguicolors_and_no_gui_running()

    -- Check ":confirm restart +echo" correctly ignores ":confirm"
    tt.feed_data(':confirm restart +echo\013')
    screen:expect({ any = vim.pesc('+cmd did not quit the server') })

    -- Check ":restart" on a modified buffer.
    tt.feed_data('ithis will be removed\027')
    tt.feed_data(':restart\013')
    screen:expect({ any = vim.pesc('Vim(qall):E37: No write since last change') })
    assert_exitreason('QuitPre:restart\nExitPre:restart\n')

    -- Check ":restart +qall!" on a modified buffer.
    tt.feed_data('ithis will be removed\027')
    tt.feed_data(':restart +qall!\013')
    screen:expect(s0)
    assert_new_pid()
    assert_exitreason()
    assert_termguicolors_and_no_gui_running()

    if not is_os('win') then
      -- No --listen conflict when server exit is delayed.
      feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd.restart()\n')
      screen:expect(s0)
      assert_new_pid()
      assert_exitreason()
      assert_termguicolors_and_no_gui_running()
    end

    screen:try_resize(60, 6)
    screen:expect([[
      ^                                                            |
      {1:~}{18:                                                           }|*2
      {3:[No Name]                                                   }|
                                                                  |
      {5:-- TERMINAL --}                                              |
    ]])

    --- Check that ":restart" uses the updated size after terminal resize.
    tt.feed_data(':restart echo "restarted"\013')
    screen:expect([[
      ^                                                            |
      {1:~}{18:                                                           }|*2
      {3:[No Name]                                                   }|
      restarted                                                   |
      {5:-- TERMINAL --}                                              |
    ]])
    assert_new_pid()
    assert_exitreason()
    assert_termguicolors_and_no_gui_running()

    -- The server is now detached and needs to be quit explicitly.
    feed_data(':qall!\r')
    screen:expect({ any = vim.pesc('[Process exited 0]') })
  end)

  it('drops "-" and "-- [files…]" from v:argv #34417', function()
    t.skip(is_os('win'), 'stdin behavior differs on Windows')
    local server_session
    finally(function()
      if server_session then
        server_session:close()
      end
    end)
    local server_pipe = new_pipename()
    local screen = tt.setup_child_nvim({
      '-u',
      'NONE',
      '-i',
      'NONE',
      '--listen',
      server_pipe,
      '--cmd',
      'set notermguicolors',
      '-s',
      '-',
      '-',
      '--',
      'Xtest-file1',
      'Xtest-file2',
    }, { env = env_notermguicolors })
    screen:expect([[
      ^                                                  |
      ~                                                 |*3
      {2:Xtest-file1                     0,0-1          All}|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    server_session = n.connect(server_pipe)
    local expr = 'index(v:argv, "-") >= 0 || index(v:argv, "--") >= 0 ? v:true : v:false'
    local has_s = 'index(v:argv, "-s") >= 0 ? v:true : v:false'
    eq({ true, true }, { server_session:request('nvim_eval', expr) })
    eq({ true, true }, { server_session:request('nvim_eval', has_s) })

    tt.feed_data(":restart put='foo'\013")
    screen:expect([[
                                                        |
      ^foo                                               |
      ~                                                 |*2
      {2:[No Name] [+]                   2,1            All}|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    server_session:close()
    server_session = n.connect(server_pipe)

    eq({ true, false }, { server_session:request('nvim_eval', expr) })
    eq({ true, false }, { server_session:request('nvim_eval', has_s) })

    -- local argv = ({ server_session:request('nvim_eval', 'v:argv') })[2] --[[@type table]]
    -- eq(13, #argv)
    -- eq("-c put='foo'", table.concat(argv, ' ', #argv - 1, #argv))

    -- The server is now detached and needs to be quit explicitly.
    feed_data(':qall!\r')
    screen:expect({ any = vim.pesc('[Process exited 0]') })
  end)

  it('[command] triggers autocommands properly #38549', function()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'autocmd FileType text echomsg "TRIGGERED: " .. bufnr()',
      '--cmd',
      'set notermguicolors noswapfile laststatus=0',
    }, { env = env_notermguicolors })
    screen:expect([[
      ^                                                  |
      ~                                                 |*4
                                      0,0-1         All |
      {5:-- TERMINAL --}                                    |
    ]])

    -- 'laststatus' should be 0 in the new Nvim and FileType event should be triggered.
    feed_data(':restart set nowrap | edit test/functional/fixtures/bigfile.txt\r')
    screen:expect([[
      ^0000;<control>;Cc;0;BN;;;;;N;NULL;;;;             |
      0001;<control>;Cc;0;BN;;;;;N;START OF HEADING;;;; |
      0002;<control>;Cc;0;BN;;;;;N;START OF TEXT;;;;    |
      0003;<control>;Cc;0;BN;;;;;N;END OF TEXT;;;;      |
      0004;<control>;Cc;0;BN;;;;;N;END OF TRANSMISSION;;|
      TRIGGERED: 1                    1,1           Top |
      {5:-- TERMINAL --}                                    |
    ]])

    -- The server is now detached and needs to be quit explicitly.
    feed_data(':qall!\r')
    screen:expect({ any = vim.pesc('[Process exited 0]') })
  end)

  it('new server loads user config after old server exits #38569', function()
    local config_file = 'Xrestart_session_config.lua'
    local session_file = 'Xrestart_session.vim'
    write_file(
      config_file,
      ([[
        vim.api.nvim_create_autocmd("VimLeavePre", {
          callback = function()
            vim.cmd('mksession! %s')
          end,
        })

        if vim.v.vim_did_enter and vim.uv.fs_stat('%s') then
          vim.cmd('source %s')
        end
      ]]):format(session_file, session_file, session_file)
    )
    finally(function()
      os.remove(config_file)
      os.remove(session_file)
    end)

    local screen = tt.setup_child_nvim({
      '--clean',
      '-u',
      config_file,
      '--cmd',
      'set notermguicolors noswapfile laststatus=0 nowrap noruler noshowcmd',
    }, { env = env_notermguicolors })
    screen:expect([[
      ^                                                  |
      ~                                                 |*4
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data(':rightbelow 28vsplit test/functional/fixtures/bigfile.txt\r')
    screen:expect([[
                           │^0000;<control>;Cc;0;BN;;;;;N|
      ~                    │0001;<control>;Cc;0;BN;;;;;N|
      ~                    │0002;<control>;Cc;0;BN;;;;;N|
      ~                    │0003;<control>;Cc;0;BN;;;;;N|
      ~                    │0004;<control>;Cc;0;BN;;;;;N|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data(':restart echo "restarted"\r')
    screen:expect([[
      ^                     │0000;<control>;Cc;0;BN;;;;;N|
      ~                    │0001;<control>;Cc;0;BN;;;;;N|
      ~                    │0002;<control>;Cc;0;BN;;;;;N|
      ~                    │0003;<control>;Cc;0;BN;;;;;N|
      ~                    │0004;<control>;Cc;0;BN;;;;;N|
      restarted                                         |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data(':set sessionoptions-=winsize | restart\r')
    screen:expect([[
      ^                         │0000;<control>;Cc;0;BN;;|
      ~                        │0001;<control>;Cc;0;BN;;|
      ~                        │0002;<control>;Cc;0;BN;;|
      ~                        │0003;<control>;Cc;0;BN;;|
      ~                        │0004;<control>;Cc;0;BN;;|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    -- The server is now detached and needs to be quit explicitly.
    feed_data(':qall!\r')
    screen:expect({ any = vim.pesc('[Process exited 0]') })
  end)
end)

describe('TUI :connect', function()
  before_each(n.clear)
  after_each(n.check_close)

  local screen_empty = [[
    ^                                                  |
    {100:~                                                 }|*5
                                                      |
  ]]

  it('leaves the current server running', function()
    local server1 = new_pipename()
    local screen1 = tt.setup_child_nvim({ '--listen', server1, '--clean' })
    screen1:expect({ any = vim.pesc('[No Name]') })

    tt.feed_data(':connect\013')
    screen1:expect({ any = 'E471: Argument required' })

    tt.feed_data('iThis is server 1.\027')
    screen1:expect({ any = vim.pesc('This is server 1^.') })

    -- Prevent screen2 from receiving the old terminal state.
    command('enew')
    screen1:expect(screen_empty)
    screen1:detach()

    local server2 = new_pipename()
    local screen2 = tt.setup_child_nvim({ '--listen', server2, '--clean' })
    screen2:expect({ any = vim.pesc('[No Name]') })

    tt.feed_data('iThis is server 2.\027')
    screen2:expect({ any = vim.pesc('This is server 2^.') })

    tt.feed_data(':connect ' .. server1 .. '\013')
    screen2:expect({ any = vim.pesc('This is server 1^.') })

    local server1_session = n.connect(server1)
    server1_session:request('nvim_command', 'qall!')
    screen2:expect({ any = vim.pesc('[Process exited 0]') })

    screen2:detach()

    local server2_session = n.connect(server2)

    local screen3 = tt.setup_child_nvim({ '--remote-ui', '--server', server2 })
    screen3:expect({ any = vim.pesc('This is server 2^.') })

    screen3:detach()
    server2_session:request('nvim_command', 'qall!')
  end)

  it('! stops the current server', function()
    local server1 = new_pipename()
    local screen1 = tt.setup_child_nvim({ '--listen', server1, '--clean' })
    screen1:expect({ any = vim.pesc('[No Name]') })

    tt.feed_data('iThis is server 1.\027')
    screen1:expect({ any = vim.pesc('This is server 1^.') })

    -- Prevent screen2 from receiving the old terminal state.
    command('enew')
    screen1:expect(screen_empty)
    screen1:detach()

    local server2 = new_pipename()
    local screen2 = tt.setup_child_nvim({ '--listen', server2, '--clean' })
    screen2:expect({ any = vim.pesc('[No Name]') })

    tt.feed_data(':connect! ' .. server1 .. '\013')
    screen2:expect({ any = vim.pesc('This is server 1^.') })

    retry(nil, nil, function()
      eq(nil, vim.uv.fs_stat(server2))
    end)

    local server1_session = n.connect(server1)
    server1_session:request('nvim_command', 'qall!')
    screen2:expect({ any = vim.pesc('[Process exited 0]') })

    screen2:detach()
  end)
end)

describe('TUI', function()
  local screen --[[@type test.functional.ui.screen]]
  local child_session --[[@type test.Session]]
  local child_exec_lua --[[@type fun(code: string, ...):any]]

  before_each(function()
    clear()
    local child_server = new_pipename()
    screen = tt.setup_child_nvim({
      '--listen',
      child_server,
      '--clean',
      '--cmd',
      nvim_set .. ' laststatus=2 background=dark',
      '--cmd',
      'colorscheme vim',
    }, { env = env_notermguicolors })
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    child_session = n.connect(child_server)
    child_exec_lua = tt.make_lua_executor(child_session)
  end)

  -- Wait for mode in the child Nvim (avoid "typeahead race" #10826).
  local function wait_for_mode(mode)
    retry(nil, nil, function()
      local _, m = child_session:request('nvim_get_mode')
      eq(mode, m.mode)
    end)
  end

  -- Assert buffer contents in the child Nvim.
  local function expect_child_buf_lines(expected)
    assert(type({}) == type(expected))
    retry(nil, nil, function()
      local _, buflines = child_session:request('nvim_buf_get_lines', 0, 0, -1, false)
      eq(expected, buflines)
    end)
  end

  -- Ensure both child client and child server have processed pending events.
  local function poke_both_eventloop()
    child_exec_lua([[
      _G.termresponse = nil
      vim.api.nvim_create_autocmd('TermResponse', {
        once = true,
        callback = function(ev) _G.termresponse = ev.data.sequence end,
      })
    ]])
    feed_data('\027P0$r\027\\')
    retry(nil, nil, function()
      eq('\027P0$r', child_exec_lua('return _G.termresponse'))
    end)
  end

  it('rapid resize #7572 #7628', function()
    -- Need buffer rows to provoke the behavior.
    feed_data(':edit test/functional/fixtures/bigfile.txt\n')
    screen:expect([[
      ^0000;<control>;Cc;0;BN;;;;;N;NULL;;;;             |
      0001;<control>;Cc;0;BN;;;;;N;START OF HEADING;;;; |
      0002;<control>;Cc;0;BN;;;;;N;START OF TEXT;;;;    |
      0003;<control>;Cc;0;BN;;;;;N;END OF TEXT;;;;      |
      {3:test/functional/fixtures/bigfile.txt              }|
      :edit test/functional/fixtures/bigfile.txt        |
      {5:-- TERMINAL --}                                    |
    ]])
    command('call jobresize(b:terminal_job_id, 58, 9)')
    command('call jobresize(b:terminal_job_id, 62, 13)')
    command('call jobresize(b:terminal_job_id, 100, 42)')
    command('call jobresize(b:terminal_job_id, 37, 1000)')
    -- Resize to <5 columns.
    screen:try_resize(4, 44)
    command('call jobresize(b:terminal_job_id, 4, 1000)')
    -- Resize to 1 row, then to 1 column, then increase rows to 4.
    screen:try_resize(44, 1)
    command('call jobresize(b:terminal_job_id, 44, 1)')
    screen:try_resize(1, 1)
    command('call jobresize(b:terminal_job_id, 1, 1)')
    screen:try_resize(1, 4)
    command('call jobresize(b:terminal_job_id, 1, 4)')
    screen:try_resize(57, 17)
    command('call jobresize(b:terminal_job_id, 57, 17)')
    retry(nil, nil, function()
      eq({ true, 57 }, { child_session:request('nvim_win_get_width', 0) })
    end)
  end)

  it('accepts resize while pager is active', function()
    tt.override_screen_expect_for_conpty(screen)
    child_session:request(
      'nvim_exec2',
      [[
      set more
      func! ManyErr()
        for i in range(20)
          echoerr "FAIL ".i
        endfor
      endfunc
    ]],
      {}
    )
    feed_data(':call ManyErr()\r')
    screen:expect([[
      {101:Error in function ManyErr:}                        |
      {103:line    2:}                                        |
      {101:FAIL 0}                                            |
      {101:FAIL 1}                                            |
      {101:FAIL 2}                                            |
      {102:-- More --}^                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    screen:try_resize(50, 10)
    screen:expect([[
      :call ManyErr()                                   |
      {101:Error in function ManyErr:}                        |
      {103:line    2:}                                        |
      {101:FAIL 0}                                            |
      {101:FAIL 1}                                            |
      {101:FAIL 2}                                            |
                                                        |*2
      {102:-- More --}^                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data('j')
    screen:expect([[
      {101:Error in function ManyErr:}                        |
      {103:line    2:}                                        |
      {101:FAIL 0}                                            |
      {101:FAIL 1}                                            |
      {101:FAIL 2}                                            |
      {101:FAIL 3}                                            |
      {101:FAIL 4}                                            |
      {101:FAIL 5}                                            |
      {102:-- More --}^                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    screen:try_resize(50, 7)
    screen:expect([[
      {101:FAIL 1}                                            |
      {101:FAIL 2}                                            |
      {101:FAIL 3}                                            |
      {101:FAIL 4}                                            |
      {101:FAIL 5}                                            |
      {102:-- More --}^                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    screen:try_resize(50, 5)
    screen:expect([[
      {101:FAIL 3}                                            |
      {101:FAIL 4}                                            |
      {101:FAIL 5}                                            |
      {102:-- More --}^                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data('g')
    screen:expect([[
      :call ManyErr()                                   |
      {101:Error in function ManyErr:}                        |
      {103:line    2:}                                        |
      {102:-- More --}^                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    screen:try_resize(50, 10)
    screen:expect([[
      :call ManyErr()                                   |
      {101:Error in function ManyErr:}                        |
      {103:line    2:}                                        |
      {101:FAIL 0}                                            |
      {101:FAIL 1}                                            |
      {101:FAIL 2}                                            |
      {101:FAIL 3}                                            |
      {101:FAIL 4}                                            |
      {102:-- More --}^                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data('\003')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*6
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('accepts basic utf-8 input', function()
    feed_data('iabc\ntest1\ntest2')
    screen:expect([[
      abc                                               |
      test1                                             |
      test2^                                             |
      {100:~                                                 }|
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027')
    screen:expect([[
      abc                                               |
      test1                                             |
      test^2                                             |
      {100:~                                                 }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('interprets leading <Esc> byte as ALT modifier in normal-mode', function()
    local keys = 'dfghjkl'
    for c in keys:gmatch('.') do
      feed_data(':nnoremap <a-' .. c .. '> ialt-' .. c .. '<cr><esc>\r')
      feed_data('\027' .. c)
    end
    screen:expect([[
      alt-j                                             |
      alt-k                                             |
      alt-l                                             |
      ^                                                  |
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('gg')
    screen:expect([[
      ^alt-d                                             |
      alt-f                                             |
      alt-g                                             |
      alt-h                                             |
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('interprets ESC+key as ALT chord in i_CTRL-V', function()
    -- Vim represents ALT/META by setting the "high bit" of the modified key:
    -- ALT+j inserts "ê". Nvim does not (#3982).
    feed_data('i\022\027j')
    screen:expect([[
      <M-j>^                                             |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('interprets <Esc> encoded with kitty keyboard protocol', function()
    child_session:request(
      'nvim_exec2',
      [[
      nnoremap <M-;> <Nop>
      nnoremap <Esc> AESC<Esc>
      nnoremap <C-Esc> ACtrlEsc<Esc>
      nnoremap <D-Esc> ASuperEsc<Esc>
      nnoremap ; Asemicolon<Esc>
    ]],
      {}
    )
    -- Works with no modifier
    feed_data('\027[27u;')
    expect_child_buf_lines({ 'ESCsemicolon' })
    -- Works with Ctrl modifier
    feed_data('\027[27;5u')
    expect_child_buf_lines({ 'ESCsemicolonCtrlEsc' })
    -- Works with Super modifier
    feed_data('\027[27;9u')
    expect_child_buf_lines({ 'ESCsemicolonCtrlEscSuperEsc' })
    -- Works with NumLock modifier (which should be the same as no modifier) #33799
    feed_data('\027[27;129u')
    expect_child_buf_lines({ 'ESCsemicolonCtrlEscSuperEscESC' })
    screen:expect([[
      ESCsemicolonCtrlEscSuperEscES^C                    |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <Esc>; should be recognized as <M-;> when <M-;> is mapped
    feed_data('\027;')
    screen:expect_unchanged()
    expect_child_buf_lines({ 'ESCsemicolonCtrlEscSuperEscESC' })
  end)

  it('interprets <Esc><Nul> as <M-C-Space> #17198', function()
    t.skip(is_os('win'), 'FIXME: does not work on Windows')
    feed_data('i\022\027\000')
    screen:expect([[
      <M-C-Space>^                                       |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it("split sequences work within 'ttimeoutlen' time", function()
    poke_both_eventloop() -- Make sure startup requests have finished.
    child_session:request('nvim_set_option_value', 'ttimeoutlen', 250, {})
    feed_data('i')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Split UTF-8 '⌂' character
    feed_data('\226')
    screen:expect_unchanged(false, 25)
    feed_data('\140')
    screen:expect_unchanged(false, 25)
    feed_data('\130')
    screen:expect([[
      ⌂^                                                 |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Split CSI u escape sequence for Ctrl-X
    feed_data('\027')
    screen:expect_unchanged(false, 25)
    feed_data('[')
    screen:expect_unchanged(false, 25)
    feed_data('120;')
    screen:expect_unchanged(false, 25)
    feed_data('5u')
    screen:expect([[
      ⌂^                                                 |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)}        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <Esc> is sent after 'ttimeoutlen' exceeds.
    feed_data('\027')
    screen:expect_unchanged(false, 25)
    vim.uv.sleep(225)
    screen:expect([[
      ^⌂                                                 |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('accepts ASCII control sequences', function()
    feed_data('i')
    feed_data('\022\007') -- ctrl+g
    feed_data('\022\022') -- ctrl+v
    feed_data('\022\013') -- ctrl+m
    screen:expect([[
      {104:^G^V^M}^                                            |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    child_session:request('nvim_set_keymap', 'i', '\031', '!!!', {})
    feed_data('\031')
    screen:expect([[
      {104:^G^V^M}!!!^                                         |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    child_session:request('nvim_buf_delete', 0, { force = true })
    child_session:request('nvim_set_option_value', 'laststatus', 0, {})
    child_session:request(
      'nvim_call_function',
      'jobstart',
      { { testprg('shell-test'), 'INTERACT' }, { term = true } }
    )
    screen:expect([[
      interact $ ^                                       |
                                                        |*4
      {5:-- TERMINAL --}                                    |*2
    ]])
    -- mappings for C0 control codes should work in Terminal mode #33750
    child_session:request('nvim_set_keymap', 't', '\031', '<Cmd>new<CR>', {})
    feed_data('\031')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|
      {3:[No Name]                                         }|
      interact $                                        |
                                                        |*2
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  local function test_mouse_wheel(esc)
    t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
    child_session:request(
      'nvim_exec2',
      [[
      set number nostartofline nowrap mousescroll=hor:1,ver:1
      call setline(1, repeat([join(range(10), '----')], 10))
      vsplit
    ]],
      {}
    )
    screen:expect([[
      {103:  1 }^0----1----2----3----4│{103:  1 }0----1----2----3----|
      {103:  2 }0----1----2----3----4│{103:  2 }0----1----2----3----|
      {103:  3 }0----1----2----3----4│{103:  3 }0----1----2----3----|
      {103:  4 }0----1----2----3----4│{103:  4 }0----1----2----3----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelDown> in active window
    if esc then
      feed_data('\027[<65;8;1M')
    else
      api.nvim_input_mouse('wheel', 'down', '', 0, 0, 7)
    end
    screen:expect([[
      {103:  2 }^0----1----2----3----4│{103:  1 }0----1----2----3----|
      {103:  3 }0----1----2----3----4│{103:  2 }0----1----2----3----|
      {103:  4 }0----1----2----3----4│{103:  3 }0----1----2----3----|
      {103:  5 }0----1----2----3----4│{103:  4 }0----1----2----3----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelDown> in inactive window
    if esc then
      feed_data('\027[<65;48;1M')
    else
      api.nvim_input_mouse('wheel', 'down', '', 0, 0, 47)
    end
    screen:expect([[
      {103:  2 }^0----1----2----3----4│{103:  2 }0----1----2----3----|
      {103:  3 }0----1----2----3----4│{103:  3 }0----1----2----3----|
      {103:  4 }0----1----2----3----4│{103:  4 }0----1----2----3----|
      {103:  5 }0----1----2----3----4│{103:  5 }0----1----2----3----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelRight> in active window
    if esc then
      feed_data('\027[<67;8;1M')
    else
      api.nvim_input_mouse('wheel', 'right', '', 0, 0, 7)
    end
    screen:expect([[
      {103:  2 }^----1----2----3----4-│{103:  2 }0----1----2----3----|
      {103:  3 }----1----2----3----4-│{103:  3 }0----1----2----3----|
      {103:  4 }----1----2----3----4-│{103:  4 }0----1----2----3----|
      {103:  5 }----1----2----3----4-│{103:  5 }0----1----2----3----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelRight> in inactive window
    if esc then
      feed_data('\027[<67;48;1M')
    else
      api.nvim_input_mouse('wheel', 'right', '', 0, 0, 47)
    end
    screen:expect([[
      {103:  2 }^----1----2----3----4-│{103:  2 }----1----2----3----4|
      {103:  3 }----1----2----3----4-│{103:  3 }----1----2----3----4|
      {103:  4 }----1----2----3----4-│{103:  4 }----1----2----3----4|
      {103:  5 }----1----2----3----4-│{103:  5 }----1----2----3----4|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelDown> in active window
    if esc then
      feed_data('\027[<69;8;1M')
    else
      api.nvim_input_mouse('wheel', 'down', 'S', 0, 0, 7)
    end
    screen:expect([[
      {103:  5 }^----1----2----3----4-│{103:  2 }----1----2----3----4|
      {103:  6 }----1----2----3----4-│{103:  3 }----1----2----3----4|
      {103:  7 }----1----2----3----4-│{103:  4 }----1----2----3----4|
      {103:  8 }----1----2----3----4-│{103:  5 }----1----2----3----4|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelDown> in inactive window
    if esc then
      feed_data('\027[<69;48;1M')
    else
      api.nvim_input_mouse('wheel', 'down', 'S', 0, 0, 47)
    end
    screen:expect([[
      {103:  5 }^----1----2----3----4-│{103:  5 }----1----2----3----4|
      {103:  6 }----1----2----3----4-│{103:  6 }----1----2----3----4|
      {103:  7 }----1----2----3----4-│{103:  7 }----1----2----3----4|
      {103:  8 }----1----2----3----4-│{103:  8 }----1----2----3----4|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelRight> in active window
    if esc then
      feed_data('\027[<71;8;1M')
    else
      api.nvim_input_mouse('wheel', 'right', 'S', 0, 0, 7)
    end
    screen:expect([[
      {103:  5 }^----6----7----8----9 │{103:  5 }----1----2----3----4|
      {103:  6 }----6----7----8----9 │{103:  6 }----1----2----3----4|
      {103:  7 }----6----7----8----9 │{103:  7 }----1----2----3----4|
      {103:  8 }----6----7----8----9 │{103:  8 }----1----2----3----4|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelRight> in inactive window
    if esc then
      feed_data('\027[<71;48;1M')
    else
      api.nvim_input_mouse('wheel', 'right', 'S', 0, 0, 47)
    end
    screen:expect([[
      {103:  5 }^----6----7----8----9 │{103:  5 }5----6----7----8----|
      {103:  6 }----6----7----8----9 │{103:  6 }5----6----7----8----|
      {103:  7 }----6----7----8----9 │{103:  7 }5----6----7----8----|
      {103:  8 }----6----7----8----9 │{103:  8 }5----6----7----8----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelUp> in active window
    if esc then
      feed_data('\027[<64;8;1M')
    else
      api.nvim_input_mouse('wheel', 'up', '', 0, 0, 7)
    end
    screen:expect([[
      {103:  4 }----6----7----8----9 │{103:  5 }5----6----7----8----|
      {103:  5 }^----6----7----8----9 │{103:  6 }5----6----7----8----|
      {103:  6 }----6----7----8----9 │{103:  7 }5----6----7----8----|
      {103:  7 }----6----7----8----9 │{103:  8 }5----6----7----8----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelUp> in inactive window
    if esc then
      feed_data('\027[<64;48;1M')
    else
      api.nvim_input_mouse('wheel', 'up', '', 0, 0, 47)
    end
    screen:expect([[
      {103:  4 }----6----7----8----9 │{103:  4 }5----6----7----8----|
      {103:  5 }^----6----7----8----9 │{103:  5 }5----6----7----8----|
      {103:  6 }----6----7----8----9 │{103:  6 }5----6----7----8----|
      {103:  7 }----6----7----8----9 │{103:  7 }5----6----7----8----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelLeft> in active window
    if esc then
      feed_data('\027[<66;8;1M')
    else
      api.nvim_input_mouse('wheel', 'left', '', 0, 0, 7)
    end
    screen:expect([[
      {103:  4 }5----6----7----8----9│{103:  4 }5----6----7----8----|
      {103:  5 }5^----6----7----8----9│{103:  5 }5----6----7----8----|
      {103:  6 }5----6----7----8----9│{103:  6 }5----6----7----8----|
      {103:  7 }5----6----7----8----9│{103:  7 }5----6----7----8----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <ScrollWheelLeft> in inactive window
    if esc then
      feed_data('\027[<66;48;1M')
    else
      api.nvim_input_mouse('wheel', 'left', '', 0, 0, 47)
    end
    screen:expect([[
      {103:  4 }5----6----7----8----9│{103:  4 }-5----6----7----8---|
      {103:  5 }5^----6----7----8----9│{103:  5 }-5----6----7----8---|
      {103:  6 }5----6----7----8----9│{103:  6 }-5----6----7----8---|
      {103:  7 }5----6----7----8----9│{103:  7 }-5----6----7----8---|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelUp> in active window
    if esc then
      feed_data('\027[<68;8;1M')
    else
      api.nvim_input_mouse('wheel', 'up', 'S', 0, 0, 7)
    end
    screen:expect([[
      {103:  1 }5----6----7----8----9│{103:  4 }-5----6----7----8---|
      {103:  2 }5----6----7----8----9│{103:  5 }-5----6----7----8---|
      {103:  3 }5----6----7----8----9│{103:  6 }-5----6----7----8---|
      {103:  4 }5^----6----7----8----9│{103:  7 }-5----6----7----8---|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelUp> in inactive window
    if esc then
      feed_data('\027[<68;48;1M')
    else
      api.nvim_input_mouse('wheel', 'up', 'S', 0, 0, 47)
    end
    screen:expect([[
      {103:  1 }5----6----7----8----9│{103:  1 }-5----6----7----8---|
      {103:  2 }5----6----7----8----9│{103:  2 }-5----6----7----8---|
      {103:  3 }5----6----7----8----9│{103:  3 }-5----6----7----8---|
      {103:  4 }5^----6----7----8----9│{103:  4 }-5----6----7----8---|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelLeft> in active window
    if esc then
      feed_data('\027[<70;8;1M')
    else
      api.nvim_input_mouse('wheel', 'left', 'S', 0, 0, 7)
    end
    screen:expect([[
      {103:  1 }0----1----2----3----4│{103:  1 }-5----6----7----8---|
      {103:  2 }0----1----2----3----4│{103:  2 }-5----6----7----8---|
      {103:  3 }0----1----2----3----4│{103:  3 }-5----6----7----8---|
      {103:  4 }0----1----2----3----^4│{103:  4 }-5----6----7----8---|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- <S-ScrollWheelLeft> in inactive window
    if esc then
      feed_data('\027[<70;48;1M')
    else
      api.nvim_input_mouse('wheel', 'left', 'S', 0, 0, 47)
    end
    screen:expect([[
      {103:  1 }0----1----2----3----4│{103:  1 }0----1----2----3----|
      {103:  2 }0----1----2----3----4│{103:  2 }0----1----2----3----|
      {103:  3 }0----1----2----3----4│{103:  3 }0----1----2----3----|
      {103:  4 }0----1----2----3----^4│{103:  4 }0----1----2----3----|
      {3:[No Name] [+]             }{2:[No Name] [+]           }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end

  describe('accepts mouse wheel events', function()
    it('(mouse events sent to host)', function()
      test_mouse_wheel(false)
    end)

    it('(escape sequences sent to child)', function()
      test_mouse_wheel(true)
    end)
  end)

  local function test_mouse_popup(esc)
    child_session:request(
      'nvim_exec2',
      [[
      call setline(1, 'popup menu test')
      set mouse=a mousemodel=popup

      aunmenu PopUp
      " Delete the default MenuPopup event handler.
      autocmd! nvim.popupmenu
      menu PopUp.foo :let g:menustr = 'foo'<CR>
      menu PopUp.bar :let g:menustr = 'bar'<CR>
      menu PopUp.baz :let g:menustr = 'baz'<CR>
      highlight Pmenu ctermbg=NONE ctermfg=NONE cterm=underline,reverse
      highlight PmenuSel ctermbg=NONE ctermfg=NONE cterm=underline,reverse,bold
    ]],
      {}
    )
    if esc then
      feed_data('\027[<2;5;1M')
    else
      api.nvim_input_mouse('right', 'press', '', 0, 0, 4)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~  }{105: foo }{100:                                          }|
      {100:~  }{105: bar }{100:                                          }|
      {100:~  }{105: baz }{100:                                          }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    if esc then
      feed_data('\027[<2;5;1m')
    else
      api.nvim_input_mouse('right', 'release', '', 0, 0, 4)
    end
    screen:expect_unchanged()
    if esc then
      feed_data('\027[<64;5;1M')
    else
      api.nvim_input_mouse('wheel', 'up', '', 0, 0, 4)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~  }{106: foo }{100:                                          }|
      {100:~  }{105: bar }{100:                                          }|
      {100:~  }{105: baz }{100:                                          }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    if esc then
      feed_data('\027[<35;7;4M')
    else
      api.nvim_input_mouse('move', '', '', 0, 3, 6)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~  }{105: foo }{100:                                          }|
      {100:~  }{105: bar }{100:                                          }|
      {100:~  }{106: baz }{100:                                          }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    if esc then
      feed_data('\027[<65;7;4M')
    else
      api.nvim_input_mouse('wheel', 'down', '', 0, 3, 6)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~  }{105: foo }{100:                                          }|
      {100:~  }{106: bar }{100:                                          }|
      {100:~  }{105: baz }{100:                                          }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    if esc then
      feed_data('\027[<0;7;3M')
    else
      api.nvim_input_mouse('left', 'press', '', 0, 2, 6)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      :let g:menustr = 'bar'                            |
      {5:-- TERMINAL --}                                    |
    ]])
    if esc then
      feed_data('\027[<0;7;3m')
    else
      api.nvim_input_mouse('left', 'release', '', 0, 2, 6)
    end
    screen:expect_unchanged()
    if esc then
      feed_data('\027[<2;45;3M')
    else
      api.nvim_input_mouse('right', 'press', '', 0, 2, 44)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~                                                 }|*2
      {100:~                                          }{105: foo }{100:  }|
      {3:[No Name] [+]                              }{105: bar }{3:  }|
      :let g:menustr = 'bar'                     {105: baz }  |
      {5:-- TERMINAL --}                                    |
    ]])
    if esc then
      feed_data('\027[<34;48;6M')
    else
      api.nvim_input_mouse('right', 'drag', '', 0, 5, 47)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~                                                 }|*2
      {100:~                                          }{105: foo }{100:  }|
      {3:[No Name] [+]                              }{105: bar }{3:  }|
      :let g:menustr = 'bar'                     {106: baz }  |
      {5:-- TERMINAL --}                                    |
    ]])
    if esc then
      feed_data('\027[<2;48;6m')
    else
      api.nvim_input_mouse('right', 'release', '', 0, 5, 47)
    end
    screen:expect([[
      ^popup menu test                                   |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      :let g:menustr = 'baz'                            |
      {5:-- TERMINAL --}                                    |
    ]])
  end

  describe('mouse events work with right-click menu', function()
    it('(mouse events sent to host)', function()
      test_mouse_popup(false)
    end)

    it('(escape sequences sent to child)', function()
      test_mouse_popup(true)
    end)
  end)

  it('accepts keypad keys from kitty keyboard protocol #19180', function()
    feed_data('i')
    feed_data(fn.nr2char(57399)) -- KP_0
    feed_data(fn.nr2char(57400)) -- KP_1
    feed_data(fn.nr2char(57401)) -- KP_2
    feed_data(fn.nr2char(57402)) -- KP_3
    feed_data(fn.nr2char(57403)) -- KP_4
    feed_data(fn.nr2char(57404)) -- KP_5
    feed_data(fn.nr2char(57405)) -- KP_6
    feed_data(fn.nr2char(57406)) -- KP_7
    feed_data(fn.nr2char(57407)) -- KP_8
    feed_data(fn.nr2char(57408)) -- KP_9
    feed_data(fn.nr2char(57409)) -- KP_DECIMAL
    feed_data(fn.nr2char(57410)) -- KP_DIVIDE
    feed_data(fn.nr2char(57411)) -- KP_MULTIPLY
    feed_data(fn.nr2char(57412)) -- KP_SUBTRACT
    feed_data(fn.nr2char(57413)) -- KP_ADD
    feed_data(fn.nr2char(57414)) -- KP_ENTER
    feed_data(fn.nr2char(57415)) -- KP_EQUAL
    screen:expect([[
      0123456789./*-+                                   |
      =^                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57417)) -- KP_LEFT
    screen:expect([[
      0123456789./*-+                                   |
      ^=                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57418)) -- KP_RIGHT
    screen:expect([[
      0123456789./*-+                                   |
      =^                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57419)) -- KP_UP
    screen:expect([[
      0^123456789./*-+                                   |
      =                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57420)) -- KP_DOWN
    screen:expect([[
      0123456789./*-+                                   |
      =^                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57425)) -- KP_INSERT
    screen:expect([[
      0123456789./*-+                                   |
      =^                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- REPLACE --}                                     |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[27u') -- ESC
    screen:expect([[
      0123456789./*-+                                   |
      ^=                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[57417;5u') -- CTRL + KP_LEFT
    screen:expect([[
      ^0123456789./*-+                                   |
      =                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[57418;2u') -- SHIFT + KP_RIGHT
    screen:expect([[
      0123456789^./*-+                                   |
      =                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57426)) -- KP_DELETE
    screen:expect([[
      0123456789^/*-+                                    |
      =                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57423)) -- KP_HOME
    screen:expect([[
      ^0123456789/*-+                                    |
      =                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(fn.nr2char(57424)) -- KP_END
    screen:expect([[
      0123456789/*-^+                                    |
      =                                                 |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    child_session:request(
      'nvim_exec2',
      [[
      tab split
      tabnew
      highlight Tabline ctermbg=NONE ctermfg=NONE cterm=underline
    ]],
      {}
    )
    screen:expect([[
      {107: + [No Name]  + [No Name] }{5: [No Name] }{2:            }{107:X}|
      ^                                                  |
      {100:~                                                 }|*2
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[57421;5u') -- CTRL + KP_PAGE_UP
    screen:expect([[
      {107: + [No Name] }{5: + [No Name] }{107: [No Name] }{2:            }{107:X}|
      0123456789/*-^+                                    |
      =                                                 |
      {100:~                                                 }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[57422;5u') -- CTRL + KP_PAGE_DOWN
    screen:expect([[
      {107: + [No Name]  + [No Name] }{5: [No Name] }{2:            }{107:X}|
      ^                                                  |
      {100:~                                                 }|*2
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('supports Super and Meta modifiers', function()
    feed_data('i')
    feed_data('\022\027[106;9u') -- Super + j
    feed_data('\022\027[107;33u') -- Meta + k
    feed_data('\022\027[13;41u') -- Super + Meta + Enter
    feed_data('\022\027[127;48u') -- Shift + Alt + Ctrl + Super + Meta + Backspace
    feed_data('\n')
    feed_data('\022\027[57376;9u') -- Super + F13
    feed_data('\022\027[57377;33u') -- Meta + F14
    feed_data('\022\027[57378;41u') -- Super + Meta + F15
    feed_data('\022\027[57379;48u') -- Shift + Alt + Ctrl + Super + Meta + F16
    screen:expect([[
      <D-j><T-k><T-D-CR><M-T-C-S-D-BS>                  |
      <D-F13><T-F14><T-D-F15><M-T-C-S-D-F16>^            |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: Insert mode', function()
    -- "bracketed paste"
    feed_data('i""\027i\027[200~')
    screen:expect([[
      "^"                                                |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('pasted from terminal')
    expect_child_buf_lines({ '"pasted from terminal"' })
    screen:expect([[
      "pasted from terminal^"                            |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[201~') -- End paste.
    poke_both_eventloop()
    screen:expect_unchanged()
    feed_data('\027[27u') -- ESC: go to Normal mode.
    wait_for_mode('n')
    screen:expect([[
      "pasted from termina^l"                            |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Dot-repeat/redo.
    feed_data('2.')
    expect_child_buf_lines({ '"pasted from terminapasted from terminalpasted from terminall"' })
    screen:expect([[
      "pasted from terminapasted from terminalpasted fro|
      m termina^ll"                                      |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Undo.
    feed_data('u')
    expect_child_buf_lines({ '"pasted from terminal"' })
    feed_data('u')
    expect_child_buf_lines({ '""' })
    feed_data('u')
    expect_child_buf_lines({ '' })
  end)

  it('paste: select-mode', function()
    feed_data('ithis is line 1\nthis is line 2\nline 3 is here\n\027')
    wait_for_mode('n')
    screen:expect([[
      this is line 1                                    |
      this is line 2                                    |
      line 3 is here                                    |
      ^                                                  |
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Select-mode. Use <C-n> to move down.
    feed_data('gg04lgh\14\14')
    screen:expect([[
      this{108: is line 1}                                    |
      {108:this is line 2}                                    |
      {108:line}^ 3 is here                                    |
                                                        |
      {3:[No Name] [+]                                     }|
      {5:-- SELECT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[200~')
    feed_data('just paste it™')
    feed_data('\027[201~')
    screen:expect([[
      thisjust paste it^™3 is here                       |
                                                        |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Undo.
    feed_data('u')
    expect_child_buf_lines {
      'this is line 1',
      'this is line 2',
      'line 3 is here',
      '',
    }
    -- Redo.
    feed_data('\18') -- <C-r>
    expect_child_buf_lines {
      'thisjust paste it™3 is here',
      '',
    }
  end)

  it('paste: terminal mode', function()
    child_exec_lua('vim.o.statusline="^^^^^^^"')
    child_exec_lua('vim.fn.jobstart({ ... }, { term = true })', testprg('tty-test'))
    feed_data('i')
    screen:expect([[
      tty ready                                         |
      ^                                                  |
                                                        |*2
      {109:^^^^^^^                                           }|
      {5:-- TERMINAL --}                                    |*2
    ]])
    feed_data('\027[200~')
    feed_data('hallo')
    feed_data('\027[201~')
    screen:expect([[
      tty ready                                         |
      hallo^                                             |
                                                        |*2
      {109:^^^^^^^                                           }|
      {5:-- TERMINAL --}                                    |*2
    ]])
  end)

  it('paste: normal-mode (+CRLF #10872)', function()
    t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
    feed_data(':set ruler | echo')
    wait_for_mode('c')
    feed_data('\n')
    wait_for_mode('n')
    local expected_lf = { 'line 1', 'ESC:\027 / CR: \rx' }
    local expected_crlf = { 'line 1', 'ESC:\027 / CR: ', 'x' }
    local expected_grid1 = [[
      line 1                                            |
      ESC:{104:^[} / CR:                                      |
      ^x                                                 |
      {100:~                                                 }|
      {3:[No Name] [+]                   3,1            All}|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    -- "bracketed paste"
    feed_data('\027[200~' .. table.concat(expected_lf, '\n') .. '\027[201~')
    screen:expect(expected_grid1)
    -- Dot-repeat/redo.
    feed_data('.')
    local expected_grid2 = [[
      ESC:{104:^[} / CR:                                      |
      xline 1                                           |
      ESC:{104:^[} / CR:                                      |
      ^x                                                 |
      {3:[No Name] [+]                   5,1            Bot}|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen:expect(expected_grid2)
    -- Undo.
    feed_data('u')
    expect_child_buf_lines(expected_crlf)
    feed_data('u')
    expect_child_buf_lines({ '' })
    feed_data(':echo')
    wait_for_mode('c')
    feed_data('\n')
    wait_for_mode('n')
    -- CRLF input
    feed_data('\027[200~' .. table.concat(expected_lf, '\r\n') .. '\027[201~')
    screen:expect(expected_grid1)
    expect_child_buf_lines(expected_crlf)
    -- Dot-repeat/redo.
    feed_data('.')
    screen:expect(expected_grid2)
    -- Undo.
    feed_data('u')
    expect_child_buf_lines(expected_crlf)
    feed_data('u')
    expect_child_buf_lines({ '' })
  end)

  it('paste: cmdline-mode inserts 1 line', function()
    feed_data('ifoo\n') -- Insert some text (for dot-repeat later).
    feed_data('\027:""') -- Enter Cmdline-mode.
    feed_data('\027[D') -- <Left> to place cursor between quotes.
    wait_for_mode('c')
    screen:expect([[
      foo                                               |
                                                        |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      :"^"                                               |
      {5:-- TERMINAL --}                                    |
    ]])
    -- "bracketed paste"
    feed_data('\027[200~line 1\nline 2\n')
    wait_for_mode('c')
    feed_data('line 3\nline 4\n\027[201~')
    poke_both_eventloop()
    wait_for_mode('c')
    screen:expect([[
      foo                                               |
                                                        |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      :"line 1^"                                         |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Dot-repeat/redo.
    feed_data('\027[27u')
    wait_for_mode('n')
    feed_data('.')
    screen:expect([[
      foo                                               |*2
      ^                                                  |
      {100:~                                                 }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: cmdline-mode collects chunks of unfinished line', function()
    local function expect_cmdline(expected)
      retry(nil, nil, function()
        local _, cmdline = child_session:request('nvim_call_function', 'getcmdline', {})
        eq(expected, cmdline)
        local _, pos = child_session:request('nvim_call_function', 'getcmdpos', {})
        eq(#expected, pos) -- Cursor is just before the last char.
      end)
    end
    feed_data('\027:""') -- Enter Cmdline-mode.
    feed_data('\027[D') -- <Left> to place cursor between quotes.
    expect_cmdline('""')
    feed_data('\027[200~stuff 1 ')
    expect_cmdline('"stuff 1 "')
    -- Discards everything after the first line.
    feed_data('more\nstuff 2\nstuff 3\n')
    expect_cmdline('"stuff 1 more"')
    feed_data('stuff 3')
    expect_cmdline('"stuff 1 more"')
    -- End the paste sequence.
    feed_data('\027[201~')
    poke_both_eventloop()
    expect_cmdline('"stuff 1 more"')
    feed_data(' typed')
    expect_cmdline('"stuff 1 more typed"')
  end)

  it('paste: recovers from vim.paste() failure', function()
    child_exec_lua([[
      _G.save_paste_fn = vim.paste
      -- Stack traces for this test are non-deterministic, so disable them
      _G.debug.traceback = function(msg) return msg end
      vim.paste = function(lines, phase) error("fake fail") end
    ]])
    -- Prepare something for dot-repeat/redo.
    feed_data('ifoo\n\027[27u')
    wait_for_mode('n')
    screen:expect([[
      foo                                               |
      ^                                                  |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Start pasting...
    feed_data('\027[200~line 1\nline 2\n')
    screen:expect([[
      foo                                               |
      ^                                                  |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {101:paste: Lua: [string "<nvim>"]:4: fake fail}        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Remaining chunks are discarded after vim.paste() failure.
    feed_data('line 3\nline 4\n')
    feed_data('line 5\nline 6\n')
    feed_data('line 7\nline 8\n')
    -- Stop paste.
    feed_data('\027[201~')
    screen:expect_unchanged()
    feed_data('\n') -- <CR> to dismiss hit-enter prompt
    expect_child_buf_lines({ 'foo', '' })
    -- Dot-repeat/redo is not modified by failed paste.
    feed_data('.')
    screen:expect([[
      foo                                               |*2
      ^                                                  |
      {100:~                                                 }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Editor should still work after failed/drained paste.
    feed_data('ityped input...\027[27u')
    screen:expect([[
      foo                                               |*2
      typed input..^.                                    |
      {100:~                                                 }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Paste works if vim.paste() succeeds.
    child_exec_lua([[vim.paste = _G.save_paste_fn]])
    feed_data('\027[200~line A\nline B\n\027[201~')
    screen:expect([[
      foo                                               |
      typed input...line A                              |
      line B                                            |
      ^                                                  |
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: vim.paste() cancel (retval=false) #10865', function()
    -- This test only exercises the "cancel" case.  Use-case would be "dangling
    -- paste", but that is not implemented yet. #10865
    child_exec_lua([[
      vim.paste = function(lines, phase) return false end
    ]])
    feed_data('\027[200~line A\nline B\n\027[201~')
    expect_child_buf_lines({ '' })
    feed_data('ifoo\n\027[27u')
    expect_child_buf_lines({ 'foo', '' })
  end)

  it('paste: vim.paste() cancel (retval=false) with streaming #30462', function()
    child_exec_lua([[
      vim.paste = (function(overridden)
        return function(lines, phase)
          for i, line in ipairs(lines) do
            if line:find('!') then
              return false
            end
          end
          return overridden(lines, phase)
        end
      end)(vim.paste)
    ]])
    feed_data('A')
    wait_for_mode('i')
    feed_data('\027[200~aaa')
    expect_child_buf_lines({ 'aaa' })
    feed_data('bbb')
    expect_child_buf_lines({ 'aaabbb' })
    feed_data('ccc!') -- This chunk is cancelled.
    expect_child_buf_lines({ 'aaabbb' })
    feed_data('ddd\027[201~') -- This chunk is ignored.
    poke_both_eventloop()
    expect_child_buf_lines({ 'aaabbb' })
    feed_data('\027[27u')
    wait_for_mode('n')
    feed_data('.') -- Dot-repeat only includes chunks actually pasted.
    expect_child_buf_lines({ 'aaabbbaaabbb' })
    feed_data('$\027[200~eee\027[201~') -- A following paste works normally.
    expect_child_buf_lines({ 'aaabbbaaabbbeee' })
  end)

  it("paste: 'nomodifiable' buffer", function()
    tt.override_screen_expect_for_conpty(screen)
    child_exec_lua([[
      vim.bo.modifiable = false
      -- Truncate the error message to hide the line number
      _G.debug.traceback = function(msg) return msg:sub(-49) end
    ]])
    feed_data('\027[200~fail 1\nfail 2\n\027[201~')
    screen:expect([[
                                                        |
      {100:~                                                 }|
      {3:                                                  }|
      {101:paste: Lua: Vim:E21: Cannot make changes, 'modifia}|
      {101:ble' is off}                                       |
      {102:Press ENTER or type command to continue}^           |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\n') -- <Enter> to dismiss hit-enter prompt
    child_exec_lua('vim.bo.modifiable = true')
    feed_data('\027[200~success 1\nsuccess 2\n\027[201~')
    screen:expect([[
      success 1                                         |
      success 2                                         |
      ^                                                  |
      {100:~                                                 }|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: exactly 64 bytes #10311', function()
    local expected = string.rep('z', 64)
    feed_data('i')
    wait_for_mode('i')
    -- "bracketed paste"
    feed_data('\027[200~' .. expected .. '\027[201~')
    expect_child_buf_lines({ expected })
    feed_data(' end')
    expected = expected .. ' end'
    screen:expect([[
      zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz|
      zzzzzzzzzzzzzz end^                                |
      {100:~                                                 }|*2
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    expect_child_buf_lines({ expected })
  end)

  it('paste: less-than sign in cmdline #11088', function()
    local expected = '<'
    feed_data(':')
    wait_for_mode('c')
    -- "bracketed paste"
    feed_data('\027[200~' .. expected .. '\027[201~')
    screen:expect([[
                                                        |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      :<^                                                |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: big burst of input', function()
    feed_data(':set ruler\n')
    local q = {}
    for i = 1, 3000 do
      q[i] = 'item ' .. tostring(i)
    end
    feed_data('i')
    wait_for_mode('i')
    -- "bracketed paste"
    feed_data('\027[200~' .. table.concat(q, '\n') .. '\027[201~')
    expect_child_buf_lines(q)
    feed_data(' end')
    screen:expect([[
      item 2997                                         |
      item 2998                                         |
      item 2999                                         |
      item 3000 end^                                     |
      {3:[No Name] [+]                   3000,14        Bot}|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[27u') -- ESC: go to Normal mode.
    wait_for_mode('n')
    -- Dot-repeat/redo.
    feed_data('.')
    screen:expect([[
      item 2997                                         |
      item 2998                                         |
      item 2999                                         |
      item 3000 en^dd                                    |
      {3:[No Name] [+]                   5999,13        Bot}|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: forwards spurious "start paste" code', function()
    -- If multiple "start paste" sequences are sent without a corresponding
    -- "stop paste" sequence, only the first occurrence should be consumed.
    feed_data('i')
    wait_for_mode('i')
    -- Send the "start paste" sequence.
    feed_data('\027[200~')
    feed_data('\npasted from terminal (1)\n')
    -- Send spurious "start paste" sequence.
    feed_data('\027[200~')
    feed_data('\n')
    -- Send the "stop paste" sequence.
    feed_data('\027[201~')
    screen:expect([[
                                                        |
      pasted from terminal (1)                          |
      {104:^[}[200~                                           |
      ^                                                  |
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: ignores spurious "stop paste" code', function()
    -- If "stop paste" sequence is received without a preceding "start paste"
    -- sequence, it should be ignored.
    feed_data('i')
    wait_for_mode('i')
    -- Send "stop paste" sequence.
    feed_data('\027[201~')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: split "start paste" code', function()
    t.skip(is_os('win'), 'FIXME: wrong behavior on Windows')
    feed_data('i')
    wait_for_mode('i')
    -- Send split "start paste" sequence.
    feed_data('\027[2')
    feed_data('00~pasted from terminal\027[201~')
    screen:expect([[
      pasted from terminal^                              |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: split "stop paste" code', function()
    t.skip(is_os('win'), 'FIXME: wrong behavior on Windows')
    feed_data('i')
    wait_for_mode('i')
    -- Send split "stop paste" sequence.
    feed_data('\027[200~pasted from terminal\027[20')
    feed_data('1~')
    screen:expect([[
      pasted from terminal^                              |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('paste: streamed paste with isolated "stop paste" code', function()
    child_exec_lua([[
      _G.paste_phases = {}
      vim.paste = (function(overridden)
        return function(lines, phase)
          table.insert(_G.paste_phases, phase)
          overridden(lines, phase)
        end
      end)(vim.paste)
    ]])
    feed_data('i')
    wait_for_mode('i')
    feed_data('\027[200~pasted') -- phase 1
    screen:expect([[
      pasted^                                            |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(' from terminal') -- phase 2
    screen:expect([[
      pasted from terminal^                              |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    -- Send isolated "stop paste" sequence.
    feed_data('\027[201~') -- phase 3
    poke_both_eventloop()
    screen:expect_unchanged()
    local rv = child_exec_lua('return _G.paste_phases')
    -- In rare cases there may be multiple chunks of phase 2 because of timing.
    eq({ 1, 2, 3 }, { rv[1], rv[2], rv[#rv] })
  end)

  it('allows termguicolors to be set at runtime', function()
    tt.override_screen_expect_for_conpty(screen)
    screen:set_option('rgb', true)
    feed_data(':hi SpecialKey ctermfg=3 guifg=SeaGreen\n')
    feed_data('i')
    feed_data('\022\007') -- ctrl+g
    feed_data('\028\014') -- crtl+\ ctrl+N
    feed_data(':set termguicolors?\n')
    screen:expect([[
      {110:^^G}                                                |
      {111:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      notermguicolors                                   |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data(':set termguicolors\n')
    screen:expect([[
      {113:^^G}                                                |
      {1:~}{18:                                                 }|*3
      {3:[No Name] [+]                                     }|
      :set termguicolors                                |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data(':set notermguicolors\n')
    screen:expect([[
      {110:^^G}                                                |
      {111:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      :set notermguicolors                              |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('forwards :term palette colors with termguicolors', function()
    t.skip(is_os('win'), 'FIXME: wrong behavior on Windows')
    screen:set_rgb_cterm(true)
    screen:set_default_attr_ids({
      [1] = { { reverse = true }, { reverse = true } },
      [2] = {
        { bold = true, background = Screen.colors.LightGreen, foreground = Screen.colors.Black },
        { bold = true },
      },
      [3] = { { bold = true }, { bold = true } },
      [4] = { { fg_indexed = true, foreground = tonumber('0xe0e000') }, { foreground = 3 } },
      [5] = { { foreground = tonumber('0xff8000') }, {} },
      [6] = {
        {
          fg_indexed = true,
          bg_indexed = true,
          bold = true,
          background = tonumber('0x66ff99'),
          foreground = Screen.colors.Black,
        },
        { bold = true, background = 121, foreground = 0 },
      },
      [7] = {
        {
          fg_indexed = true,
          bg_indexed = true,
          background = tonumber('0x66ff99'),
          foreground = Screen.colors.Black,
        },
        { background = 121, foreground = 0 },
      },
    })

    child_exec_lua('vim.o.statusline="^^^^^^^"')
    child_exec_lua('vim.o.termguicolors=true')
    child_exec_lua('vim.fn.jobstart({ ... }, { term = true })', testprg('tty-test'))
    screen:expect([[
      ^tty ready                                         |
                                                        |*3
      {2:^^^^^^^                                           }|
                                                        |
      {3:-- TERMINAL --}                                    |
    ]])
    feed_data(
      ':call chansend(&channel, "\\033[38;5;3mtext\\033[38:2:255:128:0mcolor\\033[0;10mtext")\n'
    )
    screen:expect([[
      ^tty ready                                         |
      {4:text}{5:color}text                                     |
                                                        |*2
      {2:^^^^^^^                                           }|
                                                        |
      {3:-- TERMINAL --}                                    |
    ]])

    feed_data(':set notermguicolors\n')
    screen:expect([[
      ^tty ready                                         |
      {4:text}colortext                                     |
                                                        |*2
      {6:^^^^^^^}{7:                                           }|
      :set notermguicolors                              |
      {3:-- TERMINAL --}                                    |
    ]])
  end)

  -- Note: libvterm doesn't support colored underline or undercurl.
  it('supports undercurl and underdouble when run in :terminal', function()
    child_session:request('nvim_set_hl', 0, 'Visual', { undercurl = true })
    feed_data('ifoobar\027V')
    screen:expect([[
      {114:fooba}^r                                            |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- VISUAL LINE --}                                 |
      {5:-- TERMINAL --}                                    |
    ]])
    child_session:request('nvim_set_hl', 0, 'Visual', { underdouble = true })
    screen:expect([[
      {115:fooba}^r                                            |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- VISUAL LINE --}                                 |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('in nvim_list_uis(), sets nvim_set_client_info()', function()
    -- $TERM in :terminal.
    local exp_term = (is_os('bsd') or is_os('win')) and 'xterm' or 'xterm-256color'
    local ui_chan = 1
    local expected = {
      {
        chan = ui_chan,
        ext_cmdline = false,
        ext_hlstate = false,
        ext_linegrid = true,
        ext_messages = false,
        ext_multigrid = false,
        ext_popupmenu = false,
        ext_tabline = false,
        ext_termcolors = true,
        ext_wildmenu = false,
        height = 6,
        override = false,
        rgb = false,
        stdin_tty = true,
        stdout_tty = true,
        term_background = '',
        term_colors = 256,
        term_name = exp_term,
        width = 50,
      },
    }
    local _, rv = child_session:request('nvim_list_uis')
    eq(expected, rv)

    ---@type table
    local expected_version = child_exec_lua('return vim.version()')
    -- vim.version() returns `prerelease` string. Coerce it to boolean.
    expected_version.prerelease = not not expected_version.prerelease

    local expected_chan_info = {
      client = {
        attributes = {
          license = 'Apache 2',
          -- pid = 5371,
          website = 'https://neovim.io',
        },
        methods = {},
        name = 'nvim-tui',
        type = 'ui',
        version = expected_version,
      },
      id = ui_chan,
      mode = 'rpc',
      stream = 'stdio',
    }

    local status, chan_info = child_session:request('nvim_get_chan_info', ui_chan)
    ok(status)
    local info = chan_info.client
    ok(info.attributes.pid and info.attributes.pid > 0, 'PID', info.attributes.pid or 'nil')
    ok(info.version.major >= 0)
    ok(info.version.minor >= 0)
    ok(info.version.patch >= 0)

    -- Delete variable fields so we can deep-compare.
    info.attributes.pid = nil

    eq(expected_chan_info, chan_info)
  end)

  it('allows grid to assume wider ambiwidth chars than host terminal', function()
    tt.override_screen_expect_for_conpty(screen)
    child_session:request(
      'nvim_buf_set_lines',
      0,
      0,
      -1,
      true,
      { ('℃'):rep(60), ('℃'):rep(60) }
    )
    child_session:request('nvim_set_option_value', 'cursorline', true, {})
    child_session:request('nvim_set_option_value', 'list', true, {})
    child_session:request('nvim_set_option_value', 'listchars', 'eol:$', { win = 0 })
    feed_data('gg')
    local singlewidth_screen = [[
      {107:^℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃}|
      {107:℃℃℃℃℃℃℃℃℃℃}{116:$}{107:                                       }|
      ℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃|
      ℃℃℃℃℃℃℃℃℃℃{100:$}                                       |
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    -- When grid assumes "℃" to be double-width but host terminal assumes it to be single-width,
    -- the second cell of "℃" is a space and the attributes of the "℃" are applied to it.
    local doublewidth_screen = [[
      {107:^℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ }|
      {107:℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ }|
      {107:℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ }{116:$}{107:                             }|
      ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ {100:@@@@}|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen:expect(singlewidth_screen)
    child_session:request('nvim_set_option_value', 'ambiwidth', 'double', {})
    screen:expect(doublewidth_screen)
    child_session:request('nvim_set_option_value', 'ambiwidth', 'single', {})
    screen:expect(singlewidth_screen)
    child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2103, 0x2103, 2 } } })
    screen:expect(doublewidth_screen)
    child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2103, 0x2103, 1 } } })
    screen:expect(singlewidth_screen)
  end)

  it('allows grid to assume wider non-ambiwidth chars than host terminal', function()
    tt.override_screen_expect_for_conpty(screen)
    child_session:request(
      'nvim_buf_set_lines',
      0,
      0,
      -1,
      true,
      { ('✓'):rep(60), ('✓'):rep(60) }
    )
    child_session:request('nvim_set_option_value', 'cursorline', true, {})
    child_session:request('nvim_set_option_value', 'list', true, {})
    child_session:request('nvim_set_option_value', 'listchars', 'eol:$', { win = 0 })
    feed_data('gg')
    local singlewidth_screen = [[
      {107:^✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓}|
      {107:✓✓✓✓✓✓✓✓✓✓}{116:$}{107:                                       }|
      ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓|
      ✓✓✓✓✓✓✓✓✓✓{100:$}                                       |
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    -- When grid assumes "✓" to be double-width but host terminal assumes it to be single-width,
    -- the second cell of "✓" is a space and the attributes of the "✓" are applied to it.
    local doublewidth_screen = [[
      {107:^✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ }|
      {107:✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ }|
      {107:✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ }{116:$}{107:                             }|
      ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ {100:@@@@}|
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen:expect(singlewidth_screen)
    child_session:request('nvim_set_option_value', 'ambiwidth', 'double', {})
    screen:expect_unchanged()
    child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2713, 0x2713, 2 } } })
    screen:expect(doublewidth_screen)
    child_session:request('nvim_set_option_value', 'ambiwidth', 'single', {})
    screen:expect_unchanged()
    child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2713, 0x2713, 1 } } })
    screen:expect(singlewidth_screen)
  end)

  it('draws correctly when cursor_address overflows #21643', function()
    screen:try_resize(70, 333)
    retry(nil, nil, function()
      eq({ true, 330 }, { child_session:request('nvim_win_get_height', 0) })
    end)
    child_session:request('nvim_set_option_value', 'cursorline', true, {})
    -- Use full screen message so that redrawing afterwards is more deterministic.
    child_session:notify('nvim_command', 'intro')
    screen:expect({ any = 'Nvim is open source and freely distributable' })
    -- Going to top-left corner needs 3 bytes.
    -- Setting underline attribute needs 9 bytes.
    -- A Ꝩ character takes 3 bytes.
    -- The whole line needs 3 + 9 + 3 * 21838 + 3 = 65529 bytes.
    -- The cursor_address that comes after will overflow the 65535-byte buffer.
    local line = ('Ꝩ'):rep(21838) .. '℃'
    child_session:notify('nvim_buf_set_lines', 0, 0, -1, true, { line, 'b' })
    -- Close the :intro message and redraw the lines.
    feed_data('\n')
    screen:expect([[
      {107:^ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ}|
      {107:ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ}|*310
      {107:ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ℃ }|
      b                                                                     |
      {100:~                                                                     }|*17
      {3:[No Name] [+]                                                         }|
                                                                            |
      {5:-- TERMINAL --}                                                        |
    ]])
  end)

  it('draws correctly when setting title overflows #30793', function()
    screen:try_resize(67, 327)
    retry(nil, nil, function()
      eq({ true, 324 }, { child_session:request('nvim_win_get_height', 0) })
    end)
    child_exec_lua([[
      vim.o.cmdheight = 0
      vim.o.laststatus = 0
      vim.o.ruler = false
      vim.o.showcmd = false
      vim.o.termsync = false
      vim.o.titlestring = '%t%( %M%) - Nvim'
      vim.o.title = true
    ]])
    retry(nil, nil, function()
      eq('[No Name] - Nvim', api.nvim_buf_get_var(0, 'term_title'))
      eq({ true, 326 }, { child_session:request('nvim_win_get_height', 0) })
    end)
    -- Use full screen message so that redrawing afterwards is more deterministic.
    child_session:notify('nvim_command', 'intro')
    screen:expect({ any = 'Nvim is open source and freely distributable' })
    -- Going to top-left corner needs 3 bytes.
    -- A Ꝩ character takes 3 bytes.
    -- The whole line needs 3 + 3 * 21842 = 65529 bytes.
    -- The title will be updated because the buffer is now modified.
    -- The start of the OSC 0 sequence to set title can fit in the 65535-byte buffer,
    -- but the title string cannot.
    local line = ('Ꝩ'):rep(21842)
    child_session:notify('nvim_buf_set_lines', 0, 0, -1, true, { line })
    -- Close the :intro message and redraw the lines.
    feed_data('\n')
    screen:expect([[
      ^ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ|
      ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ|*325
      {5:-- TERMINAL --}                                                     |
    ]])
    retry(nil, nil, function()
      eq('[No Name] + - Nvim', api.nvim_buf_get_var(0, 'term_title'))
    end)
  end)

  it('visual bell (padding) does not crash #21610', function()
    feed_data ':set visualbell\n'
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      :set visualbell                                   |
      {5:-- TERMINAL --}                                    |
    ]])

    -- move left is enough to invoke the bell
    feed_data 'h'
    -- visual change to show we process events after this
    feed_data 'i'
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('no assert failure on deadly signal #21896', function()
    exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]])
    screen:expect(is_os('win') and { any = '%[Process exited 1%]' } or [[
      Nvim: Caught deadly signal 'SIGTERM'              |
      ^                                                  |
      [Process exited 1]                                |
                                                        |*3
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('exit status 1 and error message with deadly signal sent to server', function()
    local _, server_pid = child_session:request('nvim_call_function', 'getpid', {})
    exec_lua([[vim.uv.kill(..., 'sigterm')]], server_pid)
    if not is_os('win') then
      screen:expect({ any = vim.pesc([[Nvim: Caught deadly signal 'SIGTERM']]) })
    end
    screen:expect({ any = vim.pesc('[Process exited 1]') })
  end)

  it('exits immediately when stdin is closed #35744', function()
    local chan = api.nvim_get_option_value('channel', { buf = 0 })
    local pid = fn.jobpid(chan)
    fn.chanclose(chan)
    retry(nil, 50, function()
      eq(vim.NIL, api.nvim_get_proc(pid))
    end)
    -- FIXME: SIGHUP sometimes isn't caught with ASAN.
    screen:expect({ any = t.is_asan() and '%[Process exited %d+%]' or '%[Process exited 1%]' })
  end)

  it('exits properly when :quit non-last window in event handler #14379', function()
    local code = [[
      vim.defer_fn(function()
        vim.cmd('vsplit | quit')
      end, 0)
      vim.cmd('quit')
    ]]
    child_session:notify('nvim_exec_lua', code, {})
    screen:expect([[
      ^                                                  |
      [Process exited 0]                                |
                                                        |*4
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('no stack-use-after-scope with cursor color #22432', function()
    screen:set_option('rgb', true)
    command('set termguicolors')
    child_session:request(
      'nvim_exec2',
      [[
      set tgc
      hi Cursor guifg=Red guibg=Green
      set guicursor=n:block-Cursor/lCursor
    ]],
      {}
    )
    screen:expect([[
      ^                                                  |
      {1:~}{18:                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('i')
    screen:expect([[
      ^                                                  |
      {1:~}{18:                                                 }|*3
      {3:[No Name]                                         }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('redraws on SIGWINCH even if terminal size is unchanged #23411', function()
    -- On Windows, SIGWINCH cannot be sent as a signal with uv_kill(), while
    -- SIGWINCH handlers are only called on terminal resize.
    t.skip(is_os('win'), 'N/A for Windows')
    child_session:request('nvim_echo', { { 'foo' } }, false, {})
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      foo                                               |
      {5:-- TERMINAL --}                                    |
    ]])
    exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigwinch')]])
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('supports hiding cursor', function()
    child_session:request(
      'nvim_command',
      "let g:id = jobstart([v:progpath, '--clean', '--headless'])"
    )
    feed_data(':call jobwait([g:id])\n')
    screen:expect([[
                                                        |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      :call jobwait([g:id])                             |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\003')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      Type  :qa  and press <Enter> to exit Nvim         |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('cursor is not hidden on incsearch with no match', function()
    feed_data('ifoo\027')
    feed_data('/foo')
    screen:expect([[
      {2:foo}                                               |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      /foo^                                              |
      {5:-- TERMINAL --}                                    |
    ]])
    screen:sleep(10)
    feed_data('b')
    screen:expect([[
      foo                                               |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      /foob^                                             |
      {5:-- TERMINAL --}                                    |
    ]])
    screen:sleep(10)
    feed_data('a')
    screen:expect([[
      foo                                               |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      /fooba^                                            |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('emits hyperlinks with OSC 8', function()
    t.skip(is_os('win'), 'FIXME: does not work on Windows')
    exec_lua([[
      local buf = vim.api.nvim_get_current_buf()
      _G.urls = {}
      vim.api.nvim_create_autocmd('TermRequest', {
        buf = buf,
        callback = function(ev)
          local req = ev.data.sequence
          if not req then
            return
          end
          local id, url = req:match('\027]8;id=(%d+);(.*)$')
          if id ~= nil and url ~= nil then
            table.insert(_G.urls, { id = tonumber(id), url = url })
          end
        end,
      })
    ]])
    child_exec_lua([[
      vim.api.nvim_buf_set_lines(0, 0, 0, true, {'Hello'})
      _G.NS = vim.api.nvim_create_namespace('test')
      vim.api.nvim_buf_set_extmark(0, _G.NS, 0, 1, {
        end_col = 3,
        url = 'https://example.com',
      })
    ]])
    retry(nil, 1000, function()
      eq({ { id = 0xE1EA0000, url = 'https://example.com' } }, exec_lua([[return _G.urls]]))
    end)
    -- No crash with very long URL #30794
    child_exec_lua([[
      vim.api.nvim_buf_set_extmark(0, _G.NS, 0, 3, {
        end_col = 5,
        url = 'https://example.com/' .. ('a'):rep(65536),
      })
    ]])
    retry(nil, nil, function()
      eq({
        { id = 0xE1EA0000, url = 'https://example.com' },
        { id = 0xE1EA0001, url = 'https://example.com/' .. ('a'):rep(65536) },
      }, exec_lua([[return _G.urls]]))
    end)
  end)

  it('TermResponse works with vim.wait() from another autocommand #32706', function()
    child_exec_lua([[
      _G.termresponse = nil
      vim.api.nvim_create_autocmd('TermResponse', {
        callback = function(ev)
          _G.sequence = ev.data.sequence
          _G.v_termresponse = vim.v.termresponse
        end,
      })
      vim.api.nvim_create_autocmd('InsertEnter', {
        buf = 0,
        callback = function()
          _G.result = vim.wait(3000, function()
            local expected = '\027P1+r5463'
            return _G.sequence == expected and _G.v_termresponse == expected
          end)
        end,
      })
    ]])
    feed_data('i')
    feed_data('\027P1+r5463\027\\')
    retry(nil, 4000, function()
      eq(true, child_exec_lua('return _G.result'))
    end)
  end)

  it('TermResponse from unblock_autocmds() sets "data"', function()
    if not child_exec_lua('return pcall(require, "ffi")') then
      pending('N/A: missing LuaJIT FFI')
    end
    child_exec_lua([[
      local ffi = require('ffi')
      ffi.cdef[=[
        void block_autocmds(void);
        void unblock_autocmds(void);
      ]=]
      ffi.C.block_autocmds()
      vim.api.nvim_create_autocmd('TermResponse', {
        once = true,
        callback = function(ev)
          _G.data = ev.data
        end,
      })
    ]])
    feed_data('\027P0$r\027\\')
    retry(nil, 4000, function()
      eq('\027P0$r', child_exec_lua('return vim.v.termresponse'))
    end)
    eq(vim.NIL, child_exec_lua('return _G.data'))
    child_exec_lua('require("ffi").C.unblock_autocmds()')
    eq({ sequence = '\027P0$r' }, child_exec_lua('return _G.data'))

    -- If TermResponse during TermResponse changes v:termresponse, data.sequence contains the actual
    -- response that triggered the autocommand.
    -- The second autocommand below forces a use-after-free when v:termresponse's value changes
    -- during TermResponse if data.sequence didn't allocate its own copy.
    child_exec_lua([[
      require('ffi').C.block_autocmds()
      vim.api.nvim_create_autocmd('TermResponse', {
        once = true,
        callback = function(ev)
          _G.au1_termresponse1 = vim.v.termresponse
          _G.au1_sequence1 = ev.data.sequence
          local chan = vim.fn.sockconnect('pipe', vim.v.servername, { rpc = true })
          vim.rpcrequest(chan, 'nvim_ui_term_event', 'termresponse', 'baz')
          _G.au1_termresponse2 = vim.v.termresponse
          _G.au1_sequence2 = ev.data.sequence
        end,
      })
      _G.au2_sequences = {}
      vim.api.nvim_create_autocmd('TermResponse', {
        callback = function(ev)
          table.insert(_G.au2_sequences, ev.data.sequence)
        end,
      })
    ]])
    child_session:request('nvim_ui_term_event', 'termresponse', 'foobar')
    eq('foobar', child_exec_lua('return vim.v.termresponse'))
    -- For good measure, check deferred TermResponse doesn't try to fire if autocmds are still
    -- blocked after unblock_autocmds.
    child_exec_lua('require("ffi").C.block_autocmds() require("ffi").C.unblock_autocmds()')
    eq(vim.NIL, child_exec_lua('return _G.au1_termresponse1'))
    child_exec_lua('require("ffi").C.unblock_autocmds()')
    eq('foobar', child_exec_lua('return _G.au1_termresponse1'))
    eq('foobar', child_exec_lua('return _G.au1_sequence1'))
    eq('baz', child_exec_lua('return _G.au1_termresponse2'))
    eq('foobar', child_exec_lua('return _G.au1_sequence2')) -- unchanged
    -- Second autocmd triggers due to "baz" (via the nested TermResponse), then from "foobar".
    eq({ 'baz', 'foobar' }, child_exec_lua('return _G.au2_sequences'))
  end)

  it('nvim_ui_send works', function()
    child_session:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\')
    retry(nil, nil, function()
      eq('TEST_TITLE', api.nvim_buf_get_var(0, 'term_title'))
    end)
  end)

  it('stdin and stdout are tty fds in embedded server #38172', function()
    eq(
      { 'tty', 'tty' },
      child_exec_lua('return { vim.uv.guess_handle(0), vim.uv.guess_handle(1) }')
    )
    -- Also works after :restart #38745
    feed_data(':restart lua ={ vim.uv.guess_handle(0), vim.uv.guess_handle(1) }\r')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      { "tty", "tty" }                                  |
      {5:-- TERMINAL --}                                    |
    ]])
    -- The server is now detached and needs to be quit explicitly.
    feed_data(':qall!\r')
    screen:expect({ any = vim.pesc('[Process exited 0]') })
  end)
end)

describe('TUI', function()
  before_each(clear)

  it('resize at startup #17285 #15044 #11330', function()
    local screen = Screen.new(50, 10)
    screen:add_extra_attr_ids({
      [100] = { foreground = tonumber('0x4040ff'), fg_indexed = true },
      [101] = { foreground = Screen.colors.Gray100, background = Screen.colors.DarkGreen },
    })
    fn.jobstart({
      nvim_prog,
      '--clean',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set notermguicolors',
      '--cmd',
      nvim_set,
      '--cmd',
      'let start = reltime() | while v:true | if reltimefloat(reltime(start)) > 2 | break | endif | endwhile',
    }, {
      term = true,
      env = {
        VIMRUNTIME = os.getenv('VIMRUNTIME'),
      },
    })
    exec([[
      sleep 500m
      vs new
    ]])
    screen:expect(([[
      ^                         │                        |
      {1:~                        }│{100:~                       }|*6
      {1:~                        }│                        |
      {3:new                       }{101:{MATCH:<.*%s} [-] }|
                                                        |
    ]]):format(is_os('win') and '[/\\]nvim%.exe' or '/nvim'))
  end)

  -- #28667, #28668
  for _, guicolors in ipairs({ 'notermguicolors', 'termguicolors' }) do
    it('has no black flicker when clearing regions during startup with ' .. guicolors, function()
      local screen = Screen.new(50, 10)
      -- Colorscheme is automatically detected as light in _core/defaults.lua, so fg
      -- should be dark except on Windows, where it doesn't respond to the OSC11 query,
      -- so bg is dark.
      local fg = is_os('win') and Screen.colors.NvimLightGrey2 or Screen.colors.NvimDarkGrey2
      local bg = is_os('win') and Screen.colors.NvimDarkGrey2 or Screen.colors.NvimLightGrey2
      screen:add_extra_attr_ids({
        [100] = {
          foreground = fg,
          background = bg,
        },
      })
      fn.jobstart({
        nvim_prog,
        '--clean',
        '--cmd',
        'set ' .. guicolors,
        '--cmd',
        'echo "foo"',
        '--cmd',
        'sleep 10',
      }, {
        term = true,
        env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
      })
      if guicolors == 'termguicolors' then
        screen:expect([[
          {100:^                                                  }|
          {100:                                                  }|*7
          {100:foo                                               }|
                                                            |
        ]])
      else
        screen:expect([[
          ^                                                  |
                                                            |*7
          foo                                               |
                                                            |
        ]])
      end
    end)
  end

  it('argv[0] can be overridden #23953', function()
    t.skip(is_os('win'), 'N/A for Windows')
    t.skip(t.is_arch('s390x'), 'FIXME s390x')
    local screen = Screen.new(50, 7, { rgb = false })
    fn.jobstart(
      { testprg('shell-test'), 'EXECVP', nvim_prog, 'Xargv0nvim', '--clean' },
      { term = true, env = { VIMRUNTIME = os.getenv('VIMRUNTIME') } }
    )
    command('startinsert')
    screen:expect([[
      ^                                                  |
      ~                                                 |*3
      [No Name]                       0,0-1          All|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(':put =v:argv + [v:progname]\n')
    screen:expect([[
      Xargv0nvim                                        |
      --embed                                           |
      --clean                                           |
      ^Xargv0nvim                                        |
      [No Name] [+]                   5,1            Bot|
      4 more lines                                      |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it("float is still highlighted with 'winblend' over uninitialized cells #34360", function()
    write_file(
      'Xblend.lua',
      [[
        local win = vim.api.nvim_open_win(0, false, { relative = 'editor', width = 3, height = 1, row = 1000, col = 0, zindex = 400 })
        vim.api.nvim_set_option_value('winblend', 30, { win = win })
        vim.fn.setline(1, "foo")
        vim.api.nvim_buf_set_extmark(0, vim.api.nvim_create_namespace(''), 0, 0, { end_col = 3, hl_group = 'Title' })
    ]]
    )
    finally(function()
      os.remove('Xblend.lua')
    end)
    local screen = tt.setup_child_nvim({ '--clean', '-u', 'Xblend.lua' })
    screen:expect([[
      {5:^foo}                                               |
      ~                                                 |*3
      [No Name] [+]                   1,1            All|
      {5:foo}                                               |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('with non-tty (pipe) stdout/stderr', function()
    t.skip(is_os('win'), 'N/A for Windows')
    finally(function()
      os.remove('testF')
      os.remove(testlog)
    end)
    local screen = tt.setup_screen(
      0,
      ('"%s" --clean --cmd "set noswapfile noshowcmd noruler" --cmd "normal iabc" > /dev/null 2>&1 && cat testF && rm testF'):format(
        nvim_prog
      ),
      nil,
      { VIMRUNTIME = os.getenv('VIMRUNTIME'), NVIM_LOG_FILE = testlog }
    )
    feed_data(':w testF\n:q\n')
    screen:expect([[
      :w testF                                          |
      :q                                                |
      abc                                               |
      ^                                                  |
      [Process exited 0]                                |
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    assert_log('TUI: timed out waiting for DA1 response', testlog)
  end)

  it('<C-h> #10134', function()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noruler',
      '--cmd',
      ':nnoremap <C-h> :echomsg "\\<C-h\\>"<CR>',
    }, { env = env_notermguicolors })
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    command([[call chansend(b:terminal_job_id, "\<C-h>")]])
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      <C-h>                                             |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('draws line with many trailing spaces correctly #24955', function()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'call setline(1, ["1st line" .. repeat(" ", 153), "2nd line"])',
    }, { cols = 80, env = env_notermguicolors })
    screen:expect([[
      ^1st line                                                                        |
                                                                                      |*2
      2nd line                                                                        |
      {3:[No Name] [+]                                                 1,1            All}|
                                                                                      |
      {5:-- TERMINAL --}                                                                  |
    ]])
    feed_data('$')
    screen:expect([[
      1st line                                                                        |
                                                                                      |
      ^                                                                                |
      2nd line                                                                        |
      {3:[No Name] [+]                                                 1,161          All}|
                                                                                      |
      {5:-- TERMINAL --}                                                                  |
    ]])
  end)

  it('draws screen lines with leading spaces correctly #29711', function()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'set foldcolumn=6 | call setline(1, ["", repeat("aabb", 1000)]) | echo 42',
    }, { extra_rows = 10, cols = 66 })
    screen:expect([[
            ^                                                            |
            aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabb|*12
            aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba@@@|
      [No Name] [+]                                   1,0-1          Top|
      42                                                                |
      {5:-- TERMINAL --}                                                    |
    ]])
    feed_data('\12') -- Ctrl-L
    -- The first line counts as 3 cells.
    -- For the second line, 6 repeated spaces at the start counts as 2 cells,
    -- so each screen line of the second line counts as 62 cells.
    -- After drawing the first line and 8 screen lines of the second line,
    -- 3 + 8 * 62 = 499 cells have been counted.
    -- The 6 repeated spaces at the start of the next screen line exceeds the
    -- 500-cell limit, so the buffer is flushed after these spaces.
    screen:expect([[
            ^                                                            |
            aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabb|*12
            aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba@@@|
      [No Name] [+]                                   1,0-1          Top|
                                                                        |
      {5:-- TERMINAL --}                                                    |
    ]])
  end)

  it('no heap-buffer-overflow when changing &columns', function()
    t.skip(is_os('win'), 'FIXME: does not work on Windows')
    -- Set a different bg colour and change $TERM to something dumber so the `print_spaces()`
    -- codepath in `clear_region()` is hit.
    local screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'set notermguicolors | highlight Normal ctermbg=red',
      '--cmd',
      'call setline(1, ["a"->repeat(&columns)])',
    }, { env = { TERM = 'ansi' } })

    screen:expect([[
      {117:^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}|
      {117:~                                                 }|*3
      {118:[No Name] [+]                   1,1            All}|
      {117:                                                  }|
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data(':set columns=12\n')
    screen:expect([[
      {117:^aaaaaaaaaaaa                                      }|
      {117:aaaaaaaaaaaa                                      }|*3
      {118:<        All}{117:                                      }|
      {117:                                                  }|
      {5:-- TERMINAL --}                                    |
    ]])

    -- Wider than TUI, so screen state will look weird.
    -- Wait for the statusline to redraw to confirm that the TUI lives and ASAN is happy.
    feed_data(':set columns=99|set stl=redrawn%m\n')
    screen:expect({ any = 'redrawn%[%+%]' })
  end)

  it('missing DSR response does not lead to hit-enter prompt #38877', function()
    local child_server = n.new_pipename()
    exec_lua('vim.uv.os_unsetenv("NVIM_TEST")')
    local job = fn.jobstart({ nvim_prog, '--clean', '--listen', child_server }, {
      -- Use pty = true for a PTY without a terminal, so that there is no DSR response.
      pty = true,
      env = {
        VIMRUNTIME = os.getenv('VIMRUNTIME'),
        COLORTERM = 'xterm-256color',
        NVIM_LOG_FILE = testlog,
      },
    })
    finally(function()
      exec_lua(function()
        vim.fn.jobstop(job)
        vim.fn.jobwait({ job }, 5000)
      end)
      os.remove(testlog)
    end)
    retry(nil, nil, function()
      t.neq(nil, vim.uv.fs_stat(child_server))
    end)
    local child_session = n.connect(child_server)
    local expected_msg =
      "E1568: Terminal did not respond to DSR request for 'background' color. Startup may be slower. :help 'ttyfast'"
    retry(nil, 4000, function()
      eq({ true, { mode = 'n', blocking = false } }, { child_session:request('nvim_get_mode') })
      if not is_os('win') then -- ConPTY provides DSR response on Windows?
        eq(
          { true, { output = expected_msg } },
          { child_session:request('nvim_exec2', 'messages', { output = true }) }
        )
      end
    end)
  end)
end)

describe('TUI UIEnter/UILeave', function()
  it('fires exactly once, after VimEnter', function()
    clear()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noswapfile noshowcmd noruler',
      '--cmd',
      'let g:evs = []',
      '--cmd',
      'autocmd UIEnter *  :call add(g:evs, "UIEnter")',
      '--cmd',
      'autocmd UILeave *  :call add(g:evs, "UILeave")',
      '--cmd',
      'autocmd VimEnter * :call add(g:evs, "VimEnter")',
    }, { env = env_notermguicolors })
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data(':echo g:evs\n')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      ['VimEnter', 'UIEnter']                           |
      {5:-- TERMINAL --}                                    |
    ]])
  end)
end)

describe('TUI FocusGained/FocusLost', function()
  local screen --[[@type test.functional.ui.screen]]
  local child_session --[[@type test.Session]]

  before_each(function()
    clear()
    local child_server = new_pipename()
    screen = tt.setup_child_nvim({
      '--listen',
      child_server,
      '-u',
      'NONE',
      '-i',
      'NONE',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noswapfile noshowcmd noruler background=dark',
    }, { env = env_notermguicolors })

    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    child_session = n.connect(child_server)
    child_session:request(
      'nvim_exec2',
      [[
      autocmd FocusGained * echo 'gained'
      autocmd FocusLost * echo 'lost'
    ]],
      {}
    )
    feed_data('\034\016') -- CTRL-\ CTRL-N
  end)

  it('in normal-mode', function()
    retry(2, 3 * screen.timeout, function()
      feed_data('\027[I')
      screen:expect([[
        ^                                                  |
        {100:~                                                 }|*3
        {3:[No Name]                                         }|
        gained                                            |
        {5:-- TERMINAL --}                                    |
      ]])

      feed_data('\027[O')
      screen:expect([[
        ^                                                  |
        {100:~                                                 }|*3
        {3:[No Name]                                         }|
        lost                                              |
        {5:-- TERMINAL --}                                    |
      ]])
    end)
  end)

  it('in insert-mode', function()
    feed_data(':set noshowmode\r')
    feed_data('i')
    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      :set noshowmode                                   |
      {5:-- TERMINAL --}                                    |
    ]])
    retry(2, 3 * screen.timeout, function()
      feed_data('\027[I')
      screen:expect([[
        ^                                                  |
        {100:~                                                 }|*3
        {3:[No Name]                                         }|
        gained                                            |
        {5:-- TERMINAL --}                                    |
      ]])
      feed_data('\027[O')
      screen:expect([[
        ^                                                  |
        {100:~                                                 }|*3
        {3:[No Name]                                         }|
        lost                                              |
        {5:-- TERMINAL --}                                    |
      ]])
    end)
  end)

  -- During cmdline-mode we ignore :echo invoked by timers/events.
  -- See commit: 5cc87d4dabd02167117be7a978b5c8faaa975419.
  it('in cmdline-mode does NOT :echo', function()
    feed_data(':')
    feed_data('\027[I')
    screen:expect([[
                                                        |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
      :^                                                 |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[O')
    screen:expect_unchanged()
  end)

  it('in cmdline-mode', function()
    -- Set up autocmds that modify the buffer, instead of just calling :echo.
    -- This is how we can test handling of focus gained/lost during cmdline-mode.
    -- See commit: 5cc87d4dabd02167117be7a978b5c8faaa975419.
    child_session:request(
      'nvim_exec2',
      [[
      autocmd!
      autocmd FocusLost * call append(line('$'), 'lost')
      autocmd FocusGained * call append(line('$'), 'gained')
    ]],
      {}
    )
    retry(2, 3 * screen.timeout, function()
      -- Enter cmdline-mode.
      feed_data(':')
      screen:sleep(1)
      -- Send focus lost/gained termcodes.
      feed_data('\027[O')
      feed_data('\027[I')
      screen:sleep(1)
      -- Exit cmdline-mode. Redraws from timers/events are blocked during
      -- cmdline-mode, so the buffer won't be updated until we exit cmdline-mode.
      feed_data('\n')
      screen:expect { any = 'lost' .. (' '):rep(46) .. '|\ngained' }
    end)
  end)

  it('in terminal-mode', function()
    feed_data(':set shell=' .. testprg('shell-test') .. ' shellcmdflag=EXE\n')
    feed_data(':set shellxquote=\n') -- win: avoid extra quotes
    feed_data(':set noshowmode laststatus=0\n')

    feed_data(':terminal zia\n')
    -- Wait for terminal to be ready.
    screen:expect([[
      ^ready $ zia                                       |
                                                        |
      [Process exited 0]                                |
                                                        |*2
      :terminal zia                                     |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data('\027[I')
    screen:expect {
      grid = [[
      ^ready $ zia                                       |
                                                        |
      [Process exited 0]                                |
                                                        |*2
      gained                                            |
      {5:-- TERMINAL --}                                    |
    ]],
      timeout = (4 * screen.timeout),
    }

    feed_data('\027[O')
    screen:expect([[
      ^ready $ zia                                       |
                                                        |
      [Process exited 0]                                |
                                                        |*2
      lost                                              |
      {5:-- TERMINAL --}                                    |
    ]])
  end)

  it('in hit-enter prompt', function()
    tt.override_screen_expect_for_conpty(screen)
    feed_data(":echom 'msg1'|echom 'msg2'|echom 'msg3'|echom 'msg4'|echom 'msg5'\n")
    screen:expect([[
      msg1                                              |
      msg2                                              |
      msg3                                              |
      msg4                                              |
      msg5                                              |
      {102:Press ENTER or type command to continue}^           |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027[I')
    feed_data('\027[I')
    screen:expect_unchanged()
  end)
end)

-- These tests require `tt` because --headless/--embed does not initialize the TUI.
describe("TUI 't_Co' (terminal colors)", function()
  local screen --[[@type test.functional.ui.screen]]

  local function assert_term_colors(term, colorterm, maxcolors)
    clear({ env = { TERM = term }, args = {} })
    screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      nvim_set .. ' notermguicolors',
    }, {
      env = {
        LANG = 'C',
        TERM = term or '',
        COLORTERM = colorterm or '',
      },
    })

    local tline --[[@type string]]
    if maxcolors == 8 then
      tline = '{112:~                                                 }'
    elseif maxcolors == 16 then
      tline = '~                                                 '
    else
      tline = '{100:~                                                 }'
    end

    screen:expect(string.format(
      [[
      ^                                                  |
      %s|*4
                                                        |
      {5:-- TERMINAL --}                                    |
    ]],
      tline
    ))

    feed_data(':echo &t_Co\n')
    screen:expect(string.format(
      [[
      ^                                                  |
      %s|*4
      %-3s                                               |
      {5:-- TERMINAL --}                                    |
    ]],
      tline,
      tostring(maxcolors and maxcolors or '')
    ))
  end

  -- ansi and no terminal type at all:

  if is_os('win') then
    it('guessed vtpcon with no TERM uses 256 colors', function()
      assert_term_colors(nil, nil, 256)
    end)
  else
    it('no TERM uses 8 colors', function()
      assert_term_colors(nil, nil, 8)
    end)
  end

  it('TERM=ansi no COLORTERM uses 8 colors', function()
    assert_term_colors('ansi', nil, 8)
  end)

  it('TERM=ansi with COLORTERM=anything-no-number uses 16 colors', function()
    assert_term_colors('ansi', 'yet-another-term', 16)
  end)

  it('unknown TERM COLORTERM with 256 in name uses 256 colors', function()
    assert_term_colors('ansi', 'yet-another-term-256color', 256)
  end)

  it('TERM=ansi-256color sets 256 colours', function()
    assert_term_colors('ansi-256color', nil, 256)
  end)

  -- Unknown terminal types:

  it('unknown TERM no COLORTERM sets 8 colours', function()
    assert_term_colors('yet-another-term', nil, 8)
  end)

  it('unknown TERM with COLORTERM=anything-no-number uses 16 colors', function()
    assert_term_colors('yet-another-term', 'yet-another-term', 16)
  end)

  it('unknown TERM with 256 in name sets 256 colours', function()
    assert_term_colors('yet-another-term-256color', nil, 256)
  end)

  it('unknown TERM COLORTERM with 256 in name uses 256 colors', function()
    assert_term_colors('yet-another-term', 'yet-another-term-256color', 256)
  end)

  -- Linux kernel terminal emulator:

  it('TERM=linux uses 256 colors', function()
    assert_term_colors('linux', nil, 256)
  end)

  it('TERM=linux-16color uses 256 colors', function()
    assert_term_colors('linux-16color', nil, 256)
  end)

  it('TERM=linux-256color uses 256 colors', function()
    assert_term_colors('linux-256color', nil, 256)
  end)

  -- screen:
  --
  -- FreeBSD and Windows fall back to the built-in screen-256colour entry.
  -- Linux and MacOS have a screen entry in external terminfo with 8 colours,
  -- which is raised to 16 by COLORTERM.

  it('TERM=screen no COLORTERM uses 8/256 colors', function()
    if is_os('freebsd') or is_os('win') then
      assert_term_colors('screen', nil, 256)
    else
      assert_term_colors('screen', nil, 8)
    end
  end)

  it('TERM=screen COLORTERM=screen uses 16/256 colors', function()
    if is_os('freebsd') or is_os('win') then
      assert_term_colors('screen', 'screen', 256)
    else
      assert_term_colors('screen', 'screen', 16)
    end
  end)

  it('TERM=screen COLORTERM=screen-256color uses 256 colors', function()
    assert_term_colors('screen', 'screen-256color', 256)
  end)

  it('TERM=screen-256color no COLORTERM uses 256 colors', function()
    assert_term_colors('screen-256color', nil, 256)
  end)

  -- tmux:
  --
  -- FreeBSD and MacOS fall back to the built-in tmux-256colour entry.
  -- Linux has a tmux entry in external terminfo with 8 colours,
  -- which is raised to 256.

  it('TERM=tmux no COLORTERM uses 256 colors', function()
    assert_term_colors('tmux', nil, 256)
  end)

  it('TERM=tmux COLORTERM=tmux uses 256 colors', function()
    assert_term_colors('tmux', 'tmux', 256)
  end)

  it('TERM=tmux COLORTERM=tmux-256color uses 256 colors', function()
    assert_term_colors('tmux', 'tmux-256color', 256)
  end)

  it('TERM=tmux-256color no COLORTERM uses 256 colors', function()
    assert_term_colors('tmux-256color', nil, 256)
  end)

  -- xterm and imitators:

  it('TERM=xterm uses 256 colors', function()
    assert_term_colors('xterm', nil, 256)
  end)

  it('TERM=xterm COLORTERM=gnome-terminal uses 256 colors', function()
    assert_term_colors('xterm', 'gnome-terminal', 256)
  end)

  it('TERM=xterm COLORTERM=mate-terminal uses 256 colors', function()
    assert_term_colors('xterm', 'mate-terminal', 256)
  end)

  it('TERM=xterm-256color uses 256 colors', function()
    assert_term_colors('xterm-256color', nil, 256)
  end)

  -- rxvt and stterm:
  --
  -- FreeBSD and MacOS fall back to the built-in rxvt-256color and
  -- st-256colour entries.
  -- Linux has an rxvt, an st, and an st-16color entry in external terminfo
  -- with 8, 8, and 16 colours respectively, which are raised to 256.

  it('TERM=rxvt no COLORTERM uses 256 colors', function()
    assert_term_colors('rxvt', nil, 256)
  end)

  it('TERM=rxvt COLORTERM=rxvt uses 256 colors', function()
    assert_term_colors('rxvt', 'rxvt', 256)
  end)

  it('TERM=rxvt-256color uses 256 colors', function()
    assert_term_colors('rxvt-256color', nil, 256)
  end)

  it('TERM=st no COLORTERM uses 256 colors', function()
    assert_term_colors('st', nil, 256)
  end)

  it('TERM=st COLORTERM=st uses 256 colors', function()
    assert_term_colors('st', 'st', 256)
  end)

  it('TERM=st COLORTERM=st-256color uses 256 colors', function()
    assert_term_colors('st', 'st-256color', 256)
  end)

  it('TERM=st-16color no COLORTERM uses 8/256 colors', function()
    assert_term_colors('st', nil, 256)
  end)

  it('TERM=st-16color COLORTERM=st uses 16/256 colors', function()
    assert_term_colors('st', 'st', 256)
  end)

  it('TERM=st-16color COLORTERM=st-256color uses 256 colors', function()
    assert_term_colors('st', 'st-256color', 256)
  end)

  it('TERM=st-256color uses 256 colors', function()
    assert_term_colors('st-256color', nil, 256)
  end)

  -- gnome and vte:
  --
  -- FreeBSD and MacOS fall back to the built-in vte-256color entry.
  -- Linux has a gnome, a vte, a gnome-256color, and a vte-256color entry in
  -- external terminfo with 8, 8, 256, and 256 colours respectively, which are
  -- raised to 256.

  it('TERM=gnome no COLORTERM uses 256 colors', function()
    assert_term_colors('gnome', nil, 256)
  end)

  it('TERM=gnome COLORTERM=gnome uses 256 colors', function()
    assert_term_colors('gnome', 'gnome', 256)
  end)

  it('TERM=gnome COLORTERM=gnome-256color uses 256 colors', function()
    assert_term_colors('gnome', 'gnome-256color', 256)
  end)

  it('TERM=gnome-256color uses 256 colors', function()
    assert_term_colors('gnome-256color', nil, 256)
  end)

  it('TERM=vte no COLORTERM uses 256 colors', function()
    assert_term_colors('vte', nil, 256)
  end)

  it('TERM=vte COLORTERM=vte uses 256 colors', function()
    assert_term_colors('vte', 'vte', 256)
  end)

  it('TERM=vte COLORTERM=vte-256color uses 256 colors', function()
    assert_term_colors('vte', 'vte-256color', 256)
  end)

  it('TERM=vte-256color uses 256 colors', function()
    assert_term_colors('vte-256color', nil, 256)
  end)

  -- others:

  -- TODO(blueyed): this is made pending, since it causes failure + later hang
  --                when using non-compatible libvterm (#9494/#10179).
  pending('TERM=interix uses 8 colors', function()
    assert_term_colors('interix', nil, 8)
  end)

  it('TERM=iTerm.app uses 256 colors', function()
    assert_term_colors('iTerm.app', nil, 256)
  end)

  it('TERM=iterm uses 256 colors', function()
    assert_term_colors('iterm', nil, 256)
  end)
end)

-- These tests require `tt` because --headless/--embed does not initialize the TUI.
describe("TUI 'term' option", function()
  local screen --[[@type test.functional.ui.screen]]

  local function assert_term(term_envvar, term_expected)
    clear()
    screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      nvim_set .. ' notermguicolors',
    }, {
      env = {
        LANG = 'C',
        TERM = term_envvar or '',
      },
    })

    local full_timeout = screen.timeout
    retry(nil, 2 * full_timeout, function() -- Wait for TUI thread to set 'term'.
      feed_data(":echo 'term='.(&term)\n")
      screen:expect { any = 'term=' .. term_expected, timeout = 250 }
    end)
  end

  it('gets builtin term if $TERM is invalid', function()
    assert_term('foo', 'ansi')
  end)

  it('gets system-provided term if $TERM is valid', function()
    if is_os('openbsd') then
      assert_term('xterm', 'xterm')
    elseif is_os('bsd') then -- BSD lacks terminfo, builtin is always used.
      assert_term('xterm', 'xterm')
    elseif is_os('mac') then
      local status, _ = pcall(assert_term, 'xterm', 'xterm')
      if not status then
        pending('macOS: unibilium could not find terminfo')
      end
    else
      assert_term('xterm', 'xterm')
    end
  end)

  it('builtin terms', function()
    -- These non-standard terminfos are always builtin.
    assert_term('win32con', 'win32con')
    assert_term('conemu', 'conemu')
    assert_term('vtpcon', 'vtpcon')
  end)
end)

-- These tests require `tt` because --headless/--embed does not initialize the TUI.
describe('TUI', function()
  local screen --[[@type test.functional.ui.screen]]

  -- Runs (child) `nvim` in a TTY (:terminal), to start the builtin TUI.
  local function nvim_tui(extra_args)
    clear()
    screen = tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      nvim_set .. ' notermguicolors',
      extra_args,
    }, {
      env = {
        LANG = 'C',
      },
    })
  end

  it('-V3log logs terminfo values', function()
    local logfile = 'Xtest_tui_verbose_log'
    nvim_tui('-V3' .. logfile)
    finally(function()
      os.remove(logfile)
    end)

    -- Wait for TUI to start.
    feed_data('Gitext')
    screen:expect([[
      text^                                              |
      {100:~                                                 }|*4
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])

    retry(nil, 3000, function() -- Wait for log file to be flushed.
      local log = read_file(logfile) or ''
      eq('--- Terminal info --- {{{\n', string.match(log, '%-%-%- Terminal.-\n')) -- }}}
      ok(#log > 50)
    end)
  end)

  it('does not crash on large inputs #26099', function()
    nvim_tui()

    screen:expect([[
      ^                                                  |
      {100:~                                                 }|*4
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data(string.format('\027]52;c;%s\027\\', string.rep('A', 8192)))

    screen:expect_unchanged()
  end)

  it('queries the terminal for truecolor support', function()
    t.skip(is_os('win'), 'FIXME: does not work on Windows')
    clear()
    exec_lua([[
      vim.api.nvim_create_autocmd('TermRequest', {
        callback = function(ev)
          local req = ev.data.sequence
          local sequence = req:match('^\027P%+q([%x;]+)$')
          if sequence then
            local t = {}
            for cap in vim.gsplit(sequence, ';') do
              local resp = string.format('\027P1+r%s\027\\', sequence)
              vim.api.nvim_chan_send(vim.bo[ev.buf].channel, resp)
              t[vim.text.hexdecode(cap)] = true
            end
            vim.g.xtgettcap = t
            return true
          end
        end,
      })
    ]])

    local child_server = new_pipename()
    screen = tt.setup_child_nvim({
      '--clean',
      '--listen',
      child_server,
    }, {
      env = {
        VIMRUNTIME = os.getenv('VIMRUNTIME'),

        -- Force COLORTERM to be unset and use a TERM that does not contain Tc or RGB in terminfo.
        -- This will force the nested nvim instance to query with XTGETTCAP
        COLORTERM = '',
        TERM = 'xterm-256colors',
      },
    })

    screen:expect({ any = '%[No Name%]' })

    local child_session = n.connect(child_server)
    retry(nil, 1000, function()
      eq({
        Tc = true,
        RGB = true,
        setrgbf = true,
        setrgbb = true,
      }, eval("get(g:, 'xtgettcap', '')"))
      eq({ true, 1 }, { child_session:request('nvim_eval', '&termguicolors') })
    end)
  end)

  it('does not query the terminal for truecolor support if $COLORTERM is set', function()
    clear()
    exec_lua([[
      vim.api.nvim_create_autocmd('TermRequest', {
        callback = function(ev)
          local req = ev.data.sequence
          vim.g.termrequest = req
          local xtgettcap = req:match('^\027P%+q([%x;]+)$')
          if xtgettcap then
            local t = {}
            for cap in vim.gsplit(xtgettcap, ';') do
              local resp = string.format('\027P1+r%s\027\\', xtgettcap)
              vim.api.nvim_chan_send(vim.bo[ev.buf].channel, resp)
              t[vim.text.hexdecode(cap)] = true
            end
            vim.g.xtgettcap = t
            return true
          elseif req:match('^\027P$qm\027\\$') then
            vim.g.decrqss = true
          end
        end,
      })
    ]])

    local child_server = new_pipename()
    screen = tt.setup_child_nvim({
      '--clean',
      '--listen',
      child_server,
    }, {
      env = {
        VIMRUNTIME = os.getenv('VIMRUNTIME'),
        -- With COLORTERM=256, Nvim should not query the terminal and should not set 'tgc'
        COLORTERM = '256',
        TERM = 'xterm-256colors',
      },
    })

    screen:expect({ any = '%[No Name%]' })

    local child_session = n.connect(child_server)
    retry(nil, 1000, function()
      local xtgettcap = eval("get(g:, 'xtgettcap', {})")
      eq(nil, xtgettcap['Tc'])
      eq(nil, xtgettcap['RGB'])
      eq(nil, xtgettcap['setrgbf'])
      eq(nil, xtgettcap['setrgbb'])
      eq(0, eval([[get(g:, 'decrqss')]]))
      eq({ true, 0 }, { child_session:request('nvim_eval', '&termguicolors') })
    end)
  end)

  it('queries the terminal for OSC 52 support with XTGETTCAP', function()
    t.skip(is_os('win'), 'FIXME: does not work on Windows')
    clear()
    if not exec_lua('return pcall(require, "ffi")') then
      pending('N/A: missing LuaJIT FFI')
    end

    -- Change vterm's DA1 response so that it doesn't include 52
    exec_lua(function()
      local ffi = require('ffi')
      ffi.cdef [[
        extern char vterm_primary_device_attr[]
      ]]

      ffi.copy(ffi.C.vterm_primary_device_attr, '61;22')
    end)

    exec_lua([[
      _G.query = false
      vim.api.nvim_create_autocmd('TermRequest', {
        callback = function(ev)
          local req = ev.data.sequence
          local sequence = req:match('^\027P%+q([%x;]+)$')
          if sequence and vim.text.hexdecode(sequence) == 'Ms' then
            local resp = string.format('\027P1+r%s=%s\027\\', sequence, vim.text.hexencode('\027]52;;\027\\'))
            vim.api.nvim_chan_send(vim.bo[ev.buf].channel, resp)
            _G.query = true
            return true
          end
        end,
      })
    ]])

    local child_server = new_pipename()
    screen = tt.setup_child_nvim({
      '--listen',
      child_server,
      '--clean',
    }, {
      env = {
        VIMRUNTIME = os.getenv('VIMRUNTIME'),
      },
    })

    screen:expect({ any = '%[No Name%]' })

    local child_session = n.connect(child_server)
    retry(nil, 1000, function()
      eq({ true, { osc52 = true } }, { child_session:request('nvim_eval', 'g:termfeatures') })
    end)
    eq(true, exec_lua([[return _G.query]]))

    -- Attach another (non-TUI) UI to the child instance
    local alt = Screen.new(nil, nil, nil, child_session)
    finally(function()
      alt:detach()
      -- Avoid a dangling process after :detach.
      child_session:request('nvim_command', 'qall!')
    end)

    -- Detach the first (primary) client so only the second UI is attached
    feed_data(':detach\n')

    alt:expect({ any = '%[No Name%]' })

    -- osc52 should be cleared from termfeatures
    eq({ true, {} }, { child_session:request('nvim_eval', 'g:termfeatures') })
  end)

  it('determines OSC 52 support from DA1 response', function()
    t.skip(is_os('win'), 'FIXME: does not work on Windows')
    clear()
    exec_lua([[
      -- Check that we do not emit an XTGETTCAP request when DA1 indicates support
      _G.query = false
      vim.api.nvim_create_autocmd('TermRequest', {
        callback = function(ev)
          local req = ev.data.sequence
          local sequence = req:match('^\027P%+q([%x;]+)$')
          if sequence and vim.text.hexdecode(sequence) == 'Ms' then
            _G.query = true
            return true
          end
        end,
      })
    ]])

    local child_server = new_pipename()
    screen = tt.setup_child_nvim({
      '--listen',
      child_server,
      '--clean',
    }, {
      env = {
        VIMRUNTIME = os.getenv('VIMRUNTIME'),
      },
    })

    screen:expect({ any = '%[No Name%]' })

    local child_session = n.connect(child_server)
    retry(nil, 1000, function()
      eq({ true, { osc52 = true } }, { child_session:request('nvim_eval', 'g:termfeatures') })
    end)
    eq(false, exec_lua([[return _G.query]]))
  end)

  it('does not query the terminal for OSC 52 support when disabled', function()
    clear()
    exec_lua([[
      _G.query = false
      vim.api.nvim_create_autocmd('TermRequest', {
        callback = function(ev)
          local req = ev.data.sequence
          local sequence = req:match('^\027P%+q([%x;]+)$')
          if sequence and vim.text.hexdecode(sequence) == 'Ms' then
            _G.query = true
            return true
          end
        end,
      })
    ]])

    local child_server = new_pipename()
    screen = tt.setup_child_nvim({
      '--listen',
      child_server,
      '--clean',
      '--cmd',
      'let g:termfeatures = #{osc52: v:false}',
    }, {
      env = {
        VIMRUNTIME = os.getenv('VIMRUNTIME'),
      },
    })

    screen:expect({ any = '%[No Name%]' })

    local child_session = n.connect(child_server)
    eq({ true, { osc52 = false } }, { child_session:request('nvim_eval', 'g:termfeatures') })
    eq(false, exec_lua([[return _G.query]]))
  end)
end)

describe('TUI bg color', function()
  if t.skip(is_os('win')) then
    return
  end

  before_each(clear)

  it('is properly set in a nested Nvim instance when background=dark', function()
    command('highlight clear Normal')
    command('set background=dark') -- set outer Nvim background
    local child_server = new_pipename()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--listen',
      child_server,
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noswapfile',
    })
    screen:expect({ any = '%[No Name%]' })
    local child_session = n.connect(child_server)
    retry(nil, nil, function()
      eq({ true, 'dark' }, { child_session:request('nvim_eval', '&background') })
    end)
  end)

  it('is properly set in a nested Nvim instance when background=light', function()
    command('highlight clear Normal')
    command('set background=light') -- set outer Nvim background
    local child_server = new_pipename()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--listen',
      child_server,
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noswapfile',
    })
    screen:expect({ any = '%[No Name%]' })
    local child_session = n.connect(child_server)
    retry(nil, nil, function()
      eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') })
    end)
  end)

  it('queries the terminal for background color', function()
    exec_lua([[
      vim.api.nvim_create_autocmd('TermRequest', {
        callback = function(ev)
          local req = ev.data.sequence
          if req == '\027]11;?' then
            vim.g.oscrequest = true
            return true
          end
        end,
      })
    ]])
    tt.setup_child_nvim({
      '--clean',
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noswapfile',
    })
    retry(nil, 1000, function()
      eq(true, eval("get(g:, 'oscrequest', v:false)"))
    end)
  end)

  it('does not trigger OptionSet from automatic background processing', function()
    command('set background=light')
    local child_server = new_pipename()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--listen',
      child_server,
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noswapfile',
      '-c',
      [[let g:background_optionset = 0]],
      '-c',
      [[autocmd OptionSet background let g:background_optionset += 1]],
    })
    screen:expect({ any = '%[No Name%]' })
    local child_session = n.connect(child_server)
    retry(nil, nil, function()
      eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') })
    end)
    eq({ true, 0 }, { child_session:request('nvim_eval', 'g:background_optionset') })
  end)

  it('sends theme update notifications when background changes #31652', function()
    command('set background=dark') -- set outer Nvim background
    local child_server = new_pipename()
    local screen = tt.setup_child_nvim({
      '--clean',
      '--listen',
      child_server,
      '--cmd',
      'colorscheme vim',
      '--cmd',
      'set noswapfile',
    })
    screen:expect({ any = '%[No Name%]' })
    local child_session = n.connect(child_server)
    retry(nil, nil, function()
      eq({ true, 'dark' }, { child_session:request('nvim_eval', '&background') })
    end)
    command('set background=light') -- set outer Nvim background
    retry(nil, nil, function()
      eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') })
    end)
  end)
end)

describe('TUI client', function()
  local function start_tui_and_remote_client()
    local server_super = n.clear()
    local client_super = n.new_session(true)
    finally(function()
      client_super:close()
      server_super:close()
    end)

    local server_pipe = new_pipename()
    local screen_server = tt.setup_child_nvim({
      '--clean',
      '--listen',
      server_pipe,
      '--cmd',
      'colorscheme vim',
      '--cmd',
      nvim_set .. ' laststatus=2 background=dark',
    }, { env = env_notermguicolors })
    screen_server:expect([[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    feed_data('iHello, World')
    screen_server:expect([[
      Hello, World^                                      |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027')
    local s0 = [[
      Hello, Worl^d                                      |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen_server:expect(s0)

    feed_data(':echo "GUI Running: " .. has("gui_running")\013')
    screen_server:expect({ any = 'GUI Running: 0' })

    set_session(client_super)
    local screen_client = tt.setup_child_nvim({
      '--remote-ui',
      '--server',
      server_pipe,
    }, { env = env_notermguicolors })
    screen_client:expect(s0)

    return server_super, screen_server, screen_client
  end

  it('connects to remote instance (with its own TUI)', function()
    local _, screen_server, screen_client = start_tui_and_remote_client()

    feed_data(':echo "GUI Running: " .. has("gui_running")\013')
    screen_client:expect({ any = 'GUI Running: 0' })

    -- grid smaller than containing terminal window is cleared properly
    feed_data(":call setline(1,['a'->repeat(&columns)]->repeat(&lines))\n")
    feed_data('0:set lines=3\n')
    local s1 = [[
      ^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
      {3:[No Name] [+]                                     }|
                                                        |*4
      {5:-- TERMINAL --}                                    |
    ]]
    screen_client:expect(s1)
    screen_server:expect(s1)
  end)

  it(':restart works when connecting to remote instance (with its own TUI)', function()
    local _, screen_server, screen_client = start_tui_and_remote_client()

    -- Both clients should attach to the new server.
    feed_data(':restart +qall!\n')
    local screen_restarted = [[
      ^                                                  |
      {100:~                                                 }|*3
      {3:[No Name]                                         }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen_client:expect(screen_restarted)
    screen_server:expect(screen_restarted)

    feed_data(':echo "GUI Running: " .. has("gui_running")\013')
    screen_client:expect({ any = 'GUI Running: 0' })

    -- The :vsplit command should only be executed once.
    feed_data(':restart vsplit\r')
    screen_restarted = [[
      ^                         │                        |
      {100:~                        }│{100:~                       }|*3
      {3:[No Name]                 }{2:[No Name]               }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    screen_client:expect(screen_restarted)
    screen_server:expect(screen_restarted)

    feed_data(':qall!\r')
    screen_client:expect({ any = vim.pesc('[Process exited 0]') })
    screen_server:expect({ any = vim.pesc('[Process exited 0]') })
  end)

  local function start_headless_server_and_client(use_testlog)
    local server = n.new_session(false, {
      args_rm = { '--cmd' },
      args = {
        '--cmd',
        'colorscheme vim',
        '--cmd',
        nvim_set .. ' notermguicolors background=dark',
      },
    })
    local client_super =
      n.new_session(true, use_testlog and { env = { NVIM_LOG_FILE = testlog } } or {})
    finally(function()
      client_super:close()
      server:close()
      os.remove(testlog)
    end)

    set_session(server)
    --- @type string
    local server_pipe = api.nvim_get_vvar('servername')
    server:request('nvim_input', 'iHalloj!<Esc>')

    set_session(client_super)
    local screen_client = tt.setup_child_nvim({
      '--remote-ui',
      '--server',
      server_pipe,
    }, { env = env_notermguicolors })
    screen_client:expect([[
      Halloj^!                                           |
      {100:~                                                 }|*4
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])

    return server, server_pipe, screen_client
  end

  it('connects to remote instance (--headless)', function()
    local server, server_pipe, screen_client = start_headless_server_and_client(false)

    -- No heap-use-after-free when receiving UI events after deadly signal #22184
    server:request('nvim_input', ('a'):rep(1000))
    exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]])
    screen_client:expect(is_os('win') and { any = '%[Process exited 1%]' } or [[
      Nvim: Caught deadly signal 'SIGTERM'              |
      ^                                                  |
      [Process exited 1]                                |
                                                        |*3
      {5:-- TERMINAL --}                                    |
    ]])

    eq(0, api.nvim_get_vvar('shell_error'))
    -- exits on input eof #22244
    -- Use system() without input so that stdin is closed.
    fn.system({ nvim_prog, '--remote-ui', '--server', server_pipe })
    eq(1, api.nvim_get_vvar('shell_error'))

    command('bwipe!')
    -- Start another remote client to attach to the same server.
    fn.jobstart({ nvim_prog, '--remote-ui', '--server', server_pipe }, { term = true })
    command('startinsert')
    screen_client:expect([[
      {100:<<<}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|*3
      aaaaaa^                                            |
      {5:-- INSERT --}                                      |
      {5:-- TERMINAL --}                                    |
    ]])
    feed_data('\027')

    feed_data(':echo "GUI Running: " .. has("gui_running")\013')
    screen_client:expect({ any = 'GUI Running: 0' })
  end)

  it(':restart works when connecting to remote instance (--headless)', function()
    local _, server_pipe, screen_client = start_headless_server_and_client(false)

    -- The client should attach to the new server and the original server should exit.
    feed_data(':restart +qall!\n')
    screen_client:expect([[
      ^                                                  |
      {100:~                                                 }|*4
                                                        |
      {5:-- TERMINAL --}                                    |
    ]])
    retry(nil, nil, function()
      eq(nil, vim.uv.fs_stat(server_pipe))
    end)

    feed_data(':echo "GUI Running: " .. has("gui_running")\013')
    screen_client:expect({ any = 'GUI Running: 0' })

    feed_data(':q!\r')
    screen_client:expect({ any = vim.pesc('[Process exited 0]') })
  end)

  local ffi_str_defs = [[
    local ffi = require('ffi')
    local cstr = ffi.typeof('char[?]')
    ffi.cdef('typedef struct { char *data; size_t size; } String;')
    local function to_api_string(str)
      return ffi.new('String', { data = cstr(#str + 1, str), size = #str })
    end
  ]]

  it('does not crash or hang with a very long title', function()
    local server, _, screen_client = start_headless_server_and_client(true)
    local server_exec_lua = tt.make_lua_executor(server)
    if not server_exec_lua('return pcall(require, "ffi")') then
      pending('N/A: missing LuaJIT FFI')
    end

    server:request('nvim_set_option_value', 'titlestring', '%t%( %M%) - Nvim', {})
    local bufname = api.nvim_buf_get_name(0)
    local old_title = api.nvim_buf_get_var(0, 'term_title')
    if not is_os('win') then
      eq(bufname, old_title)
    end
    -- Normally a title cannot be longer than the 65535-byte buffer as maketitle()
    -- limits it length. Use FFI to send a very long title directly.
    server_exec_lua(ffi_str_defs .. [[
      ffi.cdef('void ui_call_set_title(String title);')
      ffi.C.ui_call_set_title(to_api_string(('a'):rep(65536)))
    ]])
    screen_client:expect_unchanged()
    assert_log('set_title: title string too long!', testlog)
    eq(old_title, api.nvim_buf_get_var(0, 'term_title'))

    -- Following escape sequences are not affected.
    server:request('nvim_set_option_value', 'title', true, {})
    retry(nil, nil, function()
      eq('[No Name] + - Nvim', api.nvim_buf_get_var(0, 'term_title'))
    end)
  end)

  it('logs chdir failure properly', function()
    t.skip(is_os('win'), 'N/A for Windows')
    local server, _, screen_client = start_headless_server_and_client(true)
    local server_exec_lua = tt.make_lua_executor(server)
    if not server_exec_lua('return pcall(require, "ffi")') then
      pending('N/A: missing LuaJIT FFI')
    end

    -- Use FFI to send a chdir event to a non-directory path.
    server_exec_lua(ffi_str_defs .. [[
      ffi.cdef('void ui_call_chdir(String path);')
      ffi.C.ui_call_chdir(to_api_string('README.md'))
    ]])
    screen_client:expect_unchanged()
    assert_log('Failed to chdir to README%.md: not a directory', testlog)
  end)

  it('nvim_ui_send works with remote client #36317', function()
    local server, _, _ = start_headless_server_and_client(false)
    server:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\')
    retry(nil, nil, function()
      eq('TEST_TITLE', api.nvim_buf_get_var(0, 'term_title'))
    end)
  end)

  it('throws error when no server exists', function()
    clear()
    local screen = tt.setup_child_nvim({
      '--remote-ui',
      '--server',
      '127.0.0.1:2436546',
    }, { cols = 60 })

    screen:expect([[
      Remote ui failed to start: {MATCH:.*}|
      ^                                                            |
      [Process exited 1]                                          |
                                                                  |*3
      {5:-- TERMINAL --}                                              |
    ]])
  end)

  local function test_remote_tui_quit(status)
    local server_super, screen_server, screen_client = start_tui_and_remote_client()

    -- quitting the server
    set_session(server_super)
    feed_data(status and ':' .. status .. 'cquit!\n' or ':quit!\n')
    status = status and status or 0
    screen_server:expect({ any = 'Process exited ' .. status })
    screen_client:expect({ any = 'Process exited ' .. status })
  end

  describe('exits when server quits', function()
    it('with :quit', function()
      test_remote_tui_quit()
    end)

    it('with :cquit', function()
      test_remote_tui_quit(42)
    end)
  end)

  it('suspend/resume works with multiple clients', function()
    t.skip(is_os('win'), 'N/A for Windows')
    local server_super, screen_server, screen_client = start_tui_and_remote_client()

    local screen_normal = [[
      Hello, Worl^d                                      |
      {100:~                                                 }|*3
      {3:[No Name] [+]                                     }|
                                                        |
      {5:-- TERMINAL --}                                    |
    ]]
    local screen_suspended = [[
                                                        |*5
      ^[Process suspended]                               |
      {5:-- TERMINAL --}                                    |
    ]]

    screen_client:expect({ grid = screen_normal, unchanged = true })
    screen_server:expect({ grid = screen_normal, unchanged = true })

    -- Suspend both clients.
    feed_data(':suspend\r')
    screen_client:expect({ grid = screen_suspended })
    screen_server:expect({ grid = screen_suspended })

    -- Resume the remote client.
    n.feed('<Space>')
    screen_client:expect({ grid = screen_normal })
    screen_server:expect({ grid = screen_suspended, unchanged = true })

    -- Resume the embedding client.
    server_super:request('nvim_input', '<Space>')
    screen_server:expect({ grid = screen_normal })
    screen_client:expect({ grid = screen_normal, unchanged = true })

    -- Suspend both clients again.
    feed_data(':suspend\r')
    screen_client:expect({ grid = screen_suspended })
    screen_server:expect({ grid = screen_suspended })

    -- Resume the remote client.
    n.feed('<Space>')
    screen_client:expect({ grid = screen_normal })
    screen_server:expect({ grid = screen_suspended, unchanged = true })

    -- Suspend the remote client again.
    feed_data(':suspend\r')
    screen_client:expect({ grid = screen_suspended })
    screen_server:expect({ grid = screen_suspended, unchanged = true })

    -- Resume the embedding client.
    server_super:request('nvim_input', '<Space>')
    screen_server:expect({ grid = screen_normal })
    screen_client:expect({ grid = screen_suspended, unchanged = true })

    -- Resume the remote client.
    n.feed('<Space>')
    screen_client:expect({ grid = screen_normal })
    screen_server:expect({ grid = screen_normal, unchanged = true })

    feed_data(':quit!\r')
    screen_server:expect({ any = vim.pesc('[Process exited 0]') })
    screen_client:expect({ any = vim.pesc('[Process exited 0]') })
  end)
end)
