/*
 *
 *  Embedded Linux library
 *
 *  Copyright (C) 2017  Intel Corporation. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <assert.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <ell/ell.h>

enum test_op {
	OP_NULL,
	OP_OPEN,
	OP_CREAT,
	OP_UNLINK,
	OP_TRUNCATE,
	OP_RENAME,
};

struct test_result {
	const char *dir;
	const char *file;
	bool ignore;
	enum l_dir_watch_event event;
};

typedef struct test_result test_result_t;

#define MAX_RESULTS 2

struct test_data {
	enum test_op op;
	const char *orig_dir;
	const char *orig_file;
	const char *dest_dir;
	const char *dest_file;
	unsigned int length;
	const char *content;
	const struct test_result *results[MAX_RESULTS + 1];
};

struct test_entry {
	const char *name;
	const struct test_data *data;
	char **test_dirs;
	char **test_files;
	char **watch_dirs;
	unsigned int idx;
	unsigned int res_idx;
	struct l_queue *watch_list;
	bool failed;
};

struct test_watch {
	char *pathname;
	struct test_entry *entry;
};

static struct l_queue *test_queue;

#define TEST_FULL(_dir, _file, _op) \
			.orig_dir = _dir, .orig_file = _file, .op = _op

#define test_open(_dir, _file, _length) \
			TEST_FULL(_dir, _file, OP_OPEN), .length = _length
#define test_creat(_dir, _file, _content) \
			TEST_FULL(_dir, _file, OP_CREAT), .content = _content
#define test_unlink(_dir, _file) \
			TEST_FULL(_dir, _file, OP_UNLINK)
#define test_truncate(_dir, _file, _length) \
			TEST_FULL(_dir, _file, OP_TRUNCATE), .length = _length
#define test_rename(_olddir, _oldfile, _newdir, _newfile) \
			TEST_FULL(_olddir, _oldfile, OP_RENAME), \
				.dest_dir = _newdir, .dest_file = _newfile

#define RESULT_PTR(_dir, _file, _ignore, _event) \
			(&(test_result_t) { _dir, _file, _ignore, _event })

#define results(args...)	.results = { args, RESULT_PTR(NULL, NULL, true, 0) }

#define created(_dir, _file)	RESULT_PTR(_dir, _file, false, L_DIR_WATCH_EVENT_CREATED)
#define removed(_dir, _file)	RESULT_PTR(_dir, _file, false, L_DIR_WATCH_EVENT_REMOVED)
#define modified(_dir, _file)	RESULT_PTR(_dir, _file, false, L_DIR_WATCH_EVENT_MODIFIED)
#define accessed(_dir, _file)	RESULT_PTR(_dir, _file, false, L_DIR_WATCH_EVENT_ACCESSED)

#define result_ignore()			.results = { RESULT_PTR(NULL, NULL, true, 0) }
#define result_created(_dir, _file)	results(created(_dir, _file))
#define result_removed(_dir, _file)	results(removed(_dir, _file))
#define result_modified(_dir, _file)	results(modified(_dir, _file))
#define result_accessed(_dir, _file)	results(accessed(_dir, _file))

static void start_single_test(void *user_data);

static void event_callback(const char *pathname, enum l_dir_watch_event event,
								void *user_data)
{
	struct test_watch *watch_data = user_data;
	struct test_entry *entry = watch_data->entry;
	const struct test_data *data = &entry->data[entry->idx];
	const struct test_result *result = data->results[entry->res_idx];
	const char *str;

	switch (event) {
	case L_DIR_WATCH_EVENT_CREATED:
		str = "CREATED";
		break;
	case L_DIR_WATCH_EVENT_REMOVED:
		str = "REMOVED";
		break;
	case L_DIR_WATCH_EVENT_MODIFIED:
		str = "MODIFIED";
		break;
	case L_DIR_WATCH_EVENT_ACCESSED:
		str = "ACCESSED";
		break;
	default:
		str = "UNKNOWN";
		break;
	}

	l_debug("l_dir_watch event:%s pathname:%s [%s]", str, pathname,
							watch_data->pathname);

	/* This will result in waiting for the timeout */
	if (result->ignore)
		return;

	/* The watching directory needs to match the watch callback data */
	if (strcmp(result->dir, watch_data->pathname))
		return;

	/* The file inside the watch directory needs to match */
	if (strcmp(result->file, pathname)) {
		entry->failed = true;
		return;
	}

	/* The expected event needs to match as well */
	if (result->event != event) {
		entry->failed = true;
		return;
	}

	/* Successful match of the expected event data */
	l_debug("l_dir_watch ==> MATCH index %u", entry->res_idx);

	if (entry->res_idx < MAX_RESULTS) {
		entry->res_idx++;
		result = data->results[entry->res_idx];
		if (result->dir) {
			/* More results are required to match */
			return;
		}
	}

	/* Move to next test case */
	entry->idx++;
	entry->res_idx = 0;

	l_idle_oneshot(start_single_test, entry, NULL);
}

