/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "lib-signals.h"
#include "str.h"
#include "safe-mkstemp.h"
#include "test-common.h"
#include "test-private.h"
#include "test-subprocess.h"

#include <unistd.h>
#include <stdio.h>
#include <setjmp.h> /* for fatal tests */

static bool test_initialized = FALSE;
static bool test_deinit_lib = FALSE;
static bool test_deinit_lib_signals = FALSE;

/* To test the firing of i_assert, we need non-local jumps, i.e. setjmp */
static volatile bool expecting_fatal = FALSE;
static jmp_buf fatal_jmpbuf;

#define OUT_NAME_ALIGN 70

struct event *test_event = NULL;

static char *test_prefix;
static bool test_success;
static bool test_running;
static unsigned int failure_count;
static unsigned int total_count;
static unsigned int expected_errors;
static char *expected_error_str, *expected_fatal_str;
static test_fatal_callback_t *test_fatal_callback;
static void *test_fatal_context;
static void (*test_cleanup_callback)(void) = NULL;
volatile sig_atomic_t terminating = 0;

void test_begin(const char *name)
{
	test_success = TRUE;
	expected_errors = 0;
	if (!expecting_fatal)
		i_assert(test_prefix == NULL);
	else
		test_assert((test_success = (test_prefix == NULL)));
	test_prefix = i_strdup(name);
}

bool test_has_failed(void)
{
	return !test_success;
}

void test_assert_failed(const char *code, const char *file, unsigned int line)
{
	printf("%s:%u: Assert failed: %s\n", file, line, code);
	fflush(stdout);
	test_success = FALSE;
#ifdef STATIC_CHECKER
	i_unreached();
#endif
}

void test_assert_failed_idx(const char *code, const char *file, unsigned int line, long long i)
{
	printf("%s:%u: Assert(#%lld) failed: %s\n", file, line, i, code);
	fflush(stdout);
	test_success = FALSE;
#ifdef STATIC_CHECKER
	i_unreached();
#endif
}

void test_assert_failed_strcmp_idx(const char *code, const char *file, unsigned int line,
				   const char *src, const char *dst, long long i)
{
	printf("%s:%u: Assert", file, line);
	if (i == LLONG_MIN)
		printf(" failed: %s\n", code);
	else
		printf("(#%lld) failed: %s\n", i, code);
	if (src != NULL)
		printf("        \"%s\" != ", src);
	else
		printf("        NULL != ");
	if (dst != NULL)
		printf("\"%s\"\n", dst);
	else
		printf("NULL\n");
	fflush(stdout);
	test_success = FALSE;
#ifdef STATIC_CHECKER
	i_unreached();
#endif
}

static void print_memcmp_data(const void *_data, size_t len)
{
	const unsigned char *data = _data;
	size_t i;

	for (i = 0; i < len; i++) {
		if (data[i] >= 0x20 && data[i] < 0x7f)
			printf("%c", data[i]);
		else
			printf("\\x%02X", data[i]);
	}
}

void test_assert_failed_memcmp_idx(const char *code, const char *file, unsigned int line,
				   const void *src, const void *dst, size_t len, long long i)
{
	printf("%s:%u: Assert", file, line);
	if (i == LLONG_MIN)
		printf(" failed: %s\n", code);
	else
		printf("(#%lld) failed: %s\n", i, code);
	printf("        \"");
	print_memcmp_data(src, len);
	printf("\" != \n");
	printf("        \"");
	print_memcmp_data(dst, len);
	printf("\" (len == %zu)\n", len);
	fflush(stdout);
	test_success = FALSE;
#ifdef STATIC_CHECKER
	i_unreached();
#endif
}

void test_assert_failed_cmp_intmax_idx(const char *code, const char *file,
				       unsigned int line,
				       intmax_t src, intmax_t dst,
				       const char *op, long long i)
{
	printf("%s:%u: Assert", file, line);
	if (i == LLONG_MIN)
		printf(" failed: %s\n", code);
	else
		printf("(#%lld) failed: %s\n", i, code);
	printf("        %jd %s %jd is not true\n", src, op, dst);
	fflush(stdout);
	test_success = FALSE;
#ifdef STATIC_CHECKER
	i_unreached();
#endif
}

