#define DISABLE_SIGN_COMPARE_WARNINGS

#include "test-lib.h"

enum result {
	RESULT_NONE,
	RESULT_FAILURE,
	RESULT_SKIP,
	RESULT_SUCCESS,
	RESULT_TODO
};

static struct {
	enum result result;
	int count;
	unsigned failed :1;
	unsigned lazy_plan :1;
	unsigned running :1;
	unsigned skip_all :1;
	unsigned todo :1;
	char location[100];
	char description[100];
} ctx = {
	.lazy_plan = 1,
	.result = RESULT_NONE,
};

/*
 * Visual C interpolates the absolute Windows path for `__FILE__`,
 * but we want to see relative paths, as verified by t0080.
 * There are other compilers that do the same, and are not for
 * Windows.
 */
#include "dir.h"

static const char *make_relative(const char *location)
{
	static char prefix[] = __FILE__, buf[PATH_MAX], *p;
	static size_t prefix_len;
	static int need_bs_to_fs = -1;

	/* one-time preparation */
	if (need_bs_to_fs < 0) {
		size_t len = strlen(prefix);
		char needle[] = "t\\unit-tests\\test-lib.c";
		size_t needle_len = strlen(needle);

		if (len < needle_len)
			die("unexpected prefix '%s'", prefix);

		/*
		 * The path could be relative (t/unit-tests/test-lib.c)
		 * or full (/home/user/git/t/unit-tests/test-lib.c).
		 * Check the slash between "t" and "unit-tests".
		 */
		prefix_len = len - needle_len;
		if (prefix[prefix_len + 1] == '/') {
			/* Oh, we're not Windows */
			for (size_t i = 0; i < needle_len; i++)
				if (needle[i] == '\\')
					needle[i] = '/';
			need_bs_to_fs = 0;
		} else {
			need_bs_to_fs = 1;
		}

		/*
		 * prefix_len == 0 if the compiler gives paths relative
		 * to the root of the working tree.  Otherwise, we want
		 * to see that we did find the needle[] at a directory
		 * boundary.  Again we rely on that needle[] begins with
		 * "t" followed by the directory separator.
		 */
		if (fspathcmp(needle, prefix + prefix_len) ||
		    (prefix_len && prefix[prefix_len - 1] != needle[1]))
			die("unexpected suffix of '%s'", prefix);
	}

	/*
	 * Does it not start with the expected prefix?
	 * Return it as-is without making it worse.
	 */
	if (prefix_len && fspathncmp(location, prefix, prefix_len))
		return location;

	/*
	 * If we do not need to munge directory separator, we can return
	 * the substring at the tail of the location.
	 */
	if (!need_bs_to_fs)
		return location + prefix_len;

	/* convert backslashes to forward slashes */
	strlcpy(buf, location + prefix_len, sizeof(buf));
	for (p = buf; *p; p++)
		if (*p == '\\')
			*p = '/';
	return buf;
}

static void msg_with_prefix(const char *prefix, const char *format, va_list ap)
{
	fflush(stderr);
	if (prefix)
		fprintf(stdout, "%s", prefix);
	vprintf(format, ap); /* TODO: handle newlines */
	putc('\n', stdout);
	fflush(stdout);
}

void test_msg(const char *format, ...)
{
	va_list ap;

	va_start(ap, format);
	msg_with_prefix("# ", format, ap);
	va_end(ap);
}

void test_plan(int count)
{
	assert(!ctx.running);

	fflush(stderr);
	printf("1..%d\n", count);
	fflush(stdout);
	ctx.lazy_plan = 0;
}

int test_done(void)
{
	if (ctx.running && ctx.location[0] && ctx.description[0])
		test__run_end(1, ctx.location, "%s", ctx.description);
	assert(!ctx.running);

	if (ctx.lazy_plan)
		test_plan(ctx.count);

	return ctx.failed;
}

