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

#define _DEFAULT_SOURCE   /* be64toh */
#define _XOPEN_SOURCE 700 /* for nftw() */
#define _NETBSD_SOURCE    /* required  for scandir() on NetBSD */
#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 3

/*
 * test 0116
 *
 * create a tree of files and directories and use lc_syncfile() to sync
 * the top-level directory non-recursively into the destination.
 *
 * Compare the resulting directories and ensure they match.
 */

static sem_t sem_recv;

struct pkg_s {
	char *src;
	char *dst;
};

void *thread_recv(void *arg)
{
	struct pkg_s *pkg = (struct pkg_s *)arg;
	unsigned char hash[HASHSIZE];
	const int flags =
		SYNC_SUBDIR | SYNC_ATIME | SYNC_MTIME | SYNC_OWNER | SYNC_GROUP | SYNC_MODE;
	ssize_t rc;
	mdex_aliashash(pkg->src, hash, sizeof hash);
	fprintf(stderr, "%s() ", __func__);
	hash_hex_debug(stderr, hash, sizeof hash);
	lc_ctx_t *lctx = lc_ctx_new();
	pthread_cleanup_push((void (*)(void *))lc_ctx_free, lctx);
	rc = lc_syncfile(lctx, hash, pkg->dst, NULL, NULL, NULL, flags);
	test_assert(rc > 0, "lc_syncfile returned %zi", rc);
	pthread_cleanup_pop(1); /* lc_ctx_free */
	sem_post(&sem_recv);
	return NULL;
}

static int scandirfilter(const struct dirent *dir)
{
	/* filter current and parent directories */
	return !(!strcmp(dir->d_name, ".") || !strcmp(dir->d_name, ".."));
}

/* check mode, owner, group, times */
static int statcmp(const char *src, const char *dst)
{
	struct stat ssb, dsb;
	int rc;
	rc = stat(src, &ssb);
	if (!test_assert(rc == 0, "stat '%s'", src)) return -1;
	rc = stat(dst, &dsb);
	if (!test_assert(rc == 0, "stat '%s'", dst)) return -1;
	test_assert(ssb.st_mode == dsb.st_mode, "st_mode: '%s' %o %o", dst, ssb.st_mode, dsb.st_mode);
	test_assert(ssb.st_uid == dsb.st_uid, "st_uid:  '%s'", dst);
	test_assert(ssb.st_gid == dsb.st_gid, "st_gid:  '%s'", dst);
	test_assert(ssb.st_size == dsb.st_size, "st_size: '%s'", dst);
#ifndef HAVE_UTIMENSAT
	/* without utimensat(), we only have microsecond precision */
	ssb.st_mtim.tv_nsec /= 1000; ssb.st_mtim.tv_nsec *= 1000;
	ssb.st_atim.tv_nsec /= 1000; ssb.st_atim.tv_nsec *= 1000;
#endif
	/* No point testing atime unless the filesystem is mounted noatime */
	/* test_assert(ssb.st_atim.tv_nsec == dsb.st_atim.tv_nsec, "st_atim: '%s'", dst); */
	test_assert(ssb.st_mtim.tv_nsec == dsb.st_mtim.tv_nsec, "st_mtim: '%s'", dst);
	return 0;
}
#endif /* HAVE_RQ_OTI */

int main(int argc, char *argv[])
{
	(void)argc, (void)argv;
	char name[] = "lc_syncfile() - sync directory (not recursive)";
#if HAVE_RQ_OTI
	struct dirent **namelist;
	lc_ctx_t *lctx;
	mdex_t *mdex;
	lc_share_t *share;
	pthread_t tid_recv;
	struct pkg_s pkg_recv = {0};
	struct timespec timeout = {0};
	char *src = NULL, *dst = NULL;
	int rc;

	test_cap_require(CAP_NET_ADMIN);
	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;
	test_random_meta(src, TEST_OWN|TEST_MOD);
	rc = test_createtesttree(src, MAXFILESZ, MAXFILES, MAXDIRS, DEPTH, TEST_OWN|TEST_MOD);
	if (!test_assert(rc == 0, "test_createtesttree()")) goto err_free_src_dst;

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

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

	/* start receive thread, sync files */
	test_log("syncing '%s' => '%s'\n", src, dst);
	rc = sem_init(&sem_recv, 0, 0);
	if (!test_assert(rc == 0, "sem_init(sem_recv)")) goto err_unshare;
	pkg_recv.src = src;
	pkg_recv.dst = dst;
	rc = pthread_create(&tid_recv, NULL, thread_recv, &pkg_recv);
	if (!test_assert(rc == 0, "create recv thread")) goto err_sem_destroy;

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

	/* verify src and dst match */

	/* src has no trailing slash, so the directory must be created inside dst */
	struct stat ssb, dsb;
	char *destdir;
	int len;
	len = snprintf(NULL, 0, "%s/%s", dst, src);
	destdir = malloc(len + 1);
	if (!test_assert(destdir != NULL, "malloc destdir")) goto err_sem_destroy;
	if (!test_assert(len == snprintf(destdir, len + 1, "%s/%s", dst, src), "build destdir string"))
		goto err_free_destdir;
	rc = stat(destdir, &dsb);
	if (!test_assert(rc == 0, "%s exists", destdir)) goto err_free_destdir;
	rc = stat(src, &ssb);
	if (!test_assert(rc == 0, "stat src")) goto err_free_destdir;

	/* check mode, owner, group, times */
	test_assert(statcmp(src, destdir) == 0, "statcmp");

	/* we didn't ask for recursion, so make sure directory is empty */
	rc = scandir(destdir, &namelist, scandirfilter, alphasort);
	test_assert(rc == 0, "destination directory exists and is empty");

err_free_destdir:
	free(destdir);
err_sem_destroy:
	sem_destroy(&sem_recv);
err_unshare:
	lc_unshare(share);
err_free_lctx:
	lc_ctx_free(lctx);
err_mdex_free:
	mdex_free(mdex);
err_free_src_dst:
	free(src); free(dst);
	return test_status;
#else
	return test_skip(name);
#endif /* HAVE_RQ_OTI */
}
