// SPDX-License-Identifier: LGPL-2.1-or-later
/**
 * This file is part of libnvme.
 * Copyright (c) 2023 Daniel Wagner, SUSE LLC
 */

#include <shared/assert-util.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

#include <ccan/array_size/array_size.h>

#include <libnvme.h>
#include <nvme/private.h>
#include <nvme/private-fabrics.h>
#include <nvme/private-tree.h>

struct test_data {
	/* input data */
	struct libnvmf_context f;
	const char *subsysname;

	/* track controller generated by input data */
	libnvme_subsystem_t s;
	libnvme_ctrl_t c;
	int ctrl_id;
};

#define DEFAULT_HOSTID "9ba1651a-ed36-11f0-9858-6c1ff71ba506"
#define DEFAULT_HOSTNQN "nqn.2014-08.org.nvmexpress:uuid:9ba1651a-ed36-11f0-9858-6c1ff71ba506"
#define DEFAULT_SUBSYSNAME "subsysname"
#define DEFAULT_SUBSYSNQN "subsysnqn"
#define SRC_ADDR4 "192.168.56.100"
#define SRC_ADDR6 "1234:5678:abcd:EF01:1234:5678:abcd:EF01"

#define DEFAULTS(t, a, h, i, s) \
	.f = { .hostnqn = DEFAULT_HOSTNQN, \
		.ctrl_params = { .subsysnqn = DEFAULT_SUBSYSNQN,	     \
			.transport = (t), .traddr = (a),	     \
			.host_traddr = (h), .host_iface = (i),	     \
			.trsvcid = (s), }, },			     \
	.subsysname = DEFAULT_SUBSYSNAME

struct test_data test_data[] = {
	{ DEFAULTS("tcp", "192.168.1.1", "192.168.1.20", NULL, "4420") },
	{ DEFAULTS("tcp", "192.168.1.1", "192.168.1.20", NULL, "4421") },
	{ DEFAULTS("tcp", "192.168.1.2", "192.168.1.20", "eth1", "4420") },
	{ DEFAULTS("tcp", "192.168.1.2", "192.168.1.20", "eth1", "4421") },
	{ DEFAULTS("rdma", "192.168.1.3", "192.168.1.20", NULL, NULL) },
	{ DEFAULTS("rdma", "192.168.1.4", "192.168.1.20", NULL, NULL) },
	{ DEFAULTS("fc",
		   "nn-0x201700a09890f5bf:pn-0x201900a09890f5bf",
		   "nn-0x200000109b579ef3:pn-0x100000109b579ef3", NULL, NULL)
	},
	{ DEFAULTS("fc",
		   "nn-0x201700a09890f5bf:pn-0x201900a09890f5bf",
		   "nn-0x200000109b579ef6:pn-0x100000109b579ef6", NULL, NULL)
	},
};

static struct test_data *find_test_data(libnvme_ctrl_t c)
{
	for (int i = 0; i < ARRAY_SIZE(test_data); i++) {
		struct test_data *d = &test_data[i];

		if (d->c == c)
			return d;
	}

	return NULL;
}

static void show_ctrl(libnvme_ctrl_t c)
{
	struct test_data *d = find_test_data(c);

	if (d)
		printf("ctrl%d ", d->ctrl_id);
	else
		printf("      ");

	printf("0x%p: %s %s %s %s %s %s",
	       c,
	       libnvme_ctrl_get_subsysnqn(c),
	       libnvme_ctrl_get_transport(c),
	       libnvme_ctrl_get_traddr(c),
	       libnvme_ctrl_get_host_traddr(c),
	       libnvme_ctrl_get_host_iface(c),
	       libnvme_ctrl_get_trsvcid(c));
}

static bool match_ctrl(struct test_data *d, libnvme_ctrl_t c)
{
	bool pass = true;
	const char *trsvid, *host_traddr, *host_iface;

	if (d->c != c)
		pass = false;

	if (strcmp(d->f.ctrl_params.transport,
		   libnvme_ctrl_get_transport(d->c)))
		pass = false;

	if (strcmp(d->f.ctrl_params.traddr, libnvme_ctrl_get_traddr(d->c)))
		pass = false;


	host_traddr = libnvme_ctrl_get_host_traddr(c);
	if (d->f.ctrl_params.host_traddr &&
	    (!host_traddr || strcmp(d->f.ctrl_params.host_traddr, host_traddr)))
		pass = false;

	host_iface = libnvme_ctrl_get_host_iface(c);
	if (d->f.ctrl_params.host_iface &&
	    (!host_iface || strcmp(d->f.ctrl_params.host_iface, host_iface)))
		pass = false;

	trsvid = libnvme_ctrl_get_trsvcid(c);
	if (d->f.ctrl_params.trsvcid &&
	    (!trsvid || strcmp(d->f.ctrl_params.trsvcid, trsvid)))
		pass = false;

	printf("[%s]", pass ? "PASS" : "FAILED");

	return pass;
}

static struct libnvme_global_ctx *create_tree(void)
{
	struct libnvme_global_ctx *ctx;
	libnvme_host_t h;

	ctx = libnvme_create_global_ctx();
	shr_assert(ctx);

	libnvme_set_logging_file(ctx, stdout);
	libnvme_set_logging_level(ctx, LIBNVME_LOG_DEBUG, false, false);
	libnvme_set_hostnqn(ctx, DEFAULT_HOSTNQN);
	libnvme_set_hostid(ctx, DEFAULT_HOSTID);

