-- SPDX-License-Identifier: GPL-3.0-or-later
local ffi = require('ffi')
log_info(ffi.C.LOG_GRP_TESTS, 'my PID = %d', worker.pid)

trust_anchors.remove('.')

cache.size = 2*MB

net = { '{{SELF_ADDR}}' }

{% if QMIN == "false" %}
option('NO_MINIMIZE', true)
{% else %}
option('NO_MINIMIZE', false)
{% endif %}

-- Self-checks on globals
assert(help() ~= nil)
assert(worker.id ~= nil)
-- Self-checks on facilities
assert(cache.count() == 0)
assert(cache.stats() ~= nil)
assert(cache.backends() ~= nil)
assert(worker.stats() ~= nil)
assert(net.interfaces() ~= nil)
-- Self-checks on loaded stuff
assert(#modules.list() > 0)
-- Self-check timers
ev = event.recurrent(1 * sec, function (ev) return 1 end)
event.cancel(ev)

local kluautil = require('kluautil')
local tap = require('tapered')
local checks_total = 16
local n_instances = 3  -- must match deckard.yaml

worker.control_path = worker.cwd .. '/../kresd3/control/'
net.listen(worker.control_path .. worker.pid, nil, {kind = 'control'})
assert(#net.list() >= 3)  -- UDP, TCP, control

-- debug, kept for future use
--log_level("debug")
log_debug(ffi.C.LOG_GRP_TESTS, '%s', worker.control_path)
log_debug(ffi.C.LOG_GRP_TESTS, '%s', table_print(net.list()))

function wait_for_sockets()
	log_info(ffi.C.LOG_GRP_TESTS, 'waiting for control sockets')
	local timeout = 5000 -- ms
	local start_time = tonumber(ffi.C.kr_now())
	local now
	while true do
		now = tonumber(ffi.C.kr_now())
		if now > start_time + timeout then
			log_info(ffi.C.LOG_GRP_TESTS, 'timeout while waiting for control sockets to appear')
			os.exit(3)
		end
		local pids = kluautil.list_dir(worker.control_path)
		if #pids == n_instances then
			-- debug, kept for future use
			log_debug(ffi.C.LOG_GRP_TESTS, 'got control sockets:')
			log_debug(ffi.C.LOG_GRP_TESTS, table_print(pids))
			break
		else
			worker.sleep(0.1)
		end
	end
	log_info(ffi.C.LOG_GRP_TESTS, 'PIDs are visible now (waiting took %d ms)', now - start_time)
end

-- expression should throw Lua error:
-- wrap it in a function which runs the expression on leader and follower
-- separately so we can guarantee both cases are covered
function boom_follower_and_leader(boom_expr, desc)
	local variants = {leader = '~=', follower = '=='}
	for name, operator in pairs(variants) do
		-- beware, newline is not allowed in expr
		local full_expr = string.format(
			'if (worker.pid %s %s) then return true	'
				.. 'else return %s end',
			operator, worker.pid, boom_expr)
		local full_desc = name .. ': '
		if desc then
			full_desc = full_desc .. desc .. ' (' .. boom_expr .. ')'
		else
			full_desc = full_desc .. boom_expr
		end
		tap.boom(map, {full_expr}, full_desc)
	end
end

function tests()
	-- add delay to each test to force scheduler to interleave tests and DNS queries
	local test_delay = 20 / 1000 -- seconds
	log_info(ffi.C.LOG_GRP_TESTS, 'starting map() tests now')

	tap.boom(map, {'1 ++ 1'}, 'syntax error in command is detected')
	worker.sleep(test_delay)

	-- array of integers
	local pids = map('worker.pid')
	tap.same(pids.n, n_instances, 'all pids were obtained')
	table.sort(pids)
	worker.sleep(test_delay)

	-- expression produces array of integers
	local pids_plus_one = map('worker.pid + 1')
	tap.same(pids_plus_one.n, n_instances, 'all pids were obtained')
	table.sort(pids_plus_one)
	for idx=1,n_instances do
		tap.same(pids[idx] + 1, pids_plus_one[idx],
			'increment expression worked')
	end
	worker.sleep(test_delay)

	-- error detection
	boom_follower_and_leader('error("explosion")')
	worker.sleep(test_delay)

	-- unsupported number of return values
	boom_follower_and_leader('1, 2')
	worker.sleep(test_delay)
	boom_follower_and_leader('unpack({})')
	worker.sleep(test_delay)

	-- unsupported return type
	boom_follower_and_leader(
		'function() print("this cannot be serialized") end')
	worker.sleep(test_delay)

	tap.same({n = n_instances}, map('nil'),
		'nil values are counted as returned')
	worker.sleep(test_delay)

	local exp = {n = n_instances}
	for i=1,n_instances do
		table.insert(exp, {nil, 2, nil, n=3})
	end
	local got = map('require("kluautil").kr_table_pack(nil, 2, nil)')
	tap.same(got, exp, 'kr_table_pack handles nil values')
	worker.sleep(test_delay)
end

local started = false
function tests_start()
	-- just in case, duplicates should not happen
	if started then
		log_info(ffi.C.LOG_GRP_TESTS, 'huh? duplicate test invocation ignored, a retransmit?')
		return
	end
	started = true
	log_info(ffi.C.LOG_GRP_TESTS, 'start query triggered, scheduling tests')

	-- DNS queries and map() commands must be serviced while sleep is running
	worker.coroutine(function() worker.sleep(3600) end)

	worker.coroutine(tests)
end
-- Deckard query will trigger tests
policy.add(policy.suffix(tests_start, {'\5start\0'}))

function tests_done()
	print('final query triggered')
	event.after(0, function()
		tap.done(checks_total)
	end)
end
-- Deckard query will execute tap.done() which will call os.exit()
-- i.e. this callback has to be called only after answer to Deckard was sent
policy.add(policy.suffix(tests_done, {'\4done\0'}), true)

-- add delay to each query to force scheduler to interleave tests and DNS queries
policy.add(policy.all(
	function()
		local delay = 10  -- ms
		log_info(ffi.C.LOG_GRP_TESTS, 'packet delayed by %d ms', delay)
		worker.sleep(delay / 1000)
	end))

wait_for_sockets()

{% if DAEMON_NAME == "kresd1" %}

-- forward to Deckard test server
policy.add(policy.all(policy.FORWARD('192.0.2.1')))

{% else %}

-- forward to next kresd instance in chain
{# find out IP address of kresd instance with lower number,
   i.e. kresd2 forwards to kresd1 #}
policy.add(policy.all(policy.FORWARD('{{ PROGRAMS[ "kresd" ~ (DAEMON_NAME[-1]|int() - 1)]["address"] }}')))

{% endif %}
