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

#include "testdata.h"
#include "testnet.h"
#include <arpa/inet.h>
#include <librecast.h>
#include <librecast/crypto.h>
#include <librecast/mdex.h>
#include <librecast/sync.h>
#include <pthread.h>
#include <semaphore.h>

#define TEST_SIZE 1024 * 100 + 5
/* nice powers of two hide bugs - ensure test size is odd */
_Static_assert(TEST_SIZE % 2, "TEST_SIZE must be an odd number");

#define TIMEOUT_SECONDS 10

#ifdef HAVE_RQ_OTI
# ifdef HAVE_MLD
enum {
	TID_SEND,
	TID_RECV
};

static sem_t sem_recv;
static sem_t sem_send;

struct pkg_s {
	void *data;
	size_t len;
	uint8_t *hash;
};

/* wrapper for pthread_cleanup_push */
inline static void _lc_channel_part(lc_channel_t *chan)
{
	(void)lc_channel_part(chan);
}

void *thread_recv(void *arg)
{
	struct pkg_s *pkg = (struct pkg_s *)arg;
	lc_ctx_t *lctx;
	lc_socket_t *sock;
	lc_channel_t *chan;

	lctx = lc_ctx_new();
	pthread_cleanup_push((void (*)(void *))lc_ctx_free, lctx);

	sock = lc_socket_new(lctx);
	pthread_cleanup_push((void (*)(void *))lc_socket_close, sock);

	chan = lc_channel_hash(lctx, pkg->hash, HASHSIZE, LC_DEFAULT_FLAGS, LC_DEFAULT_PORT);
	pthread_cleanup_push((void (*)(void *))lc_channel_free, chan);

	char straddr[INET6_ADDRSTRLEN];
	inet_ntop(AF_INET6, lc_channel_in6addr(chan), straddr, INET6_ADDRSTRLEN);
	fprintf(stderr, "joining %s\n", straddr);

	sem_wait(&sem_send); /* wait until sender is ready */

	lc_channel_bind(sock, chan);
	lc_channel_coding_set(chan, LC_CODE_FEC_RQ);
	lc_channel_join(chan);
	pthread_cleanup_push((void (*)(void *))_lc_channel_part, chan);
	lc_channel_recv(chan, pkg->data, pkg->len, 0);

	pthread_cleanup_pop(1); /* lc_channel_part */
	pthread_cleanup_pop(1); /* lc_channel_free */
	pthread_cleanup_pop(1); /* lc_socket_close */
	pthread_cleanup_pop(1); /* lc_ctx_free */
	sem_post(&sem_recv);
	return NULL;
}

void *thread_send(void *arg)
{
	struct pkg_s *pkg = (struct pkg_s *)arg;
	lc_ctx_t *lctx;
	lc_share_t *share;
	mdex_t *mdex;
	mdex_entry_t entry = {0};
	int rc;

	lctx = lc_ctx_new();
	pthread_cleanup_push((void (*)(void *))lc_ctx_free, lctx);

	/* build mdex */
	mdex = mdex_init(1);
	pthread_cleanup_push((void (*)(void *))mdex_free, mdex);
	entry.type = MDEX_PTR;
	entry.ptr.data = pkg->data;
	entry.ptr.size = pkg->len;
	rc = mdex_put(mdex, pkg->hash, HASHSIZE, &entry);
	test_assert(rc == 0, "entry added");

	/* share data */
	share = lc_share(lctx, mdex, 0, NULL, NULL, LC_SHARE_LOOPBACK);
	pthread_cleanup_push((void (*)(void *))lc_unshare, share);
	test_assert(share != NULL, "lc_share()");
	sem_post(&sem_send); /* tell recv thread we're ready */
	pause();

	pthread_cleanup_pop(1); /* lc_unshare(share) */
	pthread_cleanup_pop(1); /* mdex_free(mdex) */
	pthread_cleanup_pop(1); /* lc_ctx_free(lctx) */
	return NULL;
}
# endif /* HAVE_MLD */
#endif /* HAVE_RQ_OTI */

int main(void)
{
#if defined HAVE_RQ_OTI && defined HAVE_MLD
	pthread_t tid[2];
	struct pkg_s pkg_send = {0}, pkg_recv = {0};
	struct timespec timeout = {0};
	uint8_t *src, *dst;
	unsigned char hash[HASHSIZE];

	test_cap_require(CAP_NET_ADMIN);
	test_name("lc_share() / lc_unshare()");
	test_require_net(TEST_NET_BASIC);

	/* allocate some memory to work with */
	src = malloc(TEST_SIZE);
	test_assert(src != NULL, "allocate source");
	if (!src) return TEST_FAIL;
	dst = malloc(TEST_SIZE);
	memset(dst, 0, TEST_SIZE);
	test_assert(dst != NULL, "allocate destination");
	if (!dst) return TEST_FAIL;

	test_assert(!test_random_bytes(src, TEST_SIZE), "randomize src");
	test_assert(memcmp(dst, src, TEST_SIZE), "dst and src differ");
	hash_generic(hash, HASHSIZE, src, sizeof src);

	/* start send thread */
	sem_init(&sem_send, 0, 0);
	pkg_send.data = src;
	pkg_send.len = TEST_SIZE;
	pkg_send.hash = hash;
	pthread_create(&tid[TID_SEND], NULL, thread_send, &pkg_send);

	/* start recv thread */
	sem_init(&sem_recv, 0, 0);
	pkg_recv.data = dst;
	pkg_recv.len = TEST_SIZE;
	pkg_recv.hash = hash;
	pthread_create(&tid[TID_RECV], NULL, thread_recv, &pkg_recv);

	/* handle timeout */
	clock_gettime(CLOCK_REALTIME, &timeout);
	timeout.tv_sec += TIMEOUT_SECONDS;
	int ret;
	if ((ret = sem_timedwait(&sem_recv, &timeout)) == -1 && errno == ETIMEDOUT) {
		for (int i = 0; i < 2; i++) pthread_cancel(tid[i]);
	}
	pthread_cancel(tid[TID_SEND]);
	test_assert(ret == 0, "timeout waiting for recv thread");
	sem_destroy(&sem_recv);
	sem_destroy(&sem_send);

	/* stop threads */
	for (int i = 0; i < 2; i++) pthread_join(tid[i], NULL);

	/* check what we received */
	test_assert(!memcmp(dst, src, TEST_SIZE), "dst matches src");

	free(dst);
	free(src);

	return test_status;
#else
	return test_skip("MLD/LCRQ not enabled");
#endif
}