static void run_cleanup(char **dirs, char **files)
{
	int i;

	for (i = 0; files[i]; i++) {
		l_debug("unlink(\"%s\")", files[i]);
		unlink(files[i]);
	}

	for (i = 0; dirs[i]; i++) {
		l_debug("rmdir(\"%s\")", dirs[i]);
		rmdir(dirs[i]);
	}
}

static void dir_watch_free(void *data)
{
	struct l_dir_watch *watch = data;

	l_debug("free l_dir_watch [%p]", watch);

	l_dir_watch_destroy(watch);
}

static void free_test_entry(void *data)
{
	struct test_entry *entry = data;

	l_debug("free test_entry [%s]", entry->name);

	l_queue_destroy(entry->watch_list, dir_watch_free);

	/* Clean run should remove any leftovers */
	run_cleanup(entry->test_dirs, entry->test_files);

	l_strv_free(entry->test_dirs);
	l_strv_free(entry->test_files);
	l_strv_free(entry->watch_dirs);

	l_free(entry);
}

static void op_open(const char *dir, const char *file, unsigned int length)
{
	char *pathname;
	int fd, err;

	pathname = l_strdup_printf("%s/%s", dir, file);

	l_debug("open(\"%s\", O_RDONLY)", pathname);
	fd = open(pathname, O_RDONLY);
	l_debug("=> %d", fd);

	if (length > 0) {
		unsigned char *buf = l_malloc(length);
		ssize_t res;

		l_debug("read(%d, %p, %u)", fd, buf, length);
		res = read(fd, buf, length);
		l_debug("=> %zd", res);

		l_free(buf);
	}

	l_debug("close(%d)", fd);
	err = close(fd);
	l_debug("=> %d", err);

	l_free(pathname);
}

static void op_creat(const char *dir, const char *file, const char *content)
{
	char *pathname;
	int fd, err;

	pathname = l_strdup_printf("%s/%s", dir, file);

	l_debug("creat(\"%s\", 0600)", pathname);
	fd = creat(pathname, 0600);
	l_debug("=> %d", fd);

	if (content) {
		int len = strlen(content);
		ssize_t res;

		l_debug("write(%d, \"%s\", %d)", fd, content, len);
		res = write(fd, content, len);
		l_debug("=> %zd", res);
	}

	l_debug("close(%d)", fd);
	err = close(fd);
	l_debug("=> %d", err);

	l_free(pathname);
}

static void op_unlink(const char *dir, const char *file)
{
	char *pathname;
	int err;

	pathname = l_strdup_printf("%s/%s", dir, file);

	l_debug("unlink(\"%s\")", pathname);
	err = unlink(pathname);
	l_debug("=> %d", err);

	l_free(pathname);
}

static void op_truncate(const char *dir, const char *file, unsigned int length)
{
	char *pathname;
	int err;

	pathname = l_strdup_printf("%s/%s", dir, file);

	l_debug("truncate(\"%s\", %u)", pathname, length);
	err = truncate(pathname, length);
	l_debug("=> %d", err);

	l_free(pathname);
}

static void op_rename(const char *olddir, const char *oldfile,
				const char *newdir, const char *newfile)
{
	char *oldpath, *newpath;
	int err;

	oldpath = l_strdup_printf("%s/%s", olddir, oldfile);
	newpath = l_strdup_printf("%s/%s", newdir, newfile);

	l_debug("rename(\"%s\", \"%s\")", oldpath, newpath);
	err = rename(oldpath, newpath);
	l_debug("=> %d", err);

	l_free(oldpath);
	l_free(newpath);
}

