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

local t_lsp = require('test.functional.plugin.lsp.testutil')

local assert_log = t.assert_log
local buf_lines = n.buf_lines
local clear = n.clear
local command = n.command
local dedent = t.dedent
local exec_lua = n.exec_lua
local eq = t.eq
local eval = n.eval
local matches = t.matches
local pcall_err = t.pcall_err
local pesc = vim.pesc
local insert = n.insert
local fn = n.fn
local retry = t.retry
local stop = n.stop
local NIL = vim.NIL
local read_file = t.read_file
local write_file = t.write_file
local is_ci = t.is_ci
local api = n.api
local is_os = t.is_os
local skip = t.skip
local mkdir = t.mkdir
local tmpname = t.tmpname

local clear_notrace = t_lsp.clear_notrace
local create_server_definition = t_lsp.create_server_definition
local fake_lsp_code = t_lsp.fake_lsp_code
local fake_lsp_logfile = t_lsp.fake_lsp_logfile
local test_rpc_server = t_lsp.test_rpc_server

local function get_buf_option(name, bufnr)
  bufnr = bufnr or 'BUFFER'
  return exec_lua(
    string.format("return vim.api.nvim_get_option_value('%s', { buf = %s })", name, bufnr)
  )
end

-- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837
if skip(is_os('win')) then
  return
end

teardown(function()
  os.remove(fake_lsp_logfile)
end)

describe('LSP', function()
  before_each(function()
    clear_notrace()

    -- Run an instance of nvim on the file which contains our "scripts".
    -- Pass TEST_NAME to pick the script.
    local test_name = 'basic_init'
    exec_lua(
      [=[
      lsp = require('vim.lsp')
      local test_name, fake_lsp_code, fake_lsp_logfile = ...
      function test__start_client()
        return lsp.start_client {
          cmd_env = {
            NVIM_LOG_FILE = fake_lsp_logfile;
            NVIM_APPNAME = "nvim_lsp_test";
          };
          cmd = {
            vim.v.progpath, '-l', fake_lsp_code, test_name;
          };
          workspace_folders = {{
              uri = 'file://' .. vim.uv.cwd(),
              name = 'test_folder',
          }};
        }
      end
      TEST_CLIENT1 = test__start_client()
    ]=],
      test_name,
      fake_lsp_code,
      fake_lsp_logfile
    )
  end)

  after_each(function()
    api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
    -- exec_lua("lsp.stop_all_clients(true)")
  end)

  describe('server_name specified', function()
    it('start_client(), stop_client()', function()
      retry(nil, 4000, function()
        eq(1, exec_lua('return #lsp.get_clients()'))
      end)
      eq(
        2,
        exec_lua([[
        TEST_CLIENT2 = test__start_client()
        return TEST_CLIENT2
      ]])
      )
      eq(
        3,
        exec_lua([[
        TEST_CLIENT3 = test__start_client()
        return TEST_CLIENT3
      ]])
      )
      retry(nil, 4000, function()
        eq(3, exec_lua('return #lsp.get_clients()'))
      end)

      eq(false, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1) == nil'))
      eq(false, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1).is_stopped()'))
      exec_lua('return lsp.get_client_by_id(TEST_CLIENT1).stop()')
      retry(nil, 4000, function()
        eq(2, exec_lua('return #lsp.get_clients()'))
      end)
      eq(true, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1) == nil'))

      exec_lua('lsp.stop_client({TEST_CLIENT2, TEST_CLIENT3})')
      retry(nil, 4000, function()
        eq(0, exec_lua('return #lsp.get_clients()'))
      end)
    end)

    it('stop_client() also works on client objects', function()
      exec_lua([[
        TEST_CLIENT2 = test__start_client()
        TEST_CLIENT3 = test__start_client()
      ]])
      retry(nil, 4000, function()
        eq(3, exec_lua('return #lsp.get_clients()'))
      end)
      -- Stop all clients.
      exec_lua('lsp.stop_client(lsp.get_clients())')
      retry(nil, 4000, function()
        eq(0, exec_lua('return #lsp.get_clients()'))
      end)
    end)
  end)
end)

