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

#define _DEFAULT_SOURCE   /* be64toh */
#define _XOPEN_SOURCE 700 /* for nftw() */
#include "test.h"
#include "testdata.h"
#include "testnet.h"
#include <sys/types.h>
#include <errno.h>
#include <dirent.h>
#include <ftw.h>
#include <librecast_pvt.h>
#include <librecast/mdex.h>
#include <librecast/mtree.h>
#include <librecast/net.h>
#include <librecast/sync.h>
#include <unistd.h>

#if HAVE_RQ_OTI
#define MAXFILESZ 1048576
#define MAXFILES 2
#define MAXDIRS 2
#define DEPTH 2
#define TIMEOUT_SECONDS 2

enum {
	TID_SEND,
	TID_RECV
};

static sem_t sem_recv;

struct pkg_s {
	net_tree_t **data;
	size_t len;
	unsigned char *hash;
};

typedef struct srclistentry_s srclistentry_t;
struct srclistentry_s {
	char *fpath;
	struct stat sb;
	int type;
	srclistentry_t *next;
};

static srclistentry_t *srclist_head;
static srclistentry_t *srclist_last;

void *thread_recv(void *arg)
{
	struct pkg_s *pkg = (struct pkg_s *)arg;
	lc_ctx_t *lctx;
	ssize_t rc;
	test_log("%s starting\n", __func__);
	lctx = lc_ctx_new();
	if (!test_assert(lctx != NULL, "%s() lc_ctx_new", __func__)) goto err_sem_post;
	pthread_cleanup_push((void (*)(void *))lc_ctx_free, lctx);
	hash_hex_debug(stderr, pkg->hash, HASHSIZE);
	rc = lc_recvdir(lctx, pkg->hash, pkg->data, NULL, NULL, 0);
	test_assert(rc > 0, "lc_recvdir() returned %zi", rc);
	pthread_cleanup_pop(1); /* lc_ctx_free */
err_sem_post:
	sem_post(&sem_recv);
	return NULL;
}

void *thread_send(void *arg)
{
	struct pkg_s *pkg = (struct pkg_s *)arg;
	ssize_t rc;
	lc_ctx_t *lctx;
	test_log("%s starting\n", __func__);
	lctx = lc_ctx_new();
	if (!test_assert(lctx != NULL, "%s() lc_ctx_new", __func__)) return NULL;
	pthread_cleanup_push((void (*)(void *))lc_ctx_free, lctx);
	rc = lc_senddir(lctx, pkg->hash, *(pkg->data), NULL, NULL, NET_LOOPBACK);
	test_assert(rc > 0, "lc_senddir() returned %zi", rc);
	pthread_cleanup_pop(1); /* lc_ctx_free */
	return NULL;
}

static void free_srclist(srclistentry_t *list)
{
	srclistentry_t *tmp;
	while (list) {
		free(list->fpath);
		tmp = list;
		list = list->next;
		free(tmp);
	}
}

static int build_srclist(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf)
{
	(void)ftwbuf;
	srclistentry_t *entry = calloc(1, sizeof(srclistentry_t));
	entry->sb = *sb;
	entry->type = typeflag;
	entry->fpath = strdup(fpath);
	if (!srclist_head) {
		srclist_head = entry;
		srclist_last = entry;
	}
	else {
		srclist_last->next = entry;
		srclist_last = entry;
	}
	return 0;
}

static int verify_dir(net_tree_t *sdir, net_tree_t *ddir)
{
	if (!test_assert(sdir != NULL, "sdir != NULL")) return -1;
	if (!test_assert(ddir != NULL, "ddir != NULL")) return -1;
	if (!test_assert(sdir->size > 0, "directory size > 0")) return -1;
	sdir->size = htobe64(sdir->size);
	if (!test_assert(sdir->size == ddir->size, "directory sizes match (%zu == %zu)",
			sdir->size, ddir->size)) return -1;
	if (!test_assert(!memcmp(sdir->tree, ddir->tree, sdir->size), "dir hashes match"))
		return -1;
	if (!test_assert(sdir->atime_s == ddir->atime_s, "atime (sec) matches"))
		return -1;
	if (!test_assert(sdir->atime_n == ddir->atime_n, "atime (nsec) matches"))
		return -1;
	if (!test_assert(sdir->mtime_s == ddir->mtime_s, "mtime (sec) matches"))
		return -1;
	if (!test_assert(sdir->mtime_n == ddir->mtime_n, "mtime (nsec) matches"))
		return -1;
	if (!test_assert(sdir->mode == ddir->mode, "mode matches")) return -1;
	if (!test_assert(sdir->uid == ddir->uid, "uid matches")) return -1;
	if (!test_assert(sdir->gid == ddir->gid, "gid matches")) return -1;
	return 0;
}

