local serialize_lua = require('krprint').serialize_lua
local deserialize_lua = require('krprint').deserialize_lua

local function gen_string(maxlen)
	maxlen = maxlen or 100
	local len = math.random(0, maxlen)
	local buf = {}
	for _=1,len do
		table.insert(buf, string.char(math.random(0, 255)))
	end
	return table.concat(buf)
end

local function test_de_serialization(orig_val, desc)
	local serial = serialize_lua(orig_val)
	ok(type(serial) == 'string' and #serial > 0,
		'serialization returns non-empty string: ' .. desc)
	local deserial_val = deserialize_lua(serial)
	same(type(orig_val), type(deserial_val),
		'deserialized value has the same type: ' .. desc)
	if type(orig_val) == 'number' then
		-- nan cannot be compared using == operator
		if tostring(orig_val) == 'nan' and tostring(deserial_val) == 'nan' then
			pass('nan value serialized and deserialized')
		elseif orig_val ~= math.huge and orig_val ~= -math.huge then
		-- tolerance measured experimentally on x86_64 LuaJIT 2.1.0-beta3
			local tolerance = 1e-14
			ok(math.abs(orig_val - deserial_val) <= tolerance,
				'deserialized number is within tolerance ' .. tolerance)
		else
			same(orig_val, deserial_val, 'deserialization returns the same infinity:' .. desc)
		end
	else
		same(orig_val, deserial_val,
			'deserialization returns the same value: ' .. desc)
	end
end

local function test_de_serialization_autodesc(orig_val)
	test_de_serialization(orig_val, tostring(orig_val))
end

local function test_bool()
	test_de_serialization_autodesc(true)
	same('true', table_print(true), 'table_print handles true')
	test_de_serialization_autodesc(false)
	same('false', table_print(false), 'table_print handles false')
end

local function test_nil()
	test_de_serialization_autodesc(nil)
	same('nil', table_print(nil), 'table_print handles nil')
end

local function gen_number_int()
	local number
	-- make "small" numbers more likely so they actually happen
	if math.random() < 0.5 then
		number = math.random(-2^32, 2^32)
	else
		number = math.random(-2^48, 2^48)
	end
	return number
end

local function gen_number_float()
	return math.random()
end

local function test_number()
	test_de_serialization_autodesc(0)
	same('0', table_print(0), 'table_print handles 0')
	test_de_serialization_autodesc(-math.huge)
	same('-inf', table_print(-math.huge), 'table_print handles -infinity')
	test_de_serialization_autodesc(math.huge)
	same('inf', table_print(math.huge), 'table_print handles +infinity')
	test_de_serialization_autodesc(tonumber('nan'))
	same('nan', table_print(tonumber('nan')), 'table_print handles nan')
	for _=1,20 do  -- integers
		test_de_serialization_autodesc(gen_number_int())
		-- bigger numbers might end up with non-exact representation
		local smallnumber = math.random(-2^32, 2^32)
		same(tostring(smallnumber), table_print(smallnumber),
			'table_print handles small numbers')
	end
	for _=1,20 do  -- floats
		local float = math.random()
		same(tostring(float), table_print(float),
			'table_print handles floats')
		test_de_serialization_autodesc(gen_number_float())
	end
end

