local assert = require('luassert.assert')
local say = require('luassert.say')

-- Example usage:
-- local arr = { "one", "two", "three" }
--
-- assert.array(arr).has.no.holes()   -- checks the array to not contain holes --> passes
-- assert.array(arr).has.no.holes(4)  -- sets explicit length to 4 --> fails
--
-- local first_hole = assert.array(arr).has.holes(4)     -- check array of size 4 to contain holes --> passes
-- assert.equal(4, first_hole)        -- passes, as the index of the first hole is returned


-- Unique key to store the object we operate on in the state object
-- key must be unique, to make sure we do not have name collissions in the shared state object
local ARRAY_STATE_KEY = "__array_state"

-- The modifier, to store the object in our state
local function array(state, args, level)
  assert(args.n > 0, "No array provided to the array-modifier")
  assert(rawget(state, ARRAY_STATE_KEY) == nil, "Array already set")
  rawset(state, ARRAY_STATE_KEY, args[1])
  return state
end

-- The actual assertion that operates on our object, stored via the modifier
local function holes(state, args, level)
  local length = args[1]
  local arr = rawget(state, ARRAY_STATE_KEY) -- retrieve previously set object
  -- only check against nil, metatable types are allowed
  assert(arr ~= nil, "No array set, please use the array modifier to set the array to validate")
  if length == nil then
    length = 0
    for i in pairs(arr) do
      if type(i) == "number" and
         i > length and
         math.floor(i) == i then
        length = i
      end
    end
  end
  assert(type(length) == "number", "expected array length to be of type 'number', got: "..tostring(length))
  -- let's do the actual assertion
  local missing
  for i = 1, length do
    if arr[i] == nil then
      missing = i
      break
    end
  end
  -- format arguments for output strings;
  args[1] = missing
  args.n = missing and 1 or 0
  return missing ~= nil, { missing } -- assert result + first missing index as return value
end

-- Register the proper assertion messages
say:set("assertion.array_holes.positive", [[
Expected array to have holes, but none was found.
]])
say:set("assertion.array_holes.negative", [[
Expected array to not have holes, hole found at position: %s
]])

-- Register the assertion, and the modifier
assert:register("assertion", "holes", holes,
                  "assertion.array_holes.positive",
                  "assertion.array_holes.negative")

assert:register("modifier", "array", array)
