/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <tap/basic.h>

#include "test_conf.h"
#include "knot/conf/confio.h"
#include "knot/conf/tools.h"
#include "libknot/yparser/yptrafo.h"
#include "contrib/string.h"
#include "contrib/openbsd/strlcat.h"

#define SKIP_OPENBSD	skip("Nested transactions are not supported on OpenBSD");
#define OUT_LEN		1024
#define ZONE1		"zone1"
#define ZONE2		"zone2"
#define ZONE3		"zone3"

char *format_key(conf_io_t *io)
{
	knot_dname_txt_storage_t id;
	size_t id_len = sizeof(id);

	// Get the textual item id.
	if (io->id_len > 0 && !io->id_as_data) {
		if (yp_item_to_txt(io->key0->var.g.id, io->id, io->id_len, id,
		                   &id_len, YP_SNOQUOTE) != KNOT_EOK) {
			return NULL;
		}
	}

	// Get the item prefix.
	const char *prefix = "";
	switch (io->type) {
	case NEW: prefix = "+"; break;
	case OLD: prefix = "-"; break;
	default: break;
	}

	// Format the item key.
	return sprintf_alloc(
		"%s%.*s%s%.*s%s%s%.*s",
		prefix, (int)io->key0->name[0], io->key0->name + 1,
		(io->id_len > 0 && !io->id_as_data ? "[" : ""),
		(io->id_len > 0 && !io->id_as_data ? (int)id_len : 0), id,
		(io->id_len > 0 && !io->id_as_data ? "]" : ""),
		(io->key1 != NULL ? "." : ""),
		(io->key1 != NULL ? (int)io->key1->name[0] : 0),
		(io->key1 != NULL ? io->key1->name + 1 : ""));
}

static int append_data(const yp_item_t *item, const uint8_t *bin, size_t bin_len,
                       char *out, size_t out_len)
{
	char buf[YP_MAX_TXT_DATA_LEN + 1] = "\0";
	size_t buf_len = sizeof(buf);

	int ret = yp_item_to_txt(item, bin, bin_len, buf, &buf_len, YP_SNONE);
	if (ret != KNOT_EOK) {
		return ret;
	}

	if (strlcat(out, buf, out_len) >= out_len) {
		return KNOT_ESPACE;
	}

	return KNOT_EOK;
}

char *format_data(conf_io_t *io)
{
	char out[YP_MAX_TXT_DATA_LEN + 1] = "\0";

	// Return the item identifier as the item data.
	if (io->id_as_data) {
		if (append_data(io->key0->var.g.id, io->id, io->id_len, out,
		                sizeof(out)) != KNOT_EOK) {
			return NULL;
		}

		return strdup(out);
	}

	// Check for no data.
	if (io->data.val == NULL && io->data.bin == NULL) {
		return NULL;
	}

	const yp_item_t *item = (io->key1 != NULL) ? io->key1 : io->key0;

	// Format explicit binary data value.
	if (io->data.bin != NULL) {
		if (append_data(item, io->data.bin, io->data.bin_len, out,
		                sizeof(out)) != KNOT_EOK) {
			return NULL;
		}
	// Format multivalued item data.
	} else if (item->flags & YP_FMULTI) {
		size_t values = conf_val_count(io->data.val);
		for (size_t i = 0; i < values; i++) {
			// Skip other values if known index (counted from 1).
			if (io->data.index > 0 &&
			    io->data.index != i + 1) {
				conf_val_next(io->data.val);
				continue;
			}

			if (i > 0) {
				if (strlcat(out, " ", sizeof(out)) >= sizeof(out)) {
					return NULL;
				}
			}

			conf_val(io->data.val);
			if (append_data(item, io->data.val->data, io->data.val->len,
			                out, sizeof(out)) != KNOT_EOK) {
				return NULL;
			}

			conf_val_next(io->data.val);
		}
	// Format singlevalued item data.
	} else {
		conf_val(io->data.val);
		if (append_data(item, io->data.val->data, io->data.val->len, out,
		                sizeof(out)) != KNOT_EOK) {
			return NULL;
		}
	}

	return strdup(out);
}

static int format_item(conf_io_t *io)
{
	char *out = (char *)io->misc;

	// Get the item key and data strings.
	char *key = format_key(io);
	char *data = format_data(io);

	// Format the item.
	char *item = sprintf_alloc(
		"%s%s%s%s",
		(*out != '\0' ? "\n" : ""),
		(key != NULL ? key : ""),
		(data != NULL ? " = " : ""),
		(data != NULL ? data : ""));
	free(key);
	free(data);
	if (item == NULL) {
		return KNOT_ENOMEM;
	}

	// Append the item.
	if (strlcat(out, item, OUT_LEN) >= OUT_LEN) {
		free(item);
		return KNOT_ESPACE;
	}

	free(item);

	return KNOT_EOK;
}

