/*
 * Copyright (C) 2013 Tobias Brunner
 * HSR Hochschule fuer Technik Rapperswil
 * Copyright (C) 2013 Martin Willi
 * Copyright (C) 2013 revosec AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program 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 General Public License
 * for more details.
 */

#include "test_runner.h"

#include <library.h>
#include <threading/thread.h>
#include <plugins/plugin_feature.h>
#include <collections/array.h>
#include <utils/test.h>

#include <stdlib.h>
#include <dirent.h>
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>
#include <time.h>

/**
 * Get a tty color escape character for stderr
 */
#define TTY(color) tty_escape_get(2, TTY_FG_##color)

/**
 * A global symbol indicating libtest linkage
 */
#ifdef WIN32
__declspec(dllexport)
#endif
bool test_runner_available = TRUE;

/**
 * Destroy data associated with a test case.
 */
static void destroy_case(test_case_t *tcase)
{
	array_destroy(tcase->functions);
	array_destroy(tcase->fixtures);
}

/**
 * Destroy a single test suite and associated data.
 */
static void destroy_suite(test_suite_t *suite)
{
	array_destroy_function(suite->tcases, (void*)destroy_case, NULL);
	free(suite);
}

/**
 * Identifies on which component to apply the given filter.
 */
typedef enum {
	FILTER_SUITES,
	FILTER_CASES,
	FILTER_FUNCTIONS,
} filter_component_t;

/**
 * Check if the component with the given name should be filtered/removed.
 */
static bool filter_name(const char *name, hashtable_t *names, bool exclude)
{
	return (exclude && names->get(names, name)) ||
		   (!exclude && !names->get(names, name));
}

/**
 * Filter loaded test suites/cases/functions, either remove components listed
 * (exclude=TRUE), or all that are not listed (exclude=FALSE).
 * Empty test cases/suites are removed and destroyed.
 */
static void apply_filter(array_t *loaded, filter_component_t comp, char *filter,
						 bool exclude)
{
	enumerator_t *enumerator, *tcases, *functions, *names;
	hashtable_t *listed;
	test_suite_t *suite;
	test_case_t *tcase;
	test_function_t *func;
	char *name;

	listed = hashtable_create(hashtable_hash_str, hashtable_equals_str, 8);
	names = enumerator_create_token(filter, ",", " ");
	while (names->enumerate(names, &name))
	{
		listed->put(listed, name, name);
	}

	enumerator = array_create_enumerator(loaded);
	while (enumerator->enumerate(enumerator, &suite))
	{
		if (comp == FILTER_SUITES)
		{
			if (filter_name(suite->name, listed, exclude))
			{
				array_remove_at(loaded, enumerator);
				destroy_suite(suite);
			}
			continue;
		}
		tcases = array_create_enumerator(suite->tcases);
		while (tcases->enumerate(tcases, &tcase))
		{
			if (comp == FILTER_CASES)
			{
				if (filter_name(tcase->name, listed, exclude))
				{
					array_remove_at(suite->tcases, tcases);
					destroy_case(tcase);
				}
				continue;
			}
			functions = array_create_enumerator(tcase->functions);
			while (functions->enumerate(functions, &func))
			{
				if (filter_name(func->name, listed, exclude))
				{
					array_remove_at(tcase->functions, functions);
				}
			}
			functions->destroy(functions);

			if (!array_count(tcase->functions))
			{
				array_remove_at(suite->tcases, tcases);
				destroy_case(tcase);
			}
		}
		tcases->destroy(tcases);

		if (!array_count(suite->tcases))
		{
			array_remove_at(loaded, enumerator);
			destroy_suite(suite);
		}
	}
	enumerator->destroy(enumerator);
	listed->destroy(listed);
	names->destroy(names);
}

/**
 * Check if the given string is contained in the filter string.
 */
static bool is_in_filter(const char *find, char *filter)
{
	enumerator_t *names;
	bool found = FALSE;
	char *name;

	names = enumerator_create_token(filter, ",", " ");
	while (names->enumerate(names, &name))
	{
		if (streq(name, find))
		{
			found = TRUE;
			break;
		}
	}
	names->destroy(names);
	return found;
}

/**
 * Removes and destroys test suites/cases/functions that are not selected or
 * explicitly excluded. Takes names of two environment variables.
 */
static void filter_components(array_t *loaded, filter_component_t comp,
							  char *sel, char *exc)
{
	char *filter;

	filter = getenv(sel);
	if (filter)
	{
		apply_filter(loaded, comp, filter, FALSE);
	}
	filter = getenv(exc);
	if (filter)
	{
		apply_filter(loaded, comp, filter, TRUE);
	}
}

/**
 * Load all available test suites, or optionally only selected ones.
 */
