#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>

#include "selftest.h"

#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>

static char *read_full(HANDLE h, int is_pipe)
{
	char *data = NULL;
	size_t data_size = 0;

	while (1) {
		CHAR buf[4096];
		DWORD bytes_read;

		if (!ReadFile(h, buf, sizeof(buf), &bytes_read, NULL)) {
			if (!is_pipe)
				cl_fail("Failed reading file handle.");
			cl_assert_equal_i(GetLastError(), ERROR_BROKEN_PIPE);
			break;
		}
		if (!bytes_read)
			break;

		data = realloc(data, data_size + bytes_read);
		cl_assert(data);
		memcpy(data + data_size, buf, bytes_read);
		data_size += bytes_read;
	}

	data = realloc(data, data_size + 1);
	cl_assert(data);
	data[data_size] = '\0';

	while (strstr(data, "\r\n")) {
		char *ptr = strstr(data, "\r\n");
		memmove(ptr, ptr + 1, strlen(ptr));
	}

	return data;
}

static char *read_file(const char *path)
{
	char *content;
	HANDLE file;

	file = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL,
			  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	cl_assert(file != INVALID_HANDLE_VALUE);
	content = read_full(file, 0);
	cl_assert_equal_b(1, CloseHandle(file));

	return content;
}

static char *execute(const char *suite, int expected_error_code, const char **args, size_t nargs)
{
	SECURITY_ATTRIBUTES security_attributes = { 0 };
	PROCESS_INFORMATION process_info = { 0 };
	STARTUPINFO startup_info = { 0 };
	char binary_path[4096] = { 0 };
	char cmdline[4096] = { 0 };
	char *output = NULL;
	HANDLE stdout_write;
	HANDLE stdout_read;
	DWORD exit_code;
	size_t i;

	snprintf(binary_path, sizeof(binary_path), "%s/%s_suite.exe",
		 selftest_suite_directory, suite);

	/*
	 * Assemble command line arguments. In theory we'd have to properly
	 * quote them. In practice none of our tests actually care.
	 */
	snprintf(cmdline, sizeof(cmdline), suite);
	for (i = 0; i < nargs; i++) {
		size_t cmdline_len = strlen(cmdline);
		const char *arg = args[i];
		cl_assert(cmdline_len + strlen(arg) < sizeof(cmdline));
		snprintf(cmdline + cmdline_len, sizeof(cmdline) - cmdline_len,
			 " %s", arg);
	}

	/*
	 * Create a pipe that we will use to read data from the child process.
	 * The writing side needs to be inheritable such that the child can use
	 * it as stdout and stderr. The reading side should only be used by the
	 * parent.
	 */
	security_attributes.nLength = sizeof(security_attributes);
	security_attributes.bInheritHandle = TRUE;
	cl_assert_equal_b(1, CreatePipe(&stdout_read, &stdout_write, &security_attributes, 0));
	cl_assert_equal_b(1, SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0));

	/*
	 * Create the child process with our pipe.
	 */
	startup_info.cb = sizeof(startup_info);
	startup_info.hStdError = stdout_write;
	startup_info.hStdOutput = stdout_write;
	startup_info.dwFlags |= STARTF_USESTDHANDLES;
	cl_assert_equal_b(1, CreateProcess(binary_path, cmdline, NULL, NULL, TRUE,
					   0, NULL, NULL, &startup_info, &process_info));
	cl_assert_equal_b(1, CloseHandle(stdout_write));

	output = read_full(stdout_read, 1);
	cl_assert_equal_b(1, CloseHandle(stdout_read));
	cl_assert_equal_b(1, GetExitCodeProcess(process_info.hProcess, &exit_code));
	cl_assert_equal_i(exit_code, expected_error_code);

	return output;
}

static void assert_output(const char *suite, const char *expected_output_file, int expected_error_code, ...)
{
	char *expected_output = NULL;
	char *output = NULL;
	const char *args[16];
	va_list ap;
	size_t i;

	va_start(ap, expected_error_code);
	for (i = 0; ; i++) {
		const char *arg = va_arg(ap, const char *);
		if (!arg)
			break;
		cl_assert(i < sizeof(args) / sizeof(*args));
		args[i] = arg;
	}
	va_end(ap);

	output = execute(suite, expected_error_code, args, i);
	expected_output = read_file(cl_fixture(expected_output_file));
	cl_assert_equal_s(output, expected_output);

	free(expected_output);
	free(output);
}

#else
# include <errno.h>
# include <fcntl.h>
# include <limits.h>
# include <unistd.h>
# include <sys/wait.h>

static char *read_full(int fd)
{
	size_t data_bytes = 0;
	char *data = NULL;

	while (1) {
		char buf[4096];
		ssize_t n;

		n = read(fd, buf, sizeof(buf));
		if (n < 0) {
			if (errno == EAGAIN || errno == EINTR)
				continue;
			cl_fail("Failed reading from child process.");
		}
		if (!n)
			break;

		data = realloc(data, data_bytes + n);
		cl_assert(data);

		memcpy(data + data_bytes, buf, n);
		data_bytes += n;
	}

	data = realloc(data, data_bytes + 1);
	cl_assert(data);
	data[data_bytes] = '\0';

	return data;
}

