/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2024-2025 Brett Sheffield <bacs@librecast.net> */

/* multi-router test of traffic forwarding with OIL filter checks */

#include "test.h"
#include <librecast_pvt.h>
#include <librecast/router.h>

#define ROUTERS 6
#define PORTS 4
#define USEC 100000
#define OVERHEAD sizeof(lc_tlv_t) + HASHSIZE

struct counter_s {
	size_t ibyt; /* bytes in (recv) */
	size_t obyt; /* bytes out (send) */
	unsigned int ipkt;    /* pkts in (recv) */
	unsigned int opkt;    /* pkts out (send) */
};
struct counter_s c[ROUTERS][PORTS];

static void print_router_port_by_addr(lc_router_t *r[], lc_socket_t *s[], void *addr)
{
	for (int j = 0; j < ROUTERS; j++) {
		for (int i = 0; i < (int)r[j]->ports; i++) {
			if (r[j]->port[i] == addr) {
				test_log(" R[%i]P[%i]\n", j, i);
				return;
			}
		}
	}
	for (int i = 0; i < 4; i++) {
		if (s[i] == addr) {
			test_log(" S[%i]\n", i);
			return;
		}
	}
	test_log(" (nil)\n");
}

static void print_port_addresses(lc_router_t *r[], lc_socket_t *s[])
{
	for (int j = 0; j < ROUTERS; j++) {
		test_log("-\n");
		for (int i = 0; i < (int)r[j]->ports; i++) {
			test_log(" R[%i]P[%i] %p -- %p", j, i, (void *)r[j]->port[i], r[j]->port[i]->pair);
			print_router_port_by_addr(r, s, r[j]->port[i]->pair);
		}
	}
	test_log("-\n");
}

static void check_byt_and_pkt_counters(lc_router_t *r[], struct counter_s c[ROUTERS][PORTS])
{
	for (int j = 0; j < ROUTERS; j++) {
		test_log("testing ports on R[%i]\n", j);
		for (int i = 0; i < (int)r[j]->ports; i++) {
			unsigned int pktin = aload(&r[j]->port[i]->pkt_in);
			unsigned int pktout = aload(&r[j]->port[i]->pkt_out);
			unsigned int bytin = aload(&r[j]->port[i]->byt_in);
			unsigned int bytout = aload(&r[j]->port[i]->byt_out);
			test_assert(pktin == c[j][i].ipkt,
					"r[%i]p[%i] pkt recv = %u/%u", j, i,
					pktin, c[j][i].ipkt);
			test_assert(bytin == c[j][i].ibyt,
					"r[%i]p[%i] byt recv = %zu/%zu", j, i,
					bytin, c[j][i].ibyt);
			test_assert(pktout == c[j][i].opkt,
					"r[%i]p[%i] pkt sent = %u/%u", j, i,
					pktout, c[j][i].opkt);
			test_assert(bytout == c[j][i].obyt,
					"r[%i]p[%i] byt sent = %zu/%zu", j, i,
					bytout, c[j][i].obyt);
		}
	}
}

static void pkt_in(int rid, int port, size_t byt)
{
	c[rid][port].ibyt += byt + OVERHEAD;
	c[rid][port].ipkt++;
}

static void pkt_out(int rid, int port, size_t byt)
{
	c[rid][port].obyt += byt + OVERHEAD;
	c[rid][port].opkt++;
}

static void *router_ready(void *arg)
{
	sem_post(arg);
	return NULL;
}

