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

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

#define MAXFILESZ 1048576
#define MAXFILES 10
#define MAXDIRS 5
#define DEPTH 3

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;

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_filemeta(mdex_entry_t *entry)
{
	struct stat sb;
	int rc;

	if (!test_assert(entry->file.file.name != NULL, "file name buffer allocated"))
		return -1;
	if (!test_assert(entry->file.file.sb != NULL, "file stat buffer allocated"))
		return -1;

	rc = stat(entry->file.file.name, &sb);
	test_assert(rc == 0, "stat '%s'", entry->file.file.name);
	/* atime is changed by the call to stat(), so forcibly set it before
	 * comparing the rest of the stat buffer */
	sb.st_atim.tv_sec = entry->file.file.sb->st_atim.tv_sec;
	sb.st_atim.tv_nsec = entry->file.file.sb->st_atim.tv_nsec;
	test_assert(!memcmp(&sb, entry->file.file.sb, sizeof sb), "stat buffer matches");
	return test_status;
}

static int verify_file(mdex_t *mdex, mdex_entry_t *entry)
{
	net_tree_t *data;
	mtree_t mtree;
	unsigned char *hash;
	size_t min, max;
	int rc;

	/* files are stored as pointers to their mtree with an OTI flag */
	if (!test_assert(entry->type == (MDEX_PTR | MDEX_OTI), "entry is file: 0x%08x", entry->type))
		return -1;

	/* verify tree */
	if (!test_assert(entry->ptr.data != NULL, "file mtree data allocated")) return -1;
	data = (net_tree_t *)entry->ptr.data;
	data->size = be64toh(data->size);
	data->namesz = ntohs(data->namesz);

	rc = mtree_init(&mtree, data->size);
	if (!test_assert(rc == 0, "mtree_init()")) return -1;
	test_log("mtree.nodes=%zu, HASHSIZE=%zu\n", mtree.nodes, HASHSIZE);
	test_log("tree has size %" PRIu64 ", hashsz=%zu\n", data->size, (size_t)mtree.nodes * (size_t)HASHSIZE);
	assert(data->size < UINT32_MAX); /* none of our trees are that large */
	memcpy(mtree.tree, data->tree + data->namesz, (size_t)mtree.nodes * (size_t)HASHSIZE);
	rc = mtree_verify(&mtree);
	if (!test_assert(rc == 0, "mtree_verify()")) goto err_mtree_free;

	/* check data chunks are indexed */
	if (!test_assert(mtree.len > 0, "tree has len %zu", mtree.len))
		goto err_mtree_free;
	if (!test_assert(mtree.base > 0, "tree has base %zu", mtree.base))
		goto err_mtree_free;
	if (!test_assert(mtree.chunks > 0, "tree has chunks %zu", mtree.chunks))
		goto err_mtree_free;
	min = mtree_subtree_data_min(mtree.base, 0);
	max = min + mtree.chunks - 1;
	test_log("testing index for chunks %zu to %zu\n", min, max);
	for (size_t z = min; z <= max; z++) {
		hash = mtree_nnode(&mtree, z);
		rc = mdex_get(mdex, hash, HASHSIZE, entry);
		if (!test_assert(rc == 0, "mdex_get() found chunk %zu", z))
			goto err_mtree_free;
		if (z == 0 && !verify_filemeta(entry)) break;
	}
err_mtree_free:
	mtree_free(&mtree);
	return test_status;
}

static int verify_dir(mdex_t *mdex, mdex_entry_t *entry)
{
	struct stat sb;
	mdex_entry_t mentry;
	mdex_hash_t *dir = entry->file.file.dir;
	int rc;

	if (!test_assert(entry->type & MDEX_DIR, "entry is dir:  0x%08x", entry->type))
		return -1;
	if (!test_assert(entry->file.file.name != NULL, "directory name buffer allocated"))
		return -1;
	if (!test_assert(entry->file.file.sb != NULL, "directory stat buffer allocated"))
		return -1;

	rc = stat(entry->file.file.name, &sb);
	test_assert(rc == 0, "stat '%s'", entry->file.file.name);
	/* atime is changed by the call to stat(), so forcibly set it before
	 * comparing the rest of the stat buffer */
	sb.st_atim.tv_sec = entry->file.file.sb->st_atim.tv_sec;
	sb.st_atim.tv_nsec = entry->file.file.sb->st_atim.tv_nsec;
	test_assert(!memcmp(&sb, entry->file.file.sb, sizeof sb), "stat buffer matches");

	/* fetch each directory entry hash from mdex,
	 * stat each file and check attributes */
	for (size_t i = 0; i < entry->file.size; i++) {
		rc = mdex_get(mdex, dir[i], sizeof(mdex_hash_t), &mentry);
		if (!test_assert(rc == 0, "checking dir entry %i: mdex_get() returned %i", i, rc))
			return -1;
		switch (mentry.type & 0xffff) {
			case MDEX_DIR:
				if (verify_dir(mdex, &mentry)) return -1;
				break;
			case MDEX_PTR:
				if (verify_file(mdex, &mentry)) return -1;
				break;
			default:
				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;
}

int main(int argc, char *argv[])
{
	(void)argc;
	mdex_t *mdex;
	char *src = NULL, *dst = NULL;
	int rc;

	test_name("mdex_addfile() - recursive file indexing");

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

	/* recursively verify tree */
	test_log("recursively verify tree\n");
	mdex_entry_t entry;
	rc = mdex_getalias(mdex, src, &entry);
	if (!test_assert(rc == 0, "mdex_getalias() - found base tree dir '%s'", src)) goto err_mdex_free;
	rc = verify_dir(mdex, &entry);
	test_assert(rc == 0, "directory tree verified");

err_mdex_free:
	mdex_free(mdex);
err_free_srclist:
	free_srclist(srclist_head);
err_free_src_dst:
	free(src); free(dst);
	return test_status;
}