	shr_assert(!libnvme_create_host(ctx, DEFAULT_HOSTNQN, DEFAULT_HOSTID, &h));
	shr_assert(h);

	printf("  ctrls created:\n");
	for (int i = 0; i < ARRAY_SIZE(test_data); i++) {
		struct test_data *d = &test_data[i];

		shr_assert(!libnvme_get_subsystem(ctx, h, d->subsysname,
			d->f.ctrl_params.subsysnqn, &d->s));
		shr_assert(d->s);

		shr_assert(!libnvme_subsystem_create_ctrl(d->s,
			&d->f.ctrl_params, &d->c));
		shr_assert(d->c);
		d->ctrl_id = i;

		printf("    ");
		show_ctrl(d->c);
		match_ctrl(d, d->c);
		printf("\n");
	}
	printf("\n");

	return ctx;
}

static unsigned int count_entries(struct libnvme_global_ctx *ctx)
{
	libnvme_host_t h;
	libnvme_subsystem_t s;
	libnvme_ctrl_t c;
	unsigned int i = 0;

	libnvme_for_each_host(ctx, h)
		libnvme_for_each_subsystem(h, s)
			libnvme_subsystem_for_each_ctrl(s, c)
				i++;

	return i;
}

static bool tcp_ctrl_lookup(libnvme_subsystem_t s, struct test_data *d)
{
	libnvme_ctrl_t c;
	bool pass = true;
	struct libnvmf_context f = d->f;

	f.ctrl_params.host_traddr = NULL;
	f.ctrl_params.host_iface = NULL;
	c = libnvme_lookup_ctrl(s, &f.ctrl_params, NULL);
	shr_assert(c);
	printf("%10s %12s %10s -> ", f.ctrl_params.trsvcid, "", "");
	show_ctrl(c);
	pass &= match_ctrl(d, c);
	printf("\n");

	if (d->f.ctrl_params.host_traddr) {
		f = d->f;
		f.ctrl_params.host_iface = NULL;
		c = libnvme_lookup_ctrl(s, &f.ctrl_params, NULL);
		printf("%10s %12s %10s -> ", f.ctrl_params.trsvcid,
		       f.ctrl_params.host_traddr, "");
		show_ctrl(c);
		pass &= match_ctrl(d, c);
		printf("\n");
	}

	if (d->f.ctrl_params.host_iface) {
		f = d->f;
		f.ctrl_params.host_traddr = NULL;
		c = libnvme_lookup_ctrl(s, &f.ctrl_params, NULL);
		printf("%10s %12s %10s -> ", f.ctrl_params.trsvcid,
		       "", f.ctrl_params.host_iface);
		show_ctrl(c);
		pass &= match_ctrl(d, c);
		printf("\n");
	}

	if (d->f.ctrl_params.host_iface && d->f.ctrl_params.traddr) {
		f = d->f;
		c = libnvme_lookup_ctrl(s, &f.ctrl_params, NULL);
		printf("%10s %12s %10s -> ", f.ctrl_params.trsvcid,
		       f.ctrl_params.host_traddr, f.ctrl_params.host_iface);
		show_ctrl(c);
		pass &= match_ctrl(d, c);
		printf("\n");
	}

	return pass;
}

static bool default_ctrl_lookup(libnvme_subsystem_t s, struct test_data *d)
{
	libnvme_ctrl_t c;
	bool pass = true;
	struct libnvmf_context f = d->f;

	f.ctrl_params.host_iface = NULL;
	f.ctrl_params.trsvcid = NULL;
	c = libnvme_lookup_ctrl(s, &f.ctrl_params, NULL);
	printf("%10s %12s %10s -> ", "", "", "");
	show_ctrl(c);
	pass &= match_ctrl(d, c);
	printf("\n");

	return pass;
}

static bool ctrl_lookups(struct libnvme_global_ctx *ctx)
{
	libnvme_host_t h;
	libnvme_subsystem_t s;
	bool pass = true;

	h = libnvme_first_host(ctx);
	shr_assert(h);
	shr_assert(!libnvme_get_subsystem(ctx, h, DEFAULT_SUBSYSNAME,
			      DEFAULT_SUBSYSNQN, &s));
	shr_assert(s);

	printf("  lookup controller:\n");
	for (int i = 0; i < ARRAY_SIZE(test_data); i++) {
		struct test_data *d = &test_data[i];

		printf("%10s %12s %10s    ", "", "", "");
		show_ctrl(d->c);
		printf("\n");

		if (!strcmp("tcp", d->f.ctrl_params.transport))
			pass &= tcp_ctrl_lookup(s, d);
		else
			pass &= default_ctrl_lookup(s, d);

		printf("\n");
	}

	return pass;
}

static bool test_lookup(void)
{
	struct libnvme_global_ctx *ctx;
	bool pass;

	printf("test_lookup:\n");

	ctx = create_tree();
	pass = count_entries(ctx) == ARRAY_SIZE(test_data);
	pass &= ctrl_lookups(ctx);

	libnvme_free_global_ctx(ctx);

	return pass;
}