void test_skip(const char *format, ...)
{
	va_list ap;

	assert(ctx.running);

	ctx.result = RESULT_SKIP;
	va_start(ap, format);
	if (format)
		msg_with_prefix("# skipping test - ", format, ap);
	va_end(ap);
}

void test_skip_all(const char *format, ...)
{
	va_list ap;
	const char *prefix;

	if (!ctx.count && ctx.lazy_plan) {
		/* We have not printed a test plan yet */
		prefix = "1..0 # SKIP ";
		ctx.lazy_plan = 0;
	} else {
		/* We have already printed a test plan */
		prefix = "Bail out! # ";
		ctx.failed = 1;
	}
	ctx.skip_all = 1;
	ctx.result = RESULT_SKIP;
	va_start(ap, format);
	msg_with_prefix(prefix, format, ap);
	va_end(ap);
}

void test__run_describe(const char *location, const char *format, ...)
{
	va_list ap;
	int len;

	assert(ctx.running);
	assert(!ctx.location[0]);
	assert(!ctx.description[0]);

	xsnprintf(ctx.location, sizeof(ctx.location), "%s",
		  make_relative(location));

	va_start(ap, format);
	len = vsnprintf(ctx.description, sizeof(ctx.description), format, ap);
	va_end(ap);
	if (len < 0)
		die("unable to format message: %s", format);
	if (len >= sizeof(ctx.description))
		BUG("ctx.description too small to format %s", format);
}

int test__run_begin(void)
{
	if (ctx.running && ctx.location[0] && ctx.description[0])
		test__run_end(1, ctx.location, "%s", ctx.description);
	assert(!ctx.running);

	ctx.count++;
	ctx.result = RESULT_NONE;
	ctx.running = 1;
	ctx.location[0] = '\0';
	ctx.description[0] = '\0';

	return ctx.skip_all;
}

static void print_description(const char *format, va_list ap)
{
	if (format) {
		fputs(" - ", stdout);
		vprintf(format, ap);
	}
}

int test__run_end(int was_run UNUSED, const char *location, const char *format, ...)
{
	va_list ap;

	assert(ctx.running);
	assert(!ctx.todo);

	fflush(stderr);
	va_start(ap, format);
	if (!ctx.skip_all) {
		switch (ctx.result) {
		case RESULT_SUCCESS:
			printf("ok %d", ctx.count);
			print_description(format, ap);
			break;

		case RESULT_FAILURE:
			printf("not ok %d", ctx.count);
			print_description(format, ap);
			break;

		case RESULT_TODO:
			printf("not ok %d", ctx.count);
			print_description(format, ap);
			printf(" # TODO");
			break;

		case RESULT_SKIP:
			printf("ok %d", ctx.count);
			print_description(format, ap);
			printf(" # SKIP");
			break;

		case RESULT_NONE:
			test_msg("BUG: test has no checks at %s",
				 make_relative(location));
			printf("not ok %d", ctx.count);
			print_description(format, ap);
			ctx.result = RESULT_FAILURE;
			break;
		}
	}
	va_end(ap);
	ctx.running = 0;
	if (ctx.skip_all)
		return 1;
	putc('\n', stdout);
	fflush(stdout);
	ctx.failed |= ctx.result == RESULT_FAILURE;

	return ctx.result != RESULT_FAILURE;
}

static void test_fail(void)
{
	assert(ctx.result != RESULT_SKIP);

	ctx.result = RESULT_FAILURE;
}

static void test_pass(void)
{
	assert(ctx.result != RESULT_SKIP);

	if (ctx.result == RESULT_NONE)
		ctx.result = RESULT_SUCCESS;
}

static void test_todo(void)
{
	assert(ctx.result != RESULT_SKIP);

	if (ctx.result != RESULT_FAILURE)
		ctx.result = RESULT_TODO;
}

