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

#include "../src/config.h"
#define _XOPEN_SOURCE 700 /* for nftw() */
#include <ftw.h>

#include "test.h"
#include "testdata.h"

#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>

typedef struct listentry_s listentry_t;
struct listentry_s {
	char *fpath;
	struct stat sb;
	int type;
	listentry_t *next;
	listentry_t *prev;
};

static listentry_t *srclist_head, *dstlist_head;
static listentry_t *srclist_last, *dstlist_last;
static listentry_t **head, **last;

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

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

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 */
int statcmp_sb(struct stat *ssb, struct stat *dsb, const char *dst)
{
	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 test_status;
}

/* check mode, owner, group, times */
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;
	return statcmp_sb(&ssb, &dsb, dst);
}

listentry_t *listfind(const char *fpath, listentry_t *head)
{
	for (listentry_t *e = head; e; e = e->next) {
		if (!strcmp(e->fpath, fpath)) return e;
	}
	return NULL;
}

void test_verify_dirs(const char *src, const char *dst)
{
	struct dirent **namelist;
	char *rsrc, *rdst, *owd;
	int n = 0, rc;
	statcmp(src, dst);

	owd = getcwd(NULL, 0);
	rsrc = realpath(src, NULL);
	rdst = realpath(dst, NULL);

	test_log("src: %s\n", src);
	test_log("dst: %s\n", dst);
	test_log("rsrc: %s\n", rsrc);
	test_log("rdst: %s\n", rdst);

	if (chdir(rsrc) == -1) goto err_free_srclist;
	head = &srclist_head; last = &srclist_last;
	rc = nftw(".", &build_list, 32, 0);
	if (!test_assert(rc == 0, "nftw() returned %i", rc)) goto err_free_srclist;
	if (chdir(rdst) == -1) goto err_free_srclist;
	head = &dstlist_head; last = &dstlist_last;
	rc = nftw(".", &build_list, 32, 0);
	if (!test_assert(rc == 0, "nftw() returned %i", rc)) goto err_free_dstlist;

	test_log("--- source ---\n");
	n = 0;
	for (listentry_t *e = srclist_head; e; e = e->next) {
		rc = scandir(e->fpath, &namelist, scandirfilter, alphasort);
		if (rc == -1) {
			test_log("%i: (f) %s\n", ++n, e->fpath);
		}
		else {
			test_log("%i: (d) %s (%i entries)\n", ++n, e->fpath, rc);
			while (rc--) { free(namelist[rc]); }
			free(namelist);
		}
	}
	test_log("%i source entries\n", n);

	test_log("--- dest ---\n");
	n = 0;
	for (listentry_t *e = dstlist_head; e; e = e->next) {
		rc = scandir(e->fpath, &namelist, scandirfilter, alphasort);
		if (rc == -1) {
			test_log("%i: (f) %s\n", ++n, e->fpath);
		}
		else {
			test_log("%i: (d) %s (%i entries)\n", ++n, e->fpath, rc);
			while (rc--) { free(namelist[rc]); }
			free(namelist);
		}
	}

	test_log("%i destination entries\n", n);
	n = 0;
	for (listentry_t *se = srclist_head, *de; se; se = se->next) {
		de = listfind(se->fpath, dstlist_head);
		if (!test_assert_q(de != NULL, "destination tree != NULL")) break;
		if (!test_assert_q(de->fpath != NULL, "destination fpath != NULL")) break;
		fprintf(stderr, "se: %s\n", se->fpath);
		fprintf(stderr, "de: %s\n", de->fpath);
		if (n && !test_expect(basename(se->fpath), basename(de->fpath))) break;
		test_assert(statcmp_sb(&se->sb, &de->sb, de->fpath) == 0, "statcmp");
		n++;
	}
	test_log("%i entries verified\n", n);
	test_assert(test_status == TEST_OK, "source and destination trees match");
	if (chdir(owd) == -1) perror("chdir");

err_free_dstlist:
	free_list(dstlist_head);
err_free_srclist:
	free_list(srclist_head);
	free(rsrc); free(rdst);
}