local function test_string()
	test_de_serialization('', 'empty string')
	for _=1,20 do
		local str = gen_string(1024*10)
		test_de_serialization(str, 'random string length ' .. #str)
	end
end

local function gen_number()
	-- pure random would not produce special cases often enough
	local generators = {
		function() return 0 end,
		function() return -math.huge end,
		function() return math.huge end,
		gen_number_int,
		gen_number_float,
	}
	return generators[math.random(1, #generators)]()
end

local function gen_boolean()
	local options = {true, false}
	return options[math.random(1, #options)]
end

local function gen_table_atomic()
	-- nil keys or values are not allowed
	-- nested tables are handled elsewhere
	local supported_types = {
		gen_number,
		gen_string,
		gen_boolean,
	}
	val = supported_types[math.random(1, #supported_types)]()
	return val
end

local function gen_test_tables_supported(level)
	level = level or 1
	local max_level = 5
	local max_items_per_table = 20
	local t = {}
	for _=1, math.random(0, max_items_per_table) do
		local val_as_table = (level <= max_level) and math.random() < 0.1
		local key, val
		-- tapered.same method cannot compare keys with type table
		key = gen_table_atomic()
		if val_as_table then
			val = gen_test_tables_supported(level + 1)
		else
			val = gen_table_atomic()
		end
		t[key] = val
	end
	return t
end

local marker = 'this string must be present somewhere in output'
local function gen_marker()
	return marker
end

local kluautil = require('kluautil')
local function random_modify_table(t, always, generator)
	assert(generator)
	local tab_len = kluautil.kr_table_len(t)
	local modified = false
	-- modify some values
	for key, val in pairs(t) do
		if math.random(1, tab_len) == 1 then
			if type(val) == 'table' then
				modified = modified or random_modify_table(val, false, generator)
			else
				t[key] = generator()
				modified = true
			end
		end
	end
	if always and not modified then
		-- fallback, add an unsupported key
		t[generator()] = true
		modified = true
	end
	return modified
end

local function test_table_supported()
	for i=1,10 do
		local t = gen_test_tables_supported()
		test_de_serialization(t, 'random table no. ' .. i)
		assert(random_modify_table(t, true, gen_marker))
		local str = table_print(t)
		ok(string.find(str, marker, 1, true),
			'table_print works on complex serializable tables')
	end
end

local ffi = require('ffi')
local const_func = tostring
local const_thread = coroutine.create(tostring)
local const_userdata = ffi.C
local const_cdata = ffi.new('int')

local function gen_unsupported_atomic()
	-- nested tables are handled elsewhere
	local unsupported_types = {
		const_func,
		const_thread,
		const_userdata,
		const_cdata
	}
	val = unsupported_types[math.random(1, #unsupported_types)]
	return val
end

local function test_unsupported(val, desc)
	desc = desc or string.format('unsupported %s', type(val))
	return function()
		boom(serialize_lua, { val, 'error' }, string.format(
			'attempt to serialize %s in error mode '
			.. 'causes error', desc))
		local output = serialize_lua(val, 'comment')
		same('string', type(output),
			string.format('attempt to serialize %s in '
				.. 'comment mode returned a string',
				desc))
		ok(string.find(output, '--', 1, true),
			'returned string contains a comment')
		output = table_print(val)
		same('string', type(output),
			string.format('table_print can stringify %s', desc))
		if type(val) ~= 'table' then
			ok(string.find(output, type(val), 1, true),
				'exotic type is mentioned in table_print output')
		end
	end
end

local function gen_test_tables_unsupported()
	local t = gen_test_tables_supported()
	random_modify_table(t, true, gen_unsupported_atomic)
	return t
end

local function test_unsupported_table()
	for i=1,10 do
		local t = gen_test_tables_unsupported()
		test_unsupported(t, 'random unsupported table no. ' .. i)()
		assert(random_modify_table(t, true, gen_marker))
		local str = table_print(t)
		ok(string.find(str, marker, 1, true),
			'table_print works on complex unserializable tables')
	end
end

local function func_2vararg_5ret(arg1, arg2, ...)
	return select('#', ...), nil, arg1 + arg2, false, nil
end
local function func_ret_nil() return nil end
local function func_ret_nothing() return end

local function test_pprint_func()
	local t = { [false] = func_2vararg_5ret }
	local output = table_print(t)
	ok(string.find(output, 'function false(arg1, arg2, ...)', 1, true),
		'function parameters are pretty printed')
end

local function test_pprint_func_ret()
	local output = table_print(func_2vararg_5ret(1, 2, 'bla'))
	local exp = [[
1	-- result # 1
nil	-- result # 2
3	-- result # 3
false	-- result # 4
nil	-- result # 5]]
	same(output, exp, 'multiple return values are pretty printed')

	output = table_print(func_ret_nil())
	same(output, 'nil', 'single return value does not have extra comments')

	output = table_print(func_ret_nothing())
	same(output, nil, 'no return values to be printed cause nil output')
end

return {
	test_bool,
	test_nil,
	test_number,
	test_string,
	test_table_supported,
	test_unsupported(const_func),
	test_unsupported(const_thread),
	test_unsupported(const_userdata),
	test_unsupported(const_cdata),
	test_unsupported_table,
	test_pprint_func,
	test_pprint_func_ret,
}