static bool test_src_addr(void)
{
	static const struct {
		const char *address;
		const char *expect;
	} cases[] = {
		{ NULL, NULL },
		{ "", NULL },
		{ "traddr=192.168.56.1,trsvcid=8009", NULL },
		{ "traddr=192.168.56.1,trsvcid=8009,src_addr=" SRC_ADDR4,
		  SRC_ADDR4 },
		{ "traddr=192.168.56.1,src_addr=" SRC_ADDR4 ",trsvcid=8009",
		  SRC_ADDR4 },
		{ "traddr=1234::abcd,trsvcid=8009,src_addr=" SRC_ADDR6,
		  SRC_ADDR6 },
		{ "traddr=1234::abcd,src_addr=" SRC_ADDR6 ",trsvcid=8009",
		  SRC_ADDR6 },
		{ "traddr=1234::abcd,trsvcid=8009,src_addr=" SRC_ADDR6 "%scope",
		  SRC_ADDR6 },
		{ "traddr=1234::abcd,src_addr=" SRC_ADDR6 "%scope,trsvcid=8009",
		  SRC_ADDR6 },
	};
	struct libnvme_global_ctx *ctx;
	char buffer[100]; /* big enough for IPv6 max length */
	const char *addr_str, *expect_str;
	char *src_addr;
	bool pass = true;
	int i;

	printf("\ntest_src_addr:\n");

	ctx = libnvme_create_global_ctx();
	shr_assert(ctx);

	libnvme_set_logging_file(ctx, stdout);
	libnvme_set_logging_level(ctx, LIBNVME_LOG_DEBUG, false, false);

	for (i = 0; i < (int)ARRAY_SIZE(cases); i++) {
		if (cases[i].address)
			addr_str = cases[i].address;
		else
			addr_str = "NULL";

		if (cases[i].expect)
			expect_str = cases[i].expect;
		else
			expect_str = "NULL";

		printf(" - Test address = \"%s\"", addr_str);
		src_addr = libnvme_parse_src_addr(ctx, cases[i].address,
						  buffer, sizeof(buffer));
		if ((!cases[i].expect && src_addr) ||
		    (cases[i].expect &&
		     (!src_addr || strcmp(src_addr, cases[i].expect)))) {
			printf(" [FAIL]\n");
			fprintf(stderr,
				"libnvme_parse_src_addr(\"%s\") expected \"%s\", got \"%s\"\n",
				addr_str, expect_str,
				src_addr ? src_addr : "NULL");
			pass = false;
		} else {
			printf(" [PASS]\n");
		}
	}

	libnvme_free_global_ctx(ctx);

	return pass;
}

struct ctrl_args {
	struct libnvmf_context f;
	const char *address;
};

static void set_ctrl_args(struct ctrl_args *args,
			  const char *transport,
			  const char *traddr,
			  const char *trsvcid,
			  const char *host_traddr,
			  const char *host_iface,
			  const char *address,
			  const char *subsysnqn)
{
	args->f.ctrl_params.transport   = transport;
	args->f.ctrl_params.traddr      = traddr;
	args->f.ctrl_params.trsvcid     = trsvcid;
	args->f.ctrl_params.host_traddr = host_traddr;
	args->f.ctrl_params.host_iface  = host_iface;
	args->address     = address;
	args->f.ctrl_params.subsysnqn   = subsysnqn;
}

static bool ctrl_match(const char *tag,
		       int reference_id,
		       int candidate_id,
		       struct ctrl_args *reference,
		       struct ctrl_args *candidate,
		       bool should_match)
{
	struct libnvme_global_ctx *ctx;
	libnvme_host_t h;
	libnvme_ctrl_t reference_ctrl; /* Existing controller (from sysfs) */
	libnvme_ctrl_t candidate_ctrl;
	libnvme_ctrl_t found_ctrl;
	libnvme_subsystem_t s;

	const struct libnvme_ctrl_params *cp = &candidate->f.ctrl_params;
	const struct libnvme_ctrl_params *rp = &reference->f.ctrl_params;

	ctx = libnvme_create_global_ctx();
	shr_assert(ctx);

	libnvme_set_logging_file(ctx, stdout);
	libnvme_set_logging_level(ctx, LIBNVME_LOG_INFO, false, false);

	shr_assert(!libnvme_create_host(ctx, DEFAULT_HOSTNQN, DEFAULT_HOSTID, &h));
	shr_assert(h);

	shr_assert(!libnvme_create_subsystem(h, DEFAULT_SUBSYSNAME,
		rp->subsysnqn ? rp->subsysnqn : DEFAULT_SUBSYSNQN, &s));
	shr_assert(s);

	shr_assert(!libnvme_subsystem_create_ctrl(s, &reference->f.ctrl_params,
		&reference_ctrl));
	shr_assert(reference_ctrl);
	reference_ctrl->name = "nvme1";  /* fake the device name */
	if (reference->address)
		reference_ctrl->address = (char *)reference->address;

	/* libnvme_ctrl_find() MUST BE RUN BEFORE libnvme_lookup_ctrl() */
	found_ctrl = libnvme_ctrl_find(s, &candidate->f.ctrl_params, NULL);

	candidate_ctrl = libnvme_lookup_ctrl(s,
		&candidate->f.ctrl_params, NULL);

	if (should_match) {
		if (candidate_ctrl != reference_ctrl) {
			printf("%s-%d-%d: Candidate (%s, %s, %s, %s, %s, %s) failed to match (%s, %s, %s, %s, %s, %s, %s)\n",
			       tag, reference_id, candidate_id,
			       cp->transport, cp->traddr,
			       cp->trsvcid, cp->subsysnqn,
			       cp->host_traddr, cp->host_iface,
			       rp->transport, rp->traddr,
			       rp->trsvcid, rp->subsysnqn,
			       rp->host_traddr, rp->host_iface,
			       reference->address);
			return false;
		}

		if (!found_ctrl) {
			printf("%s-%d-%d: Candidate (%s, %s, %s, %s, %s, %s) failed to find controller\n",
			       tag, reference_id, candidate_id,
			       cp->transport, cp->traddr,
			       cp->trsvcid, cp->subsysnqn,
			       cp->host_traddr, cp->host_iface);
			return false;
		}
	} else {
		if (candidate_ctrl == reference_ctrl) {
			printf("%s-%d-%d: Candidate (%s, %s, %s, %s, %s, %s) should not match (%s, %s, %s, %s, %s, %s, %s)\n",
			       tag, reference_id, candidate_id,
			       cp->transport, cp->traddr,
			       cp->trsvcid, cp->subsysnqn,
			       cp->host_traddr, cp->host_iface,
			       rp->transport, rp->traddr,
			       rp->trsvcid, rp->subsysnqn,
			       rp->host_traddr, rp->host_iface,
			       reference->address);
			return false;
		}

		if (found_ctrl) {
			printf("%s-%d-%d: Candidate (%s, %s, %s, %s, %s, %s) should not have found controller. found_ctrl=%p reference=%p\n",
			       tag, reference_id, candidate_id,
			       cp->transport, cp->traddr,
			       cp->trsvcid, cp->subsysnqn,
			       cp->host_traddr, cp->host_iface,
			       found_ctrl, reference_ctrl);
			return false;
		}
	}

	/* Set the faked data back to NULL before freeing the tree */
	reference_ctrl->name = NULL;
	reference_ctrl->address = NULL;

	libnvme_free_global_ctx(ctx);

	return true;
}

