-- SPDX-License-Identifier: GPL-3.0-or-later
local ffi = require('ffi')

-- test if constants work properly
local function test_constants()
	same(kres.class.IN, 1, 'class constants work')
	same(kres.type.NS, 2, 'record type constants work')
	same(kres.type.TYPE2, 2, 'unnamed record type constants work')
	same(kres.type.BADTYPE, nil, 'non-existent type constants are checked')
	same(kres.section.ANSWER, 0, 'section constants work')
	same(kres.rcode.SERVFAIL, 2, 'rcode constants work')
	same(kres.opcode.UPDATE, 5, 'opcode constants work')
	-- Test inverse tables to convert constants to text
	same(kres.tostring.class[1], 'IN', 'text class constants work')
	same(kres.tostring.type[2], 'NS', 'text record type constants work')
	same(kres.tostring.type[65535], 'TYPE65535', 'text record type undefined constants work')
	same(kres.tostring.section[0], 'ANSWER', 'text section constants work')
	same(kres.tostring.rcode[2], 'SERVFAIL', 'text rcode constants work')
	same(kres.tostring.opcode[5], 'UPDATE', 'text opcode constants work')
end

-- test globals
local function test_globals()
	ok(mode('strict'), 'changing strictness mode')
	boom(mode, {'badmode'}, 'changing to non-existent strictness mode')
	same(reorder_RR(true), true, 'answer section reordering')
	same(option('REORDER_RR', false), false, 'generic option call')
	boom(option, {'REORDER_RR', 'potato'}, 'generic option call argument check')
	boom(option, {'MARS_VACATION', false}, 'generic option check name')
	same(table_print('crabdiary'), "'crabdiary'", 'table print works')
	same(table_print({fakepizza=1}), "{\n    ['fakepizza'] = 1,\n}", 'table print works on tables')
end

-- test global API functions
local function test_global_functions()
	boom(kres.parse_rdata, {'A 127.0.0.1'}, 'parse_rdata with non-table argument')
	boom(kres.parse_rdata, {{1}}, 'parse_rdata with non-string data in arg table')
	same(kres.parse_rdata({'A 127.0.0.1'}), {string.char(127, 0, 0, 1)}, 'parse_rdata with single record')
	same(kres.parse_rdata({'A 127.0.0.1', 'A 127.0.0.2'}),
		{string.char(127, 0, 0, 1), string.char(127, 0, 0, 2)}, 'parse_rdata with multiple records')
end

-- test if dns library functions work
local function test_rrset_functions()
	local rr = {owner = '\3com\0', ttl = 1, type = kres.type.TXT, rdata = '\5hello'}
	local rr_text = tostring(kres.rr2str(rr))
	same(rr_text:gsub('%s+', ' '), 'com. 1 TXT "hello"', 'rrset to text works')
	same(kres.dname2str(todname('com.')), 'com.', 'domain name conversion works')
	-- test creating rrset
	rr = kres.rrset(todname('com.'), kres.type.A, kres.class.IN, 66)
	ok(ffi.istype(kres.rrset, rr), 'created an empty RR')
	same(rr:owner(), '\3com\0', 'created RR has correct owner')
	same(rr:class(), kres.class.IN, 'created RR has correct class')
	same(rr:class(kres.class.CH), kres.class.CH, 'can set a different class')
	same(rr:class(kres.class.IN), kres.class.IN, 'can restore a class')
	same(rr.type, kres.type.A, 'created RR has correct type')
	-- test adding rdata
	same(rr:wire_size(), 0, 'empty RR wire size is zero')
	ok(rr:add_rdata('\1\2\3\4', 4), 'adding RDATA works')
	same(rr:wire_size(), 5 + 4 + 4 + 2 + 4, 'RR wire size works after adding RDATA')
	-- test conversion to text
	local expect = 'com.                	66	A	1.2.3.4\n'
	same(rr:txt_dump(), expect, 'RR to text works')
	-- create a dummy rrsig
	local rrsig = kres.rrset(todname('com.'), kres.type.RRSIG, kres.class.IN, 0)
	rrsig:add_rdata('\0\1', 2)
	same(rr:rdcount(), 1, 'add_rdata really added RDATA')
	-- check rrsig matching
	same(rr.type, rrsig:type_covered(), 'rrsig type covered matches covered RR type')
	ok(rr:is_covered_by(rrsig), 'rrsig is covering a record')
	-- test rrset merging
	local copy = kres.rrset(rr:owner(), rr.type, kres.class.IN, 66)
	ok(copy:add_rdata('\4\3\2\1', 4), 'adding second RDATA works')
	ok(rr:merge_rdata(copy), 'merge_rdata works')
	same(rr:rdcount(), 2, 'RDATA count is correct after merge_rdata')
	expect = 'com.                	66	A	1.2.3.4\n' ..
	         'com.                	66	A	4.3.2.1\n'
	same(rr:txt_dump(), expect, 'merge_rdata actually merged RDATA')
