local helpers = require('test.functional.helpers')(after_each)
local mpack = require('mpack')
local clear, funcs, eq = helpers.clear, helpers.funcs, helpers.eq
local call = helpers.call
local meths = helpers.meths

local function read_mpack_file(fname)
  local fd = io.open(fname, 'rb')
  if fd == nil then
    return nil
  end

  local data = fd:read('*a')
  fd:close()
  local unpack = mpack.Unpacker()
  return unpack(data)
end

describe("api_info()['version']", function()
  before_each(clear)

  it("returns API level", function()
    local version = call('api_info')['version']
    local current = version['api_level']
    local compat  = version['api_compatible']
    eq("number", type(current))
    eq("number", type(compat))
    assert(current >= compat)
  end)

  it("returns Nvim version", function()
    local version = call('api_info')['version']
    local major   = version['major']
    local minor   = version['minor']
    local patch   = version['patch']
    eq("number", type(major))
    eq("number", type(minor))
    eq("number", type(patch))
    eq(1, funcs.has("nvim-"..major.."."..minor.."."..patch))
    eq(0, funcs.has("nvim-"..major.."."..minor.."."..(patch + 1)))
    eq(0, funcs.has("nvim-"..major.."."..(minor + 1).."."..patch))
    eq(0, funcs.has("nvim-"..(major + 1).."."..minor.."."..patch))
  end)
end)


describe("api metadata", function()
  before_each(clear)

  local function name_table(entries)
    local by_name = {}
    for _,e in ipairs(entries) do
      by_name[e.name] = e
    end
    return by_name
  end

  -- Remove metadata that is not essential to backwards-compatibility.
  local function filter_function_metadata(f)
    f.deprecated_since = nil
    for idx, _ in ipairs(f.parameters) do
      f.parameters[idx][2] = ''  -- Remove parameter name.
    end

    if string.sub(f.name, 1, 4) ~= "nvim" then
      f.method = nil
    end
    return f
  end

  local function check_ui_event_compatible(old_e, new_e)
    -- check types of existing params are the same
    -- adding parameters is ok, but removing params is not (gives nil error)
    eq(old_e.since, new_e.since, old_e.name)
    for i,p in ipairs(old_e.parameters) do
      eq(new_e.parameters[i][1], p[1], old_e.name)
    end
  end

  -- Level 0 represents methods from 0.1.5 and earlier, when 'since' was not
  -- yet defined, and metadata was not filtered of internal keys like 'async'.
  local function clean_level_0(metadata)
    for _, f in ipairs(metadata.functions) do
      f.can_fail = nil
      f.async = nil
      f.receives_channel_id = nil
      f.since = 0
    end
  end

  local api, compat, stable, api_level
  local old_api = {}
  setup(function()
    api = meths.get_api_info()[2]
    compat  = api.version.api_compatible
    api_level = api.version.api_level
    if api.version.api_prerelease then
      stable = api_level-1
    else
      stable = api_level
    end

    for level = compat, stable do
      local path = ('test/functional/fixtures/api_level_'..
                   tostring(level)..'.mpack')
      old_api[level] = read_mpack_file(path)
      if old_api[level] == nil then
        local errstr = "missing metadata fixture for stable level "..level..". "
        if level == api_level and not api.version.api_prerelease then
          errstr = (errstr.."If NVIM_API_CURRENT was bumped, "..
                    "don't forget to set NVIM_API_PRERELEASE to true.")
        end
        error(errstr)
      end

      if level == 0 then
        clean_level_0(old_api[level])
      end
    end
  end)

  it("functions are compatible with old metadata or have new level", function()
    local funcs_new = name_table(api.functions)
    local funcs_compat = {}
    for level = compat, stable do
      for _,f in ipairs(old_api[level].functions) do
        if funcs_new[f.name] == nil then
          if f.since >= compat then
            error('function '..f.name..' was removed but exists in level '..
                  f.since..' which nvim should be compatible with')
          end
        else
          eq(filter_function_metadata(f),
             filter_function_metadata(funcs_new[f.name]))
        end
      end
      funcs_compat[level] = name_table(old_api[level].functions)
    end

    for _,f in ipairs(api.functions) do
      if f.since <= stable then
        local f_old = funcs_compat[f.since][f.name]
        if f_old == nil then
          if string.sub(f.name, 1, 4) == "nvim" then
            local errstr = ("function "..f.name.." has too low since value. "..
                            "For new functions set it to "..(stable+1)..".")
            if not api.version.api_prerelease then
              errstr = (errstr.." Also bump NVIM_API_CURRENT and set "..
                        "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
            end
            error(errstr)
          else
            error("function name '"..f.name.."' doesn't begin with 'nvim_'")
          end
        end
      elseif f.since > api_level then
        if api.version.api_prerelease then
          error("New function "..f.name.." should use since value "..
               api_level)
        else
          error("function "..f.name.." has since value > api_level. "..
               "Bump NVIM_API_CURRENT and set "..
               "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
        end
      end
    end
  end)

  it("UI events are compatible with old metadata or have new level", function()
    local ui_events_new = name_table(api.ui_events)
    local ui_events_compat = {}

    -- UI events were formalized in level 3
    for level = 3, stable do
      for _,e in ipairs(old_api[level].ui_events) do
        local new_e = ui_events_new[e.name]
        if new_e ~= nil then
          check_ui_event_compatible(e, new_e)
        end
      end
      ui_events_compat[level] = name_table(old_api[level].ui_events)
    end

    for _,e in ipairs(api.ui_events) do
      if e.since <= stable then
        local e_old = ui_events_compat[e.since][e.name]
        if e_old == nil then
          local errstr = ("UI event "..e.name.." has too low since value. "..
                          "For new events set it to "..(stable+1)..".")
          if not api.version.api_prerelease then
            errstr = (errstr.." Also bump NVIM_API_CURRENT and set "..
                      "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
          end
          error(errstr)
        end
      elseif e.since > api_level then
        if api.version.api_prerelease then
          error("New UI event "..e.name.." should use since value "..
               api_level)
        else
          error("UI event "..e.name.." has since value > api_level. "..
               "Bump NVIM_API_CURRENT and set "..
               "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
        end
      end
    end
  end)

  it("ui_options are preserved from older levels", function()
    local available_options = {}
    for _, option in ipairs(api.ui_options) do
      available_options[option] = true
    end
    -- UI options were versioned from level 4
    for level = 4, stable do
      for _, option in ipairs(old_api[level].ui_options) do
        if not available_options[option] then
          error("UI option "..option.." from stable metadata is missing")
        end
      end
    end
  end)
end)
