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

#include "testnet.h"
#include <librecast/crypto.h>
#include <librecast/net.h>
#include <assert.h>
#include <pthread.h>
#include <fcntl.h>
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/param.h>

#undef TESTDEBUG
#ifdef TESTDEBUG
# define PKTS 10
# define PKTSZ 4
#else
# define PKTS 20
# define PKTSZ 1024
#endif

#define WAITS 20

#ifdef HAVE_LIBSODIUM
#ifdef HAVE_LIBLCRQ
enum {
	TID_SEND,
	TID_RECV
};

static const size_t SZ = PKTS * PKTSZ + 5;
static char channel_name[] = "0000-0008";
static lc_ctx_t *lctx;
static lc_socket_t *sock[2];
static sem_t receiver_ready, timeout;
static unsigned char key[crypto_secretbox_KEYBYTES];

#ifdef TESTDEBUG
#if 0
static void dumppkt(uint8_t *pkt, int haseq)
{
	uint16_t seq;
	uint8_t *dat = pkt;
	if (haseq) {
		dat += sizeof seq;
		seq = ntohs(*(uint16_t *)pkt);
		fprintf(stderr, "%u: ", seq);
	}
	else fprintf(stderr, "   ");
	for (size_t z = 0; z < PKTSZ * 8; z++) {
		if (isset(dat, z)) putc('1', stderr);
		else putc('0', stderr);
	}
	putc('\n', stderr);
}
#endif
static void dump_bufs(uint8_t buf[2][SZ])
{
	for (size_t p = 0; p < PKTS; p++) {
		fprintf(stderr, "%zu: ", p);
		for (int i = 0; i < 2; i++) {
			for (size_t z = 0; z < PKTSZ * 8; z++) {
				uint8_t *b = buf[i];
				if (isset(b, p * PKTSZ * 8 + z)) putc('1', stderr);
				else putc('0', stderr);
			}
			putc(' ', stderr);
		}
		putc('\n', stderr);
	}
	putc('\n', stderr);
}
#endif

static void generate_source_data(uint8_t *buf, size_t sz)
{
	ssize_t rc;
	int f;
	f = open("/dev/urandom", O_RDONLY);
	if (f == -1) return;
	rc = read(f, buf, sz);
	test_assert(rc == (ssize_t)sz, "%zi random bytes read", rc);
	close(f);
}

static void *recv_data_fec(void *arg)
{
	uint8_t *buf = (uint8_t *)arg;
	ssize_t rc;
	lc_channel_t * chan = lc_channel_new(lctx, channel_name);

	memset(buf, 0, SZ);    /* clear receive buffer */

	lc_channel_bind(sock[1], chan);
	lc_socket_oil_add(sock[0], lc_channel_get_hash(chan));

	sem_post(&receiver_ready);

	rc = lc_channel_recv(chan, buf, SZ, 0);
	if (rc == -1) perror("lc_channel_recv");
	test_log("lc_channel_recv returned %zi\n", rc);
	test_assert(rc >= (ssize_t)SZ, "data received %zi/%zu bytes", rc, SZ);

	lc_channel_part(chan);
	sem_post(&timeout);
	return arg;
}

static void *send_data_fec(void *arg)
{
	uint8_t *buf = (uint8_t *)arg;
	lc_channel_t * chan = lc_channel_new(lctx, channel_name);
	rq_t *rq;
	ssize_t rc;
	size_t byt = 0;

	lc_channel_bind(sock[0], chan);
	test_log("sending data with FEC + symmetric encryption\n");
	sem_wait(&receiver_ready);

	rc = lc_channel_send(chan, buf, SZ, 0);
	if (rc == -1) perror("lc_channel_send");
	if (!test_assert(rc != -1, "lc_channel_send encoded data, returned %zi", rc))
		return NULL;
	byt = rc;
	rq = lc_channel_rq(chan);

	/* Send packets. Drop packet 3 */
	int pkts = rq_KP(rq) + RQ_OVERHEAD * 2;
	test_log("sending %u packets\n", pkts);
	for (int i = 1; i < pkts; i++) {
		int flags = (i == 3) ? MSG_DROP : 0;
		rc = lc_channel_send(chan, NULL, 0, flags);
		if (rc == -1) perror("lc_channel_send");
		test_assert(rc >= 0, "lc_channel_send encoded data, returned %zi", rc);
		if (rc > 0) byt += rc;
	}
	test_assert(byt >= SZ, "data sent %zi bytes (buffer = %zu)", byt, SZ);
	return arg;
}
#endif
#endif

int main(void)
{
#if !defined(HAVE_LIBLCRQ) || !(HAVE_LIBSODIUM)
	return test_skip("lc_channel_send() / lc_socket_recv() - FEC + encrypt");
#else
	pthread_t tid[2];
	uint8_t buf[2][SZ];
	struct timespec ts;

	test_name("lc_channel_send() / lc_socket_recv() - FEC + encrypt (socketpair)");
	test_require_net(TEST_NET_BASIC);

	lctx = lc_ctx_new();
	if (!test_assert(lctx != NULL, "lc_ctx_new()")) return test_status;

	/* generate symmetric encryption key */
	crypto_secretbox_keygen(key);
	lc_ctx_set_sym_key(lctx, key, crypto_secretbox_KEYBYTES);
	lc_ctx_coding_set(lctx, LC_CODE_SYMM | LC_CODE_FEC_RQ);

	if (!test_assert(lc_socketpair(lctx, sock) == 0, "lc_socketpair()"))
		goto err_free_lctx;

	/* clear buffers */
	memset(buf[0], 0, SZ);
	memset(buf[1], 0, SZ);

	sem_init(&timeout, 0, 0);
	sem_init(&receiver_ready, 0, 0);
	generate_source_data(buf[TID_SEND], SZ);

	test_assert(memcmp(buf[TID_RECV], buf[TID_SEND], SZ), "buffer differ before sync");

	pthread_create(&tid[TID_SEND], NULL, &send_data_fec, buf[TID_SEND]);
	pthread_create(&tid[TID_RECV], NULL, &recv_data_fec, buf[TID_RECV]);
	clock_gettime(CLOCK_REALTIME, &ts);
	ts.tv_sec += WAITS;
	test_assert(!sem_timedwait(&timeout, &ts), "timeout");
	pthread_cancel(tid[TID_RECV]);
	pthread_join(tid[TID_RECV], NULL);
	pthread_cancel(tid[TID_SEND]);
	pthread_join(tid[TID_SEND], NULL);
	sem_destroy(&receiver_ready);
	sem_destroy(&timeout);

	test_assert(!memcmp(buf[TID_RECV], buf[TID_SEND], SZ), "send buffer matches received");
#ifdef TESTDEBUG
	dump_bufs(buf);
#endif
err_free_lctx:
	lc_ctx_free(lctx);
	return test_status;
#endif
}
