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

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

local clear = n.clear
local exec_lua = n.exec_lua
local eq = t.eq
local neq = t.neq

local create_server_definition = t_lsp.create_server_definition

describe('vim.lsp.diagnostic', function()
  local fake_uri --- @type string
  local client_id --- @type integer
  local diagnostic_bufnr --- @type integer

  before_each(function()
    clear { env = {
      NVIM_LUA_NOTRACK = '1',
      VIMRUNTIME = os.getenv 'VIMRUNTIME',
    } }

    exec_lua(function()
      require('vim.lsp')

      _G.make_range = function(x1, y1, x2, y2)
        return { start = { line = x1, character = y1 }, ['end'] = { line = x2, character = y2 } }
      end

      _G.make_error = function(msg, x1, y1, x2, y2)
        return {
          range = _G.make_range(x1, y1, x2, y2),
          message = msg,
          severity = 1,
        }
      end

      _G.make_warning = function(msg, x1, y1, x2, y2)
        return {
          range = _G.make_range(x1, y1, x2, y2),
          message = msg,
          severity = 2,
        }
      end

      _G.make_information = function(msg, x1, y1, x2, y2)
        return {
          range = _G.make_range(x1, y1, x2, y2),
          message = msg,
          severity = 3,
        }
      end

      function _G.get_extmarks(bufnr, client_id0)
        local namespace = vim.lsp.diagnostic.get_namespace(client_id0)
        local ns = vim.diagnostic.get_namespace(namespace)
        local extmarks = {}
        if ns.user_data.virt_text_ns then
          for _, e in
            pairs(
              vim.api.nvim_buf_get_extmarks(
                bufnr,
                ns.user_data.virt_text_ns,
                0,
                -1,
                { details = true }
              )
            )
          do
            table.insert(extmarks, e)
          end
        end
        if ns.user_data.underline_ns then
          for _, e in
            pairs(
              vim.api.nvim_buf_get_extmarks(
                bufnr,
                ns.user_data.underline_ns,
                0,
                -1,
                { details = true }
              )
            )
          do
            table.insert(extmarks, e)
          end
        end
        return extmarks
      end
    end)

    fake_uri = 'file:///fake/uri'

    exec_lua(function()
      diagnostic_bufnr = vim.uri_to_bufnr(fake_uri)
      local lines = { '1st line', '2nd line of text', 'wow', 'cool', 'more', 'lines' }
      vim.fn.bufload(diagnostic_bufnr)
      vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, 1, false, lines)
      vim.api.nvim_win_set_buf(0, diagnostic_bufnr)
    end)
  end)

  describe('vim.lsp.diagnostic.on_publish_diagnostics', function()
    before_each(function()
      exec_lua(function()
        client_id = assert(vim.lsp.start({
          cmd_env = {
            NVIM_LUA_NOTRACK = '1',
          },
          cmd = {
            vim.v.progpath,
            '-es',
            '-u',
            'NONE',
            '--headless',
          },
          offset_encoding = 'utf-16',
        }, { attach = false }))
      end)
    end)

    after_each(function()
      exec_lua(function()
        vim.lsp.get_client_by_id(client_id):stop()
        vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
      end)
    end)

    it('correctly handles UTF-16 offsets', function()
      local line = 'All 💼 and no 🎉 makes Jack a dull 👦'
      local result = exec_lua(function()
        vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, -1, false, { line })

        vim.lsp.diagnostic.on_publish_diagnostics(nil, {
          uri = fake_uri,
          diagnostics = {
            _G.make_error('UTF-16 Diagnostic', 0, 7, 0, 8),
          },
        }, { client_id = client_id })

        local diags = vim.diagnostic.get(diagnostic_bufnr)
        return diags
      end)
      eq(1, #result)
      eq(
        exec_lua(function()
          return vim.str_byteindex(line, 'utf-16', 7)
        end),
        result[1].col
      )
      eq(
        exec_lua(function()
          return vim.str_byteindex(line, 'utf-16', 8)
        end),
        result[1].end_col
      )
    end)

    it('does not create buffer on empty diagnostics', function()
      -- No buffer is created without diagnostics
      eq(
        -1,
        exec_lua(function()
          vim.lsp.diagnostic.on_publish_diagnostics(nil, {
            uri = 'file:///fake/uri2',
            diagnostics = {},
          }, { client_id = client_id })
          return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2'))
        end)
      )

      -- Create buffer on diagnostics
      neq(
        -1,
        exec_lua(function()
          vim.lsp.diagnostic.on_publish_diagnostics(nil, {
            uri = 'file:///fake/uri2',
            diagnostics = {
              _G.make_error('Diagnostic', 0, 0, 0, 0),
            },
          }, { client_id = client_id })
          return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2'))
        end)
      )
      eq(
        1,
        exec_lua(function()
          return #vim.diagnostic.get(_G.bufnr)
        end)
      )

      -- Clear diagnostics after buffer was created
      neq(
        -1,
        exec_lua(function()
          vim.lsp.diagnostic.on_publish_diagnostics(nil, {
            uri = 'file:///fake/uri2',
            diagnostics = {},
          }, { client_id = client_id })
          return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2'))
        end)
      )
      eq(
        0,
        exec_lua(function()
          return #vim.diagnostic.get(_G.bufnr)
        end)
      )
    end)

    it('clears diagnostics for the client namespace on empty publish', function()
      local before_clear, after_clear = exec_lua(function()
        local ns = vim.lsp.diagnostic.get_namespace(client_id)

        -- Publish diagnostics
        vim.lsp.diagnostic.on_publish_diagnostics(nil, {
          uri = fake_uri,
          diagnostics = {
            _G.make_error('Diagnostic', 0, 0, 0, 0),
          },
        }, { client_id = client_id })

        local before_clear = vim.diagnostic.get(diagnostic_bufnr, { namespace = ns })

        -- Publish empty diagnostics
        vim.lsp.diagnostic.on_publish_diagnostics(nil, {
          uri = fake_uri,
          diagnostics = {},
        }, { client_id = client_id })

        local after_clear = vim.diagnostic.get(diagnostic_bufnr, { namespace = ns })

        return before_clear, after_clear
      end)

      eq(1, #before_clear)
      eq(0, #after_clear)
    end)

    it('clears diagnostics when buffer is deleted', function()
      local before_delete, after_delete = exec_lua(function()
        local ns = vim.lsp.diagnostic.get_namespace(client_id)

        -- Publish diagnostics
        vim.lsp.diagnostic.on_publish_diagnostics(nil, {
          uri = fake_uri,
          diagnostics = {
            _G.make_error('Diagnostic', 0, 0, 0, 0),
          },
        }, { client_id = client_id })

        local before_delete = vim.diagnostic.get(diagnostic_bufnr, { namespace = ns })

        -- Avoid deleting the currently displayed buffer (Windows teardown edge case)
        local scratch = vim.api.nvim_create_buf(false, true)
        vim.api.nvim_win_set_buf(0, scratch)

        -- Delete the buffer deterministically
        vim.api.nvim_buf_delete(diagnostic_bufnr, { force = true })

        -- Query remaining diagnostics via valid access path
        -- (deleted buffer cannot be queried directly)
        local after_delete = vim.diagnostic.get(nil, { namespace = ns })

        return before_delete, after_delete
      end)

      eq(1, #before_delete)
      eq(0, #after_delete)
    end)
  end)

  describe('vim.lsp.diagnostic.on_diagnostic', function()
    before_each(function()
      exec_lua(create_server_definition)
      exec_lua(function()
        _G.requests = 0
        _G.server = _G._create_server({
          capabilities = {
            diagnosticProvider = {},
          },
          handlers = {
            ['textDocument/diagnostic'] = function(_, params)
              _G.params = params
              _G.requests = _G.requests + 1
            end,
          },
        })

        function _G.get_extmarks(bufnr, client_id0)
          local namespace = vim.lsp.diagnostic.get_namespace(client_id0, true)
          local ns = vim.diagnostic.get_namespace(namespace)
          local extmarks = {}
          if ns.user_data.virt_text_ns then
            for _, e in
              pairs(
                vim.api.nvim_buf_get_extmarks(
                  bufnr,
                  ns.user_data.virt_text_ns,
                  0,
                  -1,
                  { details = true }
                )
              )
            do
              table.insert(extmarks, e)
            end
          end
          if ns.user_data.underline_ns then
            for _, e in
              pairs(
                vim.api.nvim_buf_get_extmarks(
                  bufnr,
                  ns.user_data.underline_ns,
                  0,
                  -1,
                  { details = true }
                )
              )
            do
              table.insert(extmarks, e)
            end
          end
          return extmarks
        end

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

    it('adds diagnostics to vim.diagnostics', function()
      local diags = exec_lua(function()
        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {
            _G.make_error('Pull Diagnostic', 4, 4, 4, 4),
          },
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})

        return vim.diagnostic.get(diagnostic_bufnr)
      end)
      eq(1, #diags)
      eq('Pull Diagnostic', diags[1].message)
    end)

    it('preserves push diagnostics when pull diagnostics are empty', function()
      local push_ns_count, pull_ns_count, all_diags_count, push_ns, pull_ns = exec_lua(function()
        vim.lsp.diagnostic.on_publish_diagnostics(nil, {
          uri = fake_uri,
          diagnostics = {
            _G.make_error('Push Diagnostic', 0, 0, 0, 0),
          },
        }, { client_id = client_id })

        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {},
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})

        local push_ns = vim.lsp.diagnostic.get_namespace(client_id, false)
        local pull_ns = vim.lsp.diagnostic.get_namespace(client_id, true)

        return #vim.diagnostic.get(diagnostic_bufnr, { namespace = push_ns }),
          #vim.diagnostic.get(diagnostic_bufnr, { namespace = pull_ns }),
          #vim.diagnostic.get(diagnostic_bufnr),
          push_ns,
          pull_ns
      end)

      eq(1, push_ns_count)
      eq(0, pull_ns_count)
      eq(1, all_diags_count)
      neq(push_ns, pull_ns)
    end)

    it('uses pull_id to isolate pull diagnostic namespaces', function()
      local first_count, second_count, total_count, first_ns, second_ns = exec_lua(function()
        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {
            _G.make_error('Pull Diagnostic A', 0, 0, 0, 0),
          },
        }, {
          params = {
            identifier = 'provider-a',
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})

        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {},
        }, {
          params = {
            identifier = 'provider-b',
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})

        local first_ns = vim.lsp.diagnostic.get_namespace(client_id, true, 'provider-a')
        local second_ns = vim.lsp.diagnostic.get_namespace(client_id, true, 'provider-b')

        return #vim.diagnostic.get(diagnostic_bufnr, { namespace = first_ns }),
          #vim.diagnostic.get(diagnostic_bufnr, { namespace = second_ns }),
          #vim.diagnostic.get(diagnostic_bufnr),
          first_ns,
          second_ns
      end)

      eq(1, first_count)
      eq(0, second_count)
      eq(1, total_count)
      neq(first_ns, second_ns)
    end)

    it('handles multiline diagnostic ranges #33782', function()
      local diags = exec_lua(function()
        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {
            _G.make_error('Pull Diagnostic', 0, 6, 1, 10),
          },
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})

        return vim.diagnostic.get(diagnostic_bufnr)
      end)
      local lines = exec_lua(function()
        return vim.api.nvim_buf_get_lines(diagnostic_bufnr, 0, -1, false)
      end)
      -- This test case must be run over a multiline diagnostic in which the start line is shorter
      -- than the end line, and the end_col exceeds the start line's length.
      eq(#lines[1], 8)
      eq(#lines[2], 16)
      eq(1, #diags)
      eq(6, diags[1].col)
      eq(10, diags[1].end_col)
    end)

    it('severity defaults to error if missing', function()
      ---@type vim.Diagnostic[]
      local diagnostics = exec_lua(function()
        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {
            {
              range = _G.make_range(4, 4, 4, 4),
              message = 'bad!',
            },
          },
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})
        return vim.diagnostic.get(diagnostic_bufnr)
      end)
      eq(1, #diagnostics)
      eq(1, diagnostics[1].severity)
    end)

    it('clears diagnostics when client detaches', function()
      exec_lua(function()
        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {
            _G.make_error('Pull Diagnostic', 4, 4, 4, 4),
          },
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})
      end)

      eq(
        1,
        exec_lua(function()
          return #vim.diagnostic.get(diagnostic_bufnr)
        end)
      )

      exec_lua(function()
        vim.lsp.get_client_by_id(client_id):stop()
      end)

      eq(
        0,
        exec_lua(function()
          return #vim.diagnostic.get(diagnostic_bufnr)
        end)
      )
    end)

    it('keeps diagnostics when one client detaches and others still are attached', function()
      exec_lua(function()
        _G.client_id2 = assert(vim.lsp.start({ name = 'dummy2', cmd = _G.server.cmd }))

        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {
            _G.make_error('Pull Diagnostic', 4, 4, 4, 4),
          },
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})
      end)

      eq(
        1,
        exec_lua(function()
          return #vim.diagnostic.get(diagnostic_bufnr)
        end)
      )

      exec_lua(function()
        vim.lsp.get_client_by_id(_G.client_id2):stop()
      end)

      eq(
        1,
        exec_lua(function()
          return #vim.diagnostic.get(diagnostic_bufnr)
        end)
      )
    end)

    it('handles server cancellation', function()
      eq(
        1,
        exec_lua(function()
          vim.lsp.diagnostic.on_diagnostic({
            code = vim.lsp.protocol.ErrorCodes.ServerCancelled,
            -- Empty data defaults to retriggering request
            data = {},
            message = '',
          }, {}, {
            method = 'textDocument/diagnostic',
            client_id = client_id,
            bufnr = diagnostic_bufnr,
          })

          return _G.requests
        end)
      )

      eq(
        2,
        exec_lua(function()
          vim.lsp.diagnostic.on_diagnostic({
            code = vim.lsp.protocol.ErrorCodes.ServerCancelled,
            data = { retriggerRequest = true },
            message = '',
          }, {}, {
            method = 'textDocument/diagnostic',
            client_id = client_id,
            bufnr = diagnostic_bufnr,
          })

          return _G.requests
        end)
      )

      eq(
        2,
        exec_lua(function()
          vim.lsp.diagnostic.on_diagnostic({
            code = vim.lsp.protocol.ErrorCodes.ServerCancelled,
            data = { retriggerRequest = false },
            message = '',
          }, {}, {
            method = 'textDocument/diagnostic',
            client_id = client_id,
            bufnr = diagnostic_bufnr,
          })

          return _G.requests
        end)
      )
    end)

    it('supports dynamic registration', function()
      exec_lua(create_server_definition)
      exec_lua(function()
        _G.server2 = _G._create_server({
          diagnosticProvider = {
            documentSelector = vim.NIL,
          },
          handlers = {
            ['textDocument/diagnostic'] = function(_, _, callback)
              callback(nil, {
                kind = 'full',
                items = {
                  _G.make_error('Dynamic Diagnostic', 4, 4, 4, 4),
                },
              })
            end,
          },
        })

        local client_id2 = assert(vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd }))

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

      eq(
        1,
        exec_lua(function()
          return #vim.diagnostic.get(diagnostic_bufnr)
        end)
      )
    end)

    it('requests with the `previousResultId`', function()
      -- Full reports
      eq(
        'dummy_server',
        exec_lua(function()
          vim.lsp.diagnostic.on_diagnostic(nil, {
            kind = 'full',
            resultId = 'dummy_server',
            items = {
              _G.make_error('Pull Diagnostic', 4, 4, 4, 4),
            },
          }, {
            method = 'textDocument/diagnostic',
            params = {
              textDocument = { uri = fake_uri },
            },
            client_id = client_id,
            bufnr = diagnostic_bufnr,
          })
          vim.api.nvim_exec_autocmds('LspNotify', {
            buf = diagnostic_bufnr,
            data = {
              method = 'textDocument/didChange',
              client_id = client_id,
            },
          })
          return _G.params.previousResultId
        end)
      )

      -- Unchanged reports
      eq(
        'squidward',
        exec_lua(function()
          vim.lsp.diagnostic.on_diagnostic(nil, {
            kind = 'unchanged',
            resultId = 'squidward',
          }, {
            method = 'textDocument/diagnostic',
            params = {
              textDocument = { uri = fake_uri },
            },
            client_id = client_id,
            bufnr = diagnostic_bufnr,
          })
          vim.api.nvim_exec_autocmds('LspNotify', {
            buf = diagnostic_bufnr,
            data = {
              method = 'textDocument/didChange',
              client_id = client_id,
            },
          })
          return _G.params.previousResultId
        end)
      )
    end)

    it('handles relatedDocuments diagnostics', function()
      local fake_uri_2 = 'file:///fake/uri2'
      ---@type vim.Diagnostic[], vim.Diagnostic[], string?
      local diagnostics, related_diagnostics, relatedPreviousResultId = exec_lua(function()
        local second_buf = vim.uri_to_bufnr(fake_uri_2)
        vim.fn.bufload(second_buf)

        -- Attach the client to both buffers.
        vim.api.nvim_win_set_buf(0, second_buf)
        vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })

        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          relatedDocuments = {
            [fake_uri_2] = {
              kind = 'full',
              resultId = 'spongebob',
              items = {
                {
                  range = _G.make_range(4, 4, 4, 4),
                  message = 'related bad!',
                },
              },
            },
          },
          items = {},
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})

        vim.api.nvim_exec_autocmds('LspNotify', {
          buf = second_buf,
          data = {
            method = 'textDocument/didChange',
            client_id = client_id,
          },
        })

        return vim.diagnostic.get(diagnostic_bufnr),
          vim.diagnostic.get(second_buf),
          _G.params.previousResultId
      end)
      eq(0, #diagnostics)
      eq(1, #related_diagnostics)
      eq('related bad!', related_diagnostics[1].message)
      eq('spongebob', relatedPreviousResultId)
    end)
  end)

  describe('vim.lsp.diagnostic.on_refresh', function()
    it('refreshes diagnostics on server-to-client request', function()
      exec_lua(create_server_definition)
      exec_lua(function()
        _G.requests = 0
        _G.server = _G._create_server({
          capabilities = {
            diagnosticProvider = {},
          },
          handlers = {
            ['textDocument/diagnostic'] = function(_, _, callback)
              _G.requests = _G.requests + 1
              callback(nil, {
                kind = 'full',
                items = {
                  _G.make_warning('Pull Diagnostic', 4, 4, 4, 4),
                },
              })
            end,
          },
        })
        client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }))
      end)

      local diags = exec_lua(function()
        vim.lsp.diagnostic.on_diagnostic(nil, {
          kind = 'full',
          items = {
            _G.make_error('Pull Diagnostic', 4, 4, 4, 4),
          },
        }, {
          params = {
            textDocument = { uri = fake_uri },
          },
          uri = fake_uri,
          client_id = client_id,
          bufnr = diagnostic_bufnr,
        }, {})

        return vim.diagnostic.get(diagnostic_bufnr)
      end)
      eq(1, #diags)
      eq(1, diags[1].severity)

      local requests, refreshed_diags = exec_lua(function()
        vim.lsp.diagnostic.on_refresh(nil, nil, {
          method = 'workspace/diagnostic/refresh',
          client_id = client_id,
        })

        return _G.requests, vim.diagnostic.get(diagnostic_bufnr)
      end)
      eq(1, requests)
      eq(1, #refreshed_diags)
      eq(2, refreshed_diags[1].severity)
    end)

    it('refreshes workspace diagnostics', function()
      local fake_uri_3 = 'file:///fake/uri3'
      exec_lua(create_server_definition)
      exec_lua(function()
        _G.requests = 0
        _G.server = _G._create_server({
          capabilities = {
            diagnosticProvider = { workspaceDiagnostics = true },
          },
          handlers = {
            ['workspace/diagnostic'] = function(_, _, callback)
              _G.requests = _G.requests + 1
              callback(nil, {
                items = {
                  {
                    kind = 'full',
                    uri = fake_uri_3,
                    items = {
                      _G.make_error('Workspace Diagnostic', 4, 4, 4, 4),
                    },
                  },
                },
              })
            end,
          },
        })
        client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }))
      end)

      eq(
        0,
        exec_lua(function()
          return #vim.diagnostic.get()
        end)
      )

      eq(
        { vim.NIL },
        exec_lua(function()
          local client = vim.lsp.get_client_by_id(client_id)
          assert(client)
          local result = {}
          client:_provider_foreach('workspace/diagnostic', function(cap)
            table.insert(result, cap.identifier or vim.NIL)
          end)
          return result
        end)
      )

      local requests, diags = exec_lua(function()
        vim.lsp.diagnostic.on_refresh(nil, nil, {
          method = 'workspace/diagnostic/refresh',
          client_id = client_id,
        })

        return _G.requests, vim.diagnostic.get()
      end)
      eq(1, requests)
      eq(1, #diags)
      eq(1, diags[1].severity)
    end)
  end)
end)