/**
 * test_ctrl_match_fc - Test that we can look up FC controllers
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_ctrl_match_fc(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("test_ctrl_match_fc:\n");

	/*******************************************************************/
	/* Reference ID 1 */
	set_ctrl_args(&reference, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			"21:00:00:e0:8b:05:05:20", NULL, NULL, NULL);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			"21:00:00:e0:8b:05:05:20", NULL, NULL, NULL);
	pass &= ctrl_match("FC", 1, 0, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("FC", 1, 1, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("FC", 1, 2, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "192.168.2.2", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("FC", 1, 3, &reference, &candidate, false);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("FC", 1, 4, &reference, &candidate, false);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			"21:00:00:e0:8b:05:05:21", NULL, NULL, NULL);
	pass &= ctrl_match("FC", 1, 5, &reference, &candidate, false);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "8009",
			NULL, "", NULL, NULL);
	pass &= ctrl_match("FC", 1, 6, &reference, &candidate, false);


	/*******************************************************************/
	/* Reference ID 2 */
	set_ctrl_args(&reference, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, NULL, NULL, NULL);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			"21:00:00:e0:8b:05:05:20", NULL, NULL, NULL);
	pass &= ctrl_match("FC", 2, 0, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("FC", 2, 1, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("FC", 2, 2, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "192.168.2.2", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("FC", 2, 3, &reference, &candidate, false);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("FC", 2, 4, &reference, &candidate, false);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "8009",
			NULL, "", NULL, NULL);
	pass &= ctrl_match("FC", 2, 5, &reference, &candidate, false);


	/*******************************************************************/
	/* Reference ID 3 */
	set_ctrl_args(&reference, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, "eth0", NULL, NULL);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			"21:00:00:e0:8b:05:05:20", NULL, NULL, NULL);
	pass &= ctrl_match("FC", 3, 0, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("FC", 3, 1, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("FC", 3, 2, &reference, &candidate, true);

	set_ctrl_args(&candidate, "fc", "192.168.2.2", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("FC", 3, 3, &reference, &candidate, false);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("FC", 3, 4, &reference, &candidate, false);

	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "8009",
			NULL, "", NULL, NULL);
	pass &= ctrl_match("FC", 3, 5, &reference, &candidate, false);

	return pass;
}

/**
 * test_ctrl_match_rdma - Test that we can look up RDMA controllers
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_ctrl_match_rdma(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("test_ctrl_match_rdma:\n");

	/*******************************************************************/
	/* Reference ID 1 */
	set_ctrl_args(&reference, "rdma", "192.168.2.1", "4420",
			"192.168.1.20", NULL, NULL, NULL);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 1, 0, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 1, 1, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("RDMA", 1, 2, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.2", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("RDMA", 1, 3, &reference, &candidate, false);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 1, 4, &reference, &candidate, false);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 1, 5, &reference, &candidate, false);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "8009",
			NULL, "", NULL, NULL);
	pass &= ctrl_match("RDMA", 1, 6, &reference, &candidate, false);


	/*******************************************************************/
	/* Reference ID 2 */
	set_ctrl_args(&reference, "rdma", "192.168.2.1", "4420",
			NULL, NULL, NULL, NULL);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 2, 0, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 2, 1, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("RDMA", 2, 2, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.2", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("RDMA", 2, 3, &reference, &candidate, false);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 2, 4, &reference, &candidate, false);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "8009",
			NULL, "", NULL, NULL);
	pass &= ctrl_match("RDMA", 2, 5, &reference, &candidate, false);


	/*******************************************************************/
	/* Reference ID 3 */
	set_ctrl_args(&reference, "rdma", "192.168.2.1", "4420",
			NULL, "eth0", NULL, NULL);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 3, 0, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 3, 1, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("RDMA", 3, 2, &reference, &candidate, true);

	set_ctrl_args(&candidate, "rdma", "192.168.2.2", "4420",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("RDMA", 3, 3, &reference, &candidate, false);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("RDMA", 3, 4, &reference, &candidate, false);

	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "8009",
			NULL, "", NULL, NULL);
	pass &= ctrl_match("RDMA", 3, 5, &reference, &candidate, false);

	return pass;
}

/**
 * test_ctrl_match_tcp - Test that we can look up TCP controllers
 *
 * @note: The mocked getifaddrs() returns 2 interface entries with the
 *        following addresses. Therefore the tests must use IP addresses
 *        that match these.
 *
 *        eth0
 *         \_ 192.168.1.20
 *         \_ fe80::dead:beef
 *
 *        lo
 *         \_ 127.0.0.1
 *         \_ ::1
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_ctrl_match_tcp(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("\ntest_ctrl_match_tcp:\n");

	/*******************************************************************/
	/* IPv4: Reference ID 1 */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 4, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 5, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 6, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 7, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 1, 8, &reference, &candidate, true);

	/*******************************************************************/
	/* IPv4: Reference ID 2 */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 2, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv4: Reference ID 3 */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 3, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv4: Reference ID 4 */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 4, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv4: Reference ID 5 */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 1, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 2, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 3, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 4, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 5, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv4: Reference ID 6 */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			NULL,
			NULL,
			"traddr=123.123.123.123,trsvcid=8009,src_addr=192.168.1.20",
			NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 6, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv4: Reference ID 7 */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			NULL,
			NULL,
			"traddr=123.123.123.123,trsvcid=8009,src_addr=127.0.0.1",
			NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 1, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 2, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 3, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 6, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_match("IPv4", 7, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv6: Reference ID 1 */
	set_ctrl_args(&reference, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 4, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 5, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 6, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 7, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 1, 8, &reference, &candidate, true);

	/*******************************************************************/
	/* IPv6: Reference ID 2 */
	set_ctrl_args(&reference, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 2, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv6: Reference ID 3 */
	set_ctrl_args(&reference, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 3, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv6: Reference ID 4 */
	set_ctrl_args(&reference, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 4, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv6: Reference ID 5 */
	set_ctrl_args(&reference, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 1, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 2, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 3, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 4, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 5, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv6: Reference ID 6 */
	set_ctrl_args(&reference, "tcp", "aaaa::bbbb", "8009",
			NULL,
			NULL,
			"traddr=aaaa::bbbb,trsvcid=8009,src_addr=fe80::dead:beef",
			NULL);

	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 6, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 6, 8, &reference, &candidate, false);

	/*******************************************************************/
	/* IPv6: Reference ID 7 */
	set_ctrl_args(&reference, "tcp", "aaaa::bbbb", "8009",
			NULL,
			NULL,
			"traddr=aaaa::bbbb,trsvcid=8009,src_addr=::1",
			NULL);

	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 1, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 2, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 3, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", NULL, NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 4, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "eth0", NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 5, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 6, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:beef", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 7, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "aaaa::bbbb", "8009",
			"fe80::dead:cafe", "lo", NULL, NULL);
	pass &= ctrl_match("IPv6", 7, 8, &reference, &candidate, false);

	return pass;
}

static bool ctrl_config_match(const char *tag,
			      int reference_id,
			      int candidate_id,
			      struct ctrl_args *reference,
			      struct ctrl_args *candidate,
			      bool should_match)
{
	const struct libnvme_ctrl_params *cp = &candidate->f.ctrl_params;
	const struct libnvme_ctrl_params *rp = &reference->f.ctrl_params;
	struct libnvme_global_ctx *ctx;
	bool match;
	libnvme_host_t h;
	libnvme_ctrl_t reference_ctrl; /* Existing controller (from sysfs) */
	libnvme_subsystem_t s;

	ctx = libnvme_create_global_ctx();
	shr_assert(ctx);

	libnvme_set_logging_file(ctx, stdout);
	libnvme_set_logging_level(ctx, LIBNVME_LOG_INFO, false, false);

	shr_assert(!libnvme_create_host(ctx, DEFAULT_HOSTNQN, DEFAULT_HOSTID, &h));
	shr_assert(h);

	shr_assert(!libnvme_create_subsystem(h, DEFAULT_SUBSYSNAME,
		 rp->subsysnqn ? rp->subsysnqn : DEFAULT_SUBSYSNQN, &s));
	shr_assert(s);

	shr_assert(!libnvme_subsystem_create_ctrl(s,
		&reference->f.ctrl_params, &reference_ctrl));
	shr_assert(reference_ctrl);
	reference_ctrl->name = "nvme1";  /* fake the device name */
	if (reference->address)
		reference_ctrl->address = (char *)reference->address;

	match = libnvmf_ctrl_match_config(reference_ctrl, &candidate->f.ctrl_params);

	if (should_match) {
		if (!match) {
			printf("%s-%d-%d: Failed to match config for Candidate (%s, %s, %s, %s, %s, %s)\n",
			       tag, reference_id, candidate_id,
			       cp->transport, cp->traddr,
			       cp->trsvcid, cp->subsysnqn,
			       cp->host_traddr, cp->host_iface);
			return false;
		}
	} else {
		if (match) {
			printf("%s-%d-%d: Config should not have matched for Candidate (%s, %s, %s, %s, %s, %s)\n",
			       tag, reference_id, candidate_id,
			       cp->transport, cp->traddr,
			       cp->trsvcid, cp->subsysnqn,
			       cp->host_traddr, cp->host_iface);
			return false;
		}
	}

	/* Set the faked data back to NULL before freeing the tree */
	reference_ctrl->name = NULL;
	reference_ctrl->address = NULL;

	libnvme_free_global_ctx(ctx);

	return true;
}

static bool test_ctrl_config_match(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("\ntest_ctrl_config_match:\n");

	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);

	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 0, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 1, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 2, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "eth0", NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 3, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 4, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "eth0", NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 5, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "lo", NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 6, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", "lo", NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 7, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, NULL);
	pass &= ctrl_config_match("IPv4", 1, 8, &reference, &candidate, true);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, "hello");
	pass &= ctrl_config_match("IPv4", 1, 9, &reference, &candidate, false);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"192.168.1.21", "lo", NULL, DEFAULT_SUBSYSNQN);
	pass &= ctrl_config_match("IPv4", 1, 9, &reference, &candidate, true);

	return pass;
}