static char *read_file(const char *path)
{
	char *data;
	int fd;

	fd = open(path, O_RDONLY);
	if (fd < 0)
		cl_fail("Failed reading expected file.");

	data = read_full(fd);
	cl_must_pass(close(fd));

	return data;
}

static char *execute(const char *suite, int expected_error_code, const char **args, size_t nargs)
{
	int pipe_fds[2];
	pid_t pid;

	cl_must_pass(pipe(pipe_fds));

	pid = fork();
	if (!pid) {
		const char *final_args[17] = { NULL };
		char binary_path[4096];
		size_t len = 0;
		size_t i;

		cl_assert(nargs < sizeof(final_args) / sizeof(*final_args));
		final_args[0] = suite;
		for (i = 0; i < nargs; i++)
			final_args[i + 1] = args[i];

		if (dup2(pipe_fds[1], STDOUT_FILENO) < 0 ||
		    dup2(pipe_fds[1], STDERR_FILENO) < 0 ||
		    close(0) < 0 ||
		    close(pipe_fds[0]) < 0 ||
		    close(pipe_fds[1]) < 0)
			exit(1);

		cl_assert(len + strlen(selftest_suite_directory) < sizeof(binary_path));
		strcpy(binary_path, selftest_suite_directory);
		len += strlen(selftest_suite_directory);

		cl_assert(len + 1 < sizeof(binary_path));
		binary_path[len] = '/';
		len += 1;

		cl_assert(len + strlen(suite) < sizeof(binary_path));
		strcpy(binary_path + len, suite);
		len += strlen(suite);

		cl_assert(len + strlen("_suite") < sizeof(binary_path));
		strcpy(binary_path + len, "_suite");
		len += strlen("_suite");

		binary_path[len] = '\0';

		execv(binary_path, (char **) final_args);
		exit(1);
	} else if (pid > 0) {
		pid_t waited_pid;
		char *output;
		int stat;

		cl_must_pass(close(pipe_fds[1]));

		output = read_full(pipe_fds[0]);

		waited_pid = waitpid(pid, &stat, 0);
		cl_assert_equal_i(pid, waited_pid);
		cl_assert(WIFEXITED(stat));
		cl_assert_equal_i(WEXITSTATUS(stat), expected_error_code);

		return output;
	} else {
		cl_fail("Fork failed.");
	}

	return NULL;
}

static void assert_output(const char *suite, const char *expected_output_file, int expected_error_code, ...)
{
	char *expected_output, *output;
	const char *args[16];
	va_list ap;
	size_t i;

	va_start(ap, expected_error_code);
	for (i = 0; ; i++) {
		cl_assert(i < sizeof(args) / sizeof(*args));
		args[i] = va_arg(ap, const char *);
		if (!args[i])
			break;
	}
	va_end(ap);

	output = execute(suite, expected_error_code, args, i);
	expected_output = read_file(cl_fixture(expected_output_file));
	cl_assert_equal_s(output, expected_output);

	free(expected_output);
	free(output);
}
#endif

void test_selftest__help(void)
{
	cl_invoke(assert_output("combined", "help", 1, "-h", NULL));
}

void test_selftest__without_arguments(void)
{
	cl_invoke(assert_output("combined", "without_arguments", 9, NULL));
}

void test_selftest__specific_test(void)
{
	cl_invoke(assert_output("combined", "specific_test", 1, "-scombined::bool", NULL));
}

void test_selftest__stop_on_failure(void)
{
	cl_invoke(assert_output("combined", "stop_on_failure", 1, "-Q", NULL));
}

void test_selftest__quiet(void)
{
	cl_invoke(assert_output("combined", "quiet", 9, "-q", NULL));
}

void test_selftest__tap(void)
{
	cl_invoke(assert_output("combined", "tap", 9, "-t", NULL));
}

void test_selftest__suite_names(void)
{
	cl_invoke(assert_output("combined", "suite_names", 0, "-l", NULL));
}

void test_selftest__summary_without_filename(void)
{
	struct stat st;
	cl_invoke(assert_output("combined", "summary_without_filename", 9, "-r", NULL));
	/* The summary contains timestamps, so we cannot verify its contents. */
	cl_must_pass(stat("summary.xml", &st));
}

void test_selftest__summary_with_filename(void)
{
	struct stat st;
	cl_invoke(assert_output("combined", "summary_with_filename", 9, "-rdifferent.xml", NULL));
	/* The summary contains timestamps, so we cannot verify its contents. */
	cl_must_pass(stat("different.xml", &st));
}

void test_selftest__pointer_equal(void)
{
	const char *args[] = {
		"-spointer::equal",
		"-t"
	};
	char *output = execute("pointer", 0, args, 2);
	cl_assert_equal_s(output,
		   "TAP version 13\n"
		   "# start of suite 1: pointer\n"
		   "ok 1 - pointer::equal\n"
		   "1..1\n"
	);
	free(output);
}

void test_selftest__pointer_unequal(void)
{
	const char *args[] = {
		"-spointer::unequal",
	};
	char *output = execute("pointer", 1, args, 1);
	cl_assert(output);
	cl_assert(strstr(output, "Pointer mismatch: "));
	free(output);
}