describe('LSP', function()
  describe('basic_init test', function()
    after_each(function()
      stop()
      exec_lua('lsp.stop_client(lsp.get_clients(), true)')
      api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
    end)

    it('should run correctly', function()
      local expected_handlers = {
        { NIL, {}, { method = 'test', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'basic_init',
        on_init = function(client, _)
          -- client is a dummy object which will queue up commands to be run
          -- once the server initializes. It can't accept lua callbacks or
          -- other types that may be unserializable for now.
          client.stop()
        end,
        -- If the program timed out, then code will be nil.
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        -- Note that NIL must be used here.
        -- on_handler(err, method, result, client_id)
        on_handler = function(...)
          eq(table.remove(expected_handlers), { ... })
        end,
      }
    end)

    it('should fail', function()
      local expected_handlers = {
        { NIL, {}, { method = 'test', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'basic_init',
        on_init = function(client)
          client.notify('test')
          client.stop()
        end,
        on_exit = function(code, signal)
          eq(101, code, 'exit code') -- See fake-lsp-server.lua
          eq(0, signal, 'exit signal')
          assert_log(
            pesc([[assert_eq failed: left == "\"shutdown\"", right == "\"test\""]]),
            fake_lsp_logfile
          )
        end,
        on_handler = function(...)
          eq(table.remove(expected_handlers), { ... }, 'expected handler')
        end,
      }
    end)

    it('should send didChangeConfiguration after initialize if there are settings', function()
      test_rpc_server({
        test_name = 'basic_init_did_change_configuration',
        on_init = function(client, _)
          client.stop()
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        settings = {
          dummy = 1,
        },
      })
    end)

    it(
      "should set the client's offset_encoding when positionEncoding capability is supported",
      function()
        clear()
        exec_lua(create_server_definition)
        local result = exec_lua([[
        local server = _create_server({
          capabilities = {
            positionEncoding = "utf-8"
          },
        })

        local client_id = vim.lsp.start({
          name = 'dummy',
          cmd = server.cmd,
        })

        if not client_id then
          return 'vim.lsp.start did not return client_id'
        end

        local client = vim.lsp.get_client_by_id(client_id)
        if not client then
          return 'No client found with id ' .. client_id
        end
        return client.offset_encoding
      ]])
        eq('utf-8', result)
      end
    )

    it('should succeed with manual shutdown', function()
      if is_ci() then
        pending('hangs the build on CI #14028, re-enable with freeze timeout #14204')
        return
      elseif t.skip_fragile(pending) then
        return
      end
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1 } },
        { NIL, {}, { method = 'test', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'basic_init',
        on_init = function(client)
          eq(0, client.server_capabilities().textDocumentSync.change)
          client.request('shutdown')
          client.notify('exit')
          client.stop()
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(...)
          eq(table.remove(expected_handlers), { ... }, 'expected handler')
        end,
      }
    end)

    it('should detach buffer in response to nvim_buf_detach', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_finish',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
          ]]
          eq(true, exec_lua('return lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)'))
          eq(true, exec_lua('return lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID)'))
          exec_lua [[
            vim.api.nvim_command(BUFFER.."bwipeout")
          ]]
        end,
        on_init = function(_client)
          client = _client
          client.notify('finish')
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            exec_lua('return lsp.buf_detach_client(BUFFER, TEST_RPC_CLIENT_ID)')
            eq(false, exec_lua('return lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID)'))
            client.stop()
          end
        end,
      }
    end)

    it('should fire autocommands on attach and detach', function()
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_init',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_create_autocmd('LspAttach', {
              callback = function(args)
                local client = vim.lsp.get_client_by_id(args.data.client_id)
                vim.g.lsp_attached = client.name
              end,
            })
            vim.api.nvim_create_autocmd('LspDetach', {
              callback = function(args)
                local client = vim.lsp.get_client_by_id(args.data.client_id)
                vim.g.lsp_detached = client.name
              end,
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          eq(true, exec_lua('return lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)'))
          client.notify('finish')
        end,
        on_handler = function(_, _, ctx)
          if ctx.method == 'finish' then
            eq('basic_init', api.nvim_get_var('lsp_attached'))
            exec_lua('return lsp.buf_detach_client(BUFFER, TEST_RPC_CLIENT_ID)')
            eq('basic_init', api.nvim_get_var('lsp_detached'))
            client.stop()
          end
        end,
      }
    end)

    it('should set default options on attach', function()
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'set_defaults_all_capabilities',
        on_init = function(_client)
          client = _client
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
          ]]
        end,
        on_handler = function(_, _, ctx)
          if ctx.method == 'test' then
            eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc'))
            eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc'))
            eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr'))
            eq('', get_buf_option('keywordprg'))
            eq(
              true,
              exec_lua [[
              local keymap
              vim.api.nvim_buf_call(BUFFER, function()
                keymap = vim.fn.maparg("K", "n", false, true)
              end)
              return keymap.callback == vim.lsp.buf.hover
            ]]
            )
            client.stop()
          end
        end,
        on_exit = function(_, _)
          eq('', get_buf_option('tagfunc'))
          eq('', get_buf_option('omnifunc'))
          eq('', get_buf_option('formatexpr'))
          eq(
            '',
            exec_lua [[
            local keymap
            vim.api.nvim_buf_call(BUFFER, function()
              keymap = vim.fn.maparg("K", "n", false, false)
            end)
            return keymap
          ]]
          )
        end,
      }
    end)

    it('should overwrite options set by ftplugins', function()
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'set_defaults_all_capabilities',
        on_init = function(_client)
          client = _client
          exec_lua [[
            vim.api.nvim_command('filetype plugin on')
            BUFFER_1 = vim.api.nvim_create_buf(false, true)
            BUFFER_2 = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_set_option_value('filetype', 'man', { buf = BUFFER_1 })
            vim.api.nvim_set_option_value('filetype', 'xml', { buf = BUFFER_2 })
          ]]

          -- Sanity check to ensure that some values are set after setting filetype.
          eq("v:lua.require'man'.goto_tag", get_buf_option('tagfunc', 'BUFFER_1'))
          eq('xmlcomplete#CompleteTags', get_buf_option('omnifunc', 'BUFFER_2'))
          eq('xmlformat#Format()', get_buf_option('formatexpr', 'BUFFER_2'))

          exec_lua [[
            lsp.buf_attach_client(BUFFER_1, TEST_RPC_CLIENT_ID)
            lsp.buf_attach_client(BUFFER_2, TEST_RPC_CLIENT_ID)
          ]]
        end,
        on_handler = function(_, _, ctx)
          if ctx.method == 'test' then
            eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc', 'BUFFER_1'))
            eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc', 'BUFFER_2'))
            eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr', 'BUFFER_2'))
            client.stop()
          end
        end,
        on_exit = function(_, _)
          eq('', get_buf_option('tagfunc', 'BUFFER_1'))
          eq('', get_buf_option('omnifunc', 'BUFFER_2'))
          eq('', get_buf_option('formatexpr', 'BUFFER_2'))
        end,
      }
    end)

    it('should not overwrite user-defined options', function()
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'set_defaults_all_capabilities',
        on_init = function(_client)
          client = _client
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_set_option_value('tagfunc', 'tfu', { buf = BUFFER })
            vim.api.nvim_set_option_value('omnifunc', 'ofu', { buf = BUFFER })
            vim.api.nvim_set_option_value('formatexpr', 'fex', { buf = BUFFER })
            lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
          ]]
        end,
        on_handler = function(_, _, ctx)
          if ctx.method == 'test' then
            eq('tfu', get_buf_option('tagfunc'))
            eq('ofu', get_buf_option('omnifunc'))
            eq('fex', get_buf_option('formatexpr'))
            client.stop()
          end
        end,
        on_exit = function(_, _)
          eq('tfu', get_buf_option('tagfunc'))
          eq('ofu', get_buf_option('omnifunc'))
          eq('fex', get_buf_option('formatexpr'))
        end,
      }
    end)

    it('should detach buffer on bufwipe', function()
      clear()
      exec_lua(create_server_definition)
      local result = exec_lua([[
        local server = _create_server()
        local bufnr = vim.api.nvim_create_buf(false, true)
        vim.api.nvim_set_current_buf(bufnr)
        local detach_called = false
        vim.api.nvim_create_autocmd("LspDetach", {
          callback = function()
            detach_called = true
          end
        })
        local client_id = vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd })
        assert(client_id, "lsp.start must return client_id")
        local client = vim.lsp.get_client_by_id(client_id)
        local num_attached_before = vim.tbl_count(client.attached_buffers)
        vim.api.nvim_buf_delete(bufnr, { force = true })
        local num_attached_after = vim.tbl_count(client.attached_buffers)
        return {
          bufnr = bufnr,
          client_id = client_id,
          num_attached_before = num_attached_before,
          num_attached_after = num_attached_after,
          detach_called = detach_called,
        }
      ]])
      eq(true, result ~= nil, 'exec_lua must return result')
      eq(1, result.num_attached_before)
      eq(0, result.num_attached_after)
      eq(true, result.detach_called)
    end)

    it('should not re-attach buffer if it was deleted in on_init #28575', function()
      clear()
      exec_lua(create_server_definition)
      exec_lua([[
        local server = _create_server({
          handlers = {
            initialize = function(method, params, callback)
              vim.schedule(function()
                callback(nil, { capabilities = {} })
              end)
            end
          }
        })
        local bufnr = vim.api.nvim_create_buf(false, true)
        local on_init_called = false
        local client_id = vim.lsp.start({
          name = 'detach-dummy',
          cmd = server.cmd,
          on_init = function()
            vim.api.nvim_buf_delete(bufnr, {})
            on_init_called = true
          end
        })
        vim.lsp.buf_attach_client(bufnr, client_id)
        local ok = vim.wait(1000, function() return on_init_called end)
        assert(ok, "on_init was not called")
      ]])
    end)

    it('should allow on_lines + nvim_buf_delete during LSP initialization #28575', function()
      clear()
      exec_lua(create_server_definition)
      exec_lua([[
        local initialized = false
        local server = _create_server({
          handlers = {
            initialize = function(method, params, callback)
              vim.schedule(function()
                callback(nil, { capabilities = {} })
                initialized = true
              end)
            end
          }
        })
        local bufnr = vim.api.nvim_create_buf(false, true)
        vim.api.nvim_set_current_buf(bufnr)
        local client_id = vim.lsp.start({
          name = 'detach-dummy',
          cmd = server.cmd,
        })
        vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {"hello"})
        vim.api.nvim_buf_delete(bufnr, {})
        local ok = vim.wait(1000, function() return initialized end)
        assert(ok, "lsp did not initialize")
      ]])
    end)

    it('client should return settings via workspace/configuration handler', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        {
          NIL,
          {
            items = {
              { section = 'testSetting1' },
              { section = 'testSetting2' },
              { section = 'test.Setting3' },
              { section = 'test.Setting4' },
            },
          },
          { method = 'workspace/configuration', client_id = 1 },
        },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_workspace_configuration',
        on_init = function(_client)
          client = _client
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'start' then
            exec_lua([=[
              local client = vim.lsp.get_client_by_id(TEST_RPC_CLIENT_ID)
              client.settings = {
                testSetting1 = true;
                testSetting2 = false;
                test = {Setting3 = 'nested' };
            }]=])
          end
          if ctx.method == 'workspace/configuration' then
            local server_result = exec_lua(
              [=[
              local method, params = ...
              return require'vim.lsp.handlers'['workspace/configuration'](err, params, {method=method, client_id=TEST_RPC_CLIENT_ID})]=],
              ctx.method,
              result
            )
            client.notify('workspace/configuration', server_result)
          end
          if ctx.method == 'shutdown' then
            client.stop()
          end
        end,
      }
    end)
    it(
      'workspace/configuration returns NIL per section if client was started without config.settings',
      function()
        local result = nil
        test_rpc_server {
          test_name = 'basic_init',
          on_init = function(c)
            c.stop()
          end,
          on_setup = function()
            result = exec_lua [[
            local result = {
              items = {
                {section = 'foo'},
                {section = 'bar'},
              }
            }
            return vim.lsp.handlers['workspace/configuration'](nil, result, {client_id=TEST_RPC_CLIENT_ID})
          ]]
          end,
        }
        eq({ NIL, NIL }, result)
      end
    )

    it('should verify capabilities sent', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'basic_check_capabilities',
        on_init = function(client)
          client.stop()
          local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(full_kind, client.server_capabilities().textDocumentSync.change)
          eq({ includeText = false }, client.server_capabilities().textDocumentSync.save)
          eq(false, client.server_capabilities().codeLensProvider)
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(...)
          eq(table.remove(expected_handlers), { ... }, 'expected handler')
        end,
      }
    end)

    it('BufWritePost sends didSave with bool textDocumentSync.save', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'text_document_sync_save_bool',
        on_init = function(c)
          client = c
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'start' then
            exec_lua([=[
              BUFFER = vim.api.nvim_get_current_buf()
              lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
              vim.api.nvim_exec_autocmds('BufWritePost', { buffer = BUFFER, modeline = false })
            ]=])
          else
            client.stop()
          end
        end,
      }
    end)

    it('BufWritePre does not send notifications if server lacks willSave capabilities', function()
      clear()
      exec_lua(create_server_definition)
      local messages = exec_lua([[
        local server = _create_server({
          capabilities = {
            textDocumentSync = {
              willSave = false,
              willSaveWaitUntil = false,
            }
          },
        })
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        local buf = vim.api.nvim_get_current_buf()
        vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false })
        vim.lsp.stop_client(client_id)
        return server.messages
      ]])
      eq(4, #messages)
      eq('initialize', messages[1].method)
      eq('initialized', messages[2].method)
      eq('shutdown', messages[3].method)
      eq('exit', messages[4].method)
    end)

    it('BufWritePre sends willSave / willSaveWaitUntil, applies textEdits', function()
      clear()
      exec_lua(create_server_definition)
      local result = exec_lua([[
        local server = _create_server({
          capabilities = {
            textDocumentSync = {
              willSave = true,
              willSaveWaitUntil = true,
            }
          },
          handlers = {
            ['textDocument/willSaveWaitUntil'] = function(_, _, callback)
              local text_edit = {
                range = {
                  start = { line = 0, character = 0 },
                  ['end'] = { line = 0, character = 0 },
                },
                newText = 'Hello'
              }
              callback(nil, { text_edit, })
            end
          },
        })
        local buf = vim.api.nvim_get_current_buf()
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false })
        vim.lsp.stop_client(client_id)
        return {
          messages = server.messages,
          lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true)
        }
      ]])
      local messages = result.messages
      eq('textDocument/willSave', messages[3].method)
      eq('textDocument/willSaveWaitUntil', messages[4].method)
      eq({ 'Hello' }, result.lines)
    end)

    it('saveas sends didOpen if filename changed', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server({
        test_name = 'text_document_save_did_open',
        on_init = function(c)
          client = c
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'start' then
            local tmpfile_old = tmpname()
            local tmpfile_new = tmpname()
            os.remove(tmpfile_new)
            exec_lua(
              [=[
              local oldname, newname = ...
              BUFFER = vim.api.nvim_get_current_buf()
              vim.api.nvim_buf_set_name(BUFFER, oldname)
              vim.api.nvim_buf_set_lines(BUFFER, 0, -1, true, {"help me"})
              lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
              vim.api.nvim_buf_call(BUFFER, function() vim.cmd('saveas ' .. newname) end)
            ]=],
              tmpfile_old,
              tmpfile_new
            )
          else
            client.stop()
          end
        end,
      })
    end)

    it('BufWritePost sends didSave including text if server capability is set', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'text_document_sync_save_includeText',
        on_init = function(c)
          client = c
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'start' then
            exec_lua([=[
              BUFFER = vim.api.nvim_get_current_buf()
              vim.api.nvim_buf_set_lines(BUFFER, 0, -1, true, {"help me"})
              lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
              vim.api.nvim_exec_autocmds('BufWritePost', { buffer = BUFFER, modeline = false })
            ]=])
          else
            client.stop()
          end
        end,
      }
    end)

    it('client.supports_methods() should validate capabilities', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'capabilities_for_client_supports_method',
        on_init = function(client)
          client.stop()
          local expected_sync_capabilities = {
            change = 1,
            openClose = true,
            save = { includeText = false },
            willSave = false,
            willSaveWaitUntil = false,
          }
          eq(expected_sync_capabilities, client.server_capabilities().textDocumentSync)
          eq(true, client.server_capabilities().completionProvider)
          eq(true, client.server_capabilities().hoverProvider)
          eq(false, client.server_capabilities().definitionProvider)
          eq(false, client.server_capabilities().renameProvider)
          eq(true, client.server_capabilities().codeLensProvider.resolveProvider)

          -- known methods for resolved capabilities
          eq(true, client.supports_method('textDocument/hover'))
          eq(false, client.supports_method('textDocument/definition'))

          -- unknown methods are assumed to be supported.
          eq(true, client.supports_method('unknown-method'))
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(...)
          eq(table.remove(expected_handlers), { ... }, 'expected handler')
        end,
      }
    end)

    it('should not call unsupported_method when trying to call an unsupported method', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'capabilities_for_client_supports_method',
        on_setup = function()
          exec_lua([=[
              BUFFER = vim.api.nvim_get_current_buf()
              lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
              vim.lsp.handlers['textDocument/typeDefinition'] = function() end
              vim.cmd(BUFFER.."bwipeout")
            ]=])
        end,
        on_init = function(client)
          client.stop()
          exec_lua('vim.lsp.buf.type_definition()')
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(...)
          eq(table.remove(expected_handlers), { ... }, 'expected handler')
        end,
      }
    end)

    it(
      'should not call unsupported_method when no client and trying to call an unsupported method',
      function()
        local expected_handlers = {
          { NIL, {}, { method = 'shutdown', client_id = 1 } },
        }
        test_rpc_server {
          test_name = 'capabilities_for_client_supports_method',
          on_setup = function()
            exec_lua([=[
              vim.lsp.handlers['textDocument/typeDefinition'] = function() end
            ]=])
          end,
          on_init = function(client)
            client.stop()
            exec_lua('vim.lsp.buf.type_definition()')
          end,
          on_exit = function(code, signal)
            eq(0, code, 'exit code')
            eq(0, signal, 'exit signal')
          end,
          on_handler = function(...)
            eq(table.remove(expected_handlers), { ... }, 'expected handler')
          end,
        }
      end
    )

    it('should not forward RequestCancelled to callback', function()
      local expected_handlers = {
        { NIL, {}, { method = 'finish', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_forward_request_cancelled',
        on_init = function(_client)
          _client.request('error_code_test')
          client = _client
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
          eq(0, #expected_handlers, 'did not call expected handler')
        end,
        on_handler = function(err, _, ctx)
          eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should forward ServerCancelled to callback', function()
      local expected_handlers = {
        { NIL, {}, { method = 'finish', client_id = 1 } },
        {
          { code = -32802 },
          NIL,
          { method = 'error_code_test', bufnr = 1, client_id = 1 },
        },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_forward_server_cancelled',
        on_init = function(_client)
          _client.request('error_code_test')
          client = _client
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
          eq(0, #expected_handlers, 'did not call expected handler')
        end,
        on_handler = function(err, _, ctx)
          eq(table.remove(expected_handlers), { err, _, ctx }, 'expected handler')
          if ctx.method ~= 'finish' then
            client.notify('finish')
          end
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should forward ContentModified to callback', function()
      local expected_handlers = {
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { { code = -32801 }, NIL, { method = 'error_code_test', bufnr = 1, client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_forward_content_modified',
        on_init = function(_client)
          _client.request('error_code_test')
          client = _client
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
          eq(0, #expected_handlers, 'did not call expected handler')
        end,
        on_handler = function(err, _, ctx)
          eq(table.remove(expected_handlers), { err, _, ctx }, 'expected handler')
          if ctx.method ~= 'finish' then
            client.notify('finish')
          end
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should track pending requests to the language server', function()
      local expected_handlers = {
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'slow_request', bufnr = 1, client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_pending_request_tracked',
        on_init = function(_client)
          client = _client
          client.request('slow_request')
          local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
          eq('slow_request', request.method)
          eq('pending', request.type)
          client.notify('release')
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
          eq(0, #expected_handlers, 'did not call expected handler')
        end,
        on_handler = function(err, _, ctx)
          eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
          if ctx.method == 'slow_request' then
            local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
            eq(NIL, request)
            client.notify('finish')
          end
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should track cancel requests to the language server', function()
      local expected_handlers = {
        { NIL, {}, { method = 'finish', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_cancel_request_tracked',
        on_init = function(_client)
          client = _client
          client.request('slow_request')
          client.cancel_request(2)
          local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
          eq('slow_request', request.method)
          eq('cancel', request.type)
          client.notify('release')
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
          eq(0, #expected_handlers, 'did not call expected handler')
        end,
        on_handler = function(err, _, ctx)
          eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
          local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
          eq(NIL, request)
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should clear pending and cancel requests on reply', function()
      local expected_handlers = {
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'slow_request', bufnr = 1, client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_tracked_requests_cleared',
        on_init = function(_client)
          client = _client
          client.request('slow_request')
          local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
          eq('slow_request', request.method)
          eq('pending', request.type)
          client.cancel_request(2)
          request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
          eq('slow_request', request.method)
          eq('cancel', request.type)
          client.notify('release')
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
          eq(0, #expected_handlers, 'did not call expected handler')
        end,
        on_handler = function(err, _, ctx)
          eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
          if ctx.method == 'slow_request' then
            local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
            eq(NIL, request)
            client.notify('finish')
          end
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should trigger LspRequest autocmd when requests table changes', function()
      local expected_handlers = {
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'slow_request', bufnr = 1, client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'check_tracked_requests_cleared',
        on_init = function(_client)
          command('let g:requests = 0')
          command('autocmd LspRequest * let g:requests+=1')
          client = _client
          client.request('slow_request')
          eq(1, eval('g:requests'))
          client.cancel_request(2)
          eq(2, eval('g:requests'))
          client.notify('release')
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
          eq(0, #expected_handlers, 'did not call expected handler')
          eq(3, eval('g:requests'))
        end,
        on_handler = function(err, _, ctx)
          eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
          if ctx.method == 'slow_request' then
            client.notify('finish')
          end
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should not send didOpen if the buffer closes before init', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_finish',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
            assert(TEST_RPC_CLIENT_ID == 1)
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
            assert(lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID))
            vim.cmd(BUFFER.."bwipeout")
          ]]
        end,
        on_init = function(_client)
          client = _client
          local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(full_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          client.notify('finish')
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should check the body sent attaching before init', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_init = function(_client)
          client = _client
          local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(full_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Already attached, returns true")
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should check the body sent attaching after init', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(full_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should check the body and didChange full', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open_and_change',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(full_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            exec_lua [[
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "boop";
              })
            ]]
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should check the body and didChange full with noeol', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open_and_change_noeol',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
            vim.bo[BUFFER].eol = false
          ]]
        end,
        on_init = function(_client)
          client = _client
          local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(full_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            exec_lua [[
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "boop";
              })
            ]]
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should send correct range for inlay hints with noeol', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        {
          NIL,
          {},
          {
            method = 'textDocument/inlayHint',
            params = {
              textDocument = {
                uri = 'file://',
              },
              range = {
                start = { line = 0, character = 0 },
                ['end'] = { line = 1, character = 3 },
              },
            },
            bufnr = 2,
            client_id = 1,
          },
        },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'inlay_hint',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
            vim.bo[BUFFER].eol = false
          ]]
        end,
        on_init = function(_client)
          client = _client
          eq(true, client.supports_method('textDocument/inlayHint'))
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            exec_lua [[
              vim.lsp.inlay_hint.enable(true, { bufnr = BUFFER })
            ]]
          end
          if ctx.method == 'textDocument/inlayHint' then
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should check the body and didChange incremental', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open_and_change_incremental',
        options = {
          allow_incremental_sync = true,
        },
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          local sync_kind =
            exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
          eq(sync_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            exec_lua [[
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "123boop";
              })
            ]]
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)
    it('should check the body and didChange incremental with debounce', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open_and_change_incremental',
        options = {
          allow_incremental_sync = true,
          debounce_text_changes = 5,
        },
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          local sync_kind =
            exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
          eq(sync_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            exec_lua [[
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "123boop";
              })
            ]]
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    -- TODO(askhan) we don't support full for now, so we can disable these tests.
    pending('should check the body and didChange incremental normal mode editing', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open_and_change_incremental_editing',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          local sync_kind =
            exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
          eq(sync_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            n.command('normal! 1Go')
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should check the body and didChange full with 2 changes', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open_and_change_multi',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(sync_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            exec_lua [[
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "321";
              })
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "boop";
              })
            ]]
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)

    it('should check the body and didChange full lifecycle', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_check_buffer_open_and_change_multi_and_close',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
          eq(sync_kind, client.server_capabilities().textDocumentSync.change)
          eq(true, client.server_capabilities().textDocumentSync.openClose)
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          if ctx.method == 'start' then
            exec_lua [[
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "321";
              })
              vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
                "boop";
              })
              vim.api.nvim_command(BUFFER.."bwipeout")
            ]]
            client.notify('finish')
          end
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)
  end)

  describe('parsing tests', function()
    it('should handle invalid content-length correctly', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'invalid_header',
        on_setup = function() end,
        on_init = function(_client)
          client = _client
          client.stop(true)
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
        end,
      }
    end)

    it('should not trim vim.NIL from the end of a list', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'finish', client_id = 1 } },
        {
          NIL,
          {
            arguments = { 'EXTRACT_METHOD', { metadata = {} }, 3, 0, 6123, NIL },
            command = 'refactor.perform',
            title = 'EXTRACT_METHOD',
          },
          { method = 'workspace/executeCommand', client_id = 1 },
        },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'decode_nil',
        on_setup = function()
          exec_lua [[
            BUFFER = vim.api.nvim_create_buf(false, true)
            vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
              "testing";
              "123";
            })
          ]]
        end,
        on_init = function(_client)
          client = _client
          exec_lua [[
            assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
          ]]
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
          if ctx.method == 'finish' then
            client.stop()
          end
        end,
      }
    end)
  end)
end)