/**
 * test_ctrl_match_pcie - Test that PCIe controllers can be looked up.
 *
 * PCIe transport uses _tree_ctrl_match() with shr_streqcase0 for address
 * comparison (case-insensitive), since _candidate_init_fabrics() returns
 * NULL for "pcie" and the else branch is taken in _candidate_init().
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_ctrl_match_pcie(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("\ntest_ctrl_match_pcie:\n");

	/*******************************************************************/
	/* Reference ID 1: PCIe ctrl with traddr */
	set_ctrl_args(&reference, "pcie", "0000:0a:00.0", NULL,
			NULL, NULL, NULL, NULL);

	/* Same traddr: match */
	set_ctrl_args(&candidate, "pcie", "0000:0a:00.0", NULL,
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("PCIe", 1, 0, &reference, &candidate, true);

	/* Case-insensitive traddr match (shr_streqcase0) */
	set_ctrl_args(&candidate, "pcie", "0000:0A:00.0", NULL,
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("PCIe", 1, 1, &reference, &candidate, true);

	/* NULL candidate traddr: match (traddr check skipped) */
	set_ctrl_args(&candidate, "pcie", NULL, NULL, NULL, NULL, NULL, NULL);
	pass &= ctrl_match("PCIe", 1, 2, &reference, &candidate, true);

	/* Different traddr: no match */
	set_ctrl_args(&candidate, "pcie", "0000:0b:00.0", NULL,
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("PCIe", 1, 3, &reference, &candidate, false);

	/* Wrong transport: no match */
	set_ctrl_args(&candidate, "rdma", "0000:0a:00.0", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("PCIe", 1, 4, &reference, &candidate, false);

	return pass;
}

/**
 * test_ctrl_match_loop - Test that loop controllers can be looked up.
 *
 * Loop transport uses _tree_ctrl_match() with shr_streqcase0 and does not
 * require a traddr, so controllers are effectively matched by transport
 * alone within a subsystem.
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_ctrl_match_loop(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("\ntest_ctrl_match_loop:\n");

	/* Reference: loop ctrl without traddr */
	set_ctrl_args(&reference, "loop", NULL, NULL, NULL, NULL, NULL, NULL);

	/* Same transport, no traddr: match */
	set_ctrl_args(&candidate, "loop", NULL, NULL, NULL, NULL, NULL, NULL);
	pass &= ctrl_match("loop", 1, 0, &reference, &candidate, true);

	/* Different transport: no match */
	set_ctrl_args(&candidate, "rdma", "192.168.1.1", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_match("loop", 1, 1, &reference, &candidate, false);

	return pass;
}

/**
 * test_well_known_nqn - Test well-known NQN handling in _candidate_init().
 *
 * When subsysnqn == NVME_DISC_SUBSYS_NAME, _candidate_init() sets
 * well_known_nqn=true and clears subsysnqn. _tree_ctrl_match() then
 * requires the matching controller to have discovery_ctrl set to true.
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_well_known_nqn(void)
{
	struct libnvme_global_ctx *ctx;
	libnvme_host_t h;
	libnvme_subsystem_t s_disc, s_regular;
	libnvme_ctrl_t disc_ctrl, regular_ctrl, found;
	bool pass = true;
	struct libnvmf_context fctx = {
		.ctrl_params = {
			.transport = "tcp",
			.traddr = "1.2.3.4",
			.trsvcid = "8009",
		},
	};
	struct libnvmf_context fctx_wknqn = {
		.ctrl_params = {
			.transport = "tcp",
			.traddr = "1.2.3.4",
			.trsvcid = "8009",
			.subsysnqn = NVME_DISC_SUBSYS_NAME,
		},
	};

	printf("\ntest_well_known_nqn:\n");

	ctx = libnvme_create_global_ctx();
	shr_assert(ctx);

	libnvme_set_logging_file(ctx, stdout);
	libnvme_set_logging_level(ctx, LIBNVME_LOG_INFO, false, false);

	shr_assert(!libnvme_create_host(ctx, DEFAULT_HOSTNQN, DEFAULT_HOSTID, &h));
	shr_assert(h);

	/* Subsystem 1: discovery subsystem with a discovery ctrl */
	shr_assert(!libnvme_create_subsystem(h, "disc",
		NVME_DISC_SUBSYS_NAME, &s_disc));
	shr_assert(!libnvme_subsystem_create_ctrl(s_disc, &fctx.ctrl_params,
		&disc_ctrl));
	shr_assert(disc_ctrl);
	disc_ctrl->name = "nvme1";
	libnvme_ctrl_set_discovery_ctrl(disc_ctrl, true);

	found = libnvme_ctrl_find(s_disc, &fctx_wknqn.ctrl_params, NULL);
	if (found == disc_ctrl) {
		printf(" - well-known NQN matches discovery ctrl [PASS]\n");
	} else {
		printf(" - well-known NQN must match discovery ctrl [FAIL]\n");
		pass = false;
	}

	/* Subsystem 2: regular subsystem; discovery_ctrl defaults to false */
	shr_assert(!libnvme_create_subsystem(h, "regular", DEFAULT_SUBSYSNQN,
		&s_regular));
	shr_assert(!libnvme_subsystem_create_ctrl(s_regular, &fctx.ctrl_params,
		&regular_ctrl));
	shr_assert(regular_ctrl);
	regular_ctrl->name = "nvme2";

	found = libnvme_ctrl_find(s_regular, &fctx_wknqn.ctrl_params, NULL);
	if (!found) {
		printf(" - well-known NQN does not match non-discovery ctrl [PASS]\n");
	} else {
		printf(" - well-known NQN must not match non-discovery ctrl [FAIL]\n");
		pass = false;
	}

	disc_ctrl->name = NULL;
	regular_ctrl->name = NULL;
	libnvme_free_global_ctx(ctx);
	return pass;
}

/**
 * test_none_normalization - Test that "none" is normalized to NULL for
 * host_traddr and host_iface in _candidate_init().
 *
 * A candidate with host_traddr="none" or host_iface="none" should behave
 * as if those fields were NULL — i.e., they are not used as a filter.
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_none_normalization(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("\ntest_none_normalization:\n");

	/* "none" host_traddr is normalized to NULL → no filter → match */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			"192.168.1.20", NULL, NULL, NULL);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			"none", NULL, NULL, NULL);
	pass &= ctrl_match("none-host_traddr", 1, 0,
			   &reference, &candidate, true);

	/* "none" host_iface is normalized to NULL → no filter → match */
	set_ctrl_args(&reference, "tcp", "123.123.123.123", "8009",
			NULL, "eth0", NULL, NULL);
	set_ctrl_args(&candidate, "tcp", "123.123.123.123", "8009",
			NULL, "none", NULL, NULL);
	pass &= ctrl_match("none-host_iface", 1, 0,
			   &reference, &candidate, true);

	return pass;
}