end

-- test dns library packet interface
local function test_packet_functions()
	local pkt = kres.packet(512)
	isnt(pkt, nil, 'creating packets works')
	-- Test manipulating header
	ok(pkt:rcode(kres.rcode.NOERROR), 'setting rcode works')
	same(pkt:rcode(), 0, 'getting rcode works')
	same(pkt:opcode(), 0, 'getting opcode works')
	is(pkt:aa(), false, 'packet is created without AA')
	is(pkt:ra(), false, 'packet is created without RA')
	is(pkt:ad(), false, 'packet is created without AD')
	ok(pkt:rd(true), 'setting RD bit works')
	is(pkt:rd(), true, 'getting RD bit works')
	ok(pkt:tc(true), 'setting TC bit works')
	is(pkt:tc(), true, 'getting TC bit works')
	ok(pkt:tc(false), 'disabling TC bit works')
	is(pkt:tc(), false, 'getting TC bit after disable works')
	is(pkt:cd(), false, 'getting CD bit works')
	is(pkt:id(1234), 1234, 'setting MSGID works')
	is(pkt:id(), 1234, 'getting MSGID works')
	-- Test manipulating question
	is(pkt:qname(), nil, 'reading name from empty question')
	is(pkt:qtype(), 0, 'reading type from empty question')
	is(pkt:qclass(), 0, 'reading class from empty question')
	ok(pkt:question(todname('hello'), kres.class.IN, kres.type.A), 'setting question section works')
	same(pkt:qname(), todname('hello'), 'reading QNAME works')
	same(pkt:qtype(), kres.type.A, 'reading QTYPE works')
	same(pkt:qclass(), kres.class.IN, 'reading QCLASS works')
	-- Test manipulating sections
	ok(pkt:begin(kres.section.ANSWER), 'switching sections works')
	local res, err = pkt:put(nil, 0, 0, 0, '')
	isnt(res, true, 'inserting nil entry doesnt work')
	isnt(err.code, 0, 'error code is non-zero')
	isnt(tostring(res), '', 'inserting nil returns invalid parameter')
	ok(pkt:put(pkt:qname(), 900, pkt:qclass(), kres.type.A, '\1\2\3\4'), 'adding rrsets works')
	boom(pkt.begin, {pkt, 10}, 'switching to invalid section doesnt work')
	ok(pkt:begin(kres.section.ADDITIONAL), 'switching to different section works')
	boom(pkt.begin, {pkt, 0}, 'rewinding sections doesnt work')
	local before_insert = pkt:remaining_bytes()
	ok(pkt:put(pkt:qname(), 900, pkt:qclass(), kres.type.A, '\4\3\2\1'), 'adding rrsets to different section works')
	same(pkt:remaining_bytes(), before_insert - (2 + 4 + 4 + 2 + 4), 'remaining bytes count goes down with insertions')
	-- Test conversions to text
	like(pkt:tostring(), '->>HEADER<<-', 'packet to text works')
	-- Test deserialization
	local wire = pkt:towire()
	same(#wire, 55, 'packet serialization works')
	local parsed = kres.packet(#wire, wire)
	isnt(parsed, nil, 'creating packet from wire works')
	ok(parsed:parse(), 'parsing packet from wire works')
	same(parsed:qname(), pkt:qname(), 'parsed packet has same QNAME')
	same(parsed:qtype(), pkt:qtype(), 'parsed packet has same QTYPE')
	same(parsed:qclass(), pkt:qclass(), 'parsed packet has same QCLASS')
	same(parsed:opcode(), pkt:opcode(), 'parsed packet has same opcode')
	same(parsed:rcode(), pkt:rcode(), 'parsed packet has same rcode')
	same(parsed:rd(), pkt:rd(), 'parsed packet has same RD')
	same(parsed:id(), pkt:id(), 'parsed packet has same MSGID')
	same(parsed:qdcount(), pkt:qdcount(), 'parsed packet has same question count')
	same(parsed:ancount(), pkt:ancount(), 'parsed packet has same answer count')
	same(parsed:nscount(), pkt:nscount(), 'parsed packet has same authority count')
	same(parsed:arcount(), pkt:arcount(), 'parsed packet has same additional count')
	same(parsed:tostring(), pkt:tostring(), 'parsed packet is equal to source packet')

	-- Test adding RR sets directly
	local copy = kres.packet(512)
	copy:question(todname('hello'), kres.class.IN, kres.type.A)
	copy:begin(kres.section.ANSWER)
	local rr = kres.rrset(pkt:qname(), kres.type.A, kres.class.IN, 66)
	rr:add_rdata('\4\3\2\1', 4)
	ok(copy:put_rr(rr), 'adding RR sets directly works')
	ok(copy:recycle(), 'recycling packet works')

	-- Test recycling of packets
	-- Clear_payload keeps header + question intact
	local cleared = kres.packet(#wire, wire) -- same as "parsed" above
	ok(cleared:parse(), 'parsing packet from wire works')
	ok(cleared:clear_payload(), 'clear_payload works')
	same(cleared:id(), pkt:id(), 'cleared packet has same MSGID')
	same(cleared:qr(), pkt:qr(), 'cleared packet has same QR')
	same(cleared:opcode(), pkt:opcode(), 'cleared packet has same OPCODE')
	same(cleared:aa(), pkt:aa(), 'cleared packet has same AA')
	same(cleared:tc(), pkt:tc(), 'cleared packet has same TC')
	same(cleared:rd(), pkt:rd(), 'cleared packet has same RD')
	same(cleared:ra(), pkt:ra(), 'cleared packet has same RA')
	same(cleared:ad(), pkt:ad(), 'cleared packet has same AD')
	same(cleared:cd(), pkt:cd(), 'cleared packet has same CD')
	same(cleared:rcode(), pkt:rcode(), 'cleared packet has same RCODE')
	same(cleared:qdcount(), pkt:qdcount(), 'cleared packet has same question count')
	same(cleared:ancount(), 0, 'cleared packet has no answers')
	same(cleared:nscount(), 0, 'cleared packet has no authority')
	same(cleared:arcount(), 0, 'cleared packet has no additional')
	same(cleared:qname(), pkt:qname(), 'cleared packet has same QNAME')
	same(cleared:qtype(), pkt:qtype(), 'cleared packet has same QTYPE')
	same(cleared:qclass(), pkt:qclass(), 'cleared packet has same QCLASS')

	-- Recycle clears question as well
	ok(pkt:recycle(), 'recycle() works')
	is(pkt:ancount(), 0, 'recycle() clears records')
	is(pkt:qname(), nil, 'recycle() clears question')
	is(#pkt:towire(), 12, 'recycle() clears the packet wireformat')
end

-- test JSON encode/decode functions
local function test_json_functions()
	for msg, obj in pairs({
			['number'] = 0,
			['string'] = 'ok',
			['list'] = {1, 2, 3},
			['map'] = {foo='bar'},
			['nest structure'] = {foo='bar', baz={1,2,3}},
	}) do
		same(fromjson(tojson(obj)), obj, 'json test: ' .. msg)
	end

	for _, str in ipairs({
			'{', '}',
			'[', ']',
			'x,',
			'[1,2,3,]',
	}) do
		boom(fromjson, {'{'}, 'json test: invalid \'' .. str .. '\'')
	end
end

return {
	test_constants,
	test_globals,
	test_global_functions,
	test_rrset_functions,
	test_packet_functions,
	test_json_functions,
}
