--------------------------------------------------------------------------------
-- 0060-deque.lua: tests for double-ended queue wrapper
-- This file is a part of lua-nucleo library
-- Copyright (c) lua-nucleo authors (see file `COPYRIGHT` for the license)
--------------------------------------------------------------------------------

local make_suite = assert(loadfile('test/test-lib/init/strict.lua'))(...)

local ensure,
      ensure_equals,
      ensure_tequals,
      ensure_tdeepequals,
      ensure_fails_with_substring
      = import 'lua-nucleo/ensure.lua'
      {
        'ensure',
        'ensure_equals',
        'ensure_tequals',
        'ensure_tdeepequals',
        'ensure_fails_with_substring'
      }

local arguments
      = import 'lua-nucleo/args.lua'
      {
        'arguments'
      }

local tset,
      tclone,
      empty_table,
      tstr
      = import 'lua-nucleo/table.lua'
      {
        'tset',
        'tclone',
        'empty_table',
        'tstr'
      }

local make_value_generators
      = import 'test/test-lib/value_generators.lua'
      {
        'make_value_generators'
      }

--------------------------------------------------------------------------------

local make_deque,
      deque_exports
      = import 'lua-nucleo/deque.lua'
      {
        'make_deque'
      }

--------------------------------------------------------------------------------

local test = make_suite("deque", deque_exports)

--------------------------------------------------------------------------------

test:factory "make_deque" ((function()
  return debug.getmetatable(make_deque()).__index -- TODO: Hack
end))

test:methods "back"
             "pop_back"
             "push_back"
             "front"
             "pop_front"
             "push_front"
             "size"

--------------------------------------------------------------------------------

test "deque-empty-size" (function()
  local deque = make_deque()
  ensure_equals("size", deque:size(), 0)
end)

test "deque-empty-back" (function()
  local deque = make_deque()
  ensure_equals("back", deque:back(), nil)
end)

test "deque-empty-front" (function()
  local deque = make_deque()
  ensure_equals("front", deque:front(), nil)
end)

test "deque-empty-pop_back" (function()
  local deque = make_deque()
  ensure_equals("pop_back", deque:pop_back(), nil)
end)

test "deque-empty-pop_front" (function()
  local deque = make_deque()
  ensure_equals("pop_front", deque:pop_front(), nil)
end)

test "deque-empty-push-pop-back" (function()
  local deque = make_deque()

  deque:push_back(42)

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)

  ensure_equals("pop back", deque:pop_back(), 42)

  ensure_equals("size empty", deque:size(), 0)
  ensure_equals("back empty", deque:back(), nil)
  ensure_equals("front empty", deque:front(), nil)
end)

test "deque-empty-push-pop-front" (function()
  local deque = make_deque()

  deque:push_front(42)

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)

  ensure_equals("pop front", deque:pop_front(), 42)

  ensure_equals("size empty", deque:size(), 0)
  ensure_equals("back empty", deque:back(), nil)
  ensure_equals("front empty", deque:front(), nil)
end)

test "deque-empty-push-back-pop-front" (function()
  local deque = make_deque()

  deque:push_back(42)

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)

  ensure_equals("pop front", deque:pop_front(), 42)

  ensure_equals("size empty", deque:size(), 0)
  ensure_equals("back empty", deque:back(), nil)
  ensure_equals("front empty", deque:front(), nil)
end)

test "deque-empty-push-front-pop-back" (function()
  local deque = make_deque()

  deque:push_front(42)

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)

  ensure_equals("pop back", deque:pop_back(), 42)

  ensure_equals("size empty", deque:size(), 0)
  ensure_equals("back empty", deque:back(), nil)
  ensure_equals("front empty", deque:front(), nil)
end)

test "deque-empty-over-pop_front" (function()
  local deque = make_deque()

  ensure_equals("pop front 1", deque:pop_front(), nil)
  ensure_equals("pop front 2", deque:pop_front(), nil)
  ensure_equals("pop front 3", deque:pop_front(), nil)

  deque:push_front(42)

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)
end)

test "deque-empty-over-pop_back" (function()
  local deque = make_deque()

  ensure_equals("pop back 1", deque:pop_back(), nil)
  ensure_equals("pop back 2", deque:pop_back(), nil)
  ensure_equals("pop back 3", deque:pop_back(), nil)

  deque:push_back(42)

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)
end)

--------------------------------------------------------------------------------

test "deque-wraps-data" (function()
  local t = { 42 }

  local deque = make_deque(t)

  ensure_equals("deque wraps data", deque, t) -- Direct equality

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)

  ensure_equals("size", deque:size(), 1)
  ensure_equals("back", deque:back(), 42)
  ensure_equals("front", deque:front(), 42)

  ensure_equals("pop back", deque:pop_back(), 42)

  ensure_equals("size empty", deque:size(), 0)
  ensure_equals("back empty", deque:back(), nil)
  ensure_equals("front empty", deque:front(), nil)
