/*
 * Copyright © 2013 Red Hat, Inc.
 * Copyright © 2013 Marcin Slusarz <marcin.slusarz@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "config.h"

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <getopt.h>
#include <libudev.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/sysinfo.h>
#include <sys/timerfd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#include "linux/input.h"
#ifdef HAVE_LIBSYSTEMD
#include <systemd/sd-bus.h>
#endif
#ifdef __FreeBSD__
#include <termios.h>
#endif

#include <valgrind/valgrind.h>

#include "util-backtrace.h"
#include "util-files.h"
#include "util-libinput.h"

#include "builddir.h"
#include "libinput-util.h"
#include "litest-int.h"
#include "litest-runner.h"
#include "litest.h"
#include "quirks.h"

static struct list all_test_suites = LIST_INIT(all_test_suites); /* struct suite */

static int jobs;
extern bool use_colors;
extern bool in_debugger;
extern bool verbose;
extern bool run_deviceless;
extern struct suite *current_suite;

static bool
is_debugger_attached(void)
{
	int status;
	bool rc;
	int pid = fork();

	if (pid == -1)
		return 0;

	if (pid == 0) {
		int ppid = getppid();
		if (ptrace(PTRACE_ATTACH, ppid, NULL, 0) == 0) {
			waitpid(ppid, NULL, 0);
			ptrace(PTRACE_CONT, ppid, NULL, 0);
			ptrace(PTRACE_DETACH, ppid, NULL, 0);
			rc = false;
		} else {
			rc = true;
		}
		_exit(rc);
	} else {
		waitpid(pid, &status, 0);
		rc = WEXITSTATUS(status);
	}

	return !!rc;
}

static void
litest_list_tests(struct list *tests)
{
	struct suite *s;
	const char *last_test_name = "<invalid>";
	const char *last_dev_name = "<invalid>";

	printf("groups:\n");
	list_for_each(s, tests, node) {
		struct test *t;
		printf("  - group: \"%s\"\n", s->name);
		printf("    tests:\n");
		list_for_each(t, &s->tests, node) {
			bool same_test = streq(last_test_name, t->name);
			bool same_dev = streq(last_dev_name, t->devname);

			if (!same_test) {
				printf("      - name: \"%s\"\n", t->name);
				printf("        devices:\n");
			}

			if (!same_test || !same_dev) {
				last_test_name = t->name;
				last_dev_name = t->devname;
				printf("          - name: \"%s\"\n", t->devname);
			}
		}
	}
}

extern const struct test_device __start_test_device_section, __stop_test_device_section;

static void
litest_init_test_devices(void)
{
	const struct test_device *t;
	const struct test_device *start = &__start_test_device_section;
	const struct test_device *stop = &__stop_test_device_section;

	for (t = start; t < stop; t++)
		litest_add_test_device(
			&t->device->node); /* NOLINT(clang-analyzer-security.ArrayBound)
					    */
}

extern const struct test_collection __start_test_collection_section,
	__stop_test_collection_section;

static void
setup_tests(void)
{
	/* Iterate through linker-provided section boundaries.
	 * These symbols mark the start and end of the test_collection_section. */
	const struct test_collection *start = &__start_test_collection_section;
	const struct test_collection *stop = &__stop_test_collection_section;
	const struct test_collection *c;

	for (c = start; c < stop;
	     c++) { /* NOLINT(clang-analyzer-security.ArrayBound) */
		struct suite *s;
		s = zalloc(sizeof(*s));
		s->name = safe_strdup(
			c->name); /* NOLINT(clang-analyzer-security.ArrayBound) */

		list_init(&s->tests);
		list_append(&all_test_suites, &s->node);

		current_suite = s;
		c->setup();
		current_suite = NULL;
	}
}

static int
check_device_access(void)
{
	if (getuid() != 0) {
		fprintf(stderr,
			"%s must be run as root.\n",
			program_invocation_short_name);
		return 77;
	}

	if (access("/dev/uinput", F_OK) == -1 &&
	    access("/dev/input/uinput", F_OK) == -1) {
		fprintf(stderr, "uinput device is missing, skipping tests.\n");
		return 77;
	}

	return 0;
}

static void
litest_free_test_list(struct list *tests)
{
	struct suite *s;

	list_for_each_safe(s, tests, node) {
		struct test *t;

		list_for_each_safe(t, &s->tests, node) {
			litest_test_parameters_unref(t->params);
			free(t->name);
			free(t->devname);
			list_remove(&t->node);
			free(t);
		}

		list_remove(&s->node);
		free(s->name);
		free(s);
	}
}

int
main(int argc, char **argv)
{
	enum litest_mode mode;
	int rc;
	const char *meson_testthreads;

	use_colors = getenv("FORCE_COLOR") || isatty(STDERR_FILENO);
	if (getenv("NO_COLOR"))
		use_colors = false;

	in_debugger = is_debugger_attached();
	if (in_debugger) {
		jobs = 0;
	} else if ((meson_testthreads = getenv("MESON_TESTTHREADS")) == NULL ||
		   !safe_atoi(meson_testthreads, &jobs)) {
		jobs = get_nprocs();
		if (!RUNNING_ON_VALGRIND)
			jobs *= 2;
	}

	if (getenv("LITEST_VERBOSE"))
		verbose = true;

	mode = litest_parse_argv(argc, argv, &jobs);
	if (mode == LITEST_MODE_ERROR)
		return EXIT_FAILURE;

	litest_init_test_devices();

	setup_tests();
	if (list_empty(&all_test_suites)) {
		fprintf(stderr, "Error: filters are too strict, no tests to run.\n");
		return EXIT_FAILURE;
	}

	if (mode == LITEST_MODE_LIST) {
		litest_list_tests(&all_test_suites);
		return EXIT_SUCCESS;
	}

	if (!run_deviceless && (rc = check_device_access()) != 0)
		return rc;

	enum litest_runner_result result = litest_run(&all_test_suites, jobs);

	litest_free_test_list(&all_test_suites);

	switch (result) {
	case LITEST_PASS:
		return EXIT_SUCCESS;
	case LITEST_SKIP:
		return 77;
	default:
		return result;
	}
}