static array_t *load_suites(test_configuration_t configs[],
							test_runner_init_t init, char *cfg)
{
	array_t *suites;
	bool old = FALSE;
	int i;

	library_init(cfg, "test-runner");

	test_setup_handler();

	if (init && !init(TRUE))
	{
		library_deinit();
		return NULL;
	}
	lib->plugins->status(lib->plugins, LEVEL_CTRL);

	if (lib->leak_detective)
	{
		old = lib->leak_detective->set_state(lib->leak_detective, FALSE);
	}

	suites = array_create(0, 0);

	for (i = 0; configs[i].suite; i++)
	{
		if (configs[i].feature.type == 0 ||
			lib->plugins->has_feature(lib->plugins, configs[i].feature))
		{
			array_insert(suites, -1, configs[i].suite());
		}
	}
	filter_components(suites, FILTER_SUITES, "TESTS_SUITES",
					  "TESTS_SUITES_EXCLUDE");
	filter_components(suites, FILTER_CASES, "TESTS_CASES",
					  "TESTS_CASES_EXCLUDE");
	filter_components(suites, FILTER_FUNCTIONS, "TESTS_FUNCTIONS",
					  "TESTS_FUNCTIONS_EXCLUDE");

	if (lib->leak_detective)
	{
		lib->leak_detective->set_state(lib->leak_detective, old);
	}

	if (init)
	{
		init(FALSE);
	}
	library_deinit();

	return suites;
}

/**
 * Unload and destroy test suites and associated data
 */
static void unload_suites(array_t *suites)
{
	test_suite_t *suite;

	while (array_remove(suites, 0, &suite))
	{
		destroy_suite(suite);
	}
	array_destroy(suites);
}

/**
 * Run a single test function, return FALSE on failure
 */
static bool run_test(test_function_t *tfun, int i)
{
	if (test_restore_point())
	{
		tfun->cb(i);
		return TRUE;
	}
	thread_cleanup_popall();
	return FALSE;
}

/**
 * Invoke fixture setup/teardown
 */
static bool call_fixture(test_case_t *tcase, bool up, int i)
{
	enumerator_t *enumerator;
	test_fixture_t *fixture;
	bool failure = FALSE;

	enumerator = array_create_enumerator(tcase->fixtures);
	while (enumerator->enumerate(enumerator, &fixture))
	{
		if (test_restore_point())
		{
			if (up)
			{
				if (fixture->setup)
				{
					fixture->setup(i);
				}
			}
			else
			{
				if (fixture->teardown)
				{
					fixture->teardown(i);
				}
			}
		}
		else
		{
			thread_cleanup_popall();
			failure = TRUE;
			break;
		}
	}
	enumerator->destroy(enumerator);

	return !failure;
}

/**
 * Test initialization, initializes libstrongswan for the next run
 */
static bool pre_test(test_runner_init_t init, char *cfg)
{
	library_init(cfg, "test-runner");

	/* use non-blocking RNG to generate keys fast */
	lib->settings->set_default_str(lib->settings,
			"libstrongswan.plugins.random.random",
			lib->settings->get_str(lib->settings,
				"libstrongswan.plugins.random.urandom", "/dev/urandom"));
	/* same for the gcrypt plugin */
	lib->settings->set_default_str(lib->settings,
			"libstrongswan.plugins.gcrypt.quick_random", "yes");

	if (lib->leak_detective)
	{
		/* disable leak reports during testing */
		lib->leak_detective->set_report_cb(lib->leak_detective,
										   NULL, NULL, NULL);
	}
	if (init && !init(TRUE))
	{
		library_deinit();
		return FALSE;
	}
	return TRUE;
}

/**
 * Failure description
 */
typedef struct {
	char *name;
	char msg[4096 - sizeof(char*) - 2 * sizeof(int)];
	const char *file;
	int line;
	int i;
	backtrace_t *bt;
} failure_t;

/**
 * Data passed to leak report callbacks
 */
typedef struct {
	array_t *failures;
	char *name;
	int i;
	int leaks;
} report_data_t;

/**
 * Leak report callback, build failures from leaks
 */
static void report_leaks(report_data_t *data, int count, size_t bytes,
						 backtrace_t *bt, bool detailed)
{
	failure_t failure = {
		.name = data->name,
		.i = data->i,
		.bt = bt->clone(bt),
	};

	snprintf(failure.msg, sizeof(failure.msg),
			 "Leak detected: %d allocations using %zu bytes", count, bytes);

	array_insert(data->failures, -1, &failure);
}

/**
 * Leak summary callback, check if any leaks found
 */
static void sum_leaks(report_data_t *data, int count, size_t bytes,
					  int whitelisted)
{
	data->leaks = count;
}

/**
 * Do library cleanup and optionally check for memory leaks
 */
