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

#include "testnet.h"
#include "testdata.h"
#include <librecast/net.h>
#include <librecast/mdex.h>
#include <librecast/mtree.h>
#include <librecast/sync.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>

#define TIMEOUT_SECONDS 300

#ifdef HAVE_RQ_OTI
static sem_t sem_recv;

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

void *thread_recv(void *arg)
{
	struct pkg_s *pkg = (struct pkg_s *)arg;
	lc_ctx_t *lctx = lc_ctx_new();
	pthread_cleanup_push((void (*)(void *))lc_ctx_free, lctx);
	hash_hex_debug(stderr, pkg->hash, HASHSIZE);
	lc_sync(lctx, pkg->hash, pkg->data, pkg->len, NULL, NULL, NULL, 0);
	pthread_cleanup_pop(1); /* lc_ctx_free */
	sem_post(&sem_recv);
	return NULL;
}

void hash_testdata(unsigned char *hash, void *data, size_t sz)
{
	mtree_t tree = {0};
	mtree_init(&tree, sz);
	mtree_build(&tree, data, NULL);
	memcpy(hash, mtree_nnode(&tree, 0), HASHSIZE);
	mtree_free(&tree);
}

int runtest_size(size_t TEST_SIZE)
{
	unsigned char *src, *dst;
	unsigned char hash[HASHSIZE];
	unsigned char root[HASHSIZE];
	mdex_t *mdex;
	lc_ctx_t *lctx;
	lc_share_t *share;
	pthread_t tid_recv;
	struct pkg_s pkg_recv = {0};
	struct timespec timeout = {0};
	int rc;


	/* create random test data */
	src = malloc(TEST_SIZE);
	test_assert(src != NULL, "malloc(src) = %p, TEST_SIZE = %zu", (void *)src, TEST_SIZE);
	if (!src) return test_status;
	test_random_bytes(src, TEST_SIZE);
	hash_testdata(hash, src, TEST_SIZE);

	/* add test data to index */
	mdex = mdex_init(0);
	if (test_assert(mdex != NULL, "mdex_init(0) = %p", (void *)mdex)) goto free_src;
	rc = mdex_add(mdex, src, TEST_SIZE, NULL, 0);
	if (test_assert(rc == 0, "mdex_add() returned %i", rc)) goto err_mdex_free;

	size_t z = 0;
	hash_generic_key(root, HASHSIZE, hash, HASHSIZE, (unsigned char *)&z, sizeof(z));
	rc = mdex_get(mdex, root, sizeof root, NULL);
	if (test_assert(rc == 0, "root hash in mdex")) goto err_mdex_free;

	/* share the mdex */
	lctx = lc_ctx_new();
	if (test_assert(lctx != NULL, "lc_ctx_new() = %p", (void *)lctx)) goto err_mdex_free;
	share = lc_share(lctx, mdex, 0, NULL, NULL, LC_SHARE_LOOPBACK);
	if (test_assert(share != NULL, "lc_share()")) goto err_ctx_free;

	/* allocate dst */
	dst = malloc(TEST_SIZE);
	test_assert(dst != NULL, "malloc(dst) = %p, TEST_SIZE = %zu", (void *)dst, TEST_SIZE);
	if (!dst) goto err_unshare;
	memset(dst, 0, TEST_SIZE);

	rc = sem_init(&sem_recv, 0, 0);
	if (test_assert(rc == 0, "sem_init(sem_recv) returned %i", rc)) goto err_free_dst;
	pkg_recv.data = dst;
	pkg_recv.len = TEST_SIZE;
	pkg_recv.hash = root;
	rc = pthread_create(&tid_recv, NULL, thread_recv, &pkg_recv);
	if (test_assert(rc == 0, "pthread_create returned %i", rc)) goto err_sem_destroy;

	rc = clock_gettime(CLOCK_REALTIME, &timeout);
	if (test_assert(rc == 0, "clock_gettime returned %i", rc)) goto err_sem_destroy;
	timeout.tv_sec += TIMEOUT_SECONDS;
	if ((rc = sem_timedwait(&sem_recv, &timeout)) == -1 && errno == ETIMEDOUT) {
		pthread_cancel(tid_recv);
	}
	test_assert(rc == 0, "timeout waiting for recv thread");
	pthread_join(tid_recv, NULL);

	if (rc == 0) {
		test_assert(!memcmp(dst, src, TEST_SIZE), "src matches dst");
	}

	/* clean up */
err_sem_destroy:
	sem_destroy(&sem_recv);
err_free_dst:
	free(dst);
err_unshare:
	lc_unshare(share);
err_ctx_free:
	lc_ctx_free(lctx);
err_mdex_free:
	mdex_free(mdex);
free_src:
	free(src);
	return test_status;
}
#endif /* HAVE_RQ_OTI */

int main(void)
{
#if defined HAVE_MLD && defined HAVE_RQ_OTI
	test_cap_require(CAP_NET_ADMIN);
	test_name("lc_sync()");
	test_require_net(TEST_NET_BASIC);
	runtest_size(1024 * 31 + 5);   /* single node */
	runtest_size(1024 * 100 + 5);
	runtest_size(1024 * 1024 + 5); /* larger tree with padding chunks */
	runtest_size(1024 * 1024 * 32 + 5); /* multi-part tree */
	return test_status;
#else
	return test_skip("lc_sync()");
#endif
}

