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

local clear = n.clear
local eval = n.eval
local eq = t.eq
local feed_command = n.feed_command
local retry = t.retry
local ok = t.ok
local source = n.source
local poke_eventloop = n.poke_eventloop
local load_adjust = n.load_adjust
local write_file = t.write_file
local is_os = t.is_os
local is_ci = t.is_ci

clear()
if t.is_asan() then
  pending('ASAN build is difficult to estimate memory usage', function() end)
  return
elseif is_os('win') then
  if is_ci('github') then
    pending(
      'Windows runners in Github Actions do not have a stable environment to estimate memory usage',
      function() end
    )
    return
  elseif eval("executable('wmic')") == 0 then
    pending('missing "wmic" command', function() end)
    return
  end
elseif eval("executable('ps')") == 0 then
  pending('missing "ps" command', function() end)
  return
end

local monitor_memory_usage = {
  memory_usage = function(self)
    local handle
    if is_os('win') then
      handle = io.popen('wmic process where processid=' .. self.pid .. ' get WorkingSetSize')
    else
      handle = io.popen('ps -o rss= -p ' .. self.pid)
    end
    return tonumber(handle:read('*a'):match('%d+'))
  end,
  op = function(self)
    retry(nil, 10000, function()
      local val = self.memory_usage(self)
      if self.max < val then
        self.max = val
      end
      table.insert(self.hist, val)
      ok(#self.hist > 20)
      local result = {}
      for key, value in ipairs(self.hist) do
        if value ~= self.hist[key + 1] then
          table.insert(result, value)
        end
      end
      table.remove(self.hist, 1)
      self.last = self.hist[#self.hist]
      eq(1, #result)
    end)
  end,
  dump = function(self)
    return 'max: ' .. self.max .. ', last: ' .. self.last
  end,
  monitor_memory_usage = function(self, pid)
    local obj = {
      pid = pid,
      max = 0,
      last = 0,
      hist = {},
    }
    setmetatable(obj, { __index = self })
    obj:op()
    return obj
  end,
}
setmetatable(monitor_memory_usage, {
  __call = function(self, pid)
    return monitor_memory_usage.monitor_memory_usage(self, pid)
  end,
})

describe('memory usage', function()
  local tmpfile = 'X_memory_usage'

  after_each(function()
    os.remove(tmpfile)
  end)

  local function check_result(tbl, status, result)
    if not status then
      print('')
      for key, val in pairs(tbl) do
        print(key, val:dump())
      end
      error(result)
    end
  end

  before_each(clear)

  --[[
  Case: if a local variable captures a:000, funccall object will be free
  just after it finishes.
  ]]
  --
  it('function capture vargs', function()
    local pid = eval('getpid()')
    local before = monitor_memory_usage(pid)
    write_file(
      tmpfile,
      [[
      func s:f(...)
        let x = a:000
      endfunc
      for _ in range(10000)
        call s:f(0)
      endfor
    ]]
    )
    -- TODO: check_result fails if command() is used here. Why? #16064
    feed_command('source ' .. tmpfile)
    poke_eventloop()
    local after = monitor_memory_usage(pid)
    -- Estimate the limit of max usage as 2x initial usage.
    -- The lower limit can fluctuate a bit, use 97%.
    check_result({ before = before, after = after }, pcall(ok, before.last * 97 / 100 < after.max))
    check_result({ before = before, after = after }, pcall(ok, before.last * 2 > after.max))
    -- In this case, garbage collecting is not needed.
    -- The value might fluctuate a bit, allow for 3% tolerance below and 5% above.
    -- Based on various test runs.
    local lower = after.last * 97 / 100
    local upper = after.last * 105 / 100
    check_result({ before = before, after = after }, pcall(ok, lower < after.max))
    check_result({ before = before, after = after }, pcall(ok, after.max < upper))
  end)

  --[[
  Case: if a local variable captures l: dict, funccall object will not be
  free until garbage collector runs, but after that memory usage doesn't
  increase so much even when rerun Xtest.vim since system memory caches.
  ]]
  --
  it('function capture lvars', function()
    local pid = eval('getpid()')
    local before = monitor_memory_usage(pid)
    write_file(
      tmpfile,
      [[
      if !exists('s:defined_func')
        func s:f()
          let x = l:
        endfunc
      endif
      let s:defined_func = 1
      for _ in range(10000)
        call s:f()
      endfor
    ]]
    )
    feed_command('source ' .. tmpfile)
    poke_eventloop()
    local after = monitor_memory_usage(pid)
    for _ = 1, 3 do
      -- TODO: check_result fails if command() is used here. Why? #16064
      feed_command('source ' .. tmpfile)
      poke_eventloop()
    end
    local last = monitor_memory_usage(pid)
    -- The usage may be a bit less than the last value, use 80%.
    -- Allow for 20% tolerance at the upper limit. That's very permissive, but
    -- otherwise the test fails sometimes.  On FreeBSD we need to be even much
    -- more permissive.
    local upper_multiplier = is_os('freebsd') and 19 or 12
    local lower = before.last * 8 / 10
    local upper = load_adjust((after.max + (after.last - before.last)) * upper_multiplier / 10)
    check_result({ before = before, after = after, last = last }, pcall(ok, lower < last.last))
    check_result({ before = before, after = after, last = last }, pcall(ok, last.last < upper))
  end)

  it('releases memory when closing windows when folds exist', function()
    if is_os('mac') then
      pending('macOS memory compression causes flakiness')
    end
    local pid = eval('getpid()')
    source([[
      new
      " Insert lines
      call nvim_buf_set_lines(0, 0, 0, v:false, repeat([''], 999))
      " Create folds
      normal! gg
      for _ in range(500)
        normal! zfjj
      endfor
    ]])
    poke_eventloop()
    local before = monitor_memory_usage(pid)
    source([[
      " Split and close window multiple times
      for _ in range(1000)
        split
        close
      endfor
    ]])
    poke_eventloop()
    local after = monitor_memory_usage(pid)
    source('bwipe!')
    poke_eventloop()
    -- Allow for an increase of 10% in memory usage, which accommodates minor fluctuation,
    -- but is small enough that if memory were not released (prior to PR #14884), the test
    -- would fail.
    local upper = before.last * 1.10
    check_result({ before = before, after = after }, pcall(ok, after.last <= upper))
  end)
end)