/**
 * test_ctrl_config_match_rdma - Test libnvmf_ctrl_match_config() for RDMA.
 *
 * RDMA uses _tree_ctrl_match() with libnvme_ipaddrs_eq for address
 * comparison.
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_ctrl_config_match_rdma(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("\ntest_ctrl_config_match_rdma:\n");

	set_ctrl_args(&reference, "rdma", "192.168.2.1", "4420",
			"192.168.1.20", NULL, NULL, NULL);

	/* Same: match */
	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			"192.168.1.20", NULL, NULL, NULL);
	pass &= ctrl_config_match("RDMA", 1, 0, &reference, &candidate, true);

	/* Candidate without host_traddr: match (check skipped when
	 * candidate->host_traddr is NULL)
	 */
	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("RDMA", 1, 1, &reference, &candidate, true);

	/* Different traddr: no match */
	set_ctrl_args(&candidate, "rdma", "192.168.2.2", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("RDMA", 1, 2, &reference, &candidate, false);

	/* Different trsvcid: no match */
	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "8009",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("RDMA", 1, 3, &reference, &candidate, false);

	/* Different host_traddr: no match */
	set_ctrl_args(&candidate, "rdma", "192.168.2.1", "4420",
			"192.168.1.21", NULL, NULL, NULL);
	pass &= ctrl_config_match("RDMA", 1, 4, &reference, &candidate, false);

	/* Wrong transport: no match */
	set_ctrl_args(&candidate, "tcp", "192.168.2.1", "4420",
			NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("RDMA", 1, 5, &reference, &candidate, false);

	return pass;
}

/**
 * test_ctrl_config_match_fc - Test libnvmf_ctrl_match_config() for FC.
 *
 * FC uses _tree_ctrl_match() with shr_streqcase0 for address comparison
 * (case-insensitive WWN matching), since _candidate_init_fabrics() returns
 * NULL for "fc" and the generic else branch is taken.
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_ctrl_config_match_fc(void)
{
	bool pass = true;
	struct ctrl_args reference = {0};
	struct ctrl_args candidate = {0};

	printf("\ntest_ctrl_config_match_fc:\n");

	set_ctrl_args(&reference, "fc", "21:00:00:e0:8b:05:05:01", "4420",
		      "21:00:00:e0:8b:05:05:20", NULL, NULL, NULL);

	/* Same: match */
	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
		      "21:00:00:e0:8b:05:05:20", NULL, NULL, NULL);
	pass &= ctrl_config_match("FC", 1, 0, &reference, &candidate, true);

	/* Candidate without host_traddr: match */
	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
		      NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("FC", 1, 1, &reference, &candidate, true);

	/* Case-insensitive WWN traddr match (shr_streqcase0) */
	set_ctrl_args(&candidate, "fc", "21:00:00:E0:8B:05:05:01", "4420",
		      NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("FC", 1, 2, &reference, &candidate, true);

	/* Different traddr: no match */
	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:99", "4420",
		      NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("FC", 1, 3, &reference, &candidate, false);

	/* Different trsvcid: no match */
	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "8009",
		      NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("FC", 1, 4, &reference, &candidate, false);

	/* Different host_traddr: no match */
	set_ctrl_args(&candidate, "fc", "21:00:00:e0:8b:05:05:01", "4420",
		      "21:00:00:e0:8b:05:05:21", NULL, NULL, NULL);
	pass &= ctrl_config_match("FC", 1, 5, &reference, &candidate, false);

	/* Wrong transport: no match */
	set_ctrl_args(&candidate, "rdma", "21:00:00:e0:8b:05:05:01", "4420",
		      NULL, NULL, NULL, NULL);
	pass &= ctrl_config_match("FC", 1, 6, &reference, &candidate, false);

	return pass;
}