static void test_conf_io_begin(void)
{
	ok(conf_io_begin(true) == KNOT_TXN_ENOTEXISTS, "begin child txn with no parent");
	ok(conf()->io.txn == NULL, "check txn depth");

#if defined(__OpenBSD__)
	SKIP_OPENBSD
#else
	ok(conf_io_begin(false) == KNOT_EOK, "begin parent txn");
	ok(conf()->io.txn == &(conf()->io.txn_stack[0]), "check txn depth");

	ok(conf_io_begin(false) == KNOT_TXN_EEXISTS, "begin another parent txn");
	ok(conf()->io.txn == &(conf()->io.txn_stack[0]), "check txn depth");

	for (int i = 1; i < CONF_MAX_TXN_DEPTH; i++) {
		ok(conf_io_begin(true) == KNOT_EOK, "begin child txn");
		ok(conf()->io.txn == &(conf()->io.txn_stack[i]), "check txn depth");
	}
	ok(conf_io_begin(true) == KNOT_TXN_EEXISTS, "begin another child txn");
	ok(conf()->io.txn == &(conf()->io.txn_stack[CONF_MAX_TXN_DEPTH - 1]),
	   "check txn depth");

	conf_io_abort(false);
	ok(conf()->io.txn == NULL, "check txn depth");
#endif
}

static void test_conf_io_abort(void)
{
#if defined(__OpenBSD__)
	SKIP_OPENBSD
#else
	// Test child persistence after subchild abort.
	{
		char idx[2] = { '0' };
		ok(conf_io_begin(false) == KNOT_EOK, "begin parent txn");
		ok(conf_io_set("server", "version", NULL, idx) ==
		   KNOT_EOK, "set single value '%s'", idx);
	}

	for (int i = 1; i < CONF_MAX_TXN_DEPTH; i++) {
		char idx[2] = { '0' + i };
		ok(conf_io_begin(true) == KNOT_EOK, "begin child txn %s", idx);
		ok(conf_io_set("server", "version", NULL, idx) ==
		   KNOT_EOK, "set single value '%s'", idx);
	}

	for (int i = CONF_MAX_TXN_DEPTH - 1; i > 0; i--) {
		char idx[2] = { '0' + i };
		conf_io_abort(true);
		conf_val_t val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
		ok(val.code == KNOT_EOK, "check entry");
		const char *data = conf_str(&val);
		ok(*data == (idx[0] - 1), "compare txn data '%s'", data);
	}

	conf_io_abort(false);
	ok(conf()->io.txn == NULL, "check txn depth");

	// Test child abort with committed subchild.
	ok(conf_io_begin(false) == KNOT_EOK, "begin new parent txn");
	ok(conf_io_begin(true) == KNOT_EOK, "begin child txn");
	ok(conf_io_begin(true) == KNOT_EOK, "begin subchild txn");
	ok(conf_io_set("server", "version", NULL, "text") ==
	   KNOT_EOK, "set single value");
	ok(conf_io_commit(true) == KNOT_EOK, "commit subchild txn");
	conf_val_t val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_EOK, "check entry");
	const char *data = conf_str(&val);
	ok(strcmp(data, "text") == 0, "compare subchild txn data '%s'", data);
	conf_io_abort(true);
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_ENOENT, "check entry");
	conf_io_abort(false);

	// Test unchanged read_txn.
	val = conf_get_txn(conf(), &conf()->read_txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_ENOENT, "check entry");
#endif
}

static void test_conf_io_commit(void)
{
	ok(conf_io_commit(false) == KNOT_TXN_ENOTEXISTS, "commit no txt txn");
	ok(conf_io_commit(true) == KNOT_TXN_ENOTEXISTS, "commit no txt txn");

#if defined(__OpenBSD__)
	SKIP_OPENBSD
#else
	// Test subchild persistence after commit.
	{
		char idx[2] = { '0' };
		ok(conf_io_begin(false) == KNOT_EOK, "begin parent txn");
		ok(conf_io_set("server", "version", NULL, idx) ==
		   KNOT_EOK, "set single value '%s'", idx);
	}

	for (int i = 1; i < CONF_MAX_TXN_DEPTH; i++) {
		char idx[2] = { '0' + i };
		ok(conf_io_begin(true) == KNOT_EOK, "begin child txn %s", idx);
		ok(conf_io_set("server", "version", NULL, idx) ==
		   KNOT_EOK, "set single value '%s'", idx);
	}

	for (int i = CONF_MAX_TXN_DEPTH - 1; i > 0; i--) {
		char idx[2] = { '0' + i };
		ok(conf_io_commit(true) == KNOT_EOK, "commit child txn %s", idx);
		conf_val_t val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
		ok(val.code == KNOT_EOK, "check entry");
		const char *data = conf_str(&val);
		ok(*data == ('0' + CONF_MAX_TXN_DEPTH - 1), "compare txn data '%s'", data);
	}

	ok(conf_io_commit(false) == KNOT_EOK, "commit parent txn");
	ok(conf()->io.txn == NULL, "check txn depth");

	// Test child persistence after parent commit.
	ok(conf_io_begin(false) == KNOT_EOK, "begin new parent txn");
	conf_val_t val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_EOK, "check entry");
	char idx[2] = { '0' + CONF_MAX_TXN_DEPTH - 1 };
	const char *data = conf_str(&val);
	ok(strcmp(data, idx) == 0, "compare final data '%s'", data);
	conf_io_abort(false);

	// Test unchanged read_txn.
	val = conf_get_txn(conf(), &conf()->read_txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_ENOENT, "check entry");
#endif
}