static void process_test_queue(void *user_data);

static void start_single_test(void *user_data)
{
	struct test_entry *entry = user_data;
	const struct test_data *data = &entry->data[entry->idx];
	const struct test_result *result = data->results[entry->res_idx];
	bool ignore = false;

	switch (data->op) {
	case OP_NULL:
		if (entry->failed)
			l_info("[%s] FAILED", entry->name);
		else
			l_info("[%s] PASSED", entry->name);
		free_test_entry(entry);
		l_idle_oneshot(process_test_queue, NULL, NULL);
		return;
	case OP_OPEN:
		op_open(data->orig_dir, data->orig_file, data->length);
		break;
	case OP_CREAT:
		op_creat(data->orig_dir, data->orig_file, data->content);
		break;
	case OP_UNLINK:
		op_unlink(data->orig_dir, data->orig_file);
		break;
	case OP_TRUNCATE:
		op_truncate(data->orig_dir, data->orig_file, data->length);
		break;
	case OP_RENAME:
		op_rename(data->orig_dir, data->orig_file,
					data->dest_dir, data->dest_file);
		break;
	default:
		ignore = true;
		break;
	}

	if (!result->ignore && !ignore)
		return;

	/* Move to next test case */
	entry->idx++;
	entry->res_idx = 0;

	l_idle_oneshot(start_single_test, entry, NULL);
}

static void watch_data_free(void *data)
{
	struct test_watch *watch_data = data;

	l_debug("free test_watch [%s]", watch_data->pathname);

	l_free(watch_data->pathname);
	l_free(watch_data);
}

static void process_test_queue(void *user_data)
{
	struct test_entry *entry;
	int i;

	entry = l_queue_pop_head(test_queue);
	if (!entry) {
		l_main_quit();
		return;
	}

	/* In case there is any leftovers */
	run_cleanup(entry->test_dirs, entry->test_files);

	/* Create the directories in use */
	for (i = 0; entry->test_dirs[i]; i++) {
		l_debug("mkdir(%s, 0700)", entry->test_dirs[i]);
		mkdir(entry->test_dirs[i], 0700);
	}

	for (i = 0; entry->watch_dirs[i]; i++) {
		struct test_watch *watch_data;
		struct l_dir_watch *watch;

		watch_data = l_new(struct test_watch, 1);
		watch_data->pathname = l_strdup(entry->watch_dirs[i]);
		watch_data->entry = entry;

		l_debug("new test_watch [%s]", watch_data->pathname);

		watch = l_dir_watch_new(watch_data->pathname, event_callback,
						watch_data, watch_data_free);

		l_debug("new l_dir_watch [%p]", watch);

		l_queue_push_tail(entry->watch_list, watch);
	}

	l_idle_oneshot(start_single_test, entry, NULL);
}

static void add_test(const char *name, const struct test_data data[])
{
	struct test_entry *entry;
	int i;

	entry = l_new(struct test_entry, 1);
	entry->name = name;
	entry->data = data;

	l_debug("new test_entry [%s]", entry->name);

	for (i = 0; data[i].op; i++) {
		char *file;
		int n;

		if (!l_strv_contains(entry->test_dirs, data[i].orig_dir))
			entry->test_dirs = l_strv_append(entry->test_dirs,
							data[i].orig_dir);

		if (data[i].orig_dir) {
			file = l_strdup_printf("%s/%s", data[i].orig_dir,
							data[i].orig_file);
			if (!l_strv_contains(entry->test_files, file))
				entry->test_files = l_strv_append(entry->test_files,
									file);
			l_free(file);
		}

		if (data[i].dest_dir) {
			file = l_strdup_printf("%s/%s", data[i].dest_dir,
							data[i].dest_file);
			if (!l_strv_contains(entry->test_files, file))
				entry->test_files = l_strv_append(entry->test_files,
									file);
			l_free(file);
		}

		for (n = 0; n < MAX_RESULTS; n++) {
			if (!data[i].results[n])
				break;
			if (l_strv_contains(entry->watch_dirs,
						data[i].results[n]->dir))
				continue;
			entry->watch_dirs = l_strv_append(entry->watch_dirs,
						data[i].results[n]->dir);
		}

	}

	entry->watch_list = l_queue_new();
	entry->failed = false;

	l_queue_push_tail(test_queue, entry);
}

