/*
 * NPF connection tests.
 *
 * Public Domain.
 */

#ifdef _KERNEL
#include <sys/types.h>
#include <sys/kmem.h>
#endif

#include "npf.h"
#include "npf_impl.h"
#include "npf_conn.h"
#include "npf_test.h"

static bool	lverbose = false;

static unsigned
count_conns(npf_conndb_t *cd)
{
	npf_conn_t *head = npf_conndb_getlist(cd), *conn = head;
	unsigned n = 0;

	while (conn) {
		n++;
		conn = npf_conndb_getnext(cd, conn);
		if (conn == head) {
			break;
		}
	}
	return n;
}

static struct mbuf *
get_packet(unsigned i)
{
	struct mbuf *m;
	struct ip *ip;

	m = mbuf_get_pkt(AF_INET, IPPROTO_UDP,
	    "10.0.0.1", "172.16.0.1", 9000, 9000);
	(void)mbuf_return_hdrs(m, false, &ip);
	ip->ip_src.s_addr += i;
	return m;
}

static bool
enqueue_connection(unsigned i, bool expire)
{
	struct mbuf *m = get_packet(i);
	npf_cache_t *npc = get_cached_pkt(m, NULL, NPF_RULE_LAYER_3);
	npf_conn_t *con;

	con = npf_conn_establish(npc, PFIL_IN, true);
	CHECK_TRUE(con != NULL);
	if (expire) {
		npf_conn_expire(con);
	}
	npf_conn_release(con);
	put_cached_pkt(npc);
	return true;
}

static bool
run_conn_gc(unsigned active, unsigned expired, unsigned expected)
{
	npf_t *npf = npf_getkernctx();
	npf_conndb_t *cd = npf_conndb_create();
	unsigned total, n = 0;

	npf->conn_db = cd;

	/*
	 * Insert the given number of active and expired connections..
	 */
	total = active + expired;

	while (active || expired) {
		if (active) {
			enqueue_connection(n++, false);
			active--;
		}
		if (expired) {
			enqueue_connection(n++, true);
			expired--;
		}
	}

	/* Verify the count. */
	n = count_conns(cd);
	CHECK_TRUE(n == total);

	/*
	 * Run G/C.  Check the remaining.
	 */
	npf_conndb_gc(npf, cd, false, false);
	n = count_conns(cd);
	if (lverbose) {
		printf("in conndb -- %u (expected %u)\n", n, expected);
	}
	CHECK_TRUE(n == expected);

	/* Flush and destroy. */
	npf_conndb_gc(npf, cd, true, false);
	npf_conndb_destroy(cd);
	npf->conn_db = NULL;
	return true;
}

static bool
run_gc_tests(void)
{
	bool ok;
	int val;

	/* Check the default value. */
	npfk_param_get(npf_getkernctx(), "gc.step", &val);
	CHECK_TRUE(val == 256);

	/* Empty => GC => 0 in conndb. */
	ok = run_conn_gc(0, 0, 0);
	CHECK_TRUE(ok);

	/* 1 active => GC => 1 in conndb. */
	ok = run_conn_gc(1, 0, 1);
	CHECK_TRUE(ok);

	/* 1 expired => GC => 0 in conndb. */
	ok = run_conn_gc(0, 1, 0);
	CHECK_TRUE(ok);

	/* 1 active and 1 expired => GC => 1 in conndb. */
	ok = run_conn_gc(1, 1, 1);
	CHECK_TRUE(ok);

	/* 2 expired => GC => 0 in conndb. */
	ok = run_conn_gc(0, 2, 0);
	CHECK_TRUE(ok);

	/* 128 expired => GC => 0 in conndb. */
	ok = run_conn_gc(0, 128, 0);
	CHECK_TRUE(ok);

	/* 512 expired => GC => 256 in conndb. */
	ok = run_conn_gc(0, 512, 256);
	CHECK_TRUE(ok);

	/* 512 expired => GC => 127 in conndb. */
	npfk_param_set(npf_getkernctx(), "gc.step", 128);
	ok = run_conn_gc(0, 512, 384);
	CHECK_TRUE(ok);

	return true;
}

static bool
run_conndb_tests(npf_t *npf)
{
	npf_conndb_t *orig_cd = npf->conn_db;
	bool ok;

	npf_config_enter(npf);
	npf_conn_tracking(npf, true);
	npf_config_exit(npf);

	ok = run_gc_tests();

	/* We *MUST* restore the valid conndb. */
	npf->conn_db = orig_cd;
	return ok;
}


static void
worker_test_task(npf_t *npf)
{
	bool *done = atomic_load_acquire(&npf->arg);
	atomic_store_release(done, true);
}

static bool
run_worker_tests(npf_t *npf)
{
	unsigned n = 100;
	int error;

	/* Spawn a worker thread. */
	error = npf_worker_sysinit(1);
	assert(error == 0);

	/*
	 * Enlist/discharge an instance, trying to trigger a race.
	 */
	while (n--) {
		bool task_done = false;
		unsigned retry = 100;
		npf_t *test_npf;

		/*
		 * Initialize a dummy NPF instance and add a test task.
		 * We will (ab)use npf_t::arg here.
		 *
		 * XXX/TODO: We should use:
		 *
		 *	npfk_create(NPF_NO_GC, &npftest_mbufops,
		 *	    &npftest_ifops, &task_done);
		 *
		 * However, it resets the interface state and breaks
		 * other tests; to be refactor.
		 */
		test_npf = kmem_zalloc(sizeof(npf_t), KM_SLEEP);
		atomic_store_release(&test_npf->arg, &task_done);
		test_npf->ebr = npf_ebr_create();

		error = npf_worker_addfunc(test_npf, worker_test_task);
		assert(error == 0);

		/* Enlist the NPF instance. */
		npf_worker_enlist(test_npf);

		/* Wait for the task to be done. */
		while (!atomic_load_acquire(&task_done) && retry--) {
			npf_worker_signal(test_npf);
			kpause("gctest", false, MAX(1, mstohz(1)), NULL);
		}

		CHECK_TRUE(atomic_load_acquire(&task_done));
		npf_worker_discharge(test_npf);

		/* Clear the parameter and signal again. */
		atomic_store_release(&test_npf->arg, NULL);
		npf_worker_signal(test_npf);

		npf_ebr_destroy(test_npf->ebr);
		kmem_free(test_npf, sizeof(npf_t)); // npfk_destroy()
	}

	/*
	 * Destroy the worker.
	 *
	 * Attempts to enlist, discharge or signal should have no effect.
	 */

	npf_worker_sysfini();
	npf_worker_enlist(npf);
	npf_worker_signal(npf);
	npf_worker_discharge(npf);
	return true;
}

bool
npf_gc_test(bool verbose)
{
	npf_t *npf = npf_getkernctx();
	bool ok;

	lverbose = verbose;

	ok = run_conndb_tests(npf);
	CHECK_TRUE(ok);

	ok = run_worker_tests(npf);
	CHECK_TRUE(ok);

	return ok;
}