local unpack = unpack or table.unpack
local json = require("cjson")
local is_array = function(table)
  local max = 0
  local count = 0
  for k, v in pairs(table) do
    if type(k) == "number" then
      if k > max then
        max = k
      end
      count = count + 1
    else
      return -1
    end
  end
  if max > count * 2 then
    return -1
  end
  return max
end
local serialise_value
local serialise_table = function(value, indent, depth)
  local spacing, spacing2, indent2
  if indent then
    spacing = "\n" .. indent
    spacing2 = spacing .. "  "
    indent2 = indent .. "  "
  else
    spacing, spacing2, indent2 = " ", " ", false
  end
  depth = depth + 1
  if 50 < depth then
    return "Cannot serialise any further: too many nested tables"
  end
  local max = is_array(value)
  local comma = false
  local fragment = {
    "{" .. spacing2
  }
  if 0 < max then
    for i = 1, max do
      if comma then
        table.insert(fragment, "," .. spacing2)
      end
      table.insert(fragment, serialise_value(value[i], indent2, depth))
      comma = true
    end
  elseif max < 0 then
    for k, v in pairs(value) do
      if comma then
        table.insert(fragment, "," .. spacing2)
      end
      table.insert(fragment, ("[%s] = %s"):format(serialise_value(k, indent2, depth), serialise_value(v, indent2, depth)))
      comma = true
    end
  end
  table.insert(fragment, spacing .. "}")
  return table.concat(fragment)
end

function serialise_value(value, indent, depth)
  if indent == nil then
    indent = ""
  end
  if depth == nil then
    depth = 0
  end
  if value == json.null then
    return "json.null"
  elseif type(value) == "string" then
    return ("%q"):format(value)
  elseif type(value) == "nil" or type(value) == "number" or type(value) == "boolean" then
    return tostring(value)
  elseif type(value) == "table" then
    return serialise_table(value, indent, depth)
  else
    return "\"<" .. type(value) .. ">\""
  end
end

local file_load = function(filename)
  local file
  if filename == nil then
    file = io.stdin
  else
    local err
    file, err = io.open(filename, "rb")
    if file == nil then
      error(("Unable to read '%s': %s"):format(filename, err))
    end
  end
  local data = file:read("*a")
  if filename ~= nil then
    file:close()
  end
  if data == nil then
    error("Failed to read " .. filename)
  end
  return data
end
local file_save = function(filename, data)
  local file
  if filename == nil then
    file = io.stdout
  else
    local err
    file, err = io.open(filename, "wb")
    if file == nil then
      error(("Unable to write '%s': %s"):format(filename, err))
    end
  end
  file:write(data)
  if filename ~= nil then
    file:close()
  end
end

local function compare_values(val1, val2)
  local type1 = type(val1)
  local type2 = type(val2)
  if type1 ~= type2 then
    return false
  end
  if type1 == "number" and val1 ~= val1 and val2 ~= val2 then
    return true
  end
  if type1 ~= "table" then
    return val1 == val2
  end
  local check_keys = {}
  for k, _ in pairs(val1) do
    check_keys[k] = true
  end
  for k, v in pairs(val2) do
    if not check_keys[k] then
      return false
    end
    if not compare_values(val1[k], val2[k]) then
      return false
    end
    check_keys[k] = nil
  end
  for k, _ in pairs(check_keys) do
    return false
  end
  return true
end

local test_count_pass = 0
local test_count_total = 0
local run_test_summary = function()
  return test_count_pass, test_count_total
end
local run_test = function(testname, func, input, should_work, output)
  local status_line = function(name, status, value)
    local statusmap = {
      [true] = ":success",
      [false] = ":error"
    }
    if status ~= nil then
      name = name .. statusmap[status]
    end
    print(("[%s] %s"):format(name, serialise_value(value, false)))
  end
  local result = {
    pcall(func, unpack(input))
  }
  local success = table.remove(result, 1)
  local correct = false
  if success == should_work and compare_values(result, output) then
    correct = true
    test_count_pass = test_count_pass + 1
  end
  test_count_total = test_count_total + 1
  local teststatus = {
    [true] = "PASS",
    [false] = "FAIL"
  }
  print(("==> Test [%d] %s: %s"):format(test_count_total, testname, teststatus[correct]))
  status_line("Input", nil, input)
  if not correct then
    status_line("Expected", should_work, output)
  end
  status_line("Received", success, result)
  print()
  return correct, result
end
local run_test_group = function(tests)
  local run_helper = function(name, func, input)
    if type(name) == "string" and 0 < #name then
      print("==> " .. name)
    end
    func(unpack(input or {}))
    print()
  end
  for _, v in ipairs(tests) do
    if v[4] == nil then
      run_helper(unpack(v))
    else
      run_test(unpack(v))
    end
  end
end
local run_script = function(script, env)
  local env = env or {}
  local func
  if _G.setfenv then
    func = loadstring(script)
    if func then
      setfenv(func, env)
    end
  else
    func = load(script, nil, nil, env)
  end
  if func == nil then
    error("Invalid syntax.")
  end
  func()
  return env
end
return {
  serialise_value = serialise_value,
  file_load = file_load,
  file_save = file_save,
  compare_values = compare_values,
  run_test_summary = run_test_summary,
  run_test = run_test,
  run_test_group = run_test_group,
  run_script = run_script
}