void test_assert_failed_ucmp_intmax_idx(const char *code, const char *file,
					unsigned int line,
					uintmax_t src, uintmax_t dst,
					const char *op, long long i)
{
	printf("%s:%u: Assert", file, line);
	if (i == LLONG_MIN)
		printf(" failed: %s\n", code);
	else
		printf("(#%lld) failed: %s\n", i, code);
	printf("        %ju %s %ju is not true\n", src, op, dst);
	fflush(stdout);
	test_success = FALSE;
#ifdef STATIC_CHECKER
	i_unreached();
#endif
}

#ifdef DEBUG
#include "randgen.h"
static void
test_dump_rand_state(void)
{
	static int64_t seen_seed = -1;
	unsigned int seed;
	if (rand_get_last_seed(&seed) < 0) {
		if (seen_seed == -1) {
			printf("test: random sequence not reproduceable, use DOVECOT_SRAND=kiss\n");
			seen_seed = -2;
		}
		return;
	}
	if (seed == seen_seed)
		return;
	seen_seed = seed;
	printf("test: DOVECOT_SRAND random seed was %u\n", seed);
}
#else
static inline void test_dump_rand_state(void) { }
#endif

void test_end(void)
{
	if (!expecting_fatal)
		i_assert(test_prefix != NULL);
	else
		test_assert(test_prefix != NULL);

	test_out("", test_success);
	if (!test_success)
		test_dump_rand_state();
	i_free_and_null(test_prefix);
	test_success = FALSE;
}

void test_out(const char *name, bool success)
{
	test_out_reason(name, success, NULL);
}

void test_out_reason(const char *name, bool success, const char *reason)
{
	int i = 0;

	if (test_prefix != NULL) {
		fputs(test_prefix, stdout);
		i += strlen(test_prefix);
		if (*name != '\0') {
			putchar(':');
			i++;
		}
		putchar(' ');
		i++;
	}
	if (*name != '\0') {
		fputs(name, stdout);
		putchar(' ');
		i += strlen(name) + 1;
	}
	for (; i < OUT_NAME_ALIGN; i++)
		putchar('.');
	fputs(" : ", stdout);
	if (success)
		fputs("ok", stdout);
	else {
		fputs("FAILED", stdout);
		test_success = FALSE;
		failure_count++;
	}
	if (reason != NULL && *reason != '\0')
		printf(": %s", reason);
	putchar('\n');
	fflush(stdout);
	total_count++;
}

void test_out_quiet(const char *name, bool success)
{
	if (success) {
		total_count++;
		return;
	}
	test_out(name, success);
}

void test_out_reason_quiet(const char *name, bool success, const char *reason)
{
	if (success) {
		total_count++;
		return;
	}
	test_out_reason(name, success, reason);
}

void
test_expect_error_string_n_times(const char *substr, unsigned int times)
{
	i_assert(expected_errors == 0);
	expected_errors = times;
	expected_error_str = i_strdup(substr);
}
void
test_expect_error_string(const char *substr)
{
	test_expect_error_string_n_times(substr, 1);
}
void
test_expect_errors(unsigned int expected)
{
	i_assert(expected_errors == 0);
	expected_errors = expected;
}
void
test_expect_no_more_errors(void)
{
	test_assert(expected_errors == 0 && expected_error_str == NULL);
	i_free_and_null(expected_error_str);
	expected_errors = 0;
}

static bool ATTR_FORMAT(2, 0)
expect_error_check(char **error_strp, const char *format, va_list args)
{
	if (*error_strp == NULL)
		return TRUE;

	bool suppress;
	T_BEGIN {
		/* test_assert() will reset test_success if need be. */
		const char *str = t_strdup_vprintf(format, args);
		suppress = strstr(str, *error_strp) != NULL;
		test_assert(suppress == TRUE);
		i_free_and_null(*error_strp);
	} T_END;
	return suppress;
}

static void ATTR_FORMAT(2, 0)
test_error_handler(const struct failure_context *ctx,
		   const char *format, va_list args)
{
	bool suppress = FALSE;

	if (expected_errors > 0) {
		va_list args2;
		VA_COPY(args2, args);
		suppress = expect_error_check(&expected_error_str, format, args2);
		expected_errors--;
		va_end(args2);
	} else {
		test_success = FALSE;
	}

	if (!suppress) {
		test_dump_rand_state();
		default_error_handler(ctx, format, args);
	}
}

