/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2023 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 100
#define TEST_SIZE 1024 * 1024 * 1024 + 5
/* nice powers of two hide bugs - ensure test size is odd */
static_assert(TEST_SIZE % 2, "TEST_SIZE must be an odd number");

static sem_t sem_recv;

struct pkg_s {
	char *dst;
	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_syncfile(lctx, pkg->hash, pkg->dst, NULL, NULL, NULL, 0);
	pthread_cleanup_pop(1); /* lc_ctx_free */
	sem_post(&sem_recv);
	return NULL;
}

int main(void)
{
#if HAVE_MLD
	struct stat sb;
	char src[] = "0000-0062.src.tmp.XXXXXX";
	char dst[] = "0000-0062.dst.tmp.XXXXXX";
	unsigned char *testdata;
	unsigned char root[HASHSIZE];
	mdex_t *mdex;
	mtree_t mtree = {0};
	lc_ctx_t *lctx;
	lc_share_t *share;
	pthread_t tid_recv;
	struct pkg_s pkg_recv = {0};
	struct timespec timeout = {0};
	size_t off;
	int fds, rc;

	test_cap_require(CAP_NET_ADMIN);
	test_name("lc_syncfile() - large file (1GiB)");
	test_require_net(TEST_NET_BASIC);

	/* create test file */
	fds = test_data_file(src, TEST_SIZE, TEST_TMP | TEST_RND);
	test_assert(fds != -1, "test_data_file()");
	if (fds == -1) goto exit_0;
	test_assert(test_data_size(src, TEST_SIZE) == 0, "source exists and is correct size");
	test_assert(stat(src, &sb) != -1, "stat source file");

	/* set destination filename */
	off = strlen(src) - 6;
	memcpy(dst + off, src + off, 6);

	/* add test data to index */
	mdex = mdex_init(0);
	rc = mdex_addfile(mdex, src, NULL, 0);
	test_assert(rc == 0, "mdex_addfile() returned %i", rc);

	/* map file, build tree */
	size_t sz = 0;
	testdata = lc_mmapfile(src, &sz, PROT_READ, MAP_PRIVATE, 0, &sb);
	rc = mtree_init(&mtree, TEST_SIZE);
	test_assert(rc == 0, "mtree_init() returned %i", rc);
	rc = mtree_build(&mtree, testdata, NULL);
	test_assert(rc == 0, "mtree_build() returned %i", rc);

	/* ensure root hash is in mdex */
	mdex_tree_hash_sb(root, sizeof root, &mtree, 0, &sb, src);
	rc = mdex_get(mdex, root, sizeof root, NULL);
	if (!test_assert(rc == 0, "root hash in mdex")) goto exit_0;

	/* share the mdex */
	lctx = lc_ctx_new();
	share = lc_share(lctx, mdex, 0, NULL, NULL, LC_SHARE_LOOPBACK);
	test_assert(share != NULL, "lc_share()");

	/* sync files */
	sem_init(&sem_recv, 0, 0);
	pkg_recv.dst = dst;
	pkg_recv.hash = root;
	pthread_create(&tid_recv, NULL, thread_recv, &pkg_recv);

	clock_gettime(CLOCK_REALTIME, &timeout);
	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) {
		/* verify data matches */
		test_assert(test_data_size(dst, sz) == 0, "destination exists and is correct size");
		test_assert(test_file_match(dst, src) == 0, "source and destination data matches");
	}

	/* clean up */
	sem_destroy(&sem_recv);
	lc_unshare(share);
	lc_ctx_free(lctx);
	munmap(testdata, sz);
	mtree_free(&mtree);
	mdex_free(mdex);
	close(fds);
	unlink(dst);
	unlink(src);
exit_0:
	return test_status;
#else
	return test_skip("lc_syncfile()");
#endif
}
