/*
 * Copyright (c) 2013, Red Hat Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *     * Redistributions of source code must retain the above
 *       copyright notice, this list of conditions and the
 *       following disclaimer.
 *     * Redistributions in binary form must reproduce the
 *       above copyright notice, this list of conditions and
 *       the following disclaimer in the documentation and/or
 *       other materials provided with the distribution.
 *     * The names of contributors to this software may not be
 *       used to endorse or promote products derived from this
 *       software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 *
 * Author: Stef Walter <stefw@redhat.com>
 */

#include "config.h"

#define P11_TEST_SOURCE 1

#include "compat.h"
#include "test.h"
#include "debug.h"
#include "path.h"

#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef OS_UNIX
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/wait.h>
#endif

enum {
	FIXTURE,
	TEST,
};

typedef void (*func_with_arg) (void *);

typedef struct _test_item {
	int type;

	union {
		struct {
			char name[1024];
			func_with_arg func;
			void *argument;
			bool failed;
		} test;
		struct {
			func_with_arg setup;
			func_with_arg teardown;
		} fix;
	} x;

	struct _test_item *next;
} test_item;

struct {
	test_item *suite;
	test_item *last;
	int number;
	jmp_buf jump;
} gl = { NULL, NULL, 0, };

static void
print_diagnostics (const char *filename,
		   int line,
		   const char *function,
		   char *output)
{
	const char *pos;
	char *from;
	char *next;

	for (from = output; from != NULL; ) {
		next = strchr (from, '\n');
		if (next) {
			next[0] = '\0';
			next += 1;
		}

		printf ("# %s\n", from);
		from = next;
	}

	pos = strrchr (filename, '/');
	if (pos != NULL && pos[1] != '\0')
		filename = pos + 1;

	printf ("# in %s() at %s:%d\n", function, filename, line);
}

void
p11_test_fail (const char *filename,
               int line,
               const char *function,
               const char *message,
               ...)
{
	char *output;
	va_list va;

	assert (gl.last != NULL);
	assert (gl.last->type == TEST);
	gl.last->x.test.failed = true;

	printf ("not ok %d %s\n", gl.number, gl.last->x.test.name);

	va_start (va, message);
	if (vasprintf (&output, message, va) < 0)
		assert (0 && "vasprintf() failed");
	va_end (va);

	print_diagnostics (filename, line, function, output);
	free (output);

	/* Let coverity know we're not supposed to return from here */
#ifdef __COVERITY__
	abort();
#endif

	longjmp (gl.jump, 1);
}

void
p11_test_skip (const char *filename,
               int line,
               const char *function,
               const char *message,
               ...)
{
	char *output;
	char *pos;
	va_list va;

	assert (gl.last != NULL);
	assert (gl.last->type == TEST);

	printf ("ok %d %s", gl.number, gl.last->x.test.name);

	va_start (va, message);
	if (vasprintf (&output, message, va) < 0)
		assert (0 && "vasprintf() failed");
	va_end (va);

	pos = strchr (output, '\n');
	if (pos) {
		*pos = '\0';
		pos++;
	}
	printf (" # SKIP %s\n", output);

	if (pos)
		print_diagnostics (filename, line, function, pos);
	free (output);

	/* Let coverity know we're not supposed to return from here */
#ifdef __COVERITY__
	abort();
#endif

	longjmp (gl.jump, 1);
}

void
p11_test_todo (const char *filename,
               int line,
               const char *function,
               const char *message,
               ...)
{
	char *output;
	char *pos;
	va_list va;

	assert (gl.last != NULL);
	assert (gl.last->type == TEST);

	printf ("not ok %d %s", gl.number, gl.last->x.test.name);

	va_start (va, message);
	if (vasprintf (&output, message, va) < 0)
		assert (0 && "vasprintf() failed");
	va_end (va);

	pos = strchr (output, '\n');
	if (pos) {
		*pos = '\0';
		pos++;
	}
	printf (" # TODO %s\n", output);

	if (pos)
		print_diagnostics (filename, line, function, pos);
	free (output);

	/* Let coverity know we're not supposed to return from here */
#ifdef __COVERITY__
	abort();
#endif

	longjmp (gl.jump, 1);
}

static void
test_push (test_item *it)
{
	test_item *item;

	item = calloc (1, sizeof (test_item));
	assert (item != NULL);
	memcpy (item, it, sizeof (test_item));

	if (!gl.suite)
		gl.suite = item;
	if (gl.last)
		gl.last->next = item;
	gl.last = item;
}