static void test_conf_io_check(void)
{
	conf_io_t io = { NULL };

	// ERR no txn.
	ok(conf_io_check(&io) ==
	   KNOT_TXN_ENOTEXISTS, "check without active txn");

	ok(conf_io_begin(false) == KNOT_EOK, "begin txn");

	// Section check.
	ok(conf_io_set("remote", "id", NULL, "remote1") ==
	   KNOT_EOK, "set remote id");
	ok(conf_io_check(&io) ==
	   KNOT_EINVAL, "check missing remote address");
	ok(io.error.code == KNOT_EINVAL, "compare error code");

	ok(conf_io_set("remote", "address", "remote1", "1.1.1.1") ==
	   KNOT_EOK, "set remote address");
	ok(conf_io_check(&io) ==
	   KNOT_EOK, "check remote address");
	ok(io.error.code == KNOT_EOK, "compare error code");

	// Item check.
	ok(conf_io_set("zone", "domain", NULL, ZONE1) ==
	   KNOT_EOK, "set zone domain "ZONE1);
	ok(conf_io_set("zone", "master", ZONE1, "remote1") ==
	   KNOT_EOK, "set zone master");

	ok(conf_io_check(&io) ==
	   KNOT_EOK, "check all");

	ok(conf_io_unset("remote", NULL, NULL, NULL) ==
	   KNOT_EOK, "unset remotes");

	ok(conf_io_check(&io) ==
	   KNOT_ENOENT, "check missing master remote");
	ok(io.error.code == KNOT_ENOENT, "compare error code");

	conf_io_abort(false);
}

static void test_conf_io_set(void)
{
	// ERR no txn.
	ok(conf_io_set("server", "version", NULL, "text") ==
	   KNOT_TXN_ENOTEXISTS, "set without active txn");

	ok(conf_io_begin(false) == KNOT_EOK, "begin txn");

	// ERR.
	ok(conf_io_set(NULL, NULL, NULL, NULL) ==
	   KNOT_EINVAL, "set NULL key0");
	ok(conf_io_set("", NULL, NULL, NULL) ==
	   KNOT_YP_EINVAL_ITEM, "set empty key0");
	ok(conf_io_set("unknown", NULL, NULL, NULL) ==
	   KNOT_YP_EINVAL_ITEM, "set unknown key0");
	ok(conf_io_set("server", "unknown", NULL, NULL) ==
	   KNOT_YP_EINVAL_ITEM, "set unknown key1");
	ok(conf_io_set("include", NULL, NULL, NULL) ==
	   KNOT_YP_ENODATA, "set non-group without data");
	ok(conf_io_set("server", "background-workers", NULL, "x") ==
	   KNOT_EINVAL, "set invalid data");

	// ERR callback
	ok(conf_io_set("include", NULL, NULL, "invalid") ==
	   KNOT_EFILE, "set invalid callback value");

	// Single group, no item, no value.
	ok(conf_io_set("server", NULL, NULL, NULL) ==
	   KNOT_ENOTSUP, "set group no value");
	// Single group, no item, value.
	ok(conf_io_set("server", NULL, NULL, "text") ==
	   KNOT_YP_ENOTSUP_DATA, "set group value");
	// Single group, item, no value.
	ok(conf_io_set("server", "version", NULL, NULL) ==
	   KNOT_YP_ENODATA, "set group item no value");

	// Single group, single value.
	ok(conf_io_set("server", "version", NULL, "text") ==
	   KNOT_EOK, "set single value");
	conf_val_t val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_EOK, "check entry");
	ok(strcmp(conf_str(&val), "text") == 0, "check entry value");

	// Single group, multi value.
	ok(conf_io_set("server", "listen", NULL, "1.1.1.1") ==
	   KNOT_EOK, "set multivalue 1");
	ok(conf_io_set("server", "listen", NULL, "1.1.1.2") ==
	   KNOT_EOK, "set multivalue 2");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_LISTEN);
	ok(val.code == KNOT_EOK, "check entry");
	ok(conf_val_count(&val) == 2, "check entry value count");

	// Prepare dnames.
	knot_dname_t *zone1 = knot_dname_from_str_alloc(ZONE1);
	ok(zone1 != NULL, "create dname "ZONE1);
	knot_dname_t *zone2 = knot_dname_from_str_alloc(ZONE2);
	ok(zone2 != NULL, "create dname "ZONE2);
	knot_dname_t *zone3 = knot_dname_from_str_alloc(ZONE3);
	ok(zone3 != NULL, "create dname "ZONE3);

	// Multi group no id.
	ok(conf_io_set("zone", "domain", NULL, NULL) ==
	   KNOT_YP_ENOID, "set zone empty domain");

	// Multi group ids.
	ok(conf_io_set("zone", "domain", NULL, ZONE1) ==
	   KNOT_EOK, "set zone domain "ZONE1);
	ok(conf_io_set("zone", NULL, ZONE2, NULL) ==
	   KNOT_EOK, "set zone domain "ZONE2);

	// Multi group, single value.
	ok(conf_io_set("zone", "file", ZONE1, "name") ==
	   KNOT_EOK, "set zone file");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_FILE, zone1);
	ok(val.code == KNOT_EOK, "check entry");
	ok(strcmp(conf_str(&val), "name") == 0, "check entry value");

	// Multi group, single value, bad id.
	ok(conf_io_set("zone", "file", ZONE3, "name") ==
	   KNOT_YP_EINVAL_ID, "set zone file");

	// Multi group, single value, all ids.
	ok(conf_io_set("zone", "comment", NULL, "abc") ==
	   KNOT_EOK, "set zones comment");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone1);
	ok(val.code == KNOT_EOK, "check entry");
	ok(strcmp(conf_str(&val), "abc") == 0, "check entry value");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone2);
	ok(val.code == KNOT_EOK, "check entry");
	ok(strcmp(conf_str(&val), "abc") == 0, "check entry value");

	// Prepare different comment.
	ok(conf_io_set("zone", "domain", NULL, ZONE3) ==
	   KNOT_EOK, "set zone domain "ZONE3);
	ok(conf_io_set("zone", "comment", ZONE3, "xyz") ==
	   KNOT_EOK, "set zone comment");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone3);
	ok(val.code == KNOT_EOK, "check entry");
	ok(strcmp(conf_str(&val), "xyz") == 0, "check entry value");

	knot_dname_free(zone1, NULL);
	knot_dname_free(zone2, NULL);
	knot_dname_free(zone3, NULL);

	ok(conf_io_commit(false) == KNOT_EOK, "commit txn");

	// Update read-only transaction.
	ok(conf_refresh_txn(conf()) == KNOT_EOK, "update read-only txn");
}

