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

local eq = t.eq

local clear_notrace = t_lsp.clear_notrace
local create_server_definition = t_lsp.create_server_definition

local api = n.api
local exec_lua = n.exec_lua
local insert = n.insert
local command = n.command
local feed = n.feed

describe('vim.lsp.folding_range', function()
  local text = [[// foldLevel() {{{2
/// @return  fold level at line number "lnum" in the current window.
static int foldLevel(linenr_T lnum)
{
  // While updating the folds lines between invalid_top and invalid_bot have
  // an undefined fold level.  Otherwise update the folds first.
  if (invalid_top == 0) {
    checkupdate(curwin);
  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {
    return prev_lnum_lvl;
  } else if (lnum >= invalid_top && lnum <= invalid_bot) {
    return -1;
  }

  // Return quickly when there is no folding at all in this window.
  if (!hasAnyFolding(curwin)) {
    return 0;
  }

  return foldLevelWin(curwin, lnum);
}]]

  local result = {
    {
      endLine = 19,
      kind = 'region',
      startCharacter = 1,
      startLine = 3,
    },
    {
      endCharacter = 2,
      endLine = 7,
      kind = 'region',
      startCharacter = 25,
      startLine = 6,
    },
    {
      endCharacter = 2,
      endLine = 9,
      kind = 'region',
      startCharacter = 55,
      startLine = 8,
    },
    {
      endCharacter = 2,
      endLine = 11,
      kind = 'region',
      startCharacter = 58,
      startLine = 10,
    },
    {
      endCharacter = 2,
      endLine = 16,
      kind = 'region',
      startCharacter = 31,
      startLine = 15,
    },
    {
      endCharacter = 68,
      endLine = 1,
      kind = 'comment',
      startCharacter = 2,
      startLine = 0,
    },
    {
      endCharacter = 64,
      endLine = 5,
      kind = 'comment',
      startCharacter = 4,
      startLine = 4,
    },
  }

  local bufnr ---@type integer
  local client_id ---@type integer

  clear_notrace()
  before_each(function()
    clear_notrace()

    exec_lua(create_server_definition)
    bufnr = n.api.nvim_get_current_buf()
    client_id = exec_lua(function()
      _G.server = _G._create_server({
        capabilities = {
          foldingRangeProvider = true,
        },
        handlers = {
          ['textDocument/foldingRange'] = function(_, _, callback)
            callback(nil, result)
          end,
        },
      })

      vim.api.nvim_win_set_buf(0, bufnr)

      return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
    end)
    command('set foldmethod=expr foldcolumn=1 foldlevel=999')
    insert(text)
  end)
  after_each(function()
    api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
  end)

  describe('expr()', function()
    --- @type test.functional.ui.screen
    local screen
    before_each(function()
      screen = Screen.new(80, 45)
      screen:set_default_attr_ids({
        [1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue },
        [2] = { bold = true, foreground = Screen.colors.Blue1 },
        [3] = { bold = true, reverse = true },
        [4] = { reverse = true },
      })
      command([[set foldexpr=v:lua.vim.lsp.foldexpr()]])
      command([[split]])
    end)

    it('controls whether folding range is enabled', function()
      eq(
        true,
        exec_lua(function()
          return vim.lsp._capability.is_enabled('folding_range', { bufnr = 0 })
        end)
      )
      command [[setlocal foldexpr=]]
      eq(
        false,
        exec_lua(function()
          return vim.lsp._capability.is_enabled('folding_range', { bufnr = 0 })
        end)
      )
      command([[set foldexpr=v:lua.vim.lsp.foldexpr()]])
      eq(
        true,
        exec_lua(function()
          return vim.lsp._capability.is_enabled('folding_range', { bufnr = 0 })
        end)
      )
    end)

    it('can compute fold levels', function()
      ---@type table<integer, string>
      local foldlevels = {}
      for i = 1, 21 do
        foldlevels[i] = exec_lua('return vim.lsp.foldexpr(' .. i .. ')')
      end
      eq({
        [1] = '>1',
        [2] = '<1',
        [3] = '0',
        [4] = '>1',
        [5] = '>2',
        [6] = '<2',
        [7] = '>2',
        [8] = '<2',
        [9] = '>2',
        [10] = '<2',
        [11] = '>2',
        [12] = '<2',
        [13] = '1',
        [14] = '1',
        [15] = '1',
        [16] = '>2',
        [17] = '<2',
        [18] = '1',
        [19] = '1',
        [20] = '<1',
        [21] = '0',
      }, foldlevels)
    end)

    it('updates folds in all windows', function()
      screen:expect({
        grid = [[
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:[No Name] [+]                                                                   }|
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }}                                                                              |
{4:[No Name] [+]                                                                   }|
                                                                                |
  ]],
      })
    end)

    it('persists wherever foldexpr is set', function()
      command([[setlocal foldexpr=]])
      feed('<C-w><C-w>zx')
      screen:expect({
        grid = [[
{1: }// foldLevel() {{{2                                                            |
{1: }/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1: }{                                                                              |
{1: }  // While updating the folds lines between invalid_top and invalid_bot have   |
{1: }  // an undefined fold level.  Otherwise update the folds first.               |
{1: }  if (invalid_top == 0) {                                                      |
{1: }    checkupdate(curwin);                                                       |
{1: }  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1: }    return prev_lnum_lvl;                                                      |
{1: }  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1: }    return -1;                                                                 |
{1: }  }                                                                            |
{1: }                                                                               |
{1: }  // Return quickly when there is no folding at all in this window.            |
{1: }  if (!hasAnyFolding(curwin)) {                                                |
{1: }    return 0;                                                                  |
{1: }  }                                                                            |
{1: }                                                                               |
{1: }  return foldLevelWin(curwin, lnum);                                           |
{1: }}                                                                              |
{4:[No Name] [+]                                                                   }|
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:[No Name] [+]                                                                   }|
                                                                                |
  ]],
      })
    end)

    it('synchronizes changed rows with their previous foldlevels', function()
      command('1,2d')
      screen:expect({
        grid = [[
{1: }^static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }}                                                                              |
{2:~                                                                               }|*2
{3:[No Name] [+]                                                                   }|
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }}                                                                              |
{2:~                                                                               }|*2
{4:[No Name] [+]                                                                   }|
                                                                                |
]],
      })
    end)

    it('clears folds when sole client detaches', function()
      exec_lua(function()
        vim.lsp.buf_detach_client(bufnr, client_id)
      end)
      screen:expect({
        grid = [[
{1: }// foldLevel() {{{2                                                            |
{1: }/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1: }{                                                                              |
{1: }  // While updating the folds lines between invalid_top and invalid_bot have   |
{1: }  // an undefined fold level.  Otherwise update the folds first.               |
{1: }  if (invalid_top == 0) {                                                      |
{1: }    checkupdate(curwin);                                                       |
{1: }  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1: }    return prev_lnum_lvl;                                                      |
{1: }  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1: }    return -1;                                                                 |
{1: }  }                                                                            |
{1: }                                                                               |
{1: }  // Return quickly when there is no folding at all in this window.            |
{1: }  if (!hasAnyFolding(curwin)) {                                                |
{1: }    return 0;                                                                  |
{1: }  }                                                                            |
{1: }                                                                               |
{1: }  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:[No Name] [+]                                                                   }|
{1: }// foldLevel() {{{2                                                            |
{1: }/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1: }{                                                                              |
{1: }  // While updating the folds lines between invalid_top and invalid_bot have   |
{1: }  // an undefined fold level.  Otherwise update the folds first.               |
{1: }  if (invalid_top == 0) {                                                      |
{1: }    checkupdate(curwin);                                                       |
{1: }  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1: }    return prev_lnum_lvl;                                                      |
{1: }  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1: }    return -1;                                                                 |
{1: }  }                                                                            |
{1: }                                                                               |
{1: }  // Return quickly when there is no folding at all in this window.            |
{1: }  if (!hasAnyFolding(curwin)) {                                                |
{1: }    return 0;                                                                  |
{1: }  }                                                                            |
{1: }                                                                               |
{1: }  return foldLevelWin(curwin, lnum);                                           |
{1: }}                                                                              |
{4:[No Name] [+]                                                                   }|
                                                                                |
  ]],
      })
    end)

    it('remains valid after the client re-attaches.', function()
      exec_lua(function()
        vim.lsp.buf_detach_client(bufnr, client_id)
        vim.lsp.buf_attach_client(bufnr, client_id)
      end)
      screen:expect({
        grid = [[
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:[No Name] [+]                                                                   }|
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }}                                                                              |
{4:[No Name] [+]                                                                   }|
                                                                                |
  ]],
      })
    end)
  end)

  describe('foldtext()', function()
    --- @type test.functional.ui.screen
    local screen
    before_each(function()
      screen = Screen.new(80, 23)
      screen:set_default_attr_ids({
        [1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue },
        [2] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.LightGrey },
        [3] = { bold = true, foreground = Screen.colors.Blue1 },
        [4] = { bold = true, reverse = true },
        [5] = { reverse = true },
      })
      command(
        [[set foldexpr=v:lua.vim.lsp.foldexpr() foldtext=v:lua.vim.lsp.foldtext() foldlevel=1]]
      )
    end)

    it('shows the first folded line if `collapsedText` does not exist', function()
      screen:expect({
        grid = [[
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:+}{2:  // While updating the folds lines between invalid_top and invalid_bot have···}|
{1:+}{2:  if (invalid_top == 0) {······················································}|
{1:+}{2:  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {························}|
{1:+}{2:  } else if (lnum >= invalid_top && lnum <= invalid_bot) {·····················}|
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:+}{2:  if (!hasAnyFolding(curwin)) {················································}|
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:~                                                                               }|*6
                                                                                |
  ]],
      })
    end)
  end)

  describe('foldclose()', function()
    --- @type test.functional.ui.screen
    local screen
    before_each(function()
      screen = Screen.new(80, 23)
      screen:set_default_attr_ids({
        [1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue },
        [2] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.LightGrey },
        [3] = { bold = true, foreground = Screen.colors.Blue1 },
        [4] = { bold = true, reverse = true },
        [5] = { reverse = true },
      })
      command([[set foldexpr=v:lua.vim.lsp.foldexpr()]])
    end)

    it('closes all folds of one kind immediately', function()
      exec_lua(function()
        vim.lsp.foldclose('comment')
      end)
      screen:expect({
        grid = [[
{1:+}{2:+--  2 lines: foldLevel()······················································}|
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:+}{2:+---  2 lines: While updating the folds lines between invalid_top and invalid_b}|
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:~                                                                               }|*3
                                                                                |
  ]],
      })
    end)

    it('closes the smallest fold first', function()
      exec_lua(function()
        vim.lsp.foldclose('region')
      end)
      screen:expect({
        grid = [[
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:+}{2:+-- 17 lines: {································································}|
{1: }^}                                                                              |
{3:~                                                                               }|*17
                                                                                |
  ]],
      })
      command('4foldopen')
      screen:expect({
        grid = [[
{1:-}// foldLevel() {{{2                                                            |
{1:│}/// @return  fold level at line number "lnum" in the current window.           |
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:-}  // While updating the folds lines between invalid_top and invalid_bot have   |
{1:2}  // an undefined fold level.  Otherwise update the folds first.               |
{1:+}{2:+---  2 lines: if (invalid_top == 0) {·········································}|
{1:+}{2:+---  2 lines: } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {···········}|
{1:+}{2:+---  2 lines: } else if (lnum >= invalid_top && lnum <= invalid_bot) {········}|
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:+}{2:+---  2 lines: if (!hasAnyFolding(curwin)) {···································}|
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:~                                                                               }|*5
                                                                                |
  ]],
      })
    end)

    it('is deferred when the buffer is not up-to-date', function()
      exec_lua(function()
        vim.lsp.foldclose('comment')
        vim.lsp.util.buf_versions[bufnr] = 0
      end)
      screen:expect({
        grid = [[
{1:+}{2:+--  2 lines: foldLevel()······················································}|
{1: }static int foldLevel(linenr_T lnum)                                            |
{1:-}{                                                                              |
{1:+}{2:+---  2 lines: While updating the folds lines between invalid_top and invalid_b}|
{1:-}  if (invalid_top == 0) {                                                      |
{1:2}    checkupdate(curwin);                                                       |
{1:-}  } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {                        |
{1:2}    return prev_lnum_lvl;                                                      |
{1:-}  } else if (lnum >= invalid_top && lnum <= invalid_bot) {                     |
{1:2}    return -1;                                                                 |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  // Return quickly when there is no folding at all in this window.            |
{1:-}  if (!hasAnyFolding(curwin)) {                                                |
{1:2}    return 0;                                                                  |
{1:│}  }                                                                            |
{1:│}                                                                               |
{1:│}  return foldLevelWin(curwin, lnum);                                           |
{1: }^}                                                                              |
{3:~                                                                               }|*3
                                                                                |
  ]],
      })
    end)
  end)
end)
