local api = vim.api
local dap = require('dap')
local helpers = require("spec.helpers")
local wait = helpers.wait
local wait_for_response= helpers.wait_for_response
local run_and_wait_until_initialized = helpers.run_and_wait_until_initialized

local config = {
  type = 'dummy',
  request = 'launch',
  name = 'Launch file',
}


describe('dap with fake server', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    server.stop()
    dap.terminate({ all = true, hierarchy = true })
    dap.close()
    require('dap.breakpoints').clear()
    wait(function() return dap.session() == nil end)
  end)

  it("handler is called without response if there is an error", function()
    local session = run_and_wait_until_initialized(config, server)
    server.client.threads = function(self, request)
      self:send_err_response(request, "this is wrong", "err1")
    end

    local err, resp
    session:request("threads", nil, function (e, r)
      err = e
      resp = r
    end)
    wait(function() return err ~= nil end)
    assert.is_nil(resp)
    assert.are.same(tostring(err), "this is wrong")
  end)

  it('clear breakpoints clears all active breakpoints', function()
    local session = run_and_wait_until_initialized(config, server)
    assert.are_not.same(session, nil)
    assert.are.same(true, session.initialized)

    server.spy.clear()

    local buf1 = api.nvim_create_buf(false, true)
    api.nvim_buf_set_lines(buf1, 0, -1, false, {'line 1'})
    api.nvim_win_set_buf(0, buf1)
    api.nvim_win_set_cursor(0, { 1, 1 })
    dap.toggle_breakpoint()
    vim.wait(1000, function() return #server.spy.requests == 1 end, 100)
    assert.are.same(1, vim.tbl_count(require('dap.breakpoints').get()))
    assert.are.same('setBreakpoints', server.spy.requests[1].command)

    local buf2 = api.nvim_create_buf(false, true)
    api.nvim_buf_set_lines(buf2, 0, -1, false, {'line 1', 'line 2'})
    api.nvim_win_set_buf(0, buf2)
    api.nvim_win_set_cursor(0, { 1, 1 })
    dap.toggle_breakpoint()
    vim.wait(1000, function() return #server.spy.requests == 2 end, 100)

    assert.are.same(2, vim.tbl_count(require('dap.breakpoints').get()))

    server.spy.clear()
    dap.clear_breakpoints()
    vim.wait(1000, function() return #server.spy.requests == 2 end, 100)

    assert.are.same(0, vim.tbl_count(require('dap.breakpoints').get()))
    assert.are.same(2, vim.tbl_count(server.spy.requests))
    local setBreakpoints = server.spy.requests[1]
    assert.are.same({}, setBreakpoints.arguments.breakpoints)
  end)

  it('can handle stopped event for all threads', function()
    local session = run_and_wait_until_initialized(config, server)
    session:event_stopped({
      allThreadsStopped = true,
      reason = 'unknown',
    })
  end)

  it('Adds error diagnostic on stopped event due to exception', function()
    local buf = api.nvim_create_buf(true, false)
    local win = api.nvim_get_current_win()
    local tmpname = os.tmpname()
    os.remove(tmpname)
    api.nvim_buf_set_name(buf, tmpname)
    api.nvim_buf_set_lines(buf, 0, -1, false, {'line 1', 'line 2'})
    api.nvim_win_set_buf(win, buf)
    api.nvim_win_set_cursor(win, { 1, 0})

    local session = run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    local path = vim.uri_from_bufnr(buf)
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 1,
            column = 1,
            source = {
              path = path
            }
          },
        },
      })
    end
    session.capabilities.supportsExceptionInfoRequest = true
    server.client.exceptionInfo = function(self, request)
      self:send_response(request, {
        exceptionId = "XXX",
        breakMode = "unhandled"
      })
    end
    session:event_stopped({
      threadId = 1,
      reason = 'exception',
    })
    wait(function() return #server.spy.requests == 4 end, function()
      return {
        requests = server.spy.requests,
        responses = server.spy.responses
      }
    end)
    local diagnostics = vim.diagnostic.get(buf)
    for _, d in ipairs(diagnostics) do
      d._extmark_id = nil
    end
    local expected = {
      {
        bufnr = buf,
        col = 0,
        end_col = 0,
        end_lnum = 0,
        lnum = 0,
        message = 'Thread stopped due to exception (unhandled)',
        namespace = session.ns,
        severity = 1,
        source = 'nvim-dap',
      }
    }
    assert.are.same(expected, diagnostics)
  end)

  it('jumps to location if thread with same id is already stopped', function()
    local session = run_and_wait_until_initialized(config, server)

    -- Pretend to be stopped
    session.stopped_thread_id = 1

    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 1,
          },
        },
      })
    end
    session:event_stopped({
      allThreadsStopped = false,
      threadId = 1,
      reason = 'breakpoint',
    })
    vim.wait(1000, function() return #server.spy.requests == 3 end)
    local expected_commands = {"threads", "stackTrace", "scopes"}
    assert.are.same(
      expected_commands,
      vim.tbl_map(function(x) return x.command end, server.spy.requests)
    )
  end)

  it('jumps to location on stopped with reason=pause and allThreadsStopped', function()
    local session = run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 1,
          },
        },
      })
    end
    session:event_stopped({
      allThreadsStopped = true,
      threadId = 1,
      reason = 'pause',
    })
    -- should request threads, stackTrace and scopes on stopped event
    vim.wait(1000, function() return #server.spy.requests == 3 end)
    assert.are.same('threads', server.spy.requests[1].command)
    assert.are.same('stackTrace', server.spy.requests[2].command)
    assert.are_not.same(nil, session.current_frame)
    assert.are.same('stackFrame1', session.current_frame.name)
  end)

  it('jump to location results in nice error if location outside buffer contents', function()
    local buf = api.nvim_create_buf(true, false)
    local win = api.nvim_get_current_win()
    api.nvim_buf_set_lines(buf, 0, -1, false, {'line 1', 'line 2'})
    api.nvim_win_set_buf(win, buf)
    api.nvim_win_set_cursor(win, { 1, 0})

    local session = run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    local path = vim.uri_from_bufnr(buf)
    server.client.stackTrace = vim.schedule_wrap(function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 40,
            column = 3,
            source = {
              sourceReference = 0,
              path = path,
            },
          },
        },
      })
    end)
    local captured_msg
    vim.notify = function(...)
      local msg = select(1, ...)
      captured_msg = msg
    end
    session:event_stopped({
      threadId = 1,
      reason = 'breakpoint',
    })
    vim.wait(1000, function() return captured_msg ~= nil end)
    local msg = 'Adapter reported a frame in buf %d line 40 column 3, but: Cursor position outside buffer. Ensure executable is up2date and if using a source mapping ensure it is correct'
    assert.are.same(string.format(msg, vim.uri_to_bufnr(path)), captured_msg)
  end)

  it('Can handle frames without source/path on stopped event', function()
    run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 1,
          },
        },
      })
    end
    server.client:send_event('stopped', {
      threadId = 1,
      reason = 'unknown',
    })
    vim.wait(1000, function() return #server.spy.requests == 3 end, 100)
  end)

  it("Doesn't jump on stopped if continue is received before stacktrace response", function()
    local buf = api.nvim_create_buf(true, false)
    local win = api.nvim_get_current_win()
    api.nvim_buf_set_name(buf, "/tmp/dummy")
    api.nvim_buf_set_lines(buf, 0, -1, false, {"line 1", "line 2", "line 3"})
    api.nvim_win_set_buf(win, buf)
    api.nvim_win_set_cursor(win, { 1, 0})

    run_and_wait_until_initialized(config, server)
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    server.client:send_event('stopped', {
      threadId = 1,
      reason = 'unknown',
    })
    local path = vim.uri_from_bufnr(buf)
    server.client.stackTrace = function(self, request)
      self:send_event("continued", {
        threadId = 1,
      })
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 2,
            column = 4,
            source = {
              sourceReference = 0,
              path = path,
            },
          },
        },
      })
    end
    wait(function() return #server.spy.responses == 4 end)
    local expected = {
      bufnr = buf,
      cursor = {1 , 0}
    }
    assert.are.same(expected, {
      bufnr = api.nvim_win_get_buf(win),
      cursor = api.nvim_win_get_cursor(win)
    })
  end)

  it("Doesn't jump on stopped if continue is received before threads response", function()
    run_and_wait_until_initialized(config, server)
    server.client.threads = function(self, request)
      self:send_event("continued", {
        threadId = 1,
      })
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    local log = require('dap.log').create_logger('dap.log')
    local debug = log.debug
    local messages = {}
    log.debug = function(self, ...)
      table.insert(messages, {...})
      debug(self, ...)
    end
    server.client:send_event('stopped', {
      threadId = 1,
      reason = 'unknown',
    })
    wait_for_response(server, "threads")
    wait(function() return #messages >= 5 end)
    assert.are.same("Thread resumed during stopped event handling", messages[6][1])
  end)

  it("Clears stopped state on continued event", function()
    local buf = api.nvim_create_buf(true, false)
    local win = api.nvim_get_current_win()
    api.nvim_buf_set_name(buf, "/tmp/dummy1")
    api.nvim_buf_set_lines(buf, 0, -1, false, {"line 1", "line 2", "line 3"})
    api.nvim_win_set_buf(win, buf)
    api.nvim_win_set_cursor(win, { 1, 0})

    local session = run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    local path = vim.uri_from_bufnr(buf)
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 2,
            column = 4,
            source = {
              sourceReference = 0,
              path = path,
            },
          },
        },
      })
    end
    server.client.scopes = function(self, request)
      self:send_response(request, {
        scopes = {}
      })
    end
    server.client:send_event('stopped', {
      threadId = 1,
      reason = 'unknown',
    })

    wait_for_response(server, "scopes")
    assert.are.same({2, 3}, api.nvim_win_get_cursor(win))
    server.client:send_event('continued', {
      threadId = 1,
    })
    wait(function() return session.threads[1].stopped == false end)
    assert.is_nil(session.stopped_thread_id)
    assert.is_nil(session.current_frame)
    local signs = vim.fn.sign_getplaced(vim.uri_to_bufnr(path), {group = session.sign_group})
    assert.are.same({}, signs[1].signs)
  end)

  it('Deleting a buffer clears breakpoints for that buffer', function()
    local win = api.nvim_get_current_win()
    local buf1 = api.nvim_create_buf(false, true)
    api.nvim_buf_set_name(buf1, 'dummy_buf1')
    api.nvim_buf_set_lines(buf1, 0, -1, false, {'buf1: line1'})
    api.nvim_win_set_buf(win, buf1)
    api.nvim_win_set_cursor(win, { 1, 0 })
    dap.toggle_breakpoint()

    run_and_wait_until_initialized(config, server)
    -- wait for initialize, launch and one setBreakpoints request
    vim.wait(1000, function() return #server.spy.requests == 3 end, 100)
    assert.are.same(3, #server.spy.requests)
    server.spy.clear()
    api.nvim_buf_delete(buf1, { force = true })

    vim.wait(1000, function() return #server.spy.requests == 1 end, 100)
    local setBreakpoints = server.spy.requests[1]
    assert.are.same('setBreakpoints', setBreakpoints.command)
    assert.are.same('dummy_buf1', setBreakpoints.arguments.source.name)
    assert.are.same({}, setBreakpoints.arguments.breakpoints)

    assert.are.same({}, require('dap.breakpoints').get())
  end)

  it('prints formatted error on launch error', function()
    local captured_msg
    vim.notify = function(...)
      local msg = select(1, ...)
      captured_msg = msg
    end
    server.client.launch = function(self, request)
      self:send_err_response(request, 'Failure', {
        id = 1,
        format = 'Failed: {e}',
        showUser = true,
        variables = {
          e = 'Dummy'
        },
      })
    end
    run_and_wait_until_initialized(config, server)
    wait(function() return captured_msg ~= nil end)
    assert.are.same('Error on launch: Failed: Dummy', captured_msg)
  end)

  it('evaluates callable config', function()
    local callable_config = setmetatable(config, {
      __call = function(conf)
        local result = {}
        for k, v in pairs(conf) do
          result[k] = v
        end
        result.x = 1
        return result
      end,
    })
    local session = run_and_wait_until_initialized(callable_config, server)
    assert.are.same(session.config.x, 1)
  end)

  it("step does nothing if session is not stopped", function()
    local session = run_and_wait_until_initialized(config, server)
    dap.step_over()
    assert.are.same(session, dap.session())
  end)

  it("Run aborts if config value is dap.ABORT", function()
    local msg = nil
    require('dap.utils').notify = function(m)
      msg = m
    end
    dap.run({
      name = "dummy",
      type = "dummy",
      request = "launch",
      foo = function()
        return dap.ABORT
      end,
    })
    wait(function() return msg ~= nil end)
    assert.is_nil(dap.session())
    assert.are.same("Run aborted", msg)
  end)

  it("triggers on_session listener on session changes", function()
    local on_session_results = {}
    local conf1 = {
      name = "s1",
      type = "dummy",
      request = "launch"
    }
    dap.listeners.on_session["dap.testcase"] = function (old, new)
      table.insert(on_session_results, { old = old, new = new })
    end
    local session1 = run_and_wait_until_initialized(conf1, server)
    assert.are.same(1, #on_session_results)
    assert.are.same(session1.id, on_session_results[1].new.id)

    local conf2 = {
      name = "s2",
      type = "dummy",
      request = "launch"
    }
    local session2 = run_and_wait_until_initialized(conf2, server)
    assert.are.same(2, #on_session_results)
    assert.are.same(session1.id, on_session_results[2].old.id)
    assert.are.same(session2.id, on_session_results[2].new.id)

    dap.set_session(session1)
    assert.are.same(3, #on_session_results)

    dap.terminate({ all = true })
    wait(function() return dap.session() == nil end)
    assert.are.same(5, #on_session_results)
    assert.is_nil(on_session_results[5].new)
  end)
end)

describe('session disconnect', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    dap.close()
    require('dap.breakpoints').clear()
    server.stop()
    wait(function() return dap.session() == nil end)
  end)

  it('Can call close on session after session has already closed', function()
    local session = run_and_wait_until_initialized(config, server)
    assert.are.not_same(nil, session)
    local cb_called = false
    dap.disconnect(nil, function()
      cb_called = true
    end)
    vim.wait(1000, function() return cb_called end, 100)
    assert.are.same(true, cb_called)
    assert.are.same(nil, dap.session())
    session:close()
  end)

  it('Closes session on disconnect response', function()
    run_and_wait_until_initialized(config, server)
    server.spy.clear()

    local client = server.client
    -- override to not send terminate event as well
    client.disconnect = function(self, request)
      self:send_response(request, {})
    end

    local cb_called = false
    dap.disconnect(nil, function()
      cb_called = true
    end)
    vim.wait(1000, function() return cb_called end, 100)
    assert.are.same({}, server.spy.events)
    assert.are.same(1, #server.spy.responses)
    assert.are.same('disconnect', server.spy.responses[1].command)
    assert.are.same(nil, dap.session())
  end)

  it('Closes session if server closes connection', function()
    local session = run_and_wait_until_initialized(config, server)
    assert.are_not.same(nil, dap.session())
    assert.are.same(session, dap.session())
    server.stop()
    vim.wait(1000, function() return dap.session() == nil end, 100)
    assert.are.same(nil, server.client.socket)
    assert.are.same(nil, dap.session())
  end)

  it('Closes session if initialization fails', function()
    local launch_called = false
    server.client.launch = function(self, request)
      launch_called = true
      self:send_err_response(request, 'Dummy error')
    end
    local msg = nil
    require('dap.utils').notify = function(m)
      msg = m
    end
    dap.run(config)
    vim.wait(1000, function() return launch_called end, 100)
    vim.wait(1000, function() return dap.session() == nil end, 100)
    assert.are.same(nil, dap.session())
    assert.are.same('Error on launch: Dummy error', msg)
  end)

  it('Repeated disconnect after stopped server is safe and resets session', function()
    run_and_wait_until_initialized(config, server)
    server.stop()
    local cb_called = false
    for _ = 1, 10 do
      dap.disconnect()
    end
    dap.disconnect(nil, function()
      cb_called = true
    end)
    vim.wait(1000, function() return cb_called end, 100)
    assert.are.same(nil, dap.session())
  end)
end)

describe('request source', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    dap.close()
    server.stop()
    wait(function() return dap.session() == nil end)
  end)

  it('can jump to frame if source needs to be fetched', function()
    server.client.source = function(self, request)
      self:send_response(request, {
        content = 'foobar',
        mimeType = 'text/x-lldb.disassembly',
      })
    end
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 1,
            source = {
              sourceReference = 1
            }
          },
        },
      })
    end
    run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client:send_event('stopped', {
      threadId = 1,
      reason = 'breakpoint',
    })
    wait_for_response(server, 'source')
    assert.are.same('source', server.spy.responses[3].command)
    assert.are.same('foobar', server.spy.responses[3].body.content)
    wait(function()
      return 'foobar' == api.nvim_buf_get_lines(0, 0, -1, false)[1]
    end)
    local lines = api.nvim_buf_get_lines(0, 0, -1, false)
    assert.are.same({'foobar'}, lines)
  end)

  it('sets filetype based on mimetype if available', function()
    server.client.source = function(self, request)
      self:send_response(request, {
        content = '',
        mimeType = 'text/javascript',
      })
    end

    local session = run_and_wait_until_initialized(config, server)
    local source = {
      sourceReference = 1
    }
    local bufnr = nil
    session:source(source, function(_, buf)
      bufnr = buf
    end)
    vim.wait(1000, function() return bufnr ~= nil end, 100)
    assert.are.same('javascript', vim.bo[bufnr].filetype)
  end)

  it('sets filetype based on adapter option if available', function()
    server.client.source = function(self, request)
      self:send_response(request, {
        content = '',
      })
    end
    dap.adapters.dummy.options = {
      source_filetype = 'lua'
    }
    local session = run_and_wait_until_initialized(config, server)
    local source = {
      sourceReference = 1
    }
    local bufnr = nil
    session:source(source, function(_, buf)
      bufnr = buf
    end)
    vim.wait(1000, function() return bufnr ~= nil end, 100)
    assert.are.same('lua', vim.bo[bufnr].filetype)
  end)

  if vim.filetype then
    it('derives filetype from source.path if available', function()
      server.client.source = function(self, request)
        self:send_response(request, {
          content = '',
        })
      end
      local session = run_and_wait_until_initialized(config, server)
      local source = {
        sourceReference = 1,
        path = 'foo/bar/baz.lua',
      }
      local bufnr = nil
      session:source(source, function(_, buf)
        bufnr = buf
      end)
      vim.wait(1000, function() return bufnr ~= nil end, 100)
      assert.are.same('lua', vim.bo[bufnr].filetype)
    end)
  end
end)

describe('run_to_cursor', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
    server.client.setBreakpoints = function(self, request)
      local breakpoints = request.arguments.breakpoints
      self:send_response(request, {
        breakpoints = vim.tbl_map(function() return { verified = true } end, breakpoints)
      })
    end
  end)
  after_each(function()
    dap.close()
    server.stop()
    for _, buf in pairs(api.nvim_list_bufs()) do
      api.nvim_buf_delete(buf, { force = true })
    end
    wait(function() return dap.session() == nil end)
  end)

  it('clears breakpoints from buffers, adds breakpoint for current line, continues, restores breakpoints', function()
    local win = api.nvim_get_current_win()
    local buf1 = api.nvim_create_buf(false, true)
    local buf2 = api.nvim_create_buf(false, true)
    api.nvim_buf_set_name(buf1, 'dummy_buf1')
    api.nvim_buf_set_name(buf2, 'dummy_buf2')
    api.nvim_buf_set_lines(buf1, 0, -1, false, {'buf1: line1'})
    api.nvim_buf_set_lines(buf2, 0, -1, false, {'buf2: line 1', 'buf2: line2'})

    api.nvim_win_set_buf(win, buf1)
    api.nvim_win_set_cursor(win, { 1, 0 })
    dap.toggle_breakpoint()

    api.nvim_win_set_buf(win, buf2)
    api.nvim_win_set_cursor(win, { 1, 0 })
    dap.toggle_breakpoint()

    local session = run_and_wait_until_initialized(config, server)
    -- wait for initialize, launch, and setBreakpoints (two buffers, two setBreakpoints)
    vim.wait(1000, function() return #server.spy.requests == 4 end, 100)
    server.spy.clear()
    assert.are.same(2, vim.tbl_count(require('dap.breakpoints').get()))

    api.nvim_win_set_buf(win, buf2)
    api.nvim_win_set_cursor(win, { 2, 0 })

    -- Pretend to be stopped
    session.stopped_thread_id = 1

    dap.run_to_cursor()
    vim.wait(1000, function() return #server.spy.requests == 3 end, 100)

    -- sets breakpoint for current buffer to current line to run to the cursor
    local set_bps_requests = { server.spy.requests[1], server.spy.requests[2] }
    table.sort(set_bps_requests, function(a, b)
      return a.arguments.source.name > b.arguments.source.name
    end)
    local set_bps1 = set_bps_requests[1]
    assert.are.same('setBreakpoints', set_bps1.command)
    assert.are.same('dummy_buf2', set_bps1.arguments.source.name)
    assert.are.same({ { line = 2 }, }, set_bps1.arguments.breakpoints)

    -- resets breakpoints everywhere else
    local set_bps2 = set_bps_requests[2]
    assert.are.same('setBreakpoints', set_bps2.command)
    assert.are.same('dummy_buf1', set_bps2.arguments.source.name)
    assert.are.same({}, set_bps2.arguments.breakpoints)

    -- continues to run to the cursor
    local continue_req = server.spy.requests[3]
    assert.are.same('continue', continue_req.command)

    server.spy.clear()

    -- restores original breakpoints once stopped
    server.client:send_event('stopped', {
      reason = 'stopped',
      allThreadsStopped = true,
    })
    vim.wait(1000, function() return #server.spy.requests == 3 end, 100)

    set_bps_requests = { server.spy.requests[1], server.spy.requests[2] }
    table.sort(set_bps_requests, function(a, b)
      return a.arguments.source.name > b.arguments.source.name
    end)
    set_bps1 = set_bps_requests[1]
    assert.are.same('setBreakpoints', set_bps1.command)
    assert.are.same('dummy_buf2', set_bps1.arguments.source.name)
    assert.are.same({ { line = 1 }, }, set_bps1.arguments.breakpoints)

    set_bps2 = set_bps_requests[2]
    assert.are.same('setBreakpoints', set_bps2.command)
    assert.are.same('dummy_buf1', set_bps2.arguments.source.name)
    assert.are.same({ { line = 1 }, }, set_bps2.arguments.breakpoints)
  end)
  it('clears temporary run_to_cursor breakpoint if buffer contained no breakpoints before', function()
    local win = api.nvim_get_current_win()
    local buf1 = api.nvim_create_buf(false, true)
    local buf2 = api.nvim_create_buf(false, true)
    api.nvim_buf_set_name(buf1, 'dummy_buf1')
    api.nvim_buf_set_name(buf2, 'dummy_buf2')
    api.nvim_buf_set_lines(buf1, 0, -1, false, {'buf1: line1'})
    api.nvim_buf_set_lines(buf2, 0, -1, false, {'buf2: line 1', 'buf2: line2'})

    api.nvim_win_set_buf(win, buf1)
    api.nvim_win_set_cursor(win, { 1, 0 })
    dap.toggle_breakpoint()
    local session = run_and_wait_until_initialized(config, server)
    -- wait for initialize, launch, and setBreakpoints
    vim.wait(1000, function() return #server.spy.requests == 3 end, 100)
    server.spy.clear()
    assert.are.same(1, vim.tbl_count(require('dap.breakpoints').get()))

    api.nvim_win_set_buf(win, buf2)
    api.nvim_win_set_cursor(win, { 2, 0 })

    -- Pretend to be stopped
    session.stopped_thread_id = 1
    dap.run_to_cursor()
    -- sets breakpoints in two buffers
    vim.wait(1000, function() return #server.spy.requests == 2 end, 100)
    server.spy.clear()
    server.client:send_event('stopped', {
      reason = 'stopped',
      allThreadsStopped = true,
    })
    -- continues, resets breakpoints in both buffers
    vim.wait(1000, function() return #server.spy.requests == 3 end, 100)
    local set_bps_requests = { server.spy.requests[2], server.spy.requests[3] }
    table.sort(set_bps_requests, function(a, b)
      return a.arguments.source.name < b.arguments.source.name
    end)
    local set_bps1 = set_bps_requests[1]
    assert.are.same('setBreakpoints', set_bps1.command)
    assert.are.same('dummy_buf1', set_bps1.arguments.source.name)
    assert.are.same({ { line = 1 }, }, set_bps1.arguments.breakpoints)

    local set_bps2 = set_bps_requests[2]
    assert.are.same('setBreakpoints', set_bps2.command)
    assert.are.same('dummy_buf2', set_bps2.arguments.source.name)
    assert.are.same({}, set_bps2.arguments.breakpoints)
    local expected_bps = {
      [buf1] = {
        {
          line = 1,
        },
      }
    }
    assert.are.same(expected_bps, require('dap.breakpoints').get())
  end)
end)

describe('breakpoint events', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    server.stop()
    dap.close()
    require('dap.breakpoints').clear()
    wait(function() return dap.session() == nil end)
  end)
  it('can change state from rejected to verified', function()
    server.client.setBreakpoints = function(self, request)
      self:send_response(request, {
        breakpoints = {
          {
            id = 1,
            verified = false,
            message = "I don't like this breakpoint",
          }
        }
      })
    end

    run_and_wait_until_initialized(config, server)
    local win = api.nvim_get_current_win()
    local buf1 = api.nvim_create_buf(false, true)
    api.nvim_buf_set_name(buf1, 'dummy_buf1')
    api.nvim_buf_set_lines(buf1, 0, -1, false, {'buf1: line1'})
    api.nvim_win_set_buf(win, buf1)
    api.nvim_win_set_cursor(win, { 1, 0 })
    dap.toggle_breakpoint()

    local breakpoints = require('dap.breakpoints')

    -- initialize, launch, setBreakpoints == 3 requests
    wait(function() return #server.spy.requests == 3 end)
    wait(function() return #server.spy.responses == 3 end)
    wait(function() return breakpoints.get()[buf1][1].state end)

    local bps = breakpoints.get()
    assert.are.same(1, vim.tbl_count(bps))
    local expected_breakpoint = {
      line = 1,
      state = {
        id = 1,
        message = "I don't like this breakpoint",
        verified = false,
      }
    }
    assert.are.same(expected_breakpoint, bps[buf1][1])
    local signs = vim.fn.sign_getplaced(buf1, { group = 'dap_breakpoints' })
    assert.are.same('DapBreakpointRejected', signs[1].signs[1].name)

    local num_events = #server.spy.events
    server.client:send_event('breakpoint', {
      reason = 'changed',
      breakpoint = {
        id = 1,
        verified = true,
        message = "I don't like this breakpoint",
      }
    })
    wait(function() return #server.spy.events == num_events + 1 end)
    wait(function() return breakpoints.get()[buf1][1].state.verified end)
    assert.are.same(num_events + 1, #server.spy.events)
    expected_breakpoint.state.verified = true
    assert.are.same(expected_breakpoint, breakpoints.get()[buf1][1])

    signs = vim.fn.sign_getplaced(buf1, { group = 'dap_breakpoints' })
    assert.are.same('DapBreakpoint', signs[1].signs[1].name)
  end)

  it('can add and remove breakpoints from adapter events', function()
    run_and_wait_until_initialized(config, server)

    local win = api.nvim_get_current_win()
    local buf2 = api.nvim_create_buf(false, true)
    api.nvim_buf_set_name(buf2, 'dummy_buf2')
    api.nvim_buf_set_lines(buf2, 0, -1, false, {'buf2: line1'})
    api.nvim_win_set_buf(win, buf2)
    api.nvim_win_set_cursor(win, { 1, 0 })

    -- initialize, launch == 2 requests
    wait(function() return #server.spy.requests == 2 end)
    wait(function() return #server.spy.responses == 2 end)

    local breakpoint_state = {
      id = 1,
      line = 1,
      verified = true,
      source = {
        path = vim.uri_from_bufnr(buf2)
      },
    }

    local num_events = #server.spy.events
    server.client:send_event('breakpoint', {
      reason = 'new',
      breakpoint = breakpoint_state,
    })

    local breakpoints = require('dap.breakpoints')

    wait(function() return #server.spy.events == num_events + 1 end)
    wait(function() return breakpoints.get()[buf2] ~= nil end)

    local expected_breakpoints = {
      [buf2] = {
        [1] = {
          line = breakpoint_state.line,
          state = breakpoint_state,
        }
      }
    }

    assert.are.same(expected_breakpoints, breakpoints.get())

    num_events = #server.spy.events
    server.client:send_event('breakpoint', {
      reason = 'removed',
      breakpoint = { id = 1 },
    })

    wait(function() return #server.spy.events == num_events + 1 end)
    wait(function() return breakpoints.get()[buf2] == nil end)

    assert.are.same({}, breakpoints.get())
  end)
end)

describe('restart_frame', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    server.stop()
    dap.close()
    wait(function() return dap.session() == nil end)
  end)
  it('Requires restart capability', function()
    run_and_wait_until_initialized(config, server)
    local msg
    require('dap.utils').notify = function(m, _)
      msg = m
    end
    dap.restart_frame()
    assert.are.same('Debug Adapter does not support restart frame', msg)
  end)

  it('Requires to be stopped', function()
    local session = run_and_wait_until_initialized(config, server)
    assert(session)
    session.capabilities.supportsRestartFrame = true
    local msg
    require('dap.utils').notify = function(m, _)
      msg = m
    end
    dap.restart_frame()
    assert.are.same('Current frame not set. Debug adapter needs to be stopped at breakpoint to use restart frame', msg)
  end)

  it('Restarts frame if stopped at breakpoint', function()
    local session = run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            line = 1,
          },
        },
      })
    end
    assert(session)
    session.capabilities.supportsRestartFrame = true
    session:event_stopped({
      allThreadsStopped = false,
      threadId = 1,
      reason = 'breakpoint',
    })

    wait(function() return #server.spy.requests == 3 end)
    dap.restart_frame()
    wait(function() return #server.spy.requests == 4 end)
    assert.are.same('restartFrame', server.spy.requests[4].command)
  end)

  it('Asks for frame to restart, if current frame cannot', function()
    local session = run_and_wait_until_initialized(config, server)
    server.spy.clear()
    server.client.threads = function(self, request)
      self:send_response(request, {
        threads = { { id = 1, name = 'thread1' }, }
      })
    end
    server.client.stackTrace = function(self, request)
      self:send_response(request, {
        stackFrames = {
          {
            id = 1,
            name = 'stackFrame1',
            canRestart = false,
            line = 2,
          },
          {
            id = 2,
            name = 'stackFrame2',
            canRestart = true,
            line = 1,
          },
        },
      })
    end
    assert(session)
    session.capabilities.supportsRestartFrame = true
    session:event_stopped({
      allThreadsStopped = false,
      threadId = 1,
      reason = 'breakpoint',
    })
    local asked = false
    vim.ui.select = function(items, _, cb)
      asked = true
      cb(items[1])
    end

    wait(function() return #server.spy.requests == 3 end)
    dap.restart_frame()
    wait(function() return asked end)
    assert.are.same(true, asked)
    wait(function() return #server.spy.requests == 4 end)
    assert.are.same('restartFrame', server.spy.requests[4].command)
  end)
end)


describe('event_terminated', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    server.stop()
    dap.terminate()
    wait(function() return dap.session() == nil end)
  end)
  it('can restart session', function()
    local session = run_and_wait_until_initialized(config, server)

    server.spy.clear()
    server.client:send_event('terminated', {
      restart = 'dummy_value'
    })

    -- should start new session
    -- wait for initialize and launch
    wait(function() return #server.spy.requests == 2 end)
    local request = server.spy.requests[2]
    assert.are.same('launch', request.command, 'launch')
    local expected_args = vim.deepcopy(session.config)
    expected_args.__restart = 'dummy_value'
    assert.are.same(expected_args, request.arguments)
    local new_session = dap.session()
    assert.are.not_same(nil, new_session)
    assert.are.not_same(session.id, new_session.id)

    server.client:send_event('terminated')
    dap.terminate()
    wait(function() return dap.session() == nil end)
  end)
end)


describe('progress support', function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    server.stop()
    dap.terminate()
    wait(function() return dap.session() == nil end)
  end)

  it('shows progress in status', function()
    run_and_wait_until_initialized(config, server)
    local progress = require('dap.progress')
    wait(function() return #server.spy.events == 1 end)

    progress.reset()
    server.spy.clear()
    server.client:send_event('progressStart', {
      progressId = '1',
      title = 'progress title',
    })
    wait(function() return #server.spy.events == 1 end)
    assert.are.same('progressStart', server.spy.events[1].event)
    wait(function() return progress.status() ~= '' end)
    assert.are.same('progress title', progress.status())

    server.client:send_event('progressEnd', {
      progressId = '1',
    })
    wait(function() return progress.status() ~= 'progress title' end)
    assert.are.same('Running: Launch file', progress.status())
  end)
end)


describe("run_last", function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy = server.adapter
  end)
  after_each(function()
    server.stop()
    dap.terminate()
    wait(function() return dap.session() == nil end, "Session should become nil after terminate")
    assert.are.same(0, vim.tbl_count(dap.sessions()), "Sessions should go down to 0 after terminate/stop")
  end)

  it('can repeat run_last and it always clears session', function()
    server.client.initialize = function(self, request)
      self:send_response(request, {
        supportsTerminateRequest = true,
      })
      self:send_event("initialized", {})
    end

    run_and_wait_until_initialized(config, server)
    server.spy.clear()
    dap.run_last()
    wait(function() return #server.spy.requests == 3 end)
    local commands = vim.tbl_map(function(x) return x.command end, server.spy.requests)
    assert.are.same({"terminate", "initialize", "launch"}, commands)
    assert.are.same(1, vim.tbl_count(dap.sessions()))

    dap.run_last()
    wait(function() return #server.spy.requests == 3 end)
    commands = vim.tbl_map(function(x) return x.command end, server.spy.requests)
    assert.are.same({"terminate", "initialize", "launch"}, commands)
    assert.are.same(1, vim.tbl_count(dap.sessions()))
  end)

  it("re-evaluates functions if adapter supports restart", function()
    server.client.initialize = function(self, request)
      self:send_response(request, {
        supportsRestartRequest = true,
      })
      self:send_event("initialized", {})
    end
    server.client.restart = function(self, request)
      self:send_response(request, {})
    end
    local num_called = 0
    local dummy_config = {
      type = 'dummy',
      request = 'launch',
      name = 'Launch file',
      called = function()
        num_called = num_called + 1
        return num_called
      end
    }
    run_and_wait_until_initialized(dummy_config, server)
    assert.are.same(1, num_called)
    server.spy.clear()
    dap.run_last()
    wait_for_response(server, "restart")
    local commands = vim.tbl_map(function(x) return x.command end, server.spy.requests)
    assert.are.same({"restart"}, commands)
    assert.are.same(2, num_called)
  end)
end)


describe("bad debug adapter", function()
  it("calls notify with warning", function()
    dap.adapters.bad = {
      type = "executable",
      command = "python",
      args = { vim.fn.expand("%:p:h") .. "/spec/bad_adapter.py" }
    }
    local captured_msg
    local captured_log_level
    ---@diagnostic disable-next-line: duplicate-set-field
    require("dap.utils").notify = function(msg, log_level)
      captured_msg = msg
      captured_log_level = log_level
    end
    local bad_config = {
      type = 'bad',
      request = 'launch',
      name = 'Launch file',
    }
    dap.run(bad_config)
    wait(function() return captured_msg ~= nil end)
    assert.are.same("command `python` of adapter `bad` exited with 10. Run :DapShowLog to open logs", captured_msg)
    assert.are.same(vim.log.levels.WARN, captured_log_level)
  end)
end)


describe("on_output", function()
  local server
  before_each(function()
    server = require('spec.server').spawn()
    dap.adapters.dummy_on_output = server.adapter
  end)
  after_each(function()
    dap.terminate()
    server.stop()
    wait(function() return dap.session() == nil end, "Session should become nil after terminate")
    assert.are.same(0, vim.tbl_count(dap.sessions()), "Sessions should go down to 0 after terminate/stop")
    dap.defaults.dummy_on_output.on_output = nil
    assert.are.is_nil(dap.defaults.dummy_on_output.on_output)
  end)

  it("can override output handling", function()
    local captured_output = nil

    function dap.defaults.dummy_on_output.on_output(_, output)
      captured_output = output
    end

    server.client.initialize = function(self, request)
      self:send_response(request, {})
      self:send_event("initialized", {})
      self:send_event("output", {
        category = "stdout",
        output = "dummy output"
      })
    end

    run_and_wait_until_initialized({
      type = "dummy_on_output",
      request = "launch",
      name = "dummy"
    }, server)

    assert.are.same(captured_output, {
      category = "stdout",
      output = "dummy output"
    })
  end)
end)