void
p11_test (void (* function) (void),
          const char *name,
          ...)
{
	test_item item = { TEST, };
	va_list va;

	item.x.test.func = (func_with_arg)function;

	va_start (va, name);
	vsnprintf (item.x.test.name, sizeof (item.x.test.name), name, va);
	va_end (va);

	test_push (&item);
}

void
p11_testx (void (* function) (void *),
           void *argument,
           const char *name,
           ...)
{
	test_item item = { TEST, };
	va_list va;

	item.type = TEST;
	item.x.test.func = function;
	item.x.test.argument = argument;

	va_start (va, name);
	vsnprintf (item.x.test.name, sizeof (item.x.test.name), name, va);
	va_end (va);

	test_push (&item);
}

void
p11_fixture (void (* setup) (void *),
             void (* teardown) (void *))
{
	test_item item;

	item.type = FIXTURE;
	item.x.fix.setup = setup;
	item.x.fix.teardown = teardown;

	test_push (&item);
}

static int
should_run_test (int argc,
                 char **argv,
                 test_item *item)
{
	int i;
	if (argc == 0)
		return 1;
	for (i = 0; i < argc; i++) {
		if (strcmp (argv[i], item->x.test.name) == 0)
			return 1;
	}

	return 0;
}

int
p11_test_run (int argc,
              char **argv)
{
	test_item *fixture = NULL;
	test_item *item;
	test_item *next;
	int count;
	int ret = 0;
	int setup;
	int opt;

	/* p11-kit specific stuff */
	putenv ("P11_KIT_STRICT=1");
	p11_debug_init ();

	while ((opt = getopt (argc, argv, "")) != -1) {
		switch (opt) {
		default:
			fprintf (stderr, "specify only test names on the command line\n");
			return 2;
		}
	}

	argc -= optind;
	argv += optind;

	assert (gl.number == 0);
	gl.last = NULL;

	for (item = gl.suite, count = 0; item != NULL; item = item->next) {
		if (item->type == TEST && should_run_test (argc, argv, item))
			count++;
	}

	if (count == 0) {
		printf ("1..0 # No tests\n");
		return 0;
	}

	printf ("1..%d\n", count);

	for (item = gl.suite, gl.number = 0; item != NULL; item = item->next) {
		if (item->type == FIXTURE) {
			fixture = item;
			continue;
		}

		assert (item->type == TEST);

		if (!should_run_test (argc, argv, item))
			continue;

		gl.last = item;
		gl.number++;
		setup = 0;

		if (setjmp (gl.jump) == 0) {
			if (fixture && fixture->x.fix.setup)
				(fixture->x.fix.setup) (item->x.test.argument);

			setup = 1;

			assert (item->x.test.func);
			(item->x.test.func)(item->x.test.argument);

			printf ("ok %d %s\n", gl.number, item->x.test.name);
		}

		if (setup) {
			if (setjmp (gl.jump) == 0) {
				if (fixture && fixture->x.fix.teardown)
					(fixture->x.fix.teardown) (item->x.test.argument);
			}
		}

		gl.last = NULL;
	}

	for (item = gl.suite; item != NULL; item = next) {
		if (item->type == TEST) {
			if (item->x.test.failed)
				ret++;
		}

		next = item->next;
		free (item);
	}

	gl.suite = NULL;
	gl.last = 0;
	gl.number = 0;
	return ret;
}

static char *
expand_tempdir (const char *name)
{
	const char *env;

	env = secure_getenv ("TMPDIR");
	if (env && env[0]) {
		return p11_path_build (env, name, NULL);

	} else {
#ifdef OS_UNIX
#ifdef _PATH_TMP
		return p11_path_build (_PATH_TMP, name, NULL);
#else
		return p11_path_build ("/tmp", name, NULL);
#endif

#else /* OS_WIN32 */
		char directory[MAX_PATH + 1];

		if (!GetTempPathA (MAX_PATH + 1, directory)) {
			printf ("# couldn't lookup temp directory\n");
			errno = ENOTDIR;
			return NULL;
		}

		return p11_path_build (directory, name, NULL);

#endif /* OS_WIN32 */
	}
}

char *
p11_test_directory (const char *prefix)
{
	char *templ;
	char *directory;

	if (asprintf (&templ, "%s.XXXXXX", prefix) < 0)
		assert_not_reached ();

	directory = expand_tempdir (templ);
	assert (directory != NULL);

	if (!mkdtemp (directory)) {
		printf ("# couldn't create temp directory: %s: %s\n",
		        directory, strerror (errno));
		free (directory);
		assert_not_reached ();
		return NULL;
	}

	free (templ);
	return directory;
}