/**
 * test_lookup_ctrl_pagination - Test libnvme_lookup_ctrl() with p != NULL.
 *
 * With p set, __nvme_ctrl_find() starts searching from the controller
 * *after* p. This verifies that a controller appearing before p in the
 * list is skipped.
 *
 * @return true when all tests have passed. false otherwise.
 */
static bool test_lookup_ctrl_pagination(void)
{
	struct libnvme_global_ctx *ctx;
	libnvme_host_t h;
	libnvme_subsystem_t s;
	libnvme_ctrl_t ctrl_a, ctrl_b, found, ctrl_new;
	bool pass = true;
	struct libnvmf_context fctx_a = {
		.ctrl_params = {
			.transport = "rdma",
			.traddr = "192.168.1.1",
			.trsvcid = "4420",
		},
	};
	struct libnvmf_context fctx_b = {
		.ctrl_params = {
			.transport = "rdma",
			.traddr = "192.168.1.2",
			.trsvcid = "4420",
		},
	};

	printf("\ntest_lookup_ctrl_pagination:\n");

	ctx = libnvme_create_global_ctx();
	shr_assert(ctx);

	libnvme_set_logging_file(ctx, stdout);
	libnvme_set_logging_level(ctx, LIBNVME_LOG_INFO, false, false);

	shr_assert(!libnvme_create_host(ctx, DEFAULT_HOSTNQN, DEFAULT_HOSTID, &h));
	shr_assert(h);
	shr_assert(!libnvme_create_subsystem(h, DEFAULT_SUBSYSNAME,
		DEFAULT_SUBSYSNQN, &s));
	shr_assert(s);

	/* Build list: [ctrl_a, ctrl_b] */
	shr_assert(!libnvme_subsystem_create_ctrl(s, &fctx_a.ctrl_params, &ctrl_a));
	shr_assert(ctrl_a);
	shr_assert(!libnvme_subsystem_create_ctrl(s, &fctx_b.ctrl_params, &ctrl_b));
	shr_assert(ctrl_b);

	/* p=NULL: search starts from head, finds ctrl_a */
	found = libnvme_ctrl_find(s, &fctx_a.ctrl_params, NULL);
	if (found == ctrl_a) {
		printf(" - ctrl_find(p=NULL) finds ctrl_a [PASS]\n");
	} else {
		printf(" - ctrl_find(p=NULL) must find ctrl_a [FAIL]\n");
		pass = false;
	}

	/* p=ctrl_b: search starts after ctrl_b; ctrl_a is before ctrl_b so
	 * it is skipped.
	 */
	ctrl_new = libnvme_lookup_ctrl(s, &fctx_a.ctrl_params, ctrl_b);
	if (!ctrl_new) {
		printf(" - lookup_ctrl(p=ctrl_b) skips ctrl_a, returns NULL [PASS]\n");
	} else {
		printf(" - lookup_ctrl(p=ctrl_b) must skip ctrl_a and must not find a ctrl [FAIL]\n");
		pass = false;
	}

	libnvme_free_global_ctx(ctx);
	return pass;
}

/**
 * This test module uses a mocked ifaddrs library (mock-ifaddrs.c)
 * such that there are 2 fake interfaces (eth0 and lo) with the
 * following IP addresses:
 *
 *    - eth0
 *      \_ IPv4: 192.168.1.20
 *      \_ IPv6: fe80::dead:beef
 *
 *    - lo
 *      \_ IPv4: 127.0.0.1
 *      \_ IPv6: ::1
 */
int main(int argc, char *argv[])
{
	bool pass = true;

	pass &= test_lookup();
	pass &= test_src_addr();
	pass &= test_ctrl_match_fc();
	pass &= test_ctrl_match_rdma();
	pass &= test_ctrl_match_tcp();
	pass &= test_ctrl_config_match();
	pass &= test_ctrl_match_pcie();
	pass &= test_ctrl_match_loop();
	pass &= test_well_known_nqn();
	pass &= test_none_normalization();
	pass &= test_ctrl_config_match_rdma();
	pass &= test_ctrl_config_match_fc();
	pass &= test_lookup_ctrl_pagination();

	fflush(stdout);

	exit(pass ? EXIT_SUCCESS : EXIT_FAILURE);
}