int test_assert(const char *location, const char *check, int ok)
{
	if (!ctx.running) {
		test_msg("BUG: check outside of test at %s",
			 make_relative(location));
		ctx.failed = 1;
		return 0;
	}

	if (ctx.result == RESULT_SKIP) {
		test_msg("skipping check '%s' at %s", check,
			 make_relative(location));
		return 1;
	}
	if (!ctx.todo) {
		if (ok) {
			test_pass();
		} else {
			test_msg("check \"%s\" failed at %s", check,
				 make_relative(location));
			test_fail();
		}
	}

	return !!ok;
}

void test__todo_begin(void)
{
	assert(ctx.running);
	assert(!ctx.todo);

	ctx.todo = 1;
}

int test__todo_end(const char *location, const char *check, int res)
{
	assert(ctx.running);
	assert(ctx.todo);

	ctx.todo = 0;
	if (ctx.result == RESULT_SKIP)
		return 1;
	if (res) {
		test_msg("todo check '%s' succeeded at %s", check,
			 make_relative(location));
		test_fail();
	} else {
		test_todo();
	}

	return !res;
}

int check_bool_loc(const char *loc, const char *check, int ok)
{
	return test_assert(loc, check, ok);
}

union test__tmp test__tmp[2];

int check_pointer_eq_loc(const char *loc, const char *check, int ok,
			 const void *a, const void *b)
{
	int ret = test_assert(loc, check, ok);

	if (!ret) {
		test_msg("   left: %p", a);
		test_msg("  right: %p", b);
	}

	return ret;
}

int check_int_loc(const char *loc, const char *check, int ok,
		  intmax_t a, intmax_t b)
{
	int ret = test_assert(loc, check, ok);

	if (!ret) {
		test_msg("   left: %"PRIdMAX, a);
		test_msg("  right: %"PRIdMAX, b);
	}

	return ret;
}

int check_uint_loc(const char *loc, const char *check, int ok,
		   uintmax_t a, uintmax_t b)
{
	int ret = test_assert(loc, check, ok);

	if (!ret) {
		test_msg("   left: %"PRIuMAX, a);
		test_msg("  right: %"PRIuMAX, b);
	}

	return ret;
}

static void print_one_char(char ch, char quote)
{
	if ((unsigned char)ch < 0x20u || ch == 0x7f) {
		char esc;
		switch (ch) {
		case '\a': esc = 'a'; break;
		case '\b': esc = 'b'; break;
		case '\t': esc = 't'; break;
		case '\n': esc = 'n'; break;
		case '\v': esc = 'v'; break;
		case '\f': esc = 'f'; break;
		case '\r': esc = 'r'; break;
		default: esc = 0; break;
		}
		if (esc) {
			putc('\\', stdout);
			putc(esc, stdout);
		} else {
			printf("\\%03o", (unsigned char)ch);
		}
	} else {
		if (ch == '\\' || ch == quote)
			putc('\\', stdout);
		putc(ch, stdout);
	}
}

static void print_char(const char *prefix, char ch)
{
	printf("# %s: '", prefix);
	print_one_char(ch, '\'');
	fputs("'\n", stdout);
}

int check_char_loc(const char *loc, const char *check, int ok, char a, char b)
{
	int ret = test_assert(loc, check, ok);

	if (!ret) {
		fflush(stderr);
		print_char("   left", a);
		print_char("  right", b);
		fflush(stdout);
	}

	return ret;
}

static void print_str(const char *prefix, const char *str)
{
	printf("# %s: ", prefix);
	if (!str) {
		fputs("NULL\n", stdout);
	} else {
		putc('"', stdout);
		while (*str)
			print_one_char(*str++, '"');
		fputs("\"\n", stdout);
	}
}

int check_str_loc(const char *loc, const char *check,
		  const char *a, const char *b)
{
	int ok = (!a && !b) || (a && b && !strcmp(a, b));
	int ret = test_assert(loc, check, ok);

	if (!ret) {
		fflush(stderr);
		print_str("   left", a);
		print_str("  right", b);
		fflush(stdout);
	}

	return ret;
}
