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

/* 4-port router test of traffic forwarding with OIL filter checks
 *
 *   +--------------+
 *   |  router (r)  |
 *   |  0  1  2  3  | ports (r->port[])
 *   +--------------+
 *      0  2  4  6
 *      |  |  |  | (sockets (s[])
 *      1  3  5  7
 *
 * There are 8 sockets (4 socketpairs) and 4 router ports.
 */

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

#define PORTS 4
#define USEC 100000

static void create_socketpairs_and_connect(lc_ctx_t *lctx, lc_router_t *r, lc_socket_t *s[])
{
	test_log("%s()\n", __func__);
	int rc;
	for (int i = 0; i < PORTS * 2; i += 2) {
		rc = lc_socketpair(lctx, &s[i]);
		test_assert(rc == 0, "lc_socketpair()[%i]", i);
		rc = lc_router_socket_add(r, s[i]);
		test_assert(rc == 0, "lc_router_socket_add()[%i]", i);
	}
}

static void check_all_ports(lc_router_t *r, int status, char msg[])
{
	test_log("%s()\n", __func__);
	for (int i = 0; i < PORTS; i++) {
		test_assert((r->port[i]->status & status) == status, msg, i);
	}
}

static void check_byt_and_pkt_counters(lc_router_t *r,
		size_t byt_in[], unsigned int pkt_in[], size_t byt_out[], unsigned int pkt_out[])
{
	test_log("%s()\n", __func__);
	for (int i = 0; i < PORTS; i++) {
		unsigned int pktin = aload(&r->port[i]->pkt_in);
		unsigned int pktout = aload(&r->port[i]->pkt_out);
		unsigned int bytin = aload(&r->port[i]->byt_in);
		unsigned int bytout = aload(&r->port[i]->byt_out);
		test_assert(pktin == pkt_in[i],
				"port[%i] pkt recv = %u/%u", i, pktin, pkt_in[i]);
		test_assert(bytin == byt_in[i],
				"port[%i] byt recv = %zu/%zu", i, bytin, byt_in[i]);
		test_assert(pktout == pkt_out[i],
				"port[%i] pkt sent = %u/%u", i, pktout, pkt_out[i]);
		test_assert(bytout == byt_out[i],
				"port[%i] byt sent = %zu/%zu", i, bytout, byt_out[i]);
	}
}

static ssize_t send_pkt_on_port(int port, lc_socket_t *s[], size_t byt_in[], unsigned int pkt_in[])
{
	test_log("%s() [%i]\n", __func__, port);
	lc_router_t *r = s[0]->router;
	lc_socket_t *sock = r->port[port]->pair;
	char msg[] = "hello there";
	ssize_t byt;
	errno = 0;
	byt = lc_socket_send(sock, msg, sizeof msg, 0);
	if (byt == -1) perror("lc_socket_send()");
	test_assert(byt > 0, "%zi bytes sent on port %i", byt, port);
	if (byt > 0) {
		byt_in[port] += byt;
		pkt_in[port]++;
	}
	return byt;
}

int main(void)
{
	lc_ctx_t *lctx;
	lc_router_t *r;
	lc_socket_t *s[PORTS * 2];
	lc_channel_t *chan;
	unsigned int pkt_in[PORTS] = {0};
	unsigned int pkt_out[PORTS] = {0};
	size_t byt_in[PORTS] = {0};
	size_t byt_out[PORTS] = {0};
	ssize_t byt;
	int rc;

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

	/* create context and router */
	lctx = lc_ctx_new();
	if (!test_assert(lctx != NULL, "lc_ctx_new()")) return test_status;
	r = lc_router_new(lctx, PORTS, LC_ROUTER_FLAG_FIXED);
	if (!test_assert(r != NULL, "lc_router_new() - %i-port router created", PORTS))
		goto err_ctx_free;

	/* create socketpairs and connect to router ports */
	create_socketpairs_and_connect(lctx, r, s);

	/* ensure packet counters are zeroed */
	check_byt_and_pkt_counters(r, byt_in, pkt_in, byt_out, pkt_out);

	/* bring up router ports, enable forwarding and start router */
	lc_router_port_up(r, -1);
	lc_router_port_set(r, -1, LC_PORT_FWD);
	check_all_ports(r, LC_PORT_UP|LC_PORT_FWD, "port[%i] status UP + FWD");
	rc = lc_router_start(r);
	if (!test_assert(rc == 0, "lc_router_start()")) goto err_ctx_free;

	usleep(USEC); /* give router a moment to wake up */

	/* 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[1], chan);
	test_assert(rc == 0, "lc_channel_bind()");
	test_assert(chan->sock == s[1], "channel is bound");

	/* add channel hash to OIL of socket 1 to enable sending */
	rc = lc_socket_oil_add(s[1], chan->hash);
	test_assert(rc == 0, "lc_socket_oil_add()");


	/* send a packet on port 0 */
	send_pkt_on_port(0, s, byt_in, pkt_in);
	usleep(USEC); /* wait for router to process packets */
	/* test nothing forwarded */
	check_byt_and_pkt_counters(r, byt_in, pkt_in, byt_out, pkt_out);

	/* add channel to OIL of selected sockets */
	lc_socket_oil_add(s[2], chan->hash); /* s[2] => port 1 */
	lc_socket_oil_add(s[6], chan->hash); /* s[6] => port 3 */

	/* test packets received on selected sockets only */
	byt = send_pkt_on_port(0, s, byt_in, pkt_in);
	byt_out[1] += byt, pkt_out[1]++; /* port 1 subscribed */
	byt_out[3] += byt, pkt_out[3]++; /* port 3 subscribed */
	usleep(USEC); /* wait for router to process packets */
	check_byt_and_pkt_counters(r, byt_in, pkt_in, byt_out, pkt_out);

err_ctx_free:
	lc_ctx_free(lctx);
	return test_status;
}