void test_expect_fatal_string(const char *substr)
{
	i_free(expected_fatal_str);
	expected_fatal_str = i_strdup(substr);
}

#undef test_fatal_set_callback
void test_fatal_set_callback(test_fatal_callback_t *callback, void *context)
{
	i_assert(test_fatal_callback == NULL);
	test_fatal_callback = callback;
	test_fatal_context = context;
}

static void ATTR_FORMAT(2, 0) ATTR_NORETURN
test_fatal_handler(const struct failure_context *ctx,
		   const char *format, va_list args)
{
	/* Prevent recursion, we can't handle our own errors */
	i_set_fatal_handler(default_fatal_handler);
	i_assert(expecting_fatal); /* if not at the right time, bail */

	va_list args2;
	VA_COPY(args2, args);
	bool suppress = expect_error_check(&expected_fatal_str, format, args2);
	va_end(args);

	if (suppress) {
		if (test_fatal_callback != NULL) {
			test_fatal_callback(test_fatal_context);
			test_fatal_callback = NULL;
			test_fatal_context = NULL;
		}

		i_set_fatal_handler(test_fatal_handler);
		longjmp(fatal_jmpbuf, 1);
	} else {
		default_fatal_handler(ctx, format, args);
	}
	i_unreached(); /* we simply can't get here */
}

static void test_cleanup(void)
{
	if (test_subprocess_is_child()) {
		/* Child processes must not execute the cleanups */
		return;
	}

	test_subprocess_cleanup();

	/* Perform any additional important cleanup specific to the test. */
	if (test_cleanup_callback != NULL)
		test_cleanup_callback();

	test_dir_cleanup();
}

static void test_terminate(const siginfo_t *si, void *context ATTR_UNUSED)
{
	int signo = si->si_signo;

	if (terminating != 0)
		raise(signo);
	terminating = 1;

	/* Perform important cleanups */
	test_cleanup();

	(void)signal(signo, SIG_DFL);
	if (signo == SIGTERM)
		_exit(0);
	else
		raise(signo);
}

static void test_atexit(void)
{
	/* NOTICE: This is also called by children, so be careful. */

	/* Perform important cleanups */
	test_cleanup();
}

void test_init_no_event(void)
{
	if (test_initialized)
		return;
	test_initialized = TRUE;

	test_prefix = NULL;
	failure_count = 0;
	total_count = 0;

	if (!lib_is_initialized()) {
		lib_init();
		test_deinit_lib = TRUE;
	} else
		test_deinit_lib = FALSE;

	i_set_error_handler(test_error_handler);
	/* Don't set fatal handler until actually needed for fatal testing */
}

void test_init(void)
{
	if (test_initialized)
		return;

	test_init_no_event();
	test_event = event_create(NULL);
	event_set_append_log_prefix(test_event, "test: ");
}

void test_init_signals(void)
{
	lib_signals_init();
	test_deinit_lib_signals = TRUE;

	atexit(test_atexit);
	lib_signals_ignore(SIGPIPE, TRUE);
	lib_signals_set_handler(SIGTERM, 0, test_terminate, NULL);
	lib_signals_set_handler(SIGQUIT, 0, test_terminate, NULL);
	lib_signals_set_handler(SIGINT, 0, test_terminate, NULL);
	lib_signals_set_handler(SIGSEGV, 0, test_terminate, NULL);
	lib_signals_set_handler(SIGABRT, 0, test_terminate, NULL);
}

static int test_deinit(void)
{
	i_assert(test_prefix == NULL);
	printf("%u / %u tests failed\n", failure_count, total_count);

	test_dir_deinit();
	test_subprocesses_deinit();

	event_unref(&test_event);

	if (test_deinit_lib)
		lib_deinit();
	if (test_deinit_lib_signals)
		lib_signals_deinit();
	return failure_count == 0 ? 0 : 1;
}

void test_forked_deinit(void)
{
	test_dir_deinit_forked();

	event_unref(&test_event);
}