end)

--------------------------------------------------------------------------------

test "deque-empty-push_back-nil-fails" (function()
  local deque = make_deque()
  ensure_fails_with_substring(
      "push_back nil",
      function() deque:push_back(nil) end,
      "deque: can't push nil"
    )
end)

test "deque-empty-pop_back-nil-fails" (function()
  local deque = make_deque()
  ensure_fails_with_substring(
      "push_back nil",
      function() deque:push_back(nil) end,
      "deque: can't push nil"
    )
end)

test "deque-empty-pre-set-back" (function()
  -- A semi-official side effect.
  local deque = make_deque({ [0] = "EMPTY" })

  ensure_equals("empty size", deque:size(), 0)
  ensure_equals("empty back", deque:back(), nil)
  ensure_equals("empty pop_back", deque:pop_back(), nil)

  deque:push_back(42)

  ensure_equals("non-empty size", deque:size(), 1)
  ensure_equals("non-empty back", deque:size(), 1)

  ensure_equals("pop_back", deque:pop_back(), 42)

  ensure_equals("empty size again", deque:size(), 0)
  ensure_equals("empty back again", deque:back(), nil)

  ensure_equals("zero is still there", deque[0], "EMPTY")
end)

test "deque-on-metatable-fails" (function()
  local data = setmetatable({ }, { })
  ensure_fails_with_substring(
      "push_back nil",
      function() make_deque(data) end,
      "can't create deque on data with metatable"
    )
end)

--------------------------------------------------------------------------------

test "deque-random" (function()
  local NUM_ITER = 1e4
  local MAX_NUM_OPERATIONS = 1e2
  local MAX_INITIAL_SIZE = 1e3

  local value_generators = make_value_generators(tset({ "userdata" }))
  local random_value = function()
    return value_generators[math.random(#value_generators)]()
  end

  -- Not using numeric keys in hash part to avoid confusing #t
  local key_generators = make_value_generators(
      tset({ "userdata", "no-numbers" })
    )
  local random_key = function()
    return key_generators[math.random(#key_generators)]()
  end

  local check_once = function()
    local initial_data = nil

    if math.random() > 0.5 then
      -- print("using empty initial data")
    else
      -- print("using non-empty initial data")

      initial_data = { }

      for i = 1, math.random(MAX_INITIAL_SIZE) do
        if math.random() > 0.25 then
          -- Array part
          initial_data[#initial_data + 1] = random_value()
        else
          -- Hash part
          initial_data[random_key()] = random_value()
        end
      end
    end

    local expected = { }
    for k, v in pairs(initial_data or empty_table) do
      -- Note can't use tclone(), need shallow copy.
      expected[k] = v
    end

    local deque = make_deque(initial_data)

    local check_ends = function(deque, expected)
      local expected_size = #expected
      local expected_back = expected[expected_size]
      if expected_size == 0 then
        expected_back = nil
      end
      local expected_front = expected[1]

      ensure_equals("size", deque:size(), expected_size)
      ensure_equals("back", deque:back(), expected_back)
      ensure_equals("front", deque:front(), expected_front)

      return expected_size, expected_back, expected_front
    end

    local operations =
    {
      -- Push back
      function(deque, expected)
        local size, back, front = check_ends(deque, expected)

        local value = random_value()
        deque:push_back(value)

        expected[#expected + 1] = value

        check_ends(deque, expected)
      end;
      -- Push front
      function(deque, expected)
        local size, back, front = check_ends(deque, expected)

        local value = random_value()
        deque:push_front(value)

        table.insert(expected, 1, value)

        check_ends(deque, expected)
      end;
      -- Pop back
      function(deque, expected)
        local size, back, front = check_ends(deque, expected)

        --print("BEFORE POP deque   ", tstr(deque))
        --print("BEFORE POP expected", tstr(expected))

        ensure_equals("sanity check", table.remove(expected), back)
        ensure_equals("pop_back", deque:pop_back(), back)

        check_ends(deque, expected)
      end;
      -- Pop front
      function(deque, expected)
        local size, back, front = check_ends(deque, expected)

        ensure_equals("sanity check", table.remove(expected, 1), front)
        ensure_equals("pop_front", deque:pop_front(), front)

        check_ends(deque, expected)
      end;
    }

    check_ends(deque, expected)
    for i = 1, math.random(MAX_NUM_OPERATIONS) do
      operations[math.random(#operations)](deque, expected)
    end
    check_ends(deque, expected)
  end

  for i = 1, NUM_ITER do
    if i % 1000 == 0 then
      print("deque check", i, "of", NUM_ITER)
    end
    check_once()
  end
end)
