/* 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 TEST_CHECK 0
#define TIMEOUT_SECONDS 5
#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");

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-0065.src.tmp.XXXXXX";
	char lnk[] = "0000-0065.src.tmp.XXXXXX";
	char dst[] = "0000-0065.dst.tmp.XXXXXX";
	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};
	struct stat dsb = {0};
	char buf[32];
	ssize_t byt;
	size_t off;
	int fds, rc;

	test_cap_require(CAP_NET_ADMIN);
	test_name("lc_syncfile() (symlink)");
	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");

	/* create symlink (use mkstemp to create name so we get the usual format) */
	rc = mkstemp(lnk); /* generate temp filename */
	if (!test_assert(rc != -1, "mkstemp: %s", lnk)) { perror("mkstemp"); goto exit_0; }
	rc = unlink(lnk); /* delete the file - we don't need it */
	if (!test_assert(rc == 0, "unlink: %s", lnk)) goto exit_0;
	rc = symlink(src, lnk); /* create symlink to src */
	if (!test_assert(rc == 0, "symlink")) goto exit_0;

	/* set destination filename with same last 6 bytes as src */
	off = strlen(src) - 6;
	memcpy(dst + off, src + off, 6);

#if TEST_CHECK
	/* test the test */
	char *cmd;
	rc = snprintf(NULL, 0, "ln -s %s %s", src, dst);
	assert(rc != -1);
	rc++;
	cmd = malloc(rc);
	snprintf(cmd, rc, "ln -s %s %s", src, dst);
	rc = system(cmd);
	test_assert(rc == 0, "system(%s)", cmd);
#else
	/* add test data to index */
	mdex = mdex_init(0);
	rc = mdex_addfile(mdex, lnk, NULL, 0);
	test_assert(rc == 0, "mdex_addfile() returned %i", rc);

	/* ensure root hash is in mdex */
	if (!test_assert(lstat(lnk, &sb) != -1, "stat symlink")) goto exit_0;
	mdex_tree_hash_sb(root, sizeof root, NULL, 0, &sb, lnk);
	rc = mdex_get(mdex, root, sizeof root, NULL);
	test_assert(rc == 0, "root hash in mdex");

	/* 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);

#endif
	rc = lstat(dst, &dsb);
	test_assert(rc == 0, "lstat() %s", dst);
	if (!test_assert(S_ISLNK(dsb.st_mode), "S_ISLNK()")) goto exit_0;
	byt = readlink(dst, buf, sizeof buf);
	if (!test_assert((size_t)byt == strlen(src), "readlink returned correct length"))
		goto exit_0;
	test_log("buf (link target): '%.*s'\n", (int)byt, buf);
	test_assert(!strncmp(src, buf, byt), "link target points to src");

#if !TEST_CHECK
	/* clean up */
	sem_destroy(&sem_recv);
	lc_unshare(share);
	lc_ctx_free(lctx);
	mdex_free(mdex);
	close(fds);
#endif
	unlink(dst);
	unlink(src);
exit_0:
	return test_status;
#else
	return test_skip("lc_syncfile()");
#endif
}
