/*
* 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;
}