-- Copyright (c) 2020-2021 shadmansaleh
-- MIT license, see LICENSE for more details.

--- ## Testing module for lualines statusline
---
--- ###Uses:
---
--- Create a new instance with status width 120 & for active statusline
--- like following.
---
--- ``lua
--- local statusline = require('tests.statusline').new(120, 'active')
--- ```
---
--- To create a new instance with status width 80 & for inactive statusline use following.
---
--- ``lua
--- local statusline = require('tests.statusline').new(120, 'inactive')
--- ```
---
--- Now setup the state you want to test.
--- To test you'll call `expect` method on statusline for example.
---
--- To create a new instance with status width 80 & tabline
---
--- ``lua
--- local statusline = require('tests.statusline').new(120, 'tabline')
--- ```
---
--- Now setup the state you want to test.
--- To test you'll call `expect` method on statusline for example.
---
--- ``lua
--- statusline:expect([===[
---    highlights = {
---        1: lualine_c_inactive = { bg = "#3c3836", fg = "#a89984" }
---    }
---    |{1: [No Name] }
---    {1:                                                                                                     }
---    {1:   0:1  }|
---
---]===])
--- ```
---
--- For more flexibility you can match a patten in expect block.
--- ``lua
--- statusline:expect([===[
---    highlights = {
---        1: lualine_a_tabs_inactive = { bg = "#3c3836", bold = true, fg = "#a89984" }
---        2: lualine_transitional_lualine_a_tabs_inactive_to_lualine_a_tabs_active = { bg = "#a89984", fg = "#3c3836" }
---        3: lualine_a_tabs_active = { bg = "#a89984", bold = true, fg = "#282828" }
---        4: lualine_transitional_lualine_a_tabs_active_to_lualine_c_normal = { bg = "#3c3836", fg = "#a89984" }
---        5: lualine_c_normal = { bg = "#3c3836", fg = "#a89984" }
---    }
---    {MATCH:|{1: %d+ }}
---    {MATCH:{1: %d+ }}
---    {2:}
---    {MATCH:{3: %d+ }}
---    {4:}
---    {MATCH:{5:%s+}|}
---
---]===])
--- ```
---
--- An easy way to create an expect block is to call `snapshot` method
--- on statusline where you'll call expect and run the test. It will print
--- an expect block based on the state of statusline. You can copy it and
--- replace the snapshot call with the expect call.
---
--- ``lua
--- statusline:snapshot()
--- ```

local ffi = require('ffi')
local helpers = require('tests.helpers')
local stub = require('luassert.stub')

local M = {}

ffi.cdef([[
typedef unsigned char char_u;
typedef struct window_S win_T;
extern win_T    *curwin;
typedef struct {
  char_u      *start;
  int userhl;
} stl_hlrec_t;
typedef struct {
} StlClickDefinition;
typedef struct {
  StlClickDefinition def;
  const char *start;
} StlClickRecord;
int build_stl_str_hl(
  win_T *wp,
  char_u *out,
  size_t outlen,
  char_u *fmt,
  int use_sandbox,
  char_u fillchar,
  int maxwidth,
  stl_hlrec_t **hltab,
  StlClickRecord **tabtab
);
]])