describe('LSP', function()
  before_each(function()
    clear_notrace()
  end)

  local function make_edit(y_0, x_0, y_1, x_1, text)
    return {
      range = {
        start = { line = y_0, character = x_0 },
        ['end'] = { line = y_1, character = x_1 },
      },
      newText = type(text) == 'table' and table.concat(text, '\n') or (text or ''),
    }
  end

  describe('apply_text_edits', function()
    before_each(function()
      insert(dedent([[
        First line of text
        Second line of text
        Third line of text
        Fourth line of text
        å å ɧ 汉语 ↥ 🤦 🦄]]))
    end)
    it('applies simple edits', function()
      local edits = {
        make_edit(0, 0, 0, 0, { '123' }),
        make_edit(1, 0, 1, 1, { '2' }),
        make_edit(2, 0, 2, 2, { '3' }),
        make_edit(3, 2, 3, 4, { '' }),
      }
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        '123First line of text',
        '2econd line of text',
        '3ird line of text',
        'Foth line of text',
        'å å ɧ 汉语 ↥ 🤦 🦄',
      }, buf_lines(1))
    end)
    it('applies complex edits', function()
      local edits = {
        make_edit(0, 0, 0, 0, { '', '12' }),
        make_edit(0, 0, 0, 0, { '3', 'foo' }),
        make_edit(0, 1, 0, 1, { 'bar', '123' }),
        make_edit(0, #'First ', 0, #'First line of text', { 'guy' }),
        make_edit(1, 0, 1, #'Second', { 'baz' }),
        make_edit(2, #'Th', 2, #'Third', { 'e next' }),
        make_edit(3, #'', 3, #'Fourth', { 'another line of text', 'before this' }),
        make_edit(3, #'Fourth', 3, #'Fourth line of text', { '!' }),
      }
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        '',
        '123',
        'fooFbar',
        '123irst guy',
        'baz line of text',
        'The next line of text',
        'another line of text',
        'before this!',
        'å å ɧ 汉语 ↥ 🤦 🦄',
      }, buf_lines(1))
    end)
    it('applies complex edits (reversed range)', function()
      local edits = {
        make_edit(0, 0, 0, 0, { '', '12' }),
        make_edit(0, 0, 0, 0, { '3', 'foo' }),
        make_edit(0, 1, 0, 1, { 'bar', '123' }),
        make_edit(0, #'First line of text', 0, #'First ', { 'guy' }),
        make_edit(1, #'Second', 1, 0, { 'baz' }),
        make_edit(2, #'Third', 2, #'Th', { 'e next' }),
        make_edit(3, #'Fourth', 3, #'', { 'another line of text', 'before this' }),
        make_edit(3, #'Fourth line of text', 3, #'Fourth', { '!' }),
      }
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        '',
        '123',
        'fooFbar',
        '123irst guy',
        'baz line of text',
        'The next line of text',
        'another line of text',
        'before this!',
        'å å ɧ 汉语 ↥ 🤦 🦄',
      }, buf_lines(1))
    end)
    it('applies non-ASCII characters edits', function()
      local edits = {
        make_edit(4, 3, 4, 4, { 'ä' }),
      }
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        'First line of text',
        'Second line of text',
        'Third line of text',
        'Fourth line of text',
        'å ä ɧ 汉语 ↥ 🤦 🦄',
      }, buf_lines(1))
    end)
    it('applies text edits at the end of the document', function()
      local edits = {
        make_edit(5, 0, 5, 0, 'foobar'),
      }
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        'First line of text',
        'Second line of text',
        'Third line of text',
        'Fourth line of text',
        'å å ɧ 汉语 ↥ 🤦 🦄',
        'foobar',
      }, buf_lines(1))
    end)
    it('applies multiple text edits at the end of the document', function()
      local edits = {
        make_edit(4, 0, 5, 0, ''),
        make_edit(5, 0, 5, 0, 'foobar'),
      }
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        'First line of text',
        'Second line of text',
        'Third line of text',
        'Fourth line of text',
        'foobar',
      }, buf_lines(1))
    end)
    it('it restores marks', function()
      local edits = {
        make_edit(1, 0, 2, 5, 'foobar'),
        make_edit(4, 0, 5, 0, 'barfoo'),
      }
      eq(true, api.nvim_buf_set_mark(1, 'a', 2, 1, {}))
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        'First line of text',
        'foobar line of text',
        'Fourth line of text',
        'barfoo',
      }, buf_lines(1))
      local mark = api.nvim_buf_get_mark(1, 'a')
      eq({ 2, 1 }, mark)
    end)

    it('it restores marks to last valid col', function()
      local edits = {
        make_edit(1, 0, 2, 15, 'foobar'),
        make_edit(4, 0, 5, 0, 'barfoo'),
      }
      eq(true, api.nvim_buf_set_mark(1, 'a', 2, 10, {}))
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        'First line of text',
        'foobarext',
        'Fourth line of text',
        'barfoo',
      }, buf_lines(1))
      local mark = api.nvim_buf_get_mark(1, 'a')
      eq({ 2, 9 }, mark)
    end)

    it('it restores marks to last valid line', function()
      local edits = {
        make_edit(1, 0, 4, 5, 'foobar'),
        make_edit(4, 0, 5, 0, 'barfoo'),
      }
      eq(true, api.nvim_buf_set_mark(1, 'a', 4, 1, {}))
      exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
      eq({
        'First line of text',
        'foobaro',
      }, buf_lines(1))
      local mark = api.nvim_buf_get_mark(1, 'a')
      eq({ 2, 1 }, mark)
    end)

    describe('cursor position', function()
      it("don't fix the cursor if the range contains the cursor", function()
        api.nvim_win_set_cursor(0, { 2, 6 })
        local edits = {
          make_edit(1, 0, 1, 19, 'Second line of text'),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({
          'First line of text',
          'Second line of text',
          'Third line of text',
          'Fourth line of text',
          'å å ɧ 汉语 ↥ 🤦 🦄',
        }, buf_lines(1))
        eq({ 2, 6 }, api.nvim_win_get_cursor(0))
      end)

      it('fix the cursor to the valid col if the content was removed', function()
        api.nvim_win_set_cursor(0, { 2, 6 })
        local edits = {
          make_edit(1, 0, 1, 6, ''),
          make_edit(1, 6, 1, 19, ''),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({
          'First line of text',
          '',
          'Third line of text',
          'Fourth line of text',
          'å å ɧ 汉语 ↥ 🤦 🦄',
        }, buf_lines(1))
        eq({ 2, 0 }, api.nvim_win_get_cursor(0))
      end)

      it('fix the cursor to the valid row if the content was removed', function()
        api.nvim_win_set_cursor(0, { 2, 6 })
        local edits = {
          make_edit(1, 0, 1, 6, ''),
          make_edit(0, 18, 5, 0, ''),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({
          'First line of text',
        }, buf_lines(1))
        eq({ 1, 17 }, api.nvim_win_get_cursor(0))
      end)

      it('fix the cursor row', function()
        api.nvim_win_set_cursor(0, { 3, 0 })
        local edits = {
          make_edit(1, 0, 2, 0, ''),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({
          'First line of text',
          'Third line of text',
          'Fourth line of text',
          'å å ɧ 汉语 ↥ 🤦 🦄',
        }, buf_lines(1))
        eq({ 2, 0 }, api.nvim_win_get_cursor(0))
      end)

      it('fix the cursor col', function()
        -- append empty last line. See #22636
        api.nvim_buf_set_lines(1, -1, -1, true, { '' })

        api.nvim_win_set_cursor(0, { 2, 11 })
        local edits = {
          make_edit(1, 7, 1, 11, ''),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({
          'First line of text',
          'Second  of text',
          'Third line of text',
          'Fourth line of text',
          'å å ɧ 汉语 ↥ 🤦 🦄',
          '',
        }, buf_lines(1))
        eq({ 2, 7 }, api.nvim_win_get_cursor(0))
      end)

      it('fix the cursor row and col', function()
        api.nvim_win_set_cursor(0, { 2, 12 })
        local edits = {
          make_edit(0, 11, 1, 12, ''),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({
          'First line of text',
          'Third line of text',
          'Fourth line of text',
          'å å ɧ 汉语 ↥ 🤦 🦄',
        }, buf_lines(1))
        eq({ 1, 11 }, api.nvim_win_get_cursor(0))
      end)
    end)

    describe('with LSP end line after what Vim considers to be the end line', function()
      it('applies edits when the last linebreak is considered a new line', function()
        local edits = {
          make_edit(0, 0, 5, 0, { 'All replaced' }),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({ 'All replaced' }, buf_lines(1))
      end)
      it("applies edits when the end line is 2 larger than vim's", function()
        local edits = {
          make_edit(0, 0, 6, 0, { 'All replaced' }),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({ 'All replaced' }, buf_lines(1))
      end)
      it('applies edits with a column offset', function()
        local edits = {
          make_edit(0, 0, 5, 2, { 'All replaced' }),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-16')
        eq({ 'All replaced' }, buf_lines(1))
      end)
    end)
  end)

  describe('apply_text_edits regression tests for #20116', function()
    before_each(function()
      insert(dedent([[
      Test line one
      Test line two 21 char]]))
    end)
    describe('with LSP end column out of bounds and start column at 0', function()
      it('applies edits at the end of the buffer', function()
        local edits = {
          make_edit(0, 0, 1, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' }),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-8')
        eq({ '#include "whatever.h"', '#include <algorithm>' }, buf_lines(1))
      end)
      it('applies edits in the middle of the buffer', function()
        local edits = {
          make_edit(0, 0, 0, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' }),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-8')
        eq(
          { '#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char' },
          buf_lines(1)
        )
      end)
    end)
    describe('with LSP end column out of bounds and start column NOT at 0', function()
      it('applies edits at the end of the buffer', function()
        local edits = {
          make_edit(0, 2, 1, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' }),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-8')
        eq({ 'Te#include "whatever.h"', '#include <algorithm>' }, buf_lines(1))
      end)
      it('applies edits in the middle of the buffer', function()
        local edits = {
          make_edit(0, 2, 0, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' }),
        }
        exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, 'utf-8')
        eq(
          { 'Te#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char' },
          buf_lines(1)
        )
      end)
    end)
  end)

  describe('apply_text_document_edit', function()
    local target_bufnr --- @type integer
    local text_document_edit = function(editVersion)
      return {
        edits = {
          make_edit(0, 0, 0, 3, 'First ↥ 🤦 🦄'),
        },
        textDocument = {
          uri = 'file:///fake/uri',
          version = editVersion,
        },
      }
    end
    before_each(function()
      target_bufnr = exec_lua [[
        local bufnr = vim.uri_to_bufnr("file:///fake/uri")
        local lines = {"1st line of text", "2nd line of 语text"}
        vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
        return bufnr
      ]]
    end)
    it('correctly goes ahead with the edit if all is normal', function()
      exec_lua("vim.lsp.util.apply_text_document_edit(..., nil, 'utf-16')", text_document_edit(5))
      eq({
        'First ↥ 🤦 🦄 line of text',
        '2nd line of 语text',
      }, buf_lines(target_bufnr))
    end)
    it('always accepts edit with version = 0', function()
      exec_lua(
        [[
        local args = {...}
        local bufnr = select(1, ...)
        local text_edit = select(2, ...)
        vim.lsp.util.buf_versions[bufnr] = 10
        vim.lsp.util.apply_text_document_edit(text_edit, nil, 'utf-16')
      ]],
        target_bufnr,
        text_document_edit(0)
      )
      eq({
        'First ↥ 🤦 🦄 line of text',
        '2nd line of 语text',
      }, buf_lines(target_bufnr))
    end)
    it('skips the edit if the version of the edit is behind the local buffer ', function()
      local apply_edit_mocking_current_version = function(edit, versionedBuf)
        exec_lua(
          [[
          local args = {...}
          local versionedBuf = args[2]
          vim.lsp.util.buf_versions[versionedBuf.bufnr] = versionedBuf.currentVersion
          vim.lsp.util.apply_text_document_edit(args[1], nil, 'utf-16')
        ]],
          edit,
          versionedBuf
        )
      end

      local baseText = {
        '1st line of text',
        '2nd line of 语text',
      }

      eq(baseText, buf_lines(target_bufnr))

      -- Apply an edit for an old version, should skip
      apply_edit_mocking_current_version(
        text_document_edit(2),
        { currentVersion = 7, bufnr = target_bufnr }
      )
      eq(baseText, buf_lines(target_bufnr)) -- no change

      -- Sanity check that next version to current does apply change
      apply_edit_mocking_current_version(
        text_document_edit(8),
        { currentVersion = 7, bufnr = target_bufnr }
      )
      eq({
        'First ↥ 🤦 🦄 line of text',
        '2nd line of 语text',
      }, buf_lines(target_bufnr))
    end)
  end)

  describe('workspace_apply_edit', function()
    it('workspace/applyEdit returns ApplyWorkspaceEditResponse', function()
      local expected_handlers = {
        { NIL, {}, { method = 'test', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'basic_init',
        on_init = function(client, _)
          client.stop()
        end,
        -- If the program timed out, then code will be nil.
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        -- Note that NIL must be used here.
        -- on_handler(err, method, result, client_id)
        on_handler = function(...)
          local expected = {
            applied = true,
            failureReason = nil,
          }
          eq(
            expected,
            exec_lua [[
            local apply_edit = {
              label = nil;
              edit = {};
            }
            return vim.lsp.handlers['workspace/applyEdit'](nil, apply_edit, {client_id = TEST_RPC_CLIENT_ID})
          ]]
          )
          eq(table.remove(expected_handlers), { ... })
        end,
      }
    end)
  end)

  describe('apply_workspace_edit', function()
    local replace_line_edit = function(row, new_line, editVersion)
      return {
        edits = {
          -- NOTE: This is a hack if you have a line longer than 1000 it won't replace it
          make_edit(row, 0, row, 1000, new_line),
        },
        textDocument = {
          uri = 'file:///fake/uri',
          version = editVersion,
        },
      }
    end

    -- Some servers send all the edits separately, but with the same version.
    -- We should not stop applying the edits
    local make_workspace_edit = function(changes)
      return {
        documentChanges = changes,
      }
    end

    local target_bufnr, changedtick = nil, nil

    before_each(function()
      local ret = exec_lua [[
        local bufnr = vim.uri_to_bufnr("file:///fake/uri")
        local lines = {
          "Original Line #1",
          "Original Line #2"
        }

        vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)

        local update_changed_tick = function()
          vim.lsp.util.buf_versions[bufnr] = vim.api.nvim_buf_get_var(bufnr, 'changedtick')
        end

        update_changed_tick()
        vim.api.nvim_buf_attach(bufnr, false, {
          on_changedtick = function()
            update_changed_tick()
          end
        })

        return {bufnr, vim.api.nvim_buf_get_var(bufnr, 'changedtick')}
      ]]

      target_bufnr = ret[1]
      changedtick = ret[2]
    end)

    it('apply_workspace_edit applies a single edit', function()
      local new_lines = {
        'First Line',
      }

      local edits = {}
      for row, line in ipairs(new_lines) do
        table.insert(edits, replace_line_edit(row - 1, line, changedtick))
      end

      eq(
        {
          'First Line',
          'Original Line #2',
        },
        exec_lua(
          [[
        local args = {...}
        local workspace_edits = args[1]
        local target_bufnr = args[2]

        vim.lsp.util.apply_workspace_edit(workspace_edits, 'utf-16')

        return vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false)
      ]],
          make_workspace_edit(edits),
          target_bufnr
        )
      )
    end)

    it('apply_workspace_edit applies multiple edits', function()
      local new_lines = {
        'First Line',
        'Second Line',
      }

      local edits = {}
      for row, line in ipairs(new_lines) do
        table.insert(edits, replace_line_edit(row - 1, line, changedtick))
      end

      eq(
        new_lines,
        exec_lua(
          [[
        local args = {...}
        local workspace_edits = args[1]
        local target_bufnr = args[2]

        vim.lsp.util.apply_workspace_edit(workspace_edits, 'utf-16')

        return vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false)
      ]],
          make_workspace_edit(edits),
          target_bufnr
        )
      )
    end)
    it('Supports file creation with CreateFile payload', function()
      local tmpfile = tmpname()
      os.remove(tmpfile) -- Should not exist, only interested in a tmpname
      local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
      local edit = {
        documentChanges = {
          {
            kind = 'create',
            uri = uri,
          },
        },
      }
      exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
      eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
    end)
    it(
      'Supports file creation in folder that needs to be created with CreateFile payload',
      function()
        local tmpfile = tmpname()
        os.remove(tmpfile) -- Should not exist, only interested in a tmpname
        tmpfile = tmpfile .. '/dummy/x/'
        local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
        local edit = {
          documentChanges = {
            {
              kind = 'create',
              uri = uri,
            },
          },
        }
        exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
        eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
      end
    )
    it('createFile does not touch file if it exists and ignoreIfExists is set', function()
      local tmpfile = tmpname()
      write_file(tmpfile, 'Dummy content')
      local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
      local edit = {
        documentChanges = {
          {
            kind = 'create',
            uri = uri,
            options = {
              ignoreIfExists = true,
            },
          },
        },
      }
      exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
      eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
      eq('Dummy content', read_file(tmpfile))
    end)
    it('createFile overrides file if overwrite is set', function()
      local tmpfile = tmpname()
      write_file(tmpfile, 'Dummy content')
      local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
      local edit = {
        documentChanges = {
          {
            kind = 'create',
            uri = uri,
            options = {
              overwrite = true,
              ignoreIfExists = true, -- overwrite must win over ignoreIfExists
            },
          },
        },
      }
      exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
      eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
      eq('', read_file(tmpfile))
    end)
    it('DeleteFile delete file and buffer', function()
      local tmpfile = tmpname()
      write_file(tmpfile, 'Be gone')
      local uri = exec_lua(
        [[
        local fname = select(1, ...)
        local bufnr = vim.fn.bufadd(fname)
        vim.fn.bufload(bufnr)
        return vim.uri_from_fname(fname)
      ]],
        tmpfile
      )
      local edit = {
        documentChanges = {
          {
            kind = 'delete',
            uri = uri,
          },
        },
      }
      eq(true, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16'))
      eq(false, vim.uv.fs_stat(tmpfile) ~= nil)
      eq(false, api.nvim_buf_is_loaded(fn.bufadd(tmpfile)))
    end)
    it('DeleteFile fails if file does not exist and ignoreIfNotExists is false', function()
      local tmpfile = tmpname()
      os.remove(tmpfile)
      local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
      local edit = {
        documentChanges = {
          {
            kind = 'delete',
            uri = uri,
            options = {
              ignoreIfNotExists = false,
            },
          },
        },
      }
      eq(false, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit))
      eq(false, vim.uv.fs_stat(tmpfile) ~= nil)
    end)
  end)

  describe('lsp.util.rename', function()
    local pathsep = n.get_pathsep()

    it('Can rename an existing file', function()
      local old = tmpname()
      write_file(old, 'Test content')
      local new = tmpname()
      os.remove(new) -- only reserve the name, file must not exist for the test scenario
      local lines = exec_lua(
        [[
        local old = select(1, ...)
        local new = select(2, ...)
        local old_bufnr = vim.fn.bufadd(old)
        vim.fn.bufload(old_bufnr)
        vim.lsp.util.rename(old, new)
        -- the existing buffer is renamed in-place and its contents is kept
        local new_bufnr = vim.fn.bufadd(new)
        vim.fn.bufload(new_bufnr)
        return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true)
      ]],
        old,
        new
      )
      eq({ 'Test content' }, lines)
      local exists = vim.uv.fs_stat(old) ~= nil
      eq(false, exists)
      exists = vim.uv.fs_stat(new) ~= nil
      eq(true, exists)
      os.remove(new)
    end)
    it('Can rename a directory', function()
      -- only reserve the name, file must not exist for the test scenario
      local old_dir = tmpname()
      local new_dir = tmpname()
      os.remove(old_dir)
      os.remove(new_dir)

      n.mkdir_p(old_dir)

      local file = 'file.txt'
      write_file(old_dir .. pathsep .. file, 'Test content')

      local lines = exec_lua(
        [[
        local old_dir = select(1, ...)
        local new_dir = select(2, ...)
        local pathsep = select(3, ...)
        local file = select(4, ...)
        local old_bufnr = vim.fn.bufadd(old_dir .. pathsep .. file)
        vim.fn.bufload(old_bufnr)
        vim.lsp.util.rename(old_dir, new_dir)
        -- the existing buffer is renamed in-place and its contents is kept
        local new_bufnr = vim.fn.bufadd(new_dir .. pathsep .. file)
        vim.fn.bufload(new_bufnr)
        return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true)
      ]],
        old_dir,
        new_dir,
        pathsep,
        file
      )
      eq({ 'Test content' }, lines)
      eq(false, vim.uv.fs_stat(old_dir) ~= nil)
      eq(true, vim.uv.fs_stat(new_dir) ~= nil)
      eq(true, vim.uv.fs_stat(new_dir .. pathsep .. file) ~= nil)

      os.remove(new_dir)
    end)
    it('Does not touch buffers that do not match path prefix', function()
      local old = tmpname()
      local new = tmpname()
      os.remove(old)
      os.remove(new)
      n.mkdir_p(old)

      local result = exec_lua(
        [[
        local old = select(1, ...)
        local new = select(2, ...)

        local old_prefixed = 'explorer://' .. old
        local old_suffixed = old .. '.bak'
        local new_prefixed = 'explorer://' .. new
        local new_suffixed = new .. '.bak'

        local old_prefixed_buf = vim.fn.bufadd(old_prefixed)
        local old_suffixed_buf = vim.fn.bufadd(old_suffixed)
        local new_prefixed_buf = vim.fn.bufadd(new_prefixed)
        local new_suffixed_buf = vim.fn.bufadd(new_suffixed)

        vim.lsp.util.rename(old, new)

        return
          vim.api.nvim_buf_is_valid(old_prefixed_buf) and
          vim.api.nvim_buf_is_valid(old_suffixed_buf) and
          vim.api.nvim_buf_is_valid(new_prefixed_buf) and
          vim.api.nvim_buf_is_valid(new_suffixed_buf) and
          vim.api.nvim_buf_get_name(old_prefixed_buf) == old_prefixed and
          vim.api.nvim_buf_get_name(old_suffixed_buf) == old_suffixed and
          vim.api.nvim_buf_get_name(new_prefixed_buf) == new_prefixed and
          vim.api.nvim_buf_get_name(new_suffixed_buf) == new_suffixed
      ]],
        old,
        new
      )
      eq(true, result)

      os.remove(new)
    end)
    it(
      'Does not rename file if target exists and ignoreIfExists is set or overwrite is false',
      function()
        local old = tmpname()
        write_file(old, 'Old File')
        local new = tmpname()
        write_file(new, 'New file')

        exec_lua(
          [[
        local old = select(1, ...)
        local new = select(2, ...)

        vim.lsp.util.rename(old, new, { ignoreIfExists = true })
      ]],
          old,
          new
        )

        eq(true, vim.uv.fs_stat(old) ~= nil)
        eq('New file', read_file(new))

        exec_lua(
          [[
        local old = select(1, ...)
        local new = select(2, ...)

        vim.lsp.util.rename(old, new, { overwrite = false })
      ]],
          old,
          new
        )

        eq(true, vim.uv.fs_stat(old) ~= nil)
        eq('New file', read_file(new))
      end
    )
    it('Maintains undo information for loaded buffer', function()
      local old = tmpname()
      write_file(old, 'line')
      local new = tmpname()
      os.remove(new)

      local undo_kept = exec_lua(
        [[
        local old = select(1, ...)
        local new = select(2, ...)
        vim.opt.undofile = true
        vim.cmd.edit(old)
        vim.cmd.normal('dd')
        vim.cmd.write()
        local undotree = vim.fn.undotree()
        vim.lsp.util.rename(old, new)
        -- Renaming uses :saveas, which updates the "last write" information.
        -- Other than that, the undotree should remain the same.
        undotree.save_cur = undotree.save_cur + 1
        undotree.save_last = undotree.save_last + 1
        undotree.entries[1].save = undotree.entries[1].save + 1
        return vim.deep_equal(undotree, vim.fn.undotree())
      ]],
        old,
        new
      )
      eq(false, vim.uv.fs_stat(old) ~= nil)
      eq(true, vim.uv.fs_stat(new) ~= nil)
      eq(true, undo_kept)
    end)
    it('Maintains undo information for unloaded buffer', function()
      local old = tmpname()
      write_file(old, 'line')
      local new = tmpname()
      os.remove(new)

      local undo_kept = exec_lua(
        [[
        local old = select(1, ...)
        local new = select(2, ...)
        vim.opt.undofile = true
        vim.cmd.split(old)
        vim.cmd.normal('dd')
        vim.cmd.write()
        local undotree = vim.fn.undotree()
        vim.cmd.bdelete()
        vim.lsp.util.rename(old, new)
        vim.cmd.edit(new)
        return vim.deep_equal(undotree, vim.fn.undotree())
      ]],
        old,
        new
      )
      eq(false, vim.uv.fs_stat(old) ~= nil)
      eq(true, vim.uv.fs_stat(new) ~= nil)
      eq(true, undo_kept)
    end)
    it('Does not rename file when it conflicts with a buffer without file', function()
      local old = tmpname()
      write_file(old, 'Old File')
      local new = tmpname()
      os.remove(new)

      local lines = exec_lua(
        [[
          local old = select(1, ...)
          local new = select(2, ...)
          local old_buf = vim.fn.bufadd(old)
          vim.fn.bufload(old_buf)
          local conflict_buf = vim.api.nvim_create_buf(true, false)
          vim.api.nvim_buf_set_name(conflict_buf, new)
          vim.api.nvim_buf_set_lines(conflict_buf, 0, -1, true, {'conflict'})
          vim.api.nvim_win_set_buf(0, conflict_buf)
          vim.lsp.util.rename(old, new)
          return vim.api.nvim_buf_get_lines(conflict_buf, 0, -1, true)
        ]],
        old,
        new
      )
      eq({ 'conflict' }, lines)
      eq('Old File', read_file(old))
    end)
    it('Does override target if overwrite is true', function()
      local old = tmpname()
      write_file(old, 'Old file')
      local new = tmpname()
      write_file(new, 'New file')
      exec_lua(
        [[
        local old = select(1, ...)
        local new = select(2, ...)

        vim.lsp.util.rename(old, new, { overwrite = true })
      ]],
        old,
        new
      )

      eq(false, vim.uv.fs_stat(old) ~= nil)
      eq(true, vim.uv.fs_stat(new) ~= nil)
      eq('Old file', read_file(new))
    end)
  end)

  describe('lsp.util.locations_to_items', function()
    it('Convert Location[] to items', function()
      local expected = {
        {
          filename = '/fake/uri',
          lnum = 1,
          col = 3,
          text = 'testing',
          user_data = {
            uri = 'file:///fake/uri',
            range = {
              start = { line = 0, character = 2 },
              ['end'] = { line = 0, character = 3 },
            },
          },
        },
      }
      local actual = exec_lua [[
        local bufnr = vim.uri_to_bufnr("file:///fake/uri")
        local lines = {"testing", "123"}
        vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
        local locations = {
          {
            uri = 'file:///fake/uri',
            range = {
              start = { line = 0, character = 2 },
              ['end'] = { line = 0, character = 3 },
            }
          },
        }
        return vim.lsp.util.locations_to_items(locations, 'utf-16')
      ]]
      eq(expected, actual)
    end)
    it('Convert LocationLink[] to items', function()
      local expected = {
        {
          filename = '/fake/uri',
          lnum = 1,
          col = 3,
          text = 'testing',
          user_data = {
            targetUri = 'file:///fake/uri',
            targetRange = {
              start = { line = 0, character = 2 },
              ['end'] = { line = 0, character = 3 },
            },
            targetSelectionRange = {
              start = { line = 0, character = 2 },
              ['end'] = { line = 0, character = 3 },
            },
          },
        },
      }
      local actual = exec_lua [[
        local bufnr = vim.uri_to_bufnr("file:///fake/uri")
        local lines = {"testing", "123"}
        vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
        local locations = {
          {
            targetUri = vim.uri_from_bufnr(bufnr),
            targetRange = {
              start = { line = 0, character = 2 },
              ['end'] = { line = 0, character = 3 },
            },
            targetSelectionRange = {
              start = { line = 0, character = 2 },
              ['end'] = { line = 0, character = 3 },
            }
          },
        }
        return vim.lsp.util.locations_to_items(locations, 'utf-16')
      ]]
      eq(expected, actual)
    end)
  end)

  describe('lsp.util.symbols_to_items', function()
    describe('convert DocumentSymbol[] to items', function()
      it('DocumentSymbol has children', function()
        local expected = {
          {
            col = 1,
            filename = '',
            kind = 'File',
            lnum = 2,
            text = '[File] TestA',
          },
          {
            col = 1,
            filename = '',
            kind = 'Module',
            lnum = 4,
            text = '[Module] TestB',
          },
          {
            col = 1,
            filename = '',
            kind = 'Namespace',
            lnum = 6,
            text = '[Namespace] TestC',
          },
        }
        eq(
          expected,
          exec_lua [[
          local doc_syms = {
            {
              deprecated = false,
              detail = "A",
              kind = 1,
              name = "TestA",
              range = {
                start = {
                  character = 0,
                  line = 1
                },
                ["end"] = {
                  character = 0,
                  line = 2
                }
              },
              selectionRange = {
                start = {
                  character = 0,
                  line = 1
                },
                ["end"] = {
                  character = 4,
                  line = 1
                }
              },
              children = {
                {
                  children = {},
                  deprecated = false,
                  detail = "B",
                  kind = 2,
                  name = "TestB",
                  range = {
                    start = {
                      character = 0,
                      line = 3
                    },
                    ["end"] = {
                      character = 0,
                      line = 4
                    }
                  },
                  selectionRange = {
                    start = {
                      character = 0,
                      line = 3
                    },
                    ["end"] = {
                      character = 4,
                      line = 3
                    }
                  }
                }
              }
            },
            {
              deprecated = false,
              detail = "C",
              kind = 3,
              name = "TestC",
              range = {
                start = {
                  character = 0,
                  line = 5
                },
                ["end"] = {
                  character = 0,
                  line = 6
                }
              },
              selectionRange = {
                start = {
                  character = 0,
                  line = 5
                },
                ["end"] = {
                  character = 4,
                  line = 5
                }
              }
            }
          }
          return vim.lsp.util.symbols_to_items(doc_syms, nil)
        ]]
        )
      end)
      it('DocumentSymbol has no children', function()
        local expected = {
          {
            col = 1,
            filename = '',
            kind = 'File',
            lnum = 2,
            text = '[File] TestA',
          },
          {
            col = 1,
            filename = '',
            kind = 'Namespace',
            lnum = 6,
            text = '[Namespace] TestC',
          },
        }
        eq(
          expected,
          exec_lua [[
          local doc_syms = {
            {
              deprecated = false,
              detail = "A",
              kind = 1,
              name = "TestA",
              range = {
                start = {
                  character = 0,
                  line = 1
                },
                ["end"] = {
                  character = 0,
                  line = 2
                }
              },
              selectionRange = {
                start = {
                  character = 0,
                  line = 1
                },
                ["end"] = {
                  character = 4,
                  line = 1
                }
              },
            },
            {
              deprecated = false,
              detail = "C",
              kind = 3,
              name = "TestC",
              range = {
                start = {
                  character = 0,
                  line = 5
                },
                ["end"] = {
                  character = 0,
                  line = 6
                }
              },
              selectionRange = {
                start = {
                  character = 0,
                  line = 5
                },
                ["end"] = {
                  character = 4,
                  line = 5
                }
              }
            }
          }
          return vim.lsp.util.symbols_to_items(doc_syms, nil)
        ]]
        )
      end)
    end)
    it('convert SymbolInformation[] to items', function()
      local expected = {
        {
          col = 1,
          filename = '/test_a',
          kind = 'File',
          lnum = 2,
          text = '[File] TestA',
        },
        {
          col = 1,
          filename = '/test_b',
          kind = 'Module',
          lnum = 4,
          text = '[Module] TestB',
        },
      }
      eq(
        expected,
        exec_lua [[
          local sym_info = {
            {
              deprecated = false,
              kind = 1,
              name = "TestA",
              location = {
                range = {
                  start = {
                    character = 0,
                    line = 1
                  },
                  ["end"] = {
                    character = 0,
                    line = 2
                  }
                },
                uri = "file:///test_a"
              },
              containerName = "TestAContainer"
            },
            {
              deprecated = false,
              kind = 2,
              name = "TestB",
              location = {
                range = {
                  start = {
                    character = 0,
                    line = 3
                  },
                  ["end"] = {
                    character = 0,
                    line = 4
                  }
                },
                uri = "file:///test_b"
              },
              containerName = "TestBContainer"
            }
          }
          return vim.lsp.util.symbols_to_items(sym_info, nil)
        ]]
      )
    end)
  end)

  describe('lsp.util._get_symbol_kind_name', function()
    it('returns the name specified by protocol', function()
      eq('File', exec_lua('return vim.lsp.util._get_symbol_kind_name(1)'))
      eq('TypeParameter', exec_lua('return vim.lsp.util._get_symbol_kind_name(26)'))
    end)
    it('returns the name not specified by protocol', function()
      eq('Unknown', exec_lua('return vim.lsp.util._get_symbol_kind_name(nil)'))
      eq('Unknown', exec_lua('return vim.lsp.util._get_symbol_kind_name(vim.NIL)'))
      eq('Unknown', exec_lua('return vim.lsp.util._get_symbol_kind_name(1000)'))
    end)
  end)

  describe('lsp.util.jump_to_location', function()
    local target_bufnr --- @type integer

    before_each(function()
      target_bufnr = exec_lua [[
        local bufnr = vim.uri_to_bufnr("file:///fake/uri")
        local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"}
        vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
        return bufnr
      ]]
    end)

    local location = function(start_line, start_char, end_line, end_char)
      return {
        uri = 'file:///fake/uri',
        range = {
          start = { line = start_line, character = start_char },
          ['end'] = { line = end_line, character = end_char },
        },
      }
    end

    local jump = function(msg)
      eq(true, exec_lua('return vim.lsp.util.jump_to_location(...)', msg, 'utf-16'))
      eq(target_bufnr, fn.bufnr('%'))
      return {
        line = fn.line('.'),
        col = fn.col('.'),
      }
    end

    it('jumps to a Location', function()
      local pos = jump(location(0, 9, 0, 9))
      eq(1, pos.line)
      eq(10, pos.col)
    end)

    it('jumps to a LocationLink', function()
      local pos = jump({
        targetUri = 'file:///fake/uri',
        targetSelectionRange = {
          start = { line = 0, character = 4 },
          ['end'] = { line = 0, character = 4 },
        },
        targetRange = {
          start = { line = 1, character = 5 },
          ['end'] = { line = 1, character = 5 },
        },
      })
      eq(1, pos.line)
      eq(5, pos.col)
    end)

    it('jumps to the correct multibyte column', function()
      local pos = jump(location(1, 2, 1, 2))
      eq(2, pos.line)
      eq(4, pos.col)
      eq('å', fn.expand('<cword>'))
    end)

    it('adds current position to jumplist before jumping', function()
      api.nvim_win_set_buf(0, target_bufnr)
      local mark = api.nvim_buf_get_mark(target_bufnr, "'")
      eq({ 1, 0 }, mark)

      api.nvim_win_set_cursor(0, { 2, 3 })
      jump(location(0, 9, 0, 9))

      mark = api.nvim_buf_get_mark(target_bufnr, "'")
      eq({ 2, 3 }, mark)
    end)
  end)

  describe('lsp.util.show_document', function()
    local target_bufnr --- @type integer
    local target_bufnr2 --- @type integer

    before_each(function()
      target_bufnr = exec_lua([[
        local bufnr = vim.uri_to_bufnr("file:///fake/uri")
        local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"}
        vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
        return bufnr
      ]])

      target_bufnr2 = exec_lua([[
        local bufnr = vim.uri_to_bufnr("file:///fake/uri2")
        local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"}
        vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
        return bufnr
      ]])
    end)

    local location = function(start_line, start_char, end_line, end_char, second_uri)
      return {
        uri = second_uri and 'file:///fake/uri2' or 'file:///fake/uri',
        range = {
          start = { line = start_line, character = start_char },
          ['end'] = { line = end_line, character = end_char },
        },
      }
    end

    local show_document = function(msg, focus, reuse_win)
      eq(
        true,
        exec_lua(
          'return vim.lsp.util.show_document(...)',
          msg,
          'utf-16',
          { reuse_win = reuse_win, focus = focus }
        )
      )
      if focus == true or focus == nil then
        eq(target_bufnr, fn.bufnr('%'))
      end
      return {
        line = fn.line('.'),
        col = fn.col('.'),
      }
    end

    it('jumps to a Location if focus is true', function()
      local pos = show_document(location(0, 9, 0, 9), true, true)
      eq(1, pos.line)
      eq(10, pos.col)
    end)

    it('jumps to a Location if focus is true via handler', function()
      exec_lua(create_server_definition)
      local result = exec_lua([[
        local server = _create_server()
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        local result = {
          uri = 'file:///fake/uri',
          selection = {
            start = { line = 0, character = 9 },
            ['end'] = { line = 0, character = 9 }
          },
          takeFocus = true,
        }
        local ctx = {
          client_id = client_id,
          method = 'window/showDocument',
        }
        vim.lsp.handlers['window/showDocument'](nil, result, ctx)
        vim.lsp.stop_client(client_id)
        return {
          cursor = vim.api.nvim_win_get_cursor(0)
        }
      ]])
      eq(1, result.cursor[1])
      eq(9, result.cursor[2])
    end)

    it('jumps to a Location if focus not set', function()
      local pos = show_document(location(0, 9, 0, 9), nil, true)
      eq(1, pos.line)
      eq(10, pos.col)
    end)

    it('does not add current position to jumplist if not focus', function()
      api.nvim_win_set_buf(0, target_bufnr)
      local mark = api.nvim_buf_get_mark(target_bufnr, "'")
      eq({ 1, 0 }, mark)

      api.nvim_win_set_cursor(0, { 2, 3 })
      show_document(location(0, 9, 0, 9), false, true)
      show_document(location(0, 9, 0, 9, true), false, true)

      mark = api.nvim_buf_get_mark(target_bufnr, "'")
      eq({ 1, 0 }, mark)
    end)

    it('does not change cursor position if not focus and not reuse_win', function()
      api.nvim_win_set_buf(0, target_bufnr)
      local cursor = api.nvim_win_get_cursor(0)

      show_document(location(0, 9, 0, 9), false, false)
      eq(cursor, api.nvim_win_get_cursor(0))
    end)

    it('does not change window if not focus', function()
      api.nvim_win_set_buf(0, target_bufnr)
      local win = api.nvim_get_current_win()

      -- same document/bufnr
      show_document(location(0, 9, 0, 9), false, true)
      eq(win, api.nvim_get_current_win())

      -- different document/bufnr, new window/split
      show_document(location(0, 9, 0, 9, true), false, true)
      eq(2, #api.nvim_list_wins())
      eq(win, api.nvim_get_current_win())
    end)

    it("respects 'reuse_win' parameter", function()
      api.nvim_win_set_buf(0, target_bufnr)

      -- does not create a new window if the buffer is already open
      show_document(location(0, 9, 0, 9), false, true)
      eq(1, #api.nvim_list_wins())

      -- creates a new window even if the buffer is already open
      show_document(location(0, 9, 0, 9), false, false)
      eq(2, #api.nvim_list_wins())
    end)

    it('correctly sets the cursor of the split if range is given without focus', function()
      api.nvim_win_set_buf(0, target_bufnr)

      show_document(location(0, 9, 0, 9, true), false, true)

      local wins = api.nvim_list_wins()
      eq(2, #wins)
      table.sort(wins)

      eq({ 1, 0 }, api.nvim_win_get_cursor(wins[1]))
      eq({ 1, 9 }, api.nvim_win_get_cursor(wins[2]))
    end)

    it('does not change cursor of the split if not range and not focus', function()
      api.nvim_win_set_buf(0, target_bufnr)
      api.nvim_win_set_cursor(0, { 2, 3 })

      exec_lua([[vim.cmd.new()]])
      api.nvim_win_set_buf(0, target_bufnr2)
      api.nvim_win_set_cursor(0, { 2, 3 })

      show_document({ uri = 'file:///fake/uri2' }, false, true)

      local wins = api.nvim_list_wins()
      eq(2, #wins)
      eq({ 2, 3 }, api.nvim_win_get_cursor(wins[1]))
      eq({ 2, 3 }, api.nvim_win_get_cursor(wins[2]))
    end)

    it('respects existing buffers', function()
      api.nvim_win_set_buf(0, target_bufnr)
      local win = api.nvim_get_current_win()

      exec_lua([[vim.cmd.new()]])
      api.nvim_win_set_buf(0, target_bufnr2)
      api.nvim_win_set_cursor(0, { 2, 3 })
      local split = api.nvim_get_current_win()

      -- reuse win for open document/bufnr if called from split
      show_document(location(0, 9, 0, 9, true), false, true)
      eq({ 1, 9 }, api.nvim_win_get_cursor(split))
      eq(2, #api.nvim_list_wins())

      api.nvim_set_current_win(win)

      -- reuse win for open document/bufnr if called outside the split
      show_document(location(0, 9, 0, 9, true), false, true)
      eq({ 1, 9 }, api.nvim_win_get_cursor(split))
      eq(2, #api.nvim_list_wins())
    end)
  end)

  describe('lsp.util._make_floating_popup_size', function()
    before_each(function()
      exec_lua [[ contents =
      {"text tαxt txtα tex",
      "text tααt tααt text",
      "text tαxt tαxt"}
      ]]
    end)

    it('calculates size correctly', function()
      eq({ 19, 3 }, exec_lua [[ return {vim.lsp.util._make_floating_popup_size(contents)} ]])
    end)

    it('calculates size correctly with wrapping', function()
      eq(
        { 15, 5 },
        exec_lua [[ return {vim.lsp.util._make_floating_popup_size(contents,{width = 15, wrap_at = 14})} ]]
      )
    end)

    it('handles NUL bytes in text', function()
      exec_lua([[ contents = {
        '\000\001\002\003\004\005\006\007\008\009',
        '\010\011\012\013\014\015\016\017\018\019',
        '\020\021\022\023\024\025\026\027\028\029',
      } ]])
      command('set list listchars=')
      eq({ 20, 3 }, exec_lua [[ return {vim.lsp.util._make_floating_popup_size(contents)} ]])
      command('set display+=uhex')
      eq({ 40, 3 }, exec_lua [[ return {vim.lsp.util._make_floating_popup_size(contents)} ]])
    end)
  end)

  describe('lsp.util.trim.trim_empty_lines', function()
    it('properly trims empty lines', function()
      eq(
        { { 'foo', 'bar' } },
        exec_lua [[ return vim.lsp.util.trim_empty_lines({{ "foo", "bar" },  nil}) ]]
      )
    end)
  end)

  describe('lsp.util.convert_signature_help_to_markdown_lines', function()
    it('can handle negative activeSignature', function()
      local result = exec_lua [[
        local signature_help = {
          activeParameter = 0,
          activeSignature = -1,
          signatures = {
            {
              documentation = "some doc",
              label = "TestEntity.TestEntity()",
              parameters = {}
            },
          }
        }
        return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'cs', {','})
      ]]
      local expected = { '```cs', 'TestEntity.TestEntity()', '```', 'some doc' }
      eq(expected, result)
    end)
  end)

  describe('lsp.util.get_effective_tabstop', function()
    local function test_tabstop(tabsize, shiftwidth)
      exec_lua(string.format(
        [[
        vim.bo.shiftwidth = %d
        vim.bo.tabstop = 2
      ]],
        shiftwidth
      ))
      eq(tabsize, exec_lua('return vim.lsp.util.get_effective_tabstop()'))
    end

    it('with shiftwidth = 1', function()
      test_tabstop(1, 1)
    end)
    it('with shiftwidth = 0', function()
      test_tabstop(2, 0)
    end)
  end)

  describe('vim.lsp.buf.outgoing_calls', function()
    it('does nothing for an empty response', function()
      local qflist_count = exec_lua([=[
        require'vim.lsp.handlers'['callHierarchy/outgoingCalls'](nil, nil, {}, nil)
        return #vim.fn.getqflist()
      ]=])
      eq(0, qflist_count)
    end)

    it('opens the quickfix list with the right caller', function()
      local qflist = exec_lua([=[
        local rust_analyzer_response = { {
          fromRanges = { {
            ['end'] = {
              character = 7,
              line = 3
            },
            start = {
              character = 4,
              line = 3
            }
          } },
          to = {
            detail = "fn foo()",
            kind = 12,
            name = "foo",
            range = {
              ['end'] = {
                character = 11,
                line = 0
              },
              start = {
                character = 0,
                line = 0
              }
            },
            selectionRange = {
              ['end'] = {
                character = 6,
                line = 0
              },
              start = {
              character = 3,
              line = 0
              }
            },
            uri = "file:///src/main.rs"
          }
        } }
        local handler = require'vim.lsp.handlers'['callHierarchy/outgoingCalls']
        handler(nil, rust_analyzer_response, {})
        return vim.fn.getqflist()
      ]=])

      local expected = {
        {
          bufnr = 2,
          col = 5,
          end_col = 0,
          lnum = 4,
          end_lnum = 0,
          module = '',
          nr = 0,
          pattern = '',
          text = 'foo',
          type = '',
          valid = 1,
          vcol = 0,
        },
      }

      eq(expected, qflist)
    end)
  end)

  describe('vim.lsp.buf.incoming_calls', function()
    it('does nothing for an empty response', function()
      local qflist_count = exec_lua([=[
        require'vim.lsp.handlers'['callHierarchy/incomingCalls'](nil, nil, {})
        return #vim.fn.getqflist()
      ]=])
      eq(0, qflist_count)
    end)

    it('opens the quickfix list with the right callee', function()
      local qflist = exec_lua([=[
        local rust_analyzer_response = { {
          from = {
            detail = "fn main()",
            kind = 12,
            name = "main",
            range = {
              ['end'] = {
                character = 1,
                line = 4
              },
              start = {
                character = 0,
                line = 2
              }
            },
            selectionRange = {
              ['end'] = {
                character = 7,
                line = 2
              },
              start = {
                character = 3,
                line = 2
              }
            },
            uri = "file:///src/main.rs"
          },
          fromRanges = { {
            ['end'] = {
              character = 7,
              line = 3
            },
            start = {
              character = 4,
              line = 3
            }
          } }
        } }

        local handler = require'vim.lsp.handlers'['callHierarchy/incomingCalls']
        handler(nil, rust_analyzer_response, {})
        return vim.fn.getqflist()
      ]=])

      local expected = {
        {
          bufnr = 2,
          col = 5,
          end_col = 0,
          lnum = 4,
          end_lnum = 0,
          module = '',
          nr = 0,
          pattern = '',
          text = 'main',
          type = '',
          valid = 1,
          vcol = 0,
        },
      }

      eq(expected, qflist)
    end)
  end)

  describe('vim.lsp.buf.typehierarchy subtypes', function()
    it('does nothing for an empty response', function()
      local qflist_count = exec_lua([=[
        require'vim.lsp.handlers'['typeHierarchy/subtypes'](nil, nil, {})
        return #vim.fn.getqflist()
      ]=])
      eq(0, qflist_count)
    end)

    it('opens the quickfix list with the right subtypes', function()
      clear()
      exec_lua(create_server_definition)
      local qflist = exec_lua([=[
        local clangd_response = { {
          data = {
            parents = { {
              parents = { {
                parents = { {
                  parents = {},
                  symbolID = "62B3D268A01B9978"
                } },
                symbolID = "DC9B0AD433B43BEC"
              } },
              symbolID = "06B5F6A19BA9F6A8"
            } },
            symbolID = "EDC336589C09ABB2"
          },
          kind = 5,
          name = "D2",
          range = {
            ["end"] = {
              character = 8,
              line = 3,
            },
            start = {
              character = 6,
              line = 3,
            }
          },
          selectionRange = {
            ["end"] = {
              character = 8,
              line = 3,
            },
            start = {
              character = 6,
              line = 3,
            }
          },
          uri = "file:///home/jiangyinzuo/hello.cpp"
        }, {
          data = {
            parents = { {
              parents = { {
                parents = { {
                  parents = {},
                  symbolID = "62B3D268A01B9978"
                } },
                symbolID = "DC9B0AD433B43BEC"
              } },
              symbolID = "06B5F6A19BA9F6A8"
            } },
            symbolID = "AFFCAED15557EF08"
          },
          kind = 5,
          name = "D1",
          range = {
            ["end"] = {
              character = 8,
              line = 2
            },
            start = {
              character = 6,
              line = 2
            }
          },
          selectionRange = {
            ["end"] = {
              character = 8,
              line = 2
            },
            start = {
              character = 6,
              line = 2
            }
          },
          uri = "file:///home/jiangyinzuo/hello.cpp"
        } }

        local server = _create_server({
          capabilities = {
            positionEncoding = "utf-8"
          },
        })
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes']
        local bufnr = vim.api.nvim_get_current_buf()
        vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
          'class B : public A{};',
          'class C : public B{};',
          'class D1 : public C{};',
          'class D2 : public C{};',
          'class E : public D1, D2 {};',
        })
        handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr })
        return vim.fn.getqflist()
      ]=])

      local expected = {
        {
          bufnr = 2,
          col = 7,
          end_col = 0,
          end_lnum = 0,
          lnum = 4,
          module = '',
          nr = 0,
          pattern = '',
          text = 'D2',
          type = '',
          valid = 1,
          vcol = 0,
        },
        {
          bufnr = 2,
          col = 7,
          end_col = 0,
          end_lnum = 0,
          lnum = 3,
          module = '',
          nr = 0,
          pattern = '',
          text = 'D1',
          type = '',
          valid = 1,
          vcol = 0,
        },
      }

      eq(expected, qflist)
    end)

    it('opens the quickfix list with the right subtypes and details', function()
      clear()
      exec_lua(create_server_definition)
      local qflist = exec_lua([=[
        local jdtls_response = {
          {
            data = { element = '=hello-java_ed323c3c/_<{Main.java[Main[A' },
            detail = '',
            kind = 5,
            name = 'A',
            range = {
              ['end'] = { character = 26, line = 3 },
              start = { character = 1, line = 3 },
            },
            selectionRange = {
              ['end'] = { character = 8, line = 3 },
              start = { character = 7, line = 3 },
            },
            tags = {},
            uri = 'file:///home/jiangyinzuo/hello-java/Main.java',
          },
          {
            data = { element = '=hello-java_ed323c3c/_<mylist{MyList.java[MyList[Inner' },
            detail = 'mylist',
            kind = 5,
            name = 'MyList$Inner',
            range = {
              ['end'] = { character = 37, line = 3 },
              start = { character = 1, line = 3 },
            },
            selectionRange = {
              ['end'] = { character = 19, line = 3 },
              start = { character = 14, line = 3 },
            },
            tags = {},
            uri = 'file:///home/jiangyinzuo/hello-java/mylist/MyList.java',
          },
        }

        local server = _create_server({
          capabilities = {
            positionEncoding = "utf-8"
          },
        })
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes']
        local bufnr = vim.api.nvim_get_current_buf()
        vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
          'package mylist;',
          '',
          'public class MyList {',
          ' static class Inner extends MyList{}',
          '~}',
        })
        handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr })
        return vim.fn.getqflist()
      ]=])

      local expected = {
        {
          bufnr = 2,
          col = 2,
          end_col = 0,
          end_lnum = 0,
          lnum = 4,
          module = '',
          nr = 0,
          pattern = '',
          text = 'A',
          type = '',
          valid = 1,
          vcol = 0,
        },
        {
          bufnr = 3,
          col = 2,
          end_col = 0,
          end_lnum = 0,
          lnum = 4,
          module = '',
          nr = 0,
          pattern = '',
          text = 'MyList$Inner mylist',
          type = '',
          valid = 1,
          vcol = 0,
        },
      }
      eq(expected, qflist)
    end)
  end)

  describe('vim.lsp.buf.typehierarchy supertypes', function()
    it('does nothing for an empty response', function()
      local qflist_count = exec_lua([=[
        require'vim.lsp.handlers'['typeHierarchy/supertypes'](nil, nil, {})
        return #vim.fn.getqflist()
      ]=])
      eq(0, qflist_count)
    end)

    it('opens the quickfix list with the right supertypes', function()
      clear()
      exec_lua(create_server_definition)
      local qflist = exec_lua([=[
        local clangd_response = { {
          data = {
            parents = { {
              parents = { {
                parents = { {
                  parents = {},
                  symbolID = "62B3D268A01B9978"
                } },
                symbolID = "DC9B0AD433B43BEC"
              } },
              symbolID = "06B5F6A19BA9F6A8"
            } },
            symbolID = "EDC336589C09ABB2"
          },
          kind = 5,
          name = "D2",
          range = {
            ["end"] = {
              character = 8,
              line = 3,
            },
            start = {
              character = 6,
              line = 3,
            }
          },
          selectionRange = {
            ["end"] = {
              character = 8,
              line = 3,
            },
            start = {
              character = 6,
              line = 3
            }
          },
          uri = "file:///home/jiangyinzuo/hello.cpp"
        }, {
          data = {
            parents = { {
              parents = { {
                parents = { {
                  parents = {},
                  symbolID = "62B3D268A01B9978"
                } },
                symbolID = "DC9B0AD433B43BEC"
              } },
              symbolID = "06B5F6A19BA9F6A8"
            } },
            symbolID = "AFFCAED15557EF08"
          },
          kind = 5,
          name = "D1",
          range = {
            ["end"] = {
              character = 8,
              line = 2
            },
            start = {
              character = 6,
              line = 2
            }
          },
          selectionRange = {
            ["end"] = {
              character = 8,
              line = 2
            },
            start = {
              character = 6,
              line = 2
            }
          },
          uri = "file:///home/jiangyinzuo/hello.cpp"
        } }

        local server = _create_server({
          capabilities = {
            positionEncoding = "utf-8"
          },
        })
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes']
        local bufnr = vim.api.nvim_get_current_buf()
        vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
          'class B : public A{};',
          'class C : public B{};',
          'class D1 : public C{};',
          'class D2 : public C{};',
          'class E : public D1, D2 {};',
        })

        handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr })
        return vim.fn.getqflist()
      ]=])

      local expected = {
        {
          bufnr = 2,
          col = 7,
          end_col = 0,
          end_lnum = 0,
          lnum = 4,
          module = '',
          nr = 0,
          pattern = '',
          text = 'D2',
          type = '',
          valid = 1,
          vcol = 0,
        },
        {
          bufnr = 2,
          col = 7,
          end_col = 0,
          end_lnum = 0,
          lnum = 3,
          module = '',
          nr = 0,
          pattern = '',
          text = 'D1',
          type = '',
          valid = 1,
          vcol = 0,
        },
      }

      eq(expected, qflist)
    end)

    it('opens the quickfix list with the right supertypes and details', function()
      clear()
      exec_lua(create_server_definition)
      local qflist = exec_lua([=[
        local jdtls_response = {
          {
            data = { element = '=hello-java_ed323c3c/_<{Main.java[Main[A' },
            detail = '',
            kind = 5,
            name = 'A',
            range = {
              ['end'] = { character = 26, line = 3 },
              start = { character = 1, line = 3 },
            },
            selectionRange = {
              ['end'] = { character = 8, line = 3 },
              start = { character = 7, line = 3 },
            },
            tags = {},
            uri = 'file:///home/jiangyinzuo/hello-java/Main.java',
          },
          {
            data = { element = '=hello-java_ed323c3c/_<mylist{MyList.java[MyList[Inner' },
            detail = 'mylist',
            kind = 5,
            name = 'MyList$Inner',
            range = {
              ['end'] = { character = 37, line = 3 },
              start = { character = 1, line = 3 },
            },
            selectionRange = {
              ['end'] = { character = 19, line = 3 },
              start = { character = 14, line = 3 },
            },
            tags = {},
            uri = 'file:///home/jiangyinzuo/hello-java/mylist/MyList.java',
          },
        }

        local server = _create_server({
          capabilities = {
            positionEncoding = "utf-8"
          },
        })
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes']
        local bufnr = vim.api.nvim_get_current_buf()
        vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
          'package mylist;',
          '',
          'public class MyList {',
          ' static class Inner extends MyList{}',
          '~}',
        })
        handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr })
        return vim.fn.getqflist()
      ]=])

      local expected = {
        {
          bufnr = 2,
          col = 2,
          end_col = 0,
          end_lnum = 0,
          lnum = 4,
          module = '',
          nr = 0,
          pattern = '',
          text = 'A',
          type = '',
          valid = 1,
          vcol = 0,
        },
        {
          bufnr = 3,
          col = 2,
          end_col = 0,
          end_lnum = 0,
          lnum = 4,
          module = '',
          nr = 0,
          pattern = '',
          text = 'MyList$Inner mylist',
          type = '',
          valid = 1,
          vcol = 0,
        },
      }
      eq(expected, qflist)
    end)
  end)

  describe('vim.lsp.buf.rename', function()
    for _, test in ipairs({
      {
        it = 'does not attempt to rename on nil response',
        name = 'prepare_rename_nil',
        expected_handlers = {
          { NIL, {}, { method = 'shutdown', client_id = 1 } },
          { NIL, {}, { method = 'start', client_id = 1 } },
        },
      },
      {
        it = 'handles prepareRename placeholder response',
        name = 'prepare_rename_placeholder',
        expected_handlers = {
          { NIL, {}, { method = 'shutdown', client_id = 1 } },
          { NIL, NIL, { method = 'textDocument/rename', client_id = 1, bufnr = 1 } },
          { NIL, {}, { method = 'start', client_id = 1 } },
        },
        expected_text = 'placeholder', -- see fake lsp response
      },
      {
        it = 'handles range response',
        name = 'prepare_rename_range',
        expected_handlers = {
          { NIL, {}, { method = 'shutdown', client_id = 1 } },
          { NIL, NIL, { method = 'textDocument/rename', client_id = 1, bufnr = 1 } },
          { NIL, {}, { method = 'start', client_id = 1 } },
        },
        expected_text = 'line', -- see test case and fake lsp response
      },
      {
        it = 'handles error',
        name = 'prepare_rename_error',
        expected_handlers = {
          { NIL, {}, { method = 'shutdown', client_id = 1 } },
          { NIL, {}, { method = 'start', client_id = 1 } },
        },
      },
    }) do
      it(test.it, function()
        local client --- @type vim.lsp.Client
        test_rpc_server {
          test_name = test.name,
          on_init = function(_client)
            client = _client
            eq(true, client.server_capabilities().renameProvider.prepareProvider)
          end,
          on_setup = function()
            exec_lua([=[
            local bufnr = vim.api.nvim_get_current_buf()
            lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
            vim.lsp._stubs = {}
            vim.fn.input = function(opts, on_confirm)
              vim.lsp._stubs.input_prompt = opts.prompt
              vim.lsp._stubs.input_text = opts.default
              return 'renameto' -- expect this value in fake lsp
            end
            vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {'', 'this is line two'})
            vim.fn.cursor(2, 13) -- the space between "line" and "two"
          ]=])
          end,
          on_exit = function(code, signal)
            eq(0, code, 'exit code')
            eq(0, signal, 'exit signal')
          end,
          on_handler = function(err, result, ctx)
            -- Don't compare & assert params and version, they're not relevant for the testcase
            -- This allows us to be lazy and avoid declaring them
            ctx.params = nil
            ctx.version = nil

            eq(table.remove(test.expected_handlers), { err, result, ctx }, 'expected handler')
            if ctx.method == 'start' then
              exec_lua('vim.lsp.buf.rename()')
            end
            if ctx.method == 'shutdown' then
              if test.expected_text then
                eq('New Name: ', exec_lua('return vim.lsp._stubs.input_prompt'))
                eq(test.expected_text, exec_lua('return vim.lsp._stubs.input_text'))
              end
              client.stop()
            end
          end,
        }
      end)
    end
  end)

  describe('vim.lsp.buf.code_action', function()
    it('Calls client side command if available', function()
      local client --- @type vim.lsp.Client
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'code_action_with_resolve',
        on_init = function(client_)
          client = client_
        end,
        on_setup = function() end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx })
          if ctx.method == 'start' then
            exec_lua([[
              vim.lsp.commands['dummy1'] = function(cmd)
                vim.lsp.commands['dummy2'] = function()
                end
              end
              local bufnr = vim.api.nvim_get_current_buf()
              vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
              vim.fn.inputlist = function()
                return 1
              end
              vim.lsp.buf.code_action()
            ]])
          elseif ctx.method == 'shutdown' then
            eq('function', exec_lua [[return type(vim.lsp.commands['dummy2'])]])
            client.stop()
          end
        end,
      }
    end)
    it('Calls workspace/executeCommand if no client side command', function()
      local client --- @type vim.lsp.Client
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        {
          NIL,
          { command = 'dummy1', title = 'Command 1' },
          { bufnr = 1, method = 'workspace/executeCommand', client_id = 1 },
        },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      test_rpc_server({
        test_name = 'code_action_server_side_command',
        on_init = function(client_)
          client = client_
        end,
        on_setup = function() end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code', fake_lsp_logfile)
          eq(0, signal, 'exit signal', fake_lsp_logfile)
        end,
        on_handler = function(err, result, ctx)
          ctx.params = nil -- don't compare in assert
          ctx.version = nil
          eq(table.remove(expected_handlers), { err, result, ctx })
          if ctx.method == 'start' then
            exec_lua([[
              local bufnr = vim.api.nvim_get_current_buf()
              vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
              vim.fn.inputlist = function()
                return 1
              end
              vim.lsp.buf.code_action()
            ]])
          elseif ctx.method == 'shutdown' then
            client.stop()
          end
        end,
      })
    end)
    it('Filters and automatically applies action if requested', function()
      local client --- @type vim.lsp.Client
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'code_action_filter',
        on_init = function(client_)
          client = client_
        end,
        on_setup = function() end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx })
          if ctx.method == 'start' then
            exec_lua([[
              vim.lsp.commands['preferred_command'] = function(cmd)
                vim.lsp.commands['executed_preferred'] = function()
                end
              end
              vim.lsp.commands['type_annotate_command'] = function(cmd)
                vim.lsp.commands['executed_type_annotate'] = function()
                end
              end
              local bufnr = vim.api.nvim_get_current_buf()
              vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
              vim.lsp.buf.code_action({ filter = function(a) return a.isPreferred end, apply = true, })
              vim.lsp.buf.code_action({
                  -- expect to be returned actions 'type-annotate' and 'type-annotate.foo'
                  context = { only = { 'type-annotate' }, },
                  apply = true,
                  filter = function(a)
                      if a.kind == 'type-annotate.foo' then
                        vim.lsp.commands['filtered_type_annotate_foo'] = function() end
                        return false
                      elseif a.kind == 'type-annotate' then
                        return true
                      else
                        assert(nil, 'unreachable')
                      end
                  end,
              })
            ]])
          elseif ctx.method == 'shutdown' then
            eq('function', exec_lua [[return type(vim.lsp.commands['executed_preferred'])]])
            eq('function', exec_lua [[return type(vim.lsp.commands['filtered_type_annotate_foo'])]])
            eq('function', exec_lua [[return type(vim.lsp.commands['executed_type_annotate'])]])
            client.stop()
          end
        end,
      }
    end)
    it('Fallback to command execution on resolve error', function()
      clear()
      exec_lua(create_server_definition)
      local result = exec_lua([[
        local server = _create_server({
          capabilities = {
            executeCommandProvider = {
              commands = {"command:1"},
            },
            codeActionProvider = {
              resolveProvider = true
            }
          },
          handlers = {
            ["textDocument/codeAction"] = function(_, _, callback)
              callback(nil, {
                {
                  title = "Code Action 1",
                  command = {
                    title = "Command 1",
                    command = "command:1",
                  }
                }
              })
            end,
            ["codeAction/resolve"] = function(_, _, callback)
              callback("resolve failed", nil)
            end,
          }
        })

        local client_id = vim.lsp.start({
          name = "dummy",
          cmd = server.cmd,
        })

        vim.lsp.buf.code_action({ apply = true })
        vim.lsp.stop_client(client_id)
        return server.messages
      ]])
      eq('codeAction/resolve', result[4].method)
      eq('workspace/executeCommand', result[5].method)
      eq('command:1', result[5].params.command)
    end)
  end)

  describe('vim.lsp.commands', function()
    it('Accepts only string keys', function()
      matches(
        '.*The key for commands in `vim.lsp.commands` must be a string',
        pcall_err(exec_lua, 'vim.lsp.commands[1] = function() end')
      )
    end)
    it('Accepts only function values', function()
      matches(
        '.*Command added to `vim.lsp.commands` must be a function',
        pcall_err(exec_lua, 'vim.lsp.commands.dummy = 10')
      )
    end)
  end)

  describe('vim.lsp.codelens', function()
    it('uses client commands', function()
      local client --- @type vim.lsp.Client
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'clientside_commands',
        on_init = function(client_)
          client = client_
        end,
        on_setup = function() end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx })
          if ctx.method == 'start' then
            local fake_uri = 'file:///fake/uri'
            local cmd = exec_lua(
              [[
              fake_uri = ...
              local bufnr = vim.uri_to_bufnr(fake_uri)
              vim.fn.bufload(bufnr)
              vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {'One line'})
              local lenses = {
                {
                  range = {
                    start = { line = 0, character = 0, },
                    ['end'] = { line = 0, character = 8 }
                  },
                  command = { title = 'Lens1', command = 'Dummy' }
                },
              }
              vim.lsp.codelens.on_codelens(nil, lenses, {method='textDocument/codeLens', client_id=1, bufnr=bufnr})
              local cmd_called = nil
              vim.lsp.commands['Dummy'] = function(command)
                cmd_called = command
              end
              vim.api.nvim_set_current_buf(bufnr)
              vim.lsp.codelens.run()
              return cmd_called
            ]],
              fake_uri
            )
            eq({ command = 'Dummy', title = 'Lens1' }, cmd)
          elseif ctx.method == 'shutdown' then
            client.stop()
          end
        end,
      }
    end)

    it('releases buffer refresh lock', function()
      local client --- @type vim.lsp.Client
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      test_rpc_server {
        test_name = 'codelens_refresh_lock',
        on_init = function(client_)
          client = client_
        end,
        on_setup = function()
          exec_lua([=[
              local bufnr = vim.api.nvim_get_current_buf()
              vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {'One line'})
              vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)

              CALLED = false
              RESPONSE = nil
              local on_codelens = vim.lsp.codelens.on_codelens
              vim.lsp.codelens.on_codelens = function (err, result, ...)
                CALLED = true
                RESPONSE = { err = err, result = result }
                return on_codelens(err, result, ...)
              end
            ]=])
        end,
        on_exit = function(code, signal)
          eq(0, code, 'exit code')
          eq(0, signal, 'exit signal')
        end,
        on_handler = function(err, result, ctx)
          eq(table.remove(expected_handlers), { err, result, ctx })
          if ctx.method == 'start' then
            -- 1. first codelens request errors
            local response = exec_lua([=[
              CALLED = false
              vim.lsp.codelens.refresh()
              vim.wait(100, function () return CALLED end)
              return RESPONSE
            ]=])
            eq({ err = { code = -32002, message = 'ServerNotInitialized' } }, response)

            -- 2. second codelens request runs
            response = exec_lua([=[
              CALLED = false
              local cmd_called = nil
              vim.lsp.commands["Dummy"] = function (command)
                cmd_called = command
              end
              vim.lsp.codelens.refresh()
              vim.wait(100, function () return CALLED end)
              vim.lsp.codelens.run()
              vim.wait(100, function () return cmd_called end)
              return cmd_called
            ]=])
            eq({ command = 'Dummy', title = 'Lens1' }, response)

            -- 3. third codelens request runs
            response = exec_lua([=[
              CALLED = false
              local cmd_called = nil
              vim.lsp.commands["Dummy"] = function (command)
                cmd_called = command
              end
              vim.lsp.codelens.refresh()
              vim.wait(100, function () return CALLED end)
              vim.lsp.codelens.run()
              vim.wait(100, function () return cmd_called end)
              return cmd_called
            ]=])
            eq({ command = 'Dummy', title = 'Lens2' }, response)
          elseif ctx.method == 'shutdown' then
            client.stop()
          end
        end,
      }
    end)

    it('refresh multiple buffers', function()
      local lens_title_per_fake_uri = {
        ['file:///fake/uri1'] = 'Lens1',
        ['file:///fake/uri2'] = 'Lens2',
      }
      clear()
      exec_lua(create_server_definition)

      -- setup lsp
      exec_lua(
        [[
          local lens_title_per_fake_uri = ...
          local server = _create_server({
            capabilities = {
              codeLensProvider = {
                resolveProvider = true
              },
            },
            handlers = {
              ["textDocument/codeLens"] = function(method, params, callback)
                local lenses = {
                  {
                    range = {
                      start = { line = 0, character = 0 },
                      ['end'] = { line = 0, character = 0 },
                    },
                    command = {
                      title = lens_title_per_fake_uri[params.textDocument.uri],
                      command = 'Dummy',
                    },
                  },
                }
                callback(nil, lenses)
              end,
            }
          })

          CLIENT_ID = vim.lsp.start({
            name = "dummy",
            cmd = server.cmd,
          })
        ]],
        lens_title_per_fake_uri
      )

      -- create buffers and setup handler
      exec_lua(
        [[
          local lens_title_per_fake_uri = ...
          local default_buf = vim.api.nvim_get_current_buf()
          for fake_uri, _ in pairs(lens_title_per_fake_uri) do
            local bufnr = vim.uri_to_bufnr(fake_uri)
            vim.api.nvim_set_current_buf(bufnr)
            vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {'Some contents'})
            vim.lsp.buf_attach_client(bufnr, CLIENT_ID)
          end
          vim.api.nvim_buf_delete(default_buf, {force = true})

          REQUEST_COUNT = vim.tbl_count(lens_title_per_fake_uri)
          RESPONSES = {}
          local on_codelens = vim.lsp.codelens.on_codelens
          vim.lsp.codelens.on_codelens = function (err, result, ctx, ...)
            table.insert(RESPONSES, { err = err, result = result, ctx = ctx })
            return on_codelens(err, result, ctx, ...)
          end
        ]],
        lens_title_per_fake_uri
      )

      -- call codelens refresh
      local cmds = exec_lua([[
        RESPONSES = {}
        vim.lsp.codelens.refresh()
        vim.wait(100, function () return #RESPONSES >= REQUEST_COUNT end)

        local cmds = {}
        for _, resp in ipairs(RESPONSES) do
          local uri = resp.ctx.params.textDocument.uri
          cmds[uri] = resp.result[1].command
        end
        return cmds
      ]])
      eq({ command = 'Dummy', title = 'Lens1' }, cmds['file:///fake/uri1'])
      eq({ command = 'Dummy', title = 'Lens2' }, cmds['file:///fake/uri2'])
    end)
  end)

  describe('vim.lsp.buf.format', function()
    it('Aborts with notify if no client matches filter', function()
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_init',
        on_init = function(c)
          client = c
        end,
        on_handler = function()
          local notify_msg = exec_lua([[
            local bufnr = vim.api.nvim_get_current_buf()
            vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
            local notify_msg
            local notify = vim.notify
            vim.notify = function(msg, log_level)
              notify_msg = msg
            end
            vim.lsp.buf.format({ name = 'does-not-exist' })
            vim.notify = notify
            return notify_msg
          ]])
          eq('[LSP] Format request failed, no matching language servers.', notify_msg)
          client.stop()
        end,
      }
    end)
    it('Sends textDocument/formatting request to format buffer', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_formatting',
        on_init = function(c)
          client = c
        end,
        on_handler = function(_, _, ctx)
          table.remove(expected_handlers)
          if ctx.method == 'start' then
            local notify_msg = exec_lua([[
              local bufnr = vim.api.nvim_get_current_buf()
              vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
              local notify_msg
              local notify = vim.notify
              vim.notify = function(msg, log_level)
                notify_msg = msg
              end
              vim.lsp.buf.format({ bufnr = bufnr })
              vim.notify = notify
              return notify_msg
            ]])
            eq(NIL, notify_msg)
          elseif ctx.method == 'shutdown' then
            client.stop()
          end
        end,
      }
    end)
    it('Can format async', function()
      local expected_handlers = {
        { NIL, {}, { method = 'shutdown', client_id = 1 } },
        { NIL, {}, { method = 'start', client_id = 1 } },
      }
      local client --- @type vim.lsp.Client
      test_rpc_server {
        test_name = 'basic_formatting',
        on_init = function(c)
          client = c
        end,
        on_handler = function(_, _, ctx)
          table.remove(expected_handlers)
          if ctx.method == 'start' then
            local result = exec_lua([[
              local bufnr = vim.api.nvim_get_current_buf()
              vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)

              local notify_msg
              local notify = vim.notify
              vim.notify = function(msg, log_level)
                notify_msg = msg
              end

              local handler = vim.lsp.handlers['textDocument/formatting']
              local handler_called = false
              vim.lsp.handlers['textDocument/formatting'] = function(...)
                handler_called = true
              end

              vim.lsp.buf.format({ bufnr = bufnr, async = true })
              vim.wait(1000, function() return handler_called end)

              vim.notify = notify
              vim.lsp.handlers['textDocument/formatting'] = handler
              return {notify = notify_msg, handler_called = handler_called}
            ]])
            eq({ handler_called = true }, result)
          elseif ctx.method == 'shutdown' then
            client.stop()
          end
        end,
      }
    end)
    it('format formats range in visual mode', function()
      exec_lua(create_server_definition)
      local result = exec_lua([[
        local server = _create_server({ capabilities = {
          documentFormattingProvider = true,
          documentRangeFormattingProvider = true,
        }})
        local bufnr = vim.api.nvim_get_current_buf()
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        vim.api.nvim_win_set_buf(0, bufnr)
        vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {'foo', 'bar'})
        vim.api.nvim_win_set_cursor(0, { 1, 0 })
        vim.cmd.normal('v')
        vim.api.nvim_win_set_cursor(0, { 2, 3 })
        vim.lsp.buf.format({ bufnr = bufnr, false })
        vim.lsp.stop_client(client_id)
        return server.messages
      ]])
      eq('textDocument/rangeFormatting', result[3].method)
      local expected_range = {
        start = { line = 0, character = 0 },
        ['end'] = { line = 1, character = 4 },
      }
      eq(expected_range, result[3].params.range)
    end)
    it('format formats range in visual line mode', function()
      exec_lua(create_server_definition)
      local result = exec_lua([[
        local server = _create_server({ capabilities = {
          documentFormattingProvider = true,
          documentRangeFormattingProvider = true,
        }})
        local bufnr = vim.api.nvim_get_current_buf()
        local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
        vim.api.nvim_win_set_buf(0, bufnr)
        vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {'foo', 'bar baz'})
        vim.api.nvim_win_set_cursor(0, { 1, 2 })
        vim.cmd.normal('V')
        vim.api.nvim_win_set_cursor(0, { 2, 1 })
        vim.lsp.buf.format({ bufnr = bufnr, false })

        -- Format again with visual lines going from bottom to top
        -- Must result in same formatting
        vim.cmd.normal("<ESC>")
        vim.api.nvim_win_set_cursor(0, { 2, 1 })
        vim.cmd.normal('V')
        vim.api.nvim_win_set_cursor(0, { 1, 2 })
        vim.lsp.buf.format({ bufnr = bufnr, false })

        vim.lsp.stop_client(client_id)
        return server.messages
      ]])
      local expected_methods = {
        'initialize',
        'initialized',
        'textDocument/rangeFormatting',
        '$/cancelRequest',
        'textDocument/rangeFormatting',
        '$/cancelRequest',
        'shutdown',
        'exit',
      }
      eq(
        expected_methods,
        vim.tbl_map(function(x)
          return x.method
        end, result)
      )
      -- uses first column of start line and last column of end line
      local expected_range = {
        start = { line = 0, character = 0 },
        ['end'] = { line = 1, character = 7 },
      }
      eq(expected_range, result[3].params.range)
      eq(expected_range, result[5].params.range)
    end)
    it('Aborts with notify if no clients support requested method', function()
      exec_lua(create_server_definition)
      exec_lua([[
        vim.notify = function(msg, _)
          notify_msg = msg
        end
      ]])
      local fail_msg = '[LSP] Format request failed, no matching language servers.'
      --- @param name string
      --- @param formatting boolean
      --- @param range_formatting boolean
      local function check_notify(name, formatting, range_formatting)
        local timeout_msg = '[LSP][' .. name .. '] timeout'
        exec_lua(
          [[
          local formatting, range_formatting, name = ...
          local server = _create_server({ capabilities = {
            documentFormattingProvider = formatting,
            documentRangeFormattingProvider = range_formatting,
          }})
          vim.lsp.start({ name = name, cmd = server.cmd })
          notify_msg = nil
          vim.lsp.buf.format({ name = name, timeout_ms = 1 })
        ]],
          formatting,
          range_formatting,
          name
        )
        eq(formatting and timeout_msg or fail_msg, exec_lua('return notify_msg'))
        exec_lua([[
          notify_msg = nil
          vim.lsp.buf.format({ name = name, timeout_ms = 1, range = {start={1, 0}, ['end']={1, 0}}})
        ]])
        eq(range_formatting and timeout_msg or fail_msg, exec_lua('return notify_msg'))
      end
      check_notify('none', false, false)
      check_notify('formatting', true, false)
      check_notify('rangeFormatting', false, true)
      check_notify('both', true, true)
    end)
  end)

  describe('vim.lsp.tagfunc', function()
    before_each(function()
      clear()
      ---@type lsp.Location[]
      local mock_locations = {
        {
          range = {
            ['start'] = { line = 5, character = 23 },
            ['end'] = { line = 10, character = 0 },
          },
          uri = 'test://buf',
        },
        {
          range = {
            ['start'] = { line = 42, character = 10 },
            ['end'] = { line = 44, character = 0 },
          },
          uri = 'test://another-file',
        },
      }
      exec_lua(create_server_definition)
      exec_lua(
        [[
        _G.mock_locations = ...
        _G.server = _create_server({
          ---@type lsp.ServerCapabilities
          capabilities = {
            definitionProvider = true,
            workspaceSymbolProvider = true,
          },
          handlers = {
            ---@return lsp.Location[]
            ['textDocument/definition'] = function(_, _, callback)
              callback(nil, { _G.mock_locations[1] })
            end,
            ---@return lsp.WorkspaceSymbol[]
            ['workspace/symbol'] = function(_, request, callback)
              assert(request.query == 'foobar')
              callback(nil, {
                {
                  name = 'foobar',
                  kind = 13, ---@type lsp.SymbolKind
                  location = _G.mock_locations[1],
                },
                {
                  name = 'vim.foobar',
                  kind = 12, ---@type lsp.SymbolKind
                  location = _G.mock_locations[2],
                }
              })
            end,
          },
        })
        _G.client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
      ]],
        mock_locations
      )
    end)
    after_each(function()
      exec_lua [[
        vim.lsp.stop_client(_G.client_id)
      ]]
    end)

    it('with flags=c, returns matching tags using textDocument/definition', function()
      local result = exec_lua [[
        return vim.lsp.tagfunc('foobar', 'c')
      ]]
      eq({
        {
          cmd = '/\\%6l\\%1c/', -- for location (5, 23)
          filename = 'test://buf',
          name = 'foobar',
        },
      }, result)
    end)

    it('without flags=c, returns all matching tags using workspace/symbol', function()
      local result = exec_lua [[
        return vim.lsp.tagfunc('foobar', '')
      ]]
      eq({
        {
          cmd = '/\\%6l\\%1c/', -- for location (5, 23)
          filename = 'test://buf',
          kind = 'Variable',
          name = 'foobar',
        },
        {
          cmd = '/\\%43l\\%1c/', -- for location (42, 10)
          filename = 'test://another-file',
          kind = 'Function',
          name = 'vim.foobar',
        },
      }, result)
    end)
  end)

  describe('cmd', function()
    it('can connect to lsp server via rpc.connect', function()
      local result = exec_lua [[
        local uv = vim.uv
        local server = uv.new_tcp()
        local init = nil
        server:bind('127.0.0.1', 0)
        server:listen(127, function(err)
          assert(not err, err)
          local socket = uv.new_tcp()
          server:accept(socket)
          socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body)
            init = body
            socket:close()
          end))
        end)
        local port = server:getsockname().port
        vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) })
        vim.wait(1000, function() return init ~= nil end)
        assert(init, "server must receive `initialize` request")
        server:close()
        server:shutdown()
        return vim.json.decode(init)
      ]]
      eq('initialize', result.method)
    end)
    it('can connect to lsp server via pipe or domain_socket', function()
      local tmpfile --- @type string
      if is_os('win') then
        tmpfile = '\\\\.\\\\pipe\\pipe.test'
      else
        tmpfile = tmpname()
        os.remove(tmpfile)
      end
      local result = exec_lua(
        [[
        local SOCK = ...
        local uv = vim.uv
        local server = uv.new_pipe(false)
        server:bind(SOCK)
        local init = nil

        server:listen(127, function(err)
                assert(not err, err)
                local client = uv.new_pipe()
                server:accept(client)
                client:read_start(require("vim.lsp.rpc").create_read_loop(function(body)
                        init = body
                        client:close()
                end))
        end)
        vim.lsp.start({ name = "dummy", cmd = vim.lsp.rpc.connect(SOCK) })
        vim.wait(1000, function() return init ~= nil end)
        assert(init, "server must receive `initialize` request")
        server:close()
        server:shutdown()
        return vim.json.decode(init)
      ]],
        tmpfile
      )
      eq('initialize', result.method)
    end)
  end)

  describe('handlers', function()
    it('handler can return false as response', function()
      local result = exec_lua [[
        local uv = vim.uv
        local server = uv.new_tcp()
        local messages = {}
        local responses = {}
        server:bind('127.0.0.1', 0)
        server:listen(127, function(err)
          assert(not err, err)
          local socket = uv.new_tcp()
          server:accept(socket)
          socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body)
            local payload = vim.json.decode(body)
            if payload.method then
              table.insert(messages, payload.method)
              if payload.method == 'initialize' then
                local msg = vim.json.encode({
                  id = payload.id,
                  jsonrpc = '2.0',
                  result = {
                    capabilities = {}
                  },
                })
                socket:write(table.concat({'Content-Length: ', tostring(#msg), '\r\n\r\n', msg}))
              elseif payload.method == 'initialized' then
                local msg = vim.json.encode({
                  id = 10,
                  jsonrpc = '2.0',
                  method = 'dummy',
                  params = {},
                })
                socket:write(table.concat({'Content-Length: ', tostring(#msg), '\r\n\r\n', msg}))
              end
            else
              table.insert(responses, payload)
              socket:close()
            end
          end))
        end)
        local port = server:getsockname().port
        local handler_called = false
        vim.lsp.handlers['dummy'] = function(err, result)
          handler_called = true
          return false
        end
        local client_id = vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) })
        local client = vim.lsp.get_client_by_id(client_id)
        vim.wait(1000, function() return #messages == 2 and handler_called and #responses == 1 end)
        server:close()
        server:shutdown()
        return {
          messages = messages,
          handler_called = handler_called,
          responses = responses }
      ]]
      local expected = {
        messages = { 'initialize', 'initialized' },
        handler_called = true,
        responses = {
          {
            id = 10,
            jsonrpc = '2.0',
            result = false,
          },
        },
      }
      eq(expected, result)
    end)
  end)

  describe('#dynamic vim.lsp._dynamic', function()
    it('supports dynamic registration', function()
      ---@type string
      local root_dir = tmpname()
      os.remove(root_dir)
      mkdir(root_dir)
      local tmpfile = root_dir .. '/dynamic.foo'
      local file = io.open(tmpfile, 'w')
      if file then
        file:close()
      end

      exec_lua(create_server_definition)
      local result = exec_lua(
        [[
        local root_dir, tmpfile = ...

        local server = _create_server()
        local client_id = vim.lsp.start({
          name = 'dynamic-test',
          cmd = server.cmd,
          root_dir = root_dir,
          get_language_id = function()
            return "dummy-lang"
          end,
          capabilities = {
            textDocument = {
              formatting = {
                dynamicRegistration = true,
              },
              rangeFormatting = {
                dynamicRegistration = true,
              },
            },
          },
        })

        local expected_messages = 2 -- initialize, initialized

        vim.lsp.handlers['client/registerCapability'](nil, {
          registrations = {
            {
              id = 'formatting',
              method = 'textDocument/formatting',
              registerOptions = {
                documentSelector = {{
                  pattern = root_dir .. '/*.foo',
                }},
              },
            },
          },
        }, { client_id = client_id })

        vim.lsp.handlers['client/registerCapability'](nil, {
          registrations = {
            {
              id = 'range-formatting',
              method = 'textDocument/rangeFormatting',
              registerOptions = {
              documentSelector = {
                  {
                    language = "dummy-lang"
                  },
                }
              }
            },
          },
        }, { client_id = client_id })

        vim.lsp.handlers['client/registerCapability'](nil, {
          registrations = {
            {
              id = 'completion',
              method = 'textDocument/completion',
            },
          },
        }, { client_id = client_id })

        local result = {}
        local function check(method, fname)
          local bufnr = fname and vim.fn.bufadd(fname) or nil
          local client = vim.lsp.get_client_by_id(client_id)
          result[#result + 1] = {
            method = method,
            fname = fname,
            supported = client.supports_method(method, {bufnr = bufnr})
          }
        end


        check("textDocument/formatting")
        check("textDocument/formatting", tmpfile)
        check("textDocument/rangeFormatting")
        check("textDocument/rangeFormatting", tmpfile)
        check("textDocument/completion")

        return result
      ]],
        root_dir,
        tmpfile
      )

      eq(5, #result)
      eq({ method = 'textDocument/formatting', supported = false }, result[1])
      eq({ method = 'textDocument/formatting', supported = true, fname = tmpfile }, result[2])
      eq({ method = 'textDocument/rangeFormatting', supported = true }, result[3])
      eq({ method = 'textDocument/rangeFormatting', supported = true, fname = tmpfile }, result[4])
      eq({ method = 'textDocument/completion', supported = false }, result[5])
    end)
  end)

  describe('vim.lsp._watchfiles', function()
    local function test_filechanges(watchfunc)
      it(
        string.format('sends notifications when files change (watchfunc=%s)', watchfunc),
        function()
          if watchfunc == 'fswatch' then
            skip(is_os('win'), 'not supported on windows')
            skip(is_os('mac'), 'flaky test on mac')
            skip(
              not is_ci() and fn.executable('fswatch') == 0,
              'fswatch not installed and not on CI'
            )
          end

          if watchfunc == 'watch' then
            skip(is_os('mac'), 'flaky test on mac')
            skip(
              is_os('bsd'),
              'Stopped working on bsd after 3ca967387c49c754561c3b11a574797504d40f38'
            )
          else
            skip(
              is_os('bsd'),
              'kqueue only reports events on watched folder itself, not contained files #26110'
            )
          end

          local root_dir = tmpname()
          os.remove(root_dir)
          mkdir(root_dir)

          exec_lua(create_server_definition)
          local result = exec_lua(
            [[
          local root_dir, watchfunc = ...

          local server = _create_server()
          local client_id = vim.lsp.start({
            name = 'watchfiles-test',
            cmd = server.cmd,
            root_dir = root_dir,
            capabilities = {
              workspace = {
                didChangeWatchedFiles = {
                  dynamicRegistration = true,
                },
              },
            },
          })

          require('vim.lsp._watchfiles')._watchfunc = require('vim._watch')[watchfunc]

          local expected_messages = 0

          local msg_wait_timeout = watchfunc == 'watch' and 200 or 2500

          local function wait_for_message(incr)
            expected_messages = expected_messages + (incr or 1)
            assert(
              vim.wait(msg_wait_timeout, function()
                return #server.messages == expected_messages
              end),
              'Timed out waiting for expected number of messages. Current messages seen so far: '
                .. vim.inspect(server.messages)
            )
          end

          wait_for_message(2) -- initialize, initialized

          vim.lsp.handlers['client/registerCapability'](nil, {
            registrations = {
              {
                id = 'watchfiles-test-0',
                method = 'workspace/didChangeWatchedFiles',
                registerOptions = {
                  watchers = {
                    {
                      globPattern = '**/watch',
                      kind = 7,
                    },
                  },
                },
              },
            },
          }, { client_id = client_id })

          if watchfunc ~= 'watch' then
            vim.wait(100)
          end

          local path = root_dir .. '/watch'
          local tmp = vim.fn.tempname()
          io.open(tmp, 'w'):close()
          vim.uv.fs_rename(tmp, path)

          wait_for_message()

          os.remove(path)

          wait_for_message()

          vim.lsp.stop_client(client_id)

          return server.messages
        ]],
            root_dir,
            watchfunc
          )

          local uri = vim.uri_from_fname(root_dir .. '/watch')

          eq(6, #result)

          eq({
            method = 'workspace/didChangeWatchedFiles',
            params = {
              changes = {
                {
                  type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
                  uri = uri,
                },
              },
            },
          }, result[3])

          eq({
            method = 'workspace/didChangeWatchedFiles',
            params = {
              changes = {
                {
                  type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
                  uri = uri,
                },
              },
            },
          }, result[4])
        end
      )
    end

    test_filechanges('watch')
    test_filechanges('watchdirs')
    test_filechanges('fswatch')

    it('correctly registers and unregisters', function()
      local root_dir = '/some_dir'
      exec_lua(create_server_definition)
      local result = exec_lua(
        [[
        local root_dir = ...

        local server = _create_server()
        local client_id = vim.lsp.start({
          name = 'watchfiles-test',
          cmd = server.cmd,
          root_dir = root_dir,
          capabilities = {
            workspace = {
              didChangeWatchedFiles = {
                dynamicRegistration = true,
              },
            },
          },
        })

        local expected_messages = 2 -- initialize, initialized
        local function wait_for_messages()
          assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
        end

        wait_for_messages()

        local send_event
        require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
          local stopped = false
          send_event = function(...)
            if not stopped then
              callback(...)
            end
          end
          return function()
            stopped = true
          end
        end

        vim.lsp.handlers['client/registerCapability'](nil, {
          registrations = {
            {
              id = 'watchfiles-test-0',
              method = 'workspace/didChangeWatchedFiles',
              registerOptions = {
                watchers = {
                  {
                    globPattern = '**/*.watch0',
                  },
                },
              },
            },
          },
        }, { client_id = client_id })

        send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created)
        send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created)

        expected_messages = expected_messages + 1
        wait_for_messages()

        vim.lsp.handlers['client/registerCapability'](nil, {
          registrations = {
            {
              id = 'watchfiles-test-1',
              method = 'workspace/didChangeWatchedFiles',
              registerOptions = {
                watchers = {
                  {
                    globPattern = '**/*.watch1',
                  },
                },
              },
            },
          },
        }, { client_id = client_id })

        vim.lsp.handlers['client/unregisterCapability'](nil, {
          unregisterations = {
            {
              id = 'watchfiles-test-0',
              method = 'workspace/didChangeWatchedFiles',
            },
          },
        }, { client_id = client_id })

        send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created)
        send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created)

        expected_messages = expected_messages + 1
        wait_for_messages()

        return server.messages
      ]],
        root_dir
      )

      local function watched_uri(fname)
        return vim.uri_from_fname(root_dir .. '/' .. fname)
      end

      eq(4, #result)
      eq('workspace/didChangeWatchedFiles', result[3].method)
      eq({
        changes = {
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = watched_uri('file.watch0'),
          },
        },
      }, result[3].params)
      eq('workspace/didChangeWatchedFiles', result[4].method)
      eq({
        changes = {
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = watched_uri('file.watch1'),
          },
        },
      }, result[4].params)
    end)

    it('correctly handles the registered watch kind', function()
      local root_dir = 'some_dir'
      exec_lua(create_server_definition)
      local result = exec_lua(
        [[
        local root_dir = ...

        local server = _create_server()
        local client_id = vim.lsp.start({
          name = 'watchfiles-test',
          cmd = server.cmd,
          root_dir = root_dir,
          capabilities = {
            workspace = {
              didChangeWatchedFiles = {
                dynamicRegistration = true,
              },
            },
          },
        })

        local expected_messages = 2 -- initialize, initialized
        local function wait_for_messages()
          assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
        end

        wait_for_messages()

        local watch_callbacks = {}
        local function send_event(...)
          for _, cb in ipairs(watch_callbacks) do
            cb(...)
          end
        end
        require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
          table.insert(watch_callbacks, callback)
          return function()
            -- noop because this test never stops the watch
          end
        end

        local protocol = require('vim.lsp.protocol')

        local watchers = {}
        local max_kind = protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete
        for i = 0, max_kind do
          table.insert(watchers, {
            globPattern = {
              baseUri = vim.uri_from_fname('/dir'),
              pattern = 'watch'..tostring(i),
            },
            kind = i,
          })
        end
        vim.lsp.handlers['client/registerCapability'](nil, {
          registrations = {
            {
              id = 'watchfiles-test-kind',
              method = 'workspace/didChangeWatchedFiles',
              registerOptions = {
                watchers = watchers,
              },
            },
          },
        }, { client_id = client_id })

        for i = 0, max_kind do
          local filename = '/dir/watch' .. tostring(i)
          send_event(filename, vim._watch.FileChangeType.Created)
          send_event(filename, vim._watch.FileChangeType.Changed)
          send_event(filename, vim._watch.FileChangeType.Deleted)
        end

        expected_messages = expected_messages + 1
        wait_for_messages()

        return server.messages
      ]],
        root_dir
      )

      local function watched_uri(fname)
        return vim.uri_from_fname('/dir/' .. fname)
      end

      eq(3, #result)
      eq('workspace/didChangeWatchedFiles', result[3].method)
      eq({
        changes = {
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = watched_uri('watch1'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
            uri = watched_uri('watch2'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = watched_uri('watch3'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
            uri = watched_uri('watch3'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
            uri = watched_uri('watch4'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = watched_uri('watch5'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
            uri = watched_uri('watch5'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
            uri = watched_uri('watch6'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
            uri = watched_uri('watch6'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = watched_uri('watch7'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
            uri = watched_uri('watch7'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
            uri = watched_uri('watch7'),
          },
        },
      }, result[3].params)
    end)

    it('prunes duplicate events', function()
      local root_dir = 'some_dir'
      exec_lua(create_server_definition)
      local result = exec_lua(
        [[
        local root_dir = ...

        local server = _create_server()
        local client_id = vim.lsp.start({
          name = 'watchfiles-test',
          cmd = server.cmd,
          root_dir = root_dir,
          capabilities = {
            workspace = {
              didChangeWatchedFiles = {
                dynamicRegistration = true,
              },
            },
          },
        })

        local expected_messages = 2 -- initialize, initialized
        local function wait_for_messages()
          assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
        end

        wait_for_messages()

        local send_event
        require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
          send_event = callback
          return function()
            -- noop because this test never stops the watch
          end
        end

        vim.lsp.handlers['client/registerCapability'](nil, {
          registrations = {
            {
              id = 'watchfiles-test-kind',
              method = 'workspace/didChangeWatchedFiles',
              registerOptions = {
                watchers = {
                  {
                    globPattern = '**/*',
                  },
                },
              },
            },
          },
        }, { client_id = client_id })

        send_event('file1', vim._watch.FileChangeType.Created)
        send_event('file1', vim._watch.FileChangeType.Created) -- pruned
        send_event('file1', vim._watch.FileChangeType.Changed)
        send_event('file2', vim._watch.FileChangeType.Created)
        send_event('file1', vim._watch.FileChangeType.Changed) -- pruned

        expected_messages = expected_messages + 1
        wait_for_messages()

        return server.messages
      ]],
        root_dir
      )

      eq(3, #result)
      eq('workspace/didChangeWatchedFiles', result[3].method)
      eq({
        changes = {
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = vim.uri_from_fname('file1'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
            uri = vim.uri_from_fname('file1'),
          },
          {
            type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
            uri = vim.uri_from_fname('file2'),
          },
        },
      }, result[3].params)
    end)

    it("ignores registrations by servers when the client doesn't advertise support", function()
      exec_lua(create_server_definition)
      exec_lua([[
        server = _create_server()
        require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
          -- Since the registration is ignored, this should not execute and `watching` should stay false
          watching = true
          return function() end
        end
      ]])

      local function check_registered(capabilities)
        return exec_lua(
          [[
          watching = false
          local client_id = vim.lsp.start({
            name = 'watchfiles-test',
            cmd = server.cmd,
            root_dir = 'some_dir',
            capabilities = ...,
          }, {
            reuse_client = function() return false end,
          })

          vim.lsp.handlers['client/registerCapability'](nil, {
            registrations = {
              {
                id = 'watchfiles-test-kind',
                method = 'workspace/didChangeWatchedFiles',
                registerOptions = {
                  watchers = {
                    {
                      globPattern = '**/*',
                    },
                  },
                },
              },
            },
          }, { client_id = client_id })

          -- Ensure no errors occur when unregistering something that was never really registered.
          vim.lsp.handlers['client/unregisterCapability'](nil, {
            unregisterations = {
              {
                id = 'watchfiles-test-kind',
                method = 'workspace/didChangeWatchedFiles',
              },
            },
          }, { client_id = client_id })

          vim.lsp.stop_client(client_id, true)
          return watching
        ]],
          capabilities
        )
      end

      eq(is_os('mac') or is_os('win'), check_registered(nil)) -- start{_client}() defaults to make_client_capabilities().
      eq(false, check_registered(vim.empty_dict()))
      eq(
        false,
        check_registered({
          workspace = {
            ignoreMe = true,
          },
        })
      )
      eq(
        false,
        check_registered({
          workspace = {
            didChangeWatchedFiles = {
              dynamicRegistration = false,
            },
          },
        })
      )
      eq(
        true,
        check_registered({
          workspace = {
            didChangeWatchedFiles = {
              dynamicRegistration = true,
            },
          },
        })
      )
    end)
  end)
end)