static bool post_test(test_runner_init_t init, bool check_leaks,
					  array_t *failures, char *name, int i, int *leaks)
{
	report_data_t data = {
		.failures = failures,
		.name = name,
		.i = i,
	};

	if (init)
	{
		if (test_restore_point())
		{
			init(FALSE);
		}
		else
		{
			thread_cleanup_popall();
			library_deinit();
			return FALSE;
		}
	}
	if (check_leaks && lib->leak_detective)
	{
		lib->leak_detective->set_report_cb(lib->leak_detective,
								(leak_detective_report_cb_t)report_leaks,
								(leak_detective_summary_cb_t)sum_leaks, &data);
	}
	library_deinit();

	*leaks = data.leaks;
	return TRUE;
}

/**
 * Collect failure information, add failure_t to array
 */
static void collect_failure_info(array_t *failures, char *name, int i)
{
	failure_t failure = {
		.name = name,
		.i = i,
		.bt = test_failure_backtrace(),
	};

	failure.line = test_failure_get(failure.msg, sizeof(failure.msg),
									&failure.file);

	array_insert(failures, -1, &failure);
}

/**
 * Collect warning information, add failure_t to array
 */
static bool collect_warning_info(array_t *warnings, char *name, int i)
{
	failure_t warning = {
		.name = name,
		.i = i,
	};

	warning.line = test_warning_get(warning.msg, sizeof(warning.msg),
									&warning.file);
	if (warning.line)
	{
		array_insert(warnings, -1, &warning);
	}
	return warning.line;
}

/**
 * Print array of collected failure_t to stderr
 */
static void print_failures(array_t *failures, bool warnings)
{
	failure_t failure;

	threads_init();
	backtrace_init();

	while (array_remove(failures, 0, &failure))
	{
		if (warnings)
		{
			fprintf(stderr, "      %sWarning in '%s': %s (",
					TTY(YELLOW), failure.name, failure.msg);
		}
		else
		{
			fprintf(stderr, "      %sFailure in '%s': %s (",
					TTY(RED), failure.name, failure.msg);
		}
		if (failure.line)
		{
			fprintf(stderr, "%s:%d, ", failure.file, failure.line);
		}
		fprintf(stderr, "i = %d)%s\n", failure.i, TTY(DEF));
		if (failure.bt)
		{
			failure.bt->log(failure.bt, stderr, TRUE);
			failure.bt->destroy(failure.bt);
		}
	}

	backtrace_deinit();
	threads_deinit();
}

#if defined(CLOCK_THREAD_CPUTIME_ID) && defined(HAVE_CLOCK_GETTIME)

/**
 * Start a timer
 */
static void start_timing(struct timespec *start)
{
	clock_gettime(CLOCK_THREAD_CPUTIME_ID, start);
}

/**
 * End a timer, return ms
 */
static double end_timing(struct timespec *start)
{
	struct timespec end;

	if (!getenv("TESTS_TIMING"))
	{
		return 0;
	}
	clock_gettime(CLOCK_THREAD_CPUTIME_ID, &end);
	return (end.tv_nsec - start->tv_nsec) / 1000000.0 +
			(end.tv_sec - start->tv_sec) * 1000.0;
}

#else /* CLOCK_THREAD_CPUTIME_ID */

#define start_timing(start) ((start)->tv_sec = 0, (start)->tv_nsec = 0)
#define end_timing(...) (0)

#endif /* CLOCK_THREAD_CPUTIME_ID */

/**
 * Run a single test case with fixtures
 */