static int verify_entry(mdex_t *mdex, srclistentry_t *src)
{
	mdex_entry_t entry;
	int rc = mdex_getalias(mdex, src->fpath, &entry);
	test_assert(rc == 0, "%s: '%s'", __func__, src->fpath);
	return rc;
}
#endif /* HAVE_RQ_OTI */

int main(int argc, char *argv[])
{
	(void)argc, (void)argv;
	char name[] = "lc_senddir()/lc_recvdir()";
#if HAVE_RQ_OTI
	pthread_t tid[2];
	struct pkg_s pkg_send = {0}, pkg_recv = {0};
	struct timespec timeout = {0};
	mdex_t *mdex;
	char *src = NULL, *dst = NULL;
	net_tree_t *sdir, *ddir;
	int rc;

	test_name(name);
	test_require_net(TEST_NET_BASIC);

	/* create source directory tree and files */
	rc = test_createtestdirs(basename(argv[0]), &src, &dst);
	if (!test_assert(rc == 0, "test_createtestdirs()")) return test_status;
	rc = test_createtesttree(src, MAXFILESZ, MAXFILES, MAXDIRS, DEPTH, 0);
	if (!test_assert(rc == 0, "test_createtesttree()")) goto err_free_src_dst;

	/* create a list to test against */
	rc = nftw(src, &build_srclist, 32, 0);
	if (!test_assert(rc == 0, "nftw() returned %i", rc)) goto err_free_src_dst;

	/* index source tree */
	mdex = mdex_init(512);
	if (!test_assert(mdex != NULL, "mdex_init()")) goto err_free_srclist;
	rc = mdex_addfile(mdex, src, NULL, MDEX_RECURSE);
	if (!test_assert(rc == 0, "mdex_addfile() returned %i", rc)) goto err_mdex_free;

	/* verify files have been added */
	for (srclistentry_t *e = srclist_head; e; e = e->next) {
		if (verify_entry(mdex, e)) break;
	}

	/* sync */

	/* prepare directory for sending */
	mdex_entry_t entry;
	sdir = NULL; ddir = NULL;
	rc = mdex_getalias(mdex, src, &entry);
	if (!test_assert(rc == 0, "mdex_getalias() - found base tree dir '%s'", src)) goto err_mdex_free;

	size_t treesz = sizeof(hash_t) * entry.file.size;
	size_t sz = sizeof(struct net_tree_s) + treesz;
	sdir = malloc(sz);
	if (!test_assert(sdir != NULL, "malloc(sdir)")) goto err_mdex_free;
	memset(sdir, 0, sz);
	sdir->size = entry.file.size * HASHSIZE;
	memcpy(sdir->tree, entry.file.file.dir, treesz);
	sdir->mode = entry.file.file.sb->st_mode;
	sdir->uid = entry.file.file.sb->st_uid;
	sdir->gid = entry.file.file.sb->st_gid;
	sdir->atime_s = entry.file.file.sb->st_atim.tv_sec;
	sdir->atime_n = entry.file.file.sb->st_atim.tv_nsec;
	sdir->mtime_s = entry.file.file.sb->st_mtim.tv_sec;
	sdir->mtime_n = entry.file.file.sb->st_mtim.tv_nsec;
	test_assert(sdir->uid != 0, "uid set = %i", sdir->uid);
	test_assert(sdir->gid != 0, "gid set = %i", sdir->gid);
	test_assert(sdir->mode != 0, "mode set = %i", sdir->mode);
	test_assert(sdir->atime_s != 0, "atime.tv_sec set = %i", sdir->atime_s);
	test_assert(sdir->mtime_s != 0, "mtime.tv_sec set = %i", sdir->mtime_s);

	unsigned char dirhash[HASHSIZE];
	hash_generic(dirhash, sizeof dirhash, (unsigned char *)src, strlen(src));
	test_log("dirhash: ");
	hash_hex_debug(stderr, dirhash, HASHSIZE);

	/* start send thread */
	pkg_send.data = &sdir;
	pkg_send.hash = dirhash;
	pthread_create(&tid[TID_SEND], NULL, thread_send, &pkg_send);

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

	/* handle timeout */
	clock_gettime(CLOCK_REALTIME, &timeout);
	timeout.tv_sec += TIMEOUT_SECONDS;
	if ((rc = 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(rc == 0, "timeout waiting for recv thread");
	sem_destroy(&sem_recv);

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

	/* verify received directory */
	rc = verify_dir(sdir, ddir);
	test_assert(rc == 0, "directory tree verified");

	free(ddir);
	free(sdir);
err_mdex_free:
	mdex_free(mdex);
err_free_srclist:
	free_srclist(srclist_head);
err_free_src_dst:
	free(src); free(dst);
	return test_status;
#else
	return test_skip(name);
#endif /* HAVE_RQ_OTI */
}