#define DIR_1	"/tmp/ell-test-dir-1"
#define DIR_2	"/tmp/ell-test-dir-2"
#define FILE_1	"file-1"
#define FILE_2	"file-2"
#define FILE_3	"file-3"
#define FILE_4	"file-4"

static const struct test_data test_data_1[] = {
	{
		test_creat(DIR_1, FILE_1, NULL),
		result_created(DIR_1, FILE_1),
	},
	{
		test_unlink(DIR_1, FILE_1),
		result_removed(DIR_1, FILE_1),
	},
	{
		test_creat(DIR_1, FILE_2, "File content"),
		result_created(DIR_1, FILE_2),
	},
	{
		test_rename(DIR_1, FILE_2, DIR_1, FILE_3),
		results(removed(DIR_1, FILE_2), created(DIR_1, FILE_3)),
	},
	{
		test_open(DIR_1, FILE_3, 4),
		result_accessed(DIR_1, FILE_3),
	},
	{
		test_truncate(DIR_1, FILE_3, 4),
		result_modified(DIR_1, FILE_3),
	},
	{
		test_unlink(DIR_1, FILE_3),
		result_removed(DIR_1, FILE_3),
	},
	{
		test_creat(DIR_2, FILE_4, NULL),
		result_ignore(),
	},
	{
		test_rename(DIR_2, FILE_4, DIR_1, FILE_4),
		result_created(DIR_1, FILE_4),
	},
	{
		test_rename(DIR_1, FILE_4, DIR_2, FILE_4),
		result_removed(DIR_1, FILE_4),
	},
	{
		test_unlink(DIR_2, FILE_4),
		result_ignore(),
	},
	{ }
};

static const struct test_data test_data_2[] = {
	{
		test_creat(DIR_1, FILE_1, NULL),
		result_created(DIR_1, FILE_1),
	},
	{
		test_rename(DIR_1, FILE_1, DIR_2, FILE_1),
		results(removed(DIR_1, FILE_1), created(DIR_2, FILE_1)),
	},
	{
		test_unlink(DIR_2, FILE_1),
		result_removed(DIR_2, FILE_1),
	},
	{ }
};

static const struct test_data test_data_3[] = {
	{
		test_creat(DIR_1, FILE_1, "X"),
		result_created(DIR_1, FILE_1),
	},
	{
		test_open(DIR_1, FILE_1, 1),
		result_accessed(DIR_1, FILE_1),
	},
	{
		test_unlink(DIR_1, FILE_1),
		result_removed(DIR_1, FILE_1),
	},
	{ }
};

static const struct test_data test_data_4[] = {
	{
		test_creat(DIR_1, FILE_1, "ABC"),
		result_created(DIR_1, FILE_1),
	},
	{
		test_creat(DIR_1, FILE_2, "XYZ"),
		result_created(DIR_1, FILE_2),
	},
	{
		test_rename(DIR_1, FILE_2, DIR_1, FILE_1),
		results(removed(DIR_1, FILE_2), created(DIR_1, FILE_1)),
	},
	{
		test_unlink(DIR_1, FILE_1),
		result_removed(DIR_1, FILE_1),
	},
	{ }
};

int main(int argc, char *argv[])
{
	int opt, exit_status;

	l_main_init();
	l_log_set_stderr();

	while ((opt = getopt(argc, argv, "d")) != -1) {
		switch (opt) {
		case 'd':
			l_debug_enable("*");
			break;
		}
	}

	test_queue = l_queue_new();
	add_test("Single directory test", test_data_1);
	add_test("Move between directories", test_data_2);
	add_test("Create and open file", test_data_3);
	add_test("Replace existing file", test_data_4);

	l_idle_oneshot(process_test_queue, NULL, NULL);
	exit_status = l_main_run();

	l_queue_destroy(test_queue, free_test_entry);
	l_main_exit();

	return exit_status;
}