static bool run_case(test_case_t *tcase, test_runner_init_t init, char *cfg,
					 level_t level)
{
	enumerator_t *enumerator;
	test_function_t *tfun;
	double *times;
	double total_time = 0;
	int tests = 0, ti = 0, passed = 0;
	array_t *failures, *warnings;

	/* determine the number of tests we will run */
	enumerator = array_create_enumerator(tcase->functions);
	while (enumerator->enumerate(enumerator, &tfun))
	{
		tests += tfun->end - tfun->start;
	}
	enumerator->destroy(enumerator);
	times = calloc(tests, sizeof(double));

	failures = array_create(sizeof(failure_t), 0);
	warnings = array_create(sizeof(failure_t), 0);

	fprintf(stderr, "    Running case '%s': ", tcase->name);
	fflush(stderr);

	enumerator = array_create_enumerator(tcase->functions);
	while (enumerator->enumerate(enumerator, &tfun))
	{
		int i, rounds = 0;

		for (i = tfun->start; i < tfun->end; i++)
		{
			if (pre_test(init, cfg))
			{
				struct timespec start;
				bool ok = FALSE;
				int leaks = 0;

				if (level >= 0)
				{
					fprintf(stderr, "\nRunning function '%s' [%d]:\n",
							tfun->name, i);
				}

				test_setup_timeout(tcase->timeout);
				start_timing(&start);

				if (call_fixture(tcase, TRUE, i))
				{
					if (run_test(tfun, i))
					{
						if (call_fixture(tcase, FALSE, i))
						{
							ok = TRUE;
						}
					}
					else
					{
						call_fixture(tcase, FALSE, i);
					}
				}
				if (!post_test(init, ok, failures, tfun->name, i, &leaks))
				{
					ok = FALSE;
				}

				times[ti] = end_timing(&start);
				total_time += times[ti++];
				test_setup_timeout(0);

				if (ok)
				{
					if (!leaks)
					{
						rounds++;
						if (!collect_warning_info(warnings, tfun->name, i))
						{
							fprintf(stderr, "%s+%s", TTY(GREEN), TTY(DEF));
						}
						else
						{
							fprintf(stderr, "%s~%s", TTY(YELLOW), TTY(DEF));
						}
					}
				}
				else
				{
					collect_failure_info(failures, tfun->name, i);
				}
				if (!ok || leaks)
				{
					fprintf(stderr, "%s-%s", TTY(RED), TTY(DEF));
				}
			}
			else
			{
				fprintf(stderr, "!");
			}
		}
		fflush(stderr);
		if (rounds == tfun->end - tfun->start)
		{
			passed++;
		}
	}
	enumerator->destroy(enumerator);

	if (total_time)
	{
		fprintf(stderr, " %s%s%.3f ms%s", tty_escape_get(2, TTY_BOLD),
				TTY(BLUE), total_time, tty_escape_get(2, TTY_RESET));
		if (ti > 1)
		{
			fprintf(stderr, " %s[", TTY(BLUE));
			for (ti = 0; ti < tests; ti++)
			{
				fprintf(stderr, "%s%.3f ms", times[ti], ti == 0 ? "" : ", ");
			}
			fprintf(stderr, "]%s", TTY(DEF));
		}
	}
	fprintf(stderr, "\n");

	print_failures(warnings, TRUE);
	print_failures(failures, FALSE);
	array_destroy(failures);
	array_destroy(warnings);

	return passed == array_count(tcase->functions);
}

/**
 * Run a single test suite
 */
static bool run_suite(test_suite_t *suite, test_runner_init_t init, char *cfg,
					  level_t level)
{
	enumerator_t *enumerator;
	test_case_t *tcase;
	int passed = 0;

	fprintf(stderr, "  Running suite '%s':\n", suite->name);

	enumerator = array_create_enumerator(suite->tcases);
	while (enumerator->enumerate(enumerator, &tcase))
	{
		if (run_case(tcase, init, cfg, level))
		{
			passed++;
		}
	}
	enumerator->destroy(enumerator);

	if (passed == array_count(suite->tcases))
	{
		fprintf(stderr, "  %sPassed all %u '%s' test cases%s\n",
				TTY(GREEN), array_count(suite->tcases), suite->name, TTY(DEF));
		return TRUE;
	}
	fprintf(stderr, "  %sPassed %u/%u '%s' test cases%s\n",
			TTY(RED), passed, array_count(suite->tcases), suite->name, TTY(DEF));
	return FALSE;
}

/**
 * See header.
 */
int test_runner_run(const char *name, test_configuration_t configs[],
					test_runner_init_t init)
{
	array_t *suites;
	test_suite_t *suite;
	enumerator_t *enumerator;
	int passed = 0, result;
	level_t level = LEVEL_SILENT;
	char *cfg, *runners, *verbosity;

	/* redirect all output to stderr (to redirect make's stdout to /dev/null) */
	dup2(2, 1);

	runners = getenv("TESTS_RUNNERS");
	if (runners && !is_in_filter(name, runners))
	{
		return EXIT_SUCCESS;
	}

	cfg = getenv("TESTS_STRONGSWAN_CONF");

	suites = load_suites(configs, init, cfg);
	if (!suites)
	{
		return EXIT_FAILURE;
	}

	verbosity = getenv("TESTS_VERBOSITY");
	if (verbosity)
	{
		level = atoi(verbosity);
	}
	dbg_default_set_level(level);

	fprintf(stderr, "Running %u '%s' test suites:\n", array_count(suites), name);

	enumerator = array_create_enumerator(suites);
	while (enumerator->enumerate(enumerator, &suite))
	{
		if (run_suite(suite, init, cfg, level))
		{
			passed++;
		}
	}
	enumerator->destroy(enumerator);

	if (passed == array_count(suites))
	{
		fprintf(stderr, "%sPassed all %u '%s' suites%s\n",
				TTY(GREEN), array_count(suites), name, TTY(DEF));
		result = EXIT_SUCCESS;
	}
	else
	{
		fprintf(stderr, "%sPassed %u of %u '%s' suites%s\n",
				TTY(RED), passed, array_count(suites), name, TTY(DEF));
		result = EXIT_FAILURE;
	}

	unload_suites(suites);

	return result;
}