local function process_hlrec(hltab, stlbuf, eval_type)
  local function default_hl()
    if eval_type == 'tabline' then
      return 'TabLineFill'
    elseif eval_type == 'inactive' then
      return 'StatusLineNC'
    else
      return 'StatusLine'
    end
  end
  local len = #ffi.string(stlbuf)
  local hltab_data = hltab[0]
  local result = {}
  if hltab_data[0].start ~= stlbuf then
    table.insert(result, {
      group = default_hl(),
      start = 0,
    })
  end

  local n = 0
  while hltab_data[n].start ~= nil do
    local group_name
    if hltab_data[n].userhl == 0 then
      group_name = default_hl()
    elseif hltab_data[n].userhl < 0 then
      group_name = vim.fn.synIDattr(-1 * hltab_data[n].userhl, 'name')
    else
      group_name = string.format('User%d', hltab_data[n].userhl)
    end

    local hl_pos = { group = group_name }

    if n == 0 then
      hl_pos.start = hltab_data[n].start - stlbuf
    else
      hl_pos.start = result[#result].start + result[#result].len
    end
    if hltab_data[n + 1].start ~= nil then
      hl_pos.len = hltab_data[n + 1].start - hltab_data[n].start
    else
      hl_pos.len = (stlbuf + len) - hltab_data[n].start
    end
    table.insert(result, hl_pos)
    n = n + 1
  end
  return vim.tbl_filter(function(x)
    return x.len ~= 0
  end, result)
end

local function gen_stl(stl_fmt, width, eval_type)
  local stlbuf = ffi.new('char_u [?]', width + 100)
  local fmt = ffi.cast('char_u *', stl_fmt)
  local fillchar = ffi.cast('char_u', 0x20)
  local hltab = ffi.new('stl_hlrec_t *[1]', ffi.new('stl_hlrec_t *'))
  ffi.C.build_stl_str_hl(ffi.C.curwin, stlbuf, width + 100, fmt, 0, fillchar, width, hltab, nil)
  return { str = ffi.string(stlbuf), highlights = process_hlrec(hltab, stlbuf, eval_type) }
end

local function eval_stl(stl_expr, width, eval_type)
  local stl_buf, hl_list, stl_eval_res
  if vim.fn.has('nvim-0.6') == 1 then
    stl_eval_res = vim.api.nvim_eval_statusline(
      stl_expr,
      { maxwidth = width, highlights = true, fillchar = ' ', use_tabline = (eval_type == 'tabline') }
    )
  else
    stl_eval_res = gen_stl(stl_expr, width, eval_type)
  end
  stl_buf, hl_list = stl_eval_res.str, stl_eval_res.highlights

  local hl_map = {}

  local buf = { 'highlights = {' }
  local hl_id = 1
  for _, hl in ipairs(hl_list) do
    local hl_name = hl.group
    if not hl_map[hl_name] then
      hl_map[hl_name] = require('lualine.utils.utils').extract_highlight_colors(hl_name) or {}
      table.insert(
        buf,
        string.format(' %4d: %s = %s', hl_id, hl_name, vim.inspect(hl_map[hl_name], { newline = ' ', indent = '' }))
      )
      hl_map[hl_name].id = hl_id
      hl_id = hl_id + 1
    end
  end
  table.insert(buf, '}')

  local stl = {}
  for i = 1, #hl_list do
    local start, finish = hl_list[i].start, hl_list[i + 1] and hl_list[i + 1].start or #stl_buf
    if start ~= finish then
      table.insert(
        stl,
        string.format('{%d:%s}', hl_map[hl_list[i].group].id, vim.fn.strpart(stl_buf, start, finish - start))
      )
    end
  end
  table.insert(buf, '|' .. table.concat(stl, '\n') .. '|')
  table.insert(buf, '')
  return table.concat(buf, '\n')
end

function M:expect_expr(expect, expr)
  expect = helpers.dedent(expect)
  local actual = eval_stl(expr, self.width, self.type)
  local matched = true
  local errmsg = {}
  if expect ~= actual then
    expect = vim.split(expect, '\n')
    actual = vim.split(actual, '\n')
    if expect[#expect] == '' then
      expect[#expect] = nil
    end
    if actual[#actual] == '' then
      actual[#actual] = nil
    end
    for i = 1, math.max(#expect, #actual) do
      if expect[i] and actual[i] then
        local match_pat = expect[i]:match('{MATCH:(.*)}')
        if expect[i] == actual[i] or (match_pat and actual[i]:match(match_pat)) then
          expect[i] = string.rep(' ', 2) .. expect[i]
          actual[i] = string.rep(' ', 2) .. actual[i]
          goto loop_end
        end
      end
      matched = false
      if expect[i] then
        expect[i] = '*' .. string.rep(' ', 1) .. expect[i]
      end
      if actual[i] then
        actual[i] = '*' .. string.rep(' ', 1) .. actual[i]
      end
      ::loop_end::
    end
  end
  if not matched then
    table.insert(errmsg, 'Unexpected statusline')
    table.insert(errmsg, 'Expected:')
    table.insert(errmsg, table.concat(expect, '\n') .. '\n')
    table.insert(errmsg, 'Actual:')
    table.insert(errmsg, table.concat(actual, '\n'))
  end
  assert(matched, table.concat(errmsg, '\n'))
end

function M:snapshot_expr(expr)
  local type_map = {
    active = 'statusline',
    inactive = 'inactive_statusline',
    tabline = 'tabline',
  }
  print((type_map[self.type] or 'statusline') .. ':expect([===[')
  print(eval_stl(expr, self.width, self.type) .. ']===])')
end

function M:snapshot()
  local utils = require('lualine.utils.utils')
  stub(utils, 'is_focused')
  utils.is_focused.returns(self.type ~= 'inactive')
  local expr
  if self.type == 'inactive' then
    expr = require('lualine').statusline(false)
  elseif self.type == 'tabline' then
    expr = require('lualine').tabline()
  else
    expr = require('lualine').statusline(true)
  end
  self:snapshot_expr(expr)
  utils.is_focused:revert()
end

function M:expect(result)
  local utils = require('lualine.utils.utils')
  stub(utils, 'is_focused')
  utils.is_focused.returns(self.type ~= 'inactive')
  local expr
  if self.type == 'inactive' then
    expr = require('lualine').statusline(false)
  elseif self.type == 'tabline' then
    expr = require('lualine').tabline()
  else
    expr = require('lualine').statusline(true)
  end
  self:expect_expr(result, expr)
  utils.is_focused:revert()
end

function M.new(_, width, eval_type)
  if type(_) ~= 'table' then
    eval_type = width
    width = _
  end
  local self = {}
  self.width = width or 120
  self.type = eval_type
  if self.type == nil then
    self.type = 'active'
  end
  return setmetatable(self, {
    __index = M,
    __call = function(_, ...)
      M.new(...)
    end,
  })
end

return M.new()