static void test_run_funcs(void (*const test_functions[])(void))
{
	unsigned int i;

	test_running = TRUE;
	for (i = 0; test_functions[i] != NULL; i++) {
		T_BEGIN {
			test_functions[i]();
		} T_END;
	}
	test_running = FALSE;
}
static void test_run_named_funcs(const struct named_test tests[],
				 const char *match)
{
	unsigned int i;

	test_running = TRUE;
	for (i = 0; tests[i].func != NULL; i++) {
		if (strstr(tests[i].name, match) != NULL) T_BEGIN {
			tests[i].func();
		} T_END;
	}
	test_running = FALSE;
}

static void run_one_fatal(test_fatal_func_t *fatal_function)
{
	static unsigned int index = 0;
	for (;;) {
		volatile int jumped = setjmp(fatal_jmpbuf);
		if (jumped == 0) {
			/* normal flow */
			expecting_fatal = TRUE;
			enum fatal_test_state ret = fatal_function(index);
			expecting_fatal = FALSE;
			if (ret == FATAL_TEST_FINISHED) {
				/* ran out of tests - good */
				index = 0;
				break;
			} else if (ret == FATAL_TEST_FAILURE) {
				/* failed to fire assert - bad, but can continue */
				test_success = FALSE;
				i_error("Desired assert failed to fire at step %i", index);
				index++;
			} else { /* FATAL_TEST_ABORT or other value */
				test_success = FALSE;
				test_end();
				index = 0;
				break;
			}
		} else {
			/* assert fired, continue with next test */
			index++;
		}
	}
}
static void test_run_fatals(test_fatal_func_t *const fatal_functions[])
{
	unsigned int i;

	test_running = TRUE;
	for (i = 0; fatal_functions[i] != NULL; i++) {
		T_BEGIN {
			run_one_fatal(fatal_functions[i]);
		} T_END;
	}
	test_running = FALSE;
}
static void test_run_named_fatals(const struct named_fatal fatals[], const char *match)
{
	unsigned int i;

	test_running = TRUE;
	for (i = 0; fatals[i].func != NULL; i++) {
		if (strstr(fatals[i].name, match) != NULL) T_BEGIN {
			run_one_fatal(fatals[i].func);
		} T_END;
	}
	test_running = FALSE;
}

int test_run(void (*const test_functions[])(void))
{
	test_init();
	test_run_funcs(test_functions);
	return test_deinit();
}
int test_run_named(const struct named_test tests[], const char *match)
{
	test_init();
	test_run_named_funcs(tests, match);
	return test_deinit();
}
int test_run_with_fatals(void (*const test_functions[])(void),
			 test_fatal_func_t *const fatal_functions[])
{
	test_init();
	test_run_funcs(test_functions);
	i_set_fatal_handler(test_fatal_handler);
	test_run_fatals(fatal_functions);
	return test_deinit();
}
int test_run_named_with_fatals(const char *match, const struct named_test tests[],
			       const struct named_fatal fatals[])
{
	test_init();
	test_run_named_funcs(tests, match);
	i_set_fatal_handler(test_fatal_handler);
	test_run_named_fatals(fatals, match);
	return test_deinit();
}

void test_forked_end(void)
{
	test_forked_deinit();

	i_set_error_handler(default_error_handler);
	i_set_fatal_handler(default_fatal_handler);

	i_free_and_null(expected_error_str);
	i_free_and_null(expected_fatal_str);
	i_free_and_null(test_prefix);
	if (test_running)
		t_pop_last_unsafe(); /* as we were within a T_BEGIN { tests[i].func(); } T_END */
}

void ATTR_NORETURN
test_exit(int status)
{
	test_forked_deinit();

	i_free_and_null(expected_error_str);
	i_free_and_null(expected_fatal_str);
	i_free_and_null(test_prefix);
	if (test_running)
		t_pop_last_unsafe(); /* as we were within a T_BEGIN { tests[i].func(); } T_END */
	lib_deinit();
	lib_exit(status);
}

int test_create_temp_fd(void)
{
	string_t *str = t_str_new(128);
	int fd;

	str_append(str, "/tmp/dovecot-test.");
	fd = safe_mkstemp(str, 0600, (uid_t)-1, (gid_t)-1);
	if (fd == -1)
		i_fatal("safe_mkstemp(%s) failed: %m", str_c(str));
	i_unlink(str_c(str));
	return fd;
}

void test_set_cleanup_callback(void (*callback)(void))
{
	test_cleanup_callback = callback;
}