int main(void)
{
	lc_ctx_t *lctx;
	lc_router_t *r[ROUTERS];
	lc_socket_t *sv[8] = {0};
	lc_socket_t *s[4];
	lc_channel_t *chan;
	sem_t sem_ready;
	char msg[] = "butterfly";
	ssize_t byt;
	int rc;

	test_name("router packet forwarding (OIL filter) - multi-router");

	/* create context and routers */
	lctx = lc_ctx_new();
	if (!test_assert(lctx != NULL, "lc_ctx_new()")) return test_status;
	rc = lc_router_net(lctx, r, ROUTERS, LC_TOPO_BUTTERFLY, 0, LC_ROUTER_FLAG_FIXED);
	if (!test_assert(rc == 0, "lc_router_net()")) goto err_ctx_free;

	/* create socketpairs */
	for (int i = 0; i < PORTS * 2; i += 2) {
		rc = lc_socketpair(lctx, &sv[i]);
		test_assert(rc == 0, "lc_socketpair()[%i]", i);
	}
	s[0] = sv[1];
	s[1] = sv[3];
	s[2] = sv[5];
	s[3] = sv[7];

	rc = lc_router_socket_add(r[0], s[0]->pair);
	test_assert(rc == 0, "lc_router_socket_add() R[0], S[0]");
	rc = lc_router_socket_add(r[1], s[1]->pair);
	test_assert(rc == 0, "lc_router_socket_add() R[1], S[1]");
	rc = lc_router_socket_add(r[4], s[2]->pair);
	test_assert(rc == 0, "lc_router_socket_add() R[4], S[2]");
	rc = lc_router_socket_add(r[5], s[3]->pair);
	test_assert(rc == 0, "lc_router_socket_add() R[5], S[3]");

	/* verify ports connected correctly */
	print_port_addresses(r, s);
	test_assert(r[0]->port[0]->pair == r[2]->port[0], "R[0]P[0] <--> R[2]P[0]");
	test_assert(r[0]->port[1]->pair == r[4]->port[0], "R[0]P[1] <--> R[4]P[0]");
	test_assert(r[1]->port[0]->pair == r[2]->port[1], "R[1]P[0] <--> R[2]P[1]");
	test_assert(r[1]->port[1]->pair == r[5]->port[0], "R[1]P[1] <--> R[5]P[0]");
	test_assert(r[2]->port[2]->pair == r[3]->port[0], "R[2]P[2] <--> R[3]P[0]");
	test_assert(r[3]->port[1]->pair == r[4]->port[1], "R[3]P[1] <--> R[4]P[1]");
	test_assert(r[3]->port[2]->pair == r[5]->port[1], "R[3]P[2] <--> R[5]P[1]");
	test_assert(r[0]->port[2]->pair == s[0], "R[0]P[2] <--> S[0]");
	test_assert(r[1]->port[2]->pair == s[1], "R[1]P[2] <--> S[1]");
	test_assert(r[4]->port[2]->pair == s[2], "R[4]P[2] <--> S[2]");
	test_assert(r[5]->port[2]->pair == s[3], "R[5]P[2] <--> S[3]");

	sem_init(&sem_ready, 0, 0);

	/* bring up all ports on all routers and enable forwarding */
	for (int i = 0; i < ROUTERS; i++) {
		lc_router_port_up(r[i], -1);
		lc_router_port_set(r[i], -1, LC_PORT_FWD);
		lc_router_onready(r[i], &router_ready, &sem_ready);
	}

	/* gentlemen, start your routers... */
	for (int i = 0; i < ROUTERS; i++) {
		rc = lc_router_start(r[i]);
		test_assert(rc == 0, "lc_router_start()[%i]", i);
	}

	/* wait until routers ready */
	for (int i = 0; i < ROUTERS; i++) {
		sem_wait(&sem_ready);
	}

	/* create channel and bind to sock[1] */
	chan = lc_channel_random(lctx);
	if (!(test_assert(chan != NULL, "lc_channel_random()"))) goto err_ctx_free;
	rc = lc_channel_bind(s[0], chan);
	test_assert(rc == 0, "lc_channel_bind()");
	test_assert(chan->sock == s[0], "channel is bound");

	memset(c, 0, sizeof c);
	check_byt_and_pkt_counters(r, c); /* zeroed */

	/* set OIL for various ports */
	/* JOIN r[5]p[2] and propagate back to r0 */
	lc_socket_oil_add(r[5]->port[2], chan->hash);
	lc_socket_oil_add(r[3]->port[2], chan->hash);
	lc_socket_oil_add(r[2]->port[2], chan->hash);
	lc_socket_oil_add(r[0]->port[0], chan->hash);
	lc_socket_oil_add(r[0]->port[2]->pair, chan->hash); /* for socket[0] */
	/* JOIN r[4]p[2] and propagate back to r0 */
	lc_socket_oil_add(r[4]->port[2], chan->hash);
	lc_socket_oil_add(r[3]->port[1], chan->hash);

	/* send on socket 0 */
	byt = lc_socket_send(s[0], msg, sizeof msg, 0);
	if (!test_assert(byt > 0, "%zi bytes sent on socket[0] -> r[0]p[2]", byt))
		goto err_ctx_free;
	/* packet flows should be: */
	pkt_in(0, 2, byt); pkt_out(0, 0, byt);
	pkt_in(2, 0, byt); pkt_out(2, 2, byt);
	pkt_in(3, 0, byt); pkt_out(3, 1, byt); pkt_out(3, 2, byt);
	pkt_in(4, 1, byt); pkt_out(4, 2, byt);
	pkt_in(5, 1, byt); pkt_out(5, 2, byt);

	sleep(1); /* wait for routers to process packets */
	sem_destroy(&sem_ready);

	check_byt_and_pkt_counters(r, c);

err_ctx_free:
	lc_ctx_free(lctx);
	return test_status;
}