static void test_conf_io_unset(void)
{
	// ERR no txn.
	ok(conf_io_unset("server", "version", NULL, "text") ==
	   KNOT_TXN_ENOTEXISTS, "unset without active txn");

	ok(conf_io_begin(false) == KNOT_EOK, "begin txn");

	// ERR.
	ok(conf_io_unset("", NULL, NULL, NULL) ==
	   KNOT_YP_EINVAL_ITEM, "unset unknown key0");
	ok(conf_io_unset("unknown", NULL, NULL, NULL) ==
	   KNOT_YP_EINVAL_ITEM, "unset unknown key0");
	ok(conf_io_unset("server", "unknown", NULL, NULL) ==
	   KNOT_YP_EINVAL_ITEM, "unset unknown key1");
	ok(conf_io_unset("include", NULL, NULL, "file") ==
	   KNOT_ENOTSUP, "unset non-group item");
	ok(conf_io_unset("server", "background-workers", NULL, "x") ==
	   KNOT_EINVAL, "unset invalid data");

	// Single group, single value.
	ok(conf_io_unset("server", "version", NULL, "") ==
	   KNOT_ENOENT, "unset zero length text value");
	conf_val_t val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_EOK, "check entry");

	ok(conf_io_unset("server", "version", NULL, "bad text") ==
	   KNOT_ENOENT, "unset bad value");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_EOK, "check entry");

	ok(conf_io_unset("server", "version", NULL, "text") ==
	   KNOT_EOK, "unset explicit value");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_ENOENT, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	ok(conf_io_unset("server", "version", NULL, NULL) ==
	   KNOT_EOK, "unset value");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_ENOENT, "check entry");

	// Single group, multi value.
	ok(conf_io_unset("server", "listen", NULL, "9.9.9.9") ==
	   KNOT_ENOENT, "unset bad value");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_LISTEN);
	ok(val.code == KNOT_EOK, "check entry");

	ok(conf_io_unset("server", "listen", NULL, "1.1.1.1") ==
	   KNOT_EOK, "unset explicit value");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_LISTEN);
	ok(val.code == KNOT_EOK, "check entry");
	ok(conf_val_count(&val) == 1, "check entry value count");

	ok(conf_io_unset("server", "listen", NULL, NULL) ==
	   KNOT_EOK, "unset value");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_LISTEN);
	ok(val.code == KNOT_ENOENT, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	// Whole section items.
	ok(conf_io_unset("server", NULL, NULL, NULL) ==
	   KNOT_EOK, "unset section");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_ENOENT, "check entry");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_LISTEN);
	ok(val.code == KNOT_ENOENT, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	// Prepare dnames.
	knot_dname_t *zone1 = knot_dname_from_str_alloc(ZONE1);
	ok(zone1 != NULL, "create dname "ZONE1);
	knot_dname_t *zone2 = knot_dname_from_str_alloc(ZONE2);
	ok(zone2 != NULL, "create dname "ZONE2);
	knot_dname_t *zone3 = knot_dname_from_str_alloc(ZONE3);
	ok(zone3 != NULL, "create dname "ZONE3);

	// Multi group, single value.
	ok(conf_io_unset("zone", "file", ZONE1, "name") ==
	   KNOT_EOK, "unset zone file");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_FILE, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");

	// Multi group, single bad value, all ids.
	ok(conf_io_unset("zone", "comment", NULL, "other") ==
	   KNOT_EOK, "unset zones comment");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone1);
	ok(val.code == KNOT_EOK, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone2);
	ok(val.code == KNOT_EOK, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone3);
	ok(val.code == KNOT_EOK, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	// Multi group, single value (not all match), all ids.
	ok(conf_io_unset("zone", "comment", NULL, "abc") ==
	   KNOT_EOK, "unset some zones comment");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone2);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone3);
	ok(val.code == KNOT_EOK, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	// Multi group, single value (all match), all ids.
	ok(conf_io_unset("zone", "comment", NULL, NULL) ==
	   KNOT_EOK, "unset all zones comment");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone2);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone3);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	// Multi group, all items, specific id.
	ok(conf_io_unset("zone", NULL, ZONE1, NULL) ==
	   KNOT_EOK, "unset zone items");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_FILE, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone2);
	ok(val.code == KNOT_EOK, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	// Multi group, all items, all ids.
	ok(conf_io_unset("zone", NULL, NULL, NULL) ==
	   KNOT_EOK, "unset zone items");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_FILE, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone2);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");

	// Restart transaction.
	conf_io_abort(false);
	ok(conf_io_begin(false) == KNOT_EOK, "restart txn");

	// All groups.
	ok(conf_io_unset(NULL, NULL, NULL, NULL) ==
	   KNOT_EOK, "unset all");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_VERSION);
	ok(val.code == KNOT_ENOENT, "check entry");
	val = conf_get_txn(conf(), conf()->io.txn, C_SERVER, C_LISTEN);
	ok(val.code == KNOT_ENOENT, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_FILE, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone1);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");
	val = conf_zone_get_txn(conf(), conf()->io.txn, C_COMMENT, zone2);
	ok(val.code == KNOT_YP_EINVAL_ID, "check entry");

	knot_dname_free(zone1, NULL);
	knot_dname_free(zone2, NULL);
	knot_dname_free(zone3, NULL);

	conf_io_abort(false);
}