void
p11_test_file_write (const char *base,
                     const char *name,
                     const void *contents,
                     size_t length)
{
	char *path = NULL;
	FILE *f;

	if (base) {
		if (asprintf (&path, "%s/%s", base, name) < 0)
			assert_not_reached ();
		name = path;
	}

	f = fopen (name, "wb");
	if (f == NULL) {
		printf ("# couldn't open file for writing: %s: %s\n", name, strerror (errno));
		assert_not_reached ();
	}

	if (fwrite (contents, 1, length, f) != length ||
	    fclose (f) != 0) {
		printf ("# couldn't write to file: %s: %s\n", name, strerror (errno));
		assert_not_reached ();
	}

	free (path);
}

void
p11_test_file_delete (const char *base,
                      const char *name)
{
	char *path = NULL;

	if (base) {
		if (asprintf (&path, "%s/%s", base, name) < 0)
			assert_not_reached ();
		name = path;
	}

	if (unlink (name) < 0) {
		printf ("# Couldn't delete file: %s\n", name);
		assert_not_reached ();
	}

	free (path);
}

void
p11_test_directory_delete (const char *directory)
{
	struct dirent *dp;
	DIR *dir;
	char *path;
	struct stat st;

	dir = opendir (directory);
	if (dir == NULL) {
		printf ("# Couldn't open directory: %s\n", directory);
		assert_not_reached ();
	}

	while ((dp = readdir (dir)) != NULL) {
		if (strcmp (dp->d_name, ".") == 0 ||
		    strcmp (dp->d_name, "..") == 0)
			continue;

		if (asprintf (&path, "%s/%s", directory, dp->d_name) < 0)
			assert_not_reached ();
		if (stat (path, &st) < 0)
			assert_not_reached ();
		if (S_ISDIR (st.st_mode))
			p11_test_directory_delete (path);
		else
			p11_test_file_delete (directory, dp->d_name);
		free (path);
	}

	closedir (dir);

	if (rmdir (directory) < 0) {
		printf ("# Couldn't remove directory: %s\n", directory);
		assert_not_reached ();
	}
}


#ifdef OS_UNIX

static void
copy_file (const char *input,
           int fd)
{
	p11_mmap *mmap;
	const char *data;
	ssize_t written;
	size_t size;

	mmap = p11_mmap_open (input, NULL, (void **)&data, &size);
	assert (mmap != NULL);

	while (size > 0) {
		written = write (fd, data, size);
		assert (written >= 0);

		data += written;
		size -= written;
	}

	p11_mmap_close (mmap);
}

char *
p11_test_copy_setgid (const char *input,
		      const char *tmpdir)
{
	gid_t groups[128];
		char *path;
		gid_t group = 0;
		int ret;
		int fd;
		int i;
	struct statvfs f;

	statvfs (tmpdir, &f);
	if (f.f_flag & ST_NOSUID) {
		fprintf (stderr, "cannot perform setgid test on nosuid filesystem\n");
		return NULL;
	}

	ret = getgroups (128, groups);
	for (i = 0; i < ret; ++i) {
		if (groups[i] != getgid ()) {
			group = groups[i];
			break;
		}
	}
	if (i == ret) {
		fprintf (stderr, "# no suitable group, skipping test\n");
		return NULL;
	}

	if (asprintf(&path, "%s/test-setgid.XXXXXX", tmpdir) < 0)
		assert_not_reached();

	assert (path != NULL);

	fd = mkstemp (path);
	assert (fd >= 0);

	copy_file (input, fd);
	if (fchown (fd, getuid (), group) < 0)
		assert_not_reached ();
	if (fchmod (fd, 02750) < 0)
		assert_not_reached ();
	if (close (fd) < 0)
		assert_not_reached ();

	return path;
}

int
p11_test_run_child (const char **argv,
                    bool quiet_out)
{
	pid_t child;
	int status;

	child = fork ();
	assert (child >= 0);

	/* In the child process? */
	if (child == 0) {
		if (quiet_out)
			close (1); /* stdout */
		execv (argv[0], (char **)argv);
		assert_not_reached ();
	}

	if (waitpid (child, &status, 0) < 0)
		assert_not_reached ();

	assert (!WIFSIGNALED (status));
	assert (WIFEXITED (status));

	return WEXITSTATUS (status);
}

#endif /* OS_UNIX */