static void test_conf_io_get(void)
{
	const char *ref;
	char out[OUT_LEN];

	conf_io_t io = {
		.fcn = format_item,
		.misc = out
	};

	// ERR no txn.
	ok(conf_io_get("server", "version", NULL, false, &io) ==
	   KNOT_TXN_ENOTEXISTS, "get without active txn");

	// Get current, no active txn.
	*out = '\0';
	ok(conf_io_get("server", "version", NULL, true, &io) ==
	   KNOT_EOK, "get current without active txn");
	ref = "server.version = \"text\"";
	ok(strcmp(ref, out) == 0, "compare result");

	ok(conf_io_begin(false) == KNOT_EOK, "begin txn");

	// ERR.
	ok(conf_io_get("", NULL, NULL, true, &io) ==
	   KNOT_YP_EINVAL_ITEM, "get empty key0");
	ok(conf_io_get("unknown", NULL, NULL, true, &io) ==
	   KNOT_YP_EINVAL_ITEM, "get unknown key0");
	ok(conf_io_get("server", "unknown", NULL, true, &io) ==
	   KNOT_YP_EINVAL_ITEM, "get unknown key1");
	ok(conf_io_get("include", NULL, NULL, true, &io) ==
	   KNOT_ENOTSUP, "get non-group item");

	// Update item in the active txn.
	ok(conf_io_set("server", "version", NULL, "new text") ==
	   KNOT_EOK, "set single value");

	// Get new, active txn.
	*out = '\0';
	ok(conf_io_get("server", "version", NULL, false, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "server.version = \"new text\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Get current, active txn.
	*out = '\0';
	ok(conf_io_get("server", "version", NULL, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "server.version = \"text\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Multi value.
	*out = '\0';
	ok(conf_io_get("server", "listen", NULL, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "server.listen = \"1.1.1.1\" \"1.1.1.2\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Single group.
	*out = '\0';
	ok(conf_io_get("server", NULL, NULL, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "server.version = \"text\"\n"
	      "server.listen = \"1.1.1.1\" \"1.1.1.2\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Prepare dnames.
	knot_dname_t *zone1 = knot_dname_from_str_alloc(ZONE1);
	ok(zone1 != NULL, "create dname "ZONE1);

	// Multi group, all values, all ids.
	*out = '\0';
	ok(conf_io_get("zone", NULL, NULL, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "zone.domain = \"zone1.\"\n"
	      "zone[zone1.].file = \"name\"\n"
	      "zone[zone1.].comment = \"abc\"\n"
	      "zone.domain = \"zone2.\"\n"
	      "zone[zone2.].comment = \"abc\"\n"
	      "zone.domain = \"zone3.\"\n"
	      "zone[zone3.].comment = \"xyz\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Multi group ids.
	*out = '\0';
	ok(conf_io_get("zone", "domain", NULL, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "zone.domain = \"zone1.\"\n"
	      "zone.domain = \"zone2.\"\n"
	      "zone.domain = \"zone3.\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Multi group, all values, single id.
	*out = '\0';
	ok(conf_io_get("zone", NULL, ZONE1, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "zone.domain = \"zone1.\"\n"
	      "zone[zone1.].file = \"name\"\n"
	      "zone[zone1.].comment = \"abc\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Multi group, single value, single id.
	*out = '\0';
	ok(conf_io_get("zone", "file", ZONE1, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "zone[zone1.].file = \"name\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// All groups.
	*out = '\0';
	ok(conf_io_get(NULL, NULL, NULL, true, &io) ==
	   KNOT_EOK, "get with active txn");
	ref = "server.version = \"text\"\n"
	      "server.listen = \"1.1.1.1\" \"1.1.1.2\"\n"
	      "zone.domain = \"zone1.\"\n"
	      "zone[zone1.].file = \"name\"\n"
	      "zone[zone1.].comment = \"abc\"\n"
	      "zone.domain = \"zone2.\"\n"
	      "zone[zone2.].comment = \"abc\"\n"
	      "zone.domain = \"zone3.\"\n"
	      "zone[zone3.].comment = \"xyz\"";
	ok(strcmp(ref, out) == 0, "compare result");

	knot_dname_free(zone1, NULL);

	conf_io_abort(false);
}

static void test_conf_io_diff(void)
{
	const char *ref;
	char out[OUT_LEN];

	conf_io_t io = {
		.fcn = format_item,
		.misc = out
	};

	// ERR no txn.
	ok(conf_io_diff("server", "version", NULL, &io) ==
	   KNOT_TXN_ENOTEXISTS, "diff without active txn");

	ok(conf_io_begin(false) == KNOT_EOK, "begin txn");

	// ERR.
	ok(conf_io_diff("", NULL, NULL, &io) ==
	   KNOT_YP_EINVAL_ITEM, "diff empty key0");
	ok(conf_io_diff("unknown", NULL, NULL, &io) ==
	   KNOT_YP_EINVAL_ITEM, "diff unknown key0");
	ok(conf_io_diff("server", "unknown", NULL, &io) ==
	   KNOT_YP_EINVAL_ITEM, "diff unknown key1");
	ok(conf_io_diff("include", NULL, NULL, &io) ==
	   KNOT_ENOTSUP, "diff non-group item");

	*out = '\0';
	ok(conf_io_diff(NULL, NULL, NULL, &io) == KNOT_EOK, "diff no change");
	ref = "";
	ok(strcmp(ref, out) == 0, "compare result");

	// Update singlevalued item.
	ok(conf_io_set("server", "version", NULL, "new text") ==
	   KNOT_EOK, "set single value");

	*out = '\0';
	ok(conf_io_diff("server", "version", NULL, &io) == KNOT_EOK, "diff single item");
	ref = "-server.version = \"text\"\n"
	      "+server.version = \"new text\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Update multivalued item.
	ok(conf_io_unset("server", "listen", NULL, "1.1.1.1") ==
	   KNOT_EOK, "unset multivalue");
	ok(conf_io_set("server", "listen", NULL, "1.1.1.3") ==
	   KNOT_EOK, "set multivalue");

	*out = '\0';
	ok(conf_io_diff("server", "listen", NULL, &io) == KNOT_EOK, "diff multi item");
	ref = "-server.listen = \"1.1.1.1\" \"1.1.1.2\"\n"
	      "+server.listen = \"1.1.1.2\" \"1.1.1.3\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Unset single item.
	ok(conf_io_unset("zone", "comment", ZONE3, NULL) ==
	   KNOT_EOK, "unset multivalue");

	*out = '\0';
	ok(conf_io_diff("zone", NULL, ZONE3, &io) == KNOT_EOK, "diff section");
	ref = "-zone[zone3.].comment = \"xyz\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// Unset id.
	ok(conf_io_unset("zone", NULL, ZONE1, NULL) ==
	   KNOT_EOK, "unset id");
	ok(conf_io_unset("zone", NULL, ZONE2, NULL) ==
	   KNOT_EOK, "unset id");

	*out = '\0';
	ok(conf_io_diff("zone", NULL, ZONE2, &io) == KNOT_EOK, "diff id section");
	ref = "-zone.domain = \"zone2.\"\n"
	      "-zone[zone2.].comment = \"abc\"";
	ok(strcmp(ref, out) == 0, "compare result");

	*out = '\0';
	ok(conf_io_diff("zone", "domain", NULL, &io) == KNOT_EOK, "diff id");
	ref = "-zone.domain = \"zone1.\"\n"
	      "-zone.domain = \"zone2.\"";
	ok(strcmp(ref, out) == 0, "compare result");

	*out = '\0';
	ok(conf_io_diff(NULL, NULL, NULL, &io) == KNOT_EOK, "diff whole change");
	ref = "-server.version = \"text\"\n"
	      "+server.version = \"new text\"\n"
	      "-server.listen = \"1.1.1.1\" \"1.1.1.2\"\n"
	      "+server.listen = \"1.1.1.2\" \"1.1.1.3\"\n"
	      "-zone.domain = \"zone1.\"\n"
	      "-zone[zone1.].file = \"name\"\n"
	      "-zone[zone1.].comment = \"abc\"\n"
	      "-zone.domain = \"zone2.\"\n"
	      "-zone[zone2.].comment = \"abc\"\n"
	      "-zone[zone3.].comment = \"xyz\"";
	ok(strcmp(ref, out) == 0, "compare result");

	conf_io_abort(false);
}

static void test_conf_io_list(void)
{
	const char *ref;
	char out[OUT_LEN];

	conf_io_t io = {
		.fcn = format_item,
		.misc = out
	};

	// ERR.
	ok(conf_io_list("", NULL, NULL, false, true, &io) ==
	   KNOT_YP_EINVAL_ITEM, "list empty key0");
	ok(conf_io_list("unknown", NULL, NULL, false, true, &io) ==
	   KNOT_YP_EINVAL_ITEM, "list unknown key0");
	ok(conf_io_list("include", NULL, NULL, false, true, &io) ==
	   KNOT_EOK, "list non-group item");
	ok(conf_io_list("template", NULL, NULL, false, false, &io) ==
	   KNOT_TXN_ENOTEXISTS, "no active txn");

	// Desc schema.
	*out = '\0';
	ok(conf_io_list(NULL, NULL, NULL, true, true, &io) ==
	   KNOT_EOK, "list schema");
	ref = "server\n"
	      "xdp\n"
	      "control\n"
	      "database\n"
	      "remote\n"
	      "template\n"
	      "zone\n"
	      "include";
	ok(strcmp(ref, out) == 0, "compare result");

	// Desc group.
	*out = '\0';
	ok(conf_io_list("server", NULL, NULL, true, true, &io) ==
	   KNOT_EOK, "list group");
	ref = "server.version\n"
	      "server.listen\n"
	      "server.tcp-idle-timeout\n"
	      "server.tcp-io-timeout\n"
	      "server.tcp-remote-io-timeout\n"
	      "server.tcp-max-clients\n"
	      "server.tcp-reuseport\n"
	      "server.tcp-fastopen\n"
	      "server.quic-max-clients\n"
	      "server.quic-idle-close-timeout\n"
	      "server.quic-outbuf-max-size\n"
	      "server.socket-affinity\n"
	      "server.udp-workers\n"
	      "server.tcp-workers\n"
	      "server.background-workers\n"
	      "server.udp-max-payload\n"
	      "server.udp-max-payload-ipv4\n"
	      "server.udp-max-payload-ipv6\n"
	      "server.edns-client-subnet\n"
	      "server.answer-rotation\n"
	      "server.automatic-acl\n"
	      "server.dbus-event";
	ok(strcmp(ref, out) == 0, "compare result");

	// List item options.
	*out = '\0';
	ok(conf_io_list("zone", "zonefile-load", NULL, true, true, &io) ==
	   KNOT_EOK, "list item options");
	ref = "zone.zonefile-load = \"opt1\"\n"
	      "zone.zonefile-load = \"opt2\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// List zone identifiers 1.
	*out = '\0';
	ok(conf_io_list("zone", NULL, NULL, false, true, &io) ==
	   KNOT_EOK, "list zone identifiers 1");
	ref = "zone[zone1.]\n"
	      "zone[zone2.]\n"
	      "zone[zone3.]";
	ok(strcmp(ref, out) == 0, "compare result");

	// List zone identifiers 2.
	*out = '\0';
	ok(conf_io_list("zone", "domain", NULL, false, true, &io) ==
	   KNOT_EOK, "list zone identifiers 2");
	ref = "zone = \"zone1.\"\n"
	      "zone = \"zone2.\"\n"
	      "zone = \"zone3.\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// List item values.
	*out = '\0';
	ok(conf_io_list("server", "listen", NULL, false, true, &io) ==
	   KNOT_EOK, "list item values");
	ref = "server.listen = \"1.1.1.1\" \"1.1.1.2\"";
	ok(strcmp(ref, out) == 0, "compare result");

	// List multi-group item values.
	*out = '\0';
	ok(conf_io_list("zone", "file", "zone1", false, true, &io) ==
	   KNOT_EOK, "list multi-group item values");
	ref = "zone[zone1.].file = \"name\"";
	ok(strcmp(ref, out) == 0, "compare result");
}

static const yp_item_t desc_server[] = {
	{ C_VERSION,              YP_TSTR,  YP_VNONE },
	{ C_LISTEN,               YP_TADDR, YP_VNONE, YP_FMULTI },
	// Required config cache items - assert fix.
	{ C_TCP_IDLE_TIMEOUT,	  YP_TINT,  YP_VNONE },
	{ C_TCP_IO_TIMEOUT,	  YP_TINT,  YP_VNONE },
	{ C_TCP_RMT_IO_TIMEOUT,	  YP_TINT,  YP_VNONE },
	{ C_TCP_MAX_CLIENTS,	  YP_TINT,  YP_VNONE },
	{ C_TCP_REUSEPORT,	  YP_TBOOL, YP_VNONE },
	{ C_TCP_FASTOPEN,	  YP_TBOOL, YP_VNONE },
	{ C_QUIC_MAX_CLIENTS,	  YP_TINT,  YP_VNONE },
	{ C_QUIC_IDLE_CLOSE,	  YP_TINT,  YP_VNONE },
	{ C_QUIC_OUTBUF_MAX_SIZE, YP_TINT,  YP_VNONE },
	{ C_SOCKET_AFFINITY,	  YP_TBOOL, YP_VNONE },
	{ C_UDP_WORKERS,	  YP_TINT,  YP_VNONE },
	{ C_TCP_WORKERS,	  YP_TINT,  YP_VNONE },
	{ C_BG_WORKERS,		  YP_TINT,  YP_VNONE },
	{ C_UDP_MAX_PAYLOAD,      YP_TINT,  YP_VNONE },
	{ C_UDP_MAX_PAYLOAD_IPV4, YP_TINT,  YP_VNONE },
	{ C_UDP_MAX_PAYLOAD_IPV6, YP_TINT,  YP_VNONE },
	{ C_ECS,                  YP_TBOOL, YP_VNONE },
	{ C_ANS_ROTATION,         YP_TBOOL, YP_VNONE },
	{ C_AUTO_ACL,             YP_TBOOL, YP_VNONE },
	{ C_DBUS_EVENT,           YP_TOPT,  YP_VNONE },
	{ NULL }
};

static const yp_item_t desc_xdp[] = {
	{ C_UDP,                YP_TBOOL, YP_VNONE },
	{ C_TCP,                YP_TBOOL, YP_VNONE },
	{ C_QUIC,               YP_TBOOL, YP_VNONE },
	{ C_TCP_MAX_CLIENTS,    YP_TINT,  YP_VNONE },
	{ C_TCP_INBUF_MAX_SIZE, YP_TINT,  YP_VNONE },
	{ C_TCP_OUTBUF_MAX_SIZE,YP_TINT,  YP_VNONE },
	{ C_TCP_IDLE_CLOSE,     YP_TINT,  YP_VNONE },
	{ C_TCP_IDLE_RESET,     YP_TINT,  YP_VNONE },
	{ C_TCP_RESEND,         YP_TINT,  YP_VNONE },
	{ C_ROUTE_CHECK,        YP_TBOOL, YP_VNONE },
	{ C_ZERO_COPY,          YP_TBOOL, YP_VNONE },
	{ C_RING_SIZE,          YP_TINT,  YP_VNONE },
	{ C_BUSYPOLL_BUDGET,    YP_TINT,  YP_VNONE },
	{ C_BUSYPOLL_TIMEOUT,   YP_TINT,  YP_VNONE },
	{ NULL }
};

static const yp_item_t desc_control[] = {
	{ C_TIMEOUT, YP_TINT, YP_VNONE },
	{ NULL }
};

static const yp_item_t desc_remote[] = {
	{ C_ID,   YP_TSTR,  YP_VNONE },
	{ C_ADDR, YP_TADDR, YP_VNONE, YP_FMULTI },
	{ NULL }
};

static const yp_item_t desc_database[] = {
	{ C_TIMER_DB_SYNC, YP_TOPTINT, YP_VNONE },
	{ NULL }
};

static const knot_lookup_t opts[] = {
	{ 1, "opt1" },
	{ 2, "opt2" },
	{ 0, NULL }
};

#define ZONE_ITEMS \
	{ C_FILE,              YP_TSTR,  YP_VNONE }, \
	{ C_MASTER,            YP_TREF,  YP_VREF = { C_RMT }, YP_FMULTI, { check_ref } }, \
	{ C_ZONEFILE_LOAD,     YP_TOPT,  YP_VOPT = { opts, 0 } }, \
	{ C_JOURNAL_CONTENT,   YP_TOPT,  YP_VOPT = { opts, 0 } }, \
	{ C_DNSSEC_SIGNING,    YP_TBOOL, YP_VNONE }, \
	{ C_DNSSEC_VALIDATION, YP_TBOOL, YP_VNONE }, \
	{ C_SERIAL_MODULO,     YP_TSTR,  YP_VSTR = { "0/1" } }, \
	{ C_ZONEMD_VERIFY,     YP_TBOOL, YP_VNONE }, \
	{ C_CATALOG_ROLE,      YP_TOPT,  YP_VOPT = { opts, 0 } }, \
	{ C_CATALOG_TPL,       YP_TREF,  YP_VREF = { C_RMT } }, \
	{ C_COMMENT,           YP_TSTR,  YP_VNONE },

static const yp_item_t desc_template[] = {
	{ C_ID, YP_TSTR, YP_VNONE },
	ZONE_ITEMS
	{ NULL }
};

static const yp_item_t desc_zone[] = {
	{ C_DOMAIN, YP_TDNAME, YP_VNONE },
	ZONE_ITEMS
	{ NULL }
};

const yp_item_t test_schema[] = {
	{ C_SRV,  YP_TGRP, YP_VGRP = { desc_server } },
	{ C_XDP,  YP_TGRP, YP_VGRP = { desc_xdp } },
	{ C_CTL,  YP_TGRP, YP_VGRP = { desc_control } },
	{ C_DB,   YP_TGRP, YP_VGRP = { desc_database } },
	{ C_RMT,  YP_TGRP, YP_VGRP = { desc_remote }, YP_FMULTI, { check_remote } },
	{ C_TPL,  YP_TGRP, YP_VGRP = { desc_template }, YP_FMULTI, { check_template } },
	{ C_ZONE, YP_TGRP, YP_VGRP = { desc_zone }, YP_FMULTI, { check_zone } },
	{ C_INCL, YP_TSTR, YP_VNONE, YP_FNONE, { include_file } },
	{ NULL }
};

int main(int argc, char *argv[])
{
	plan_lazy();

	ok(test_conf("", test_schema) == KNOT_EOK, "Prepare configuration");

	diag("conf_io_begin");
	test_conf_io_begin();

	diag("conf_io_abort");
	test_conf_io_abort();

	diag("conf_io_commit");
	test_conf_io_commit();

	diag("conf_io_check");
	test_conf_io_check();

	diag("conf_io_set");
	test_conf_io_set();

	diag("conf_io_unset");
	test_conf_io_unset();

	diag("conf_io_get");
	test_conf_io_get();

	diag("conf_io_diff");
	test_conf_io_diff();

	diag("conf_io_list");
	test_conf_io_list();

	conf_free(conf());

	return 0;
}
