/*
 * Copyright (c) 2017-2024 Apple Inc. All rights reserved.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. The rights granted to you under the License
 * may not be used to create, or enable the creation or redistribution of,
 * unlawful or unlicensed copies of an Apple operating system, or to
 * circumvent, violate, or enable the circumvention or violation of, any
 * terms of an Apple operating system software license agreement.
 *
 * Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_END@
 */

/*
 * Testing Framework for skywalk wrappers and syscalls
 *
 * This version forks many subchildren and sequences them via
 * control messages on fd 3 (MPTEST_SEQ_FILENO)
 */

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <spawn.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <os/log.h>
#include <darwintest.h>
#include "skywalk_test_driver.h"
#include "skywalk_test_common.h"
#include "skywalk_test_utils.h"

static uint64_t skywalk_features;
extern char **environ;
bool skywalk_in_driver;
static struct timeval inittime;
static struct skywalk_mptest *curr_test;

#define test_exit(ecode)                                                                           \
	{                                                                                          \
	        T_LOG("%s:%d := Test exiting with error code %d\n", __func__, __LINE__, (ecode)); \
	        T_END;                                                                            \
	}

/*
 * Print extra stats (e.g. AQM, ARP) that doesn't have to be done by the child.
 */
static void
print_extra_stats(struct skywalk_mptest *test)
{
	struct protox *tp;

	skt_aqstatpr("feth0");  /* netstat -qq -I feth0 */
	skt_aqstatpr("feth1");  /* netstat -qq -I feth1 */
	if ((!strncmp(test->skt_testname, "xferrdudpping",
	    strlen("xferrdudpping")))) {
		skt_aqstatpr("rd0");  /* netstat -qq -I rd0 */
	}

	/* netstat -sp arp */
	tp = &protox[0];
	skt_printproto(tp, tp->pr_name);
}

void
skywalk_mptest_driver_SIGINT_handler(int sig)
{
	signal(sig, SIG_IGN);

	if (curr_test != NULL) {
		if (curr_test->skt_fini != NULL) {
			curr_test->skt_fini();
		}
	}

	exit(0);
}

static void
print_fsw_stats(void)
{
	struct sk_stats_flow_switch *sfsw;
	struct sk_stats_flow_switch *entry;
	size_t len;
	int ret;

	ret = sktu_get_nexus_flowswitch_stats(&sfsw, &len);
	assert(ret == 0);

	os_log(OS_LOG_DEFAULT, "Flowswitch stats\n");
	for (entry = sfsw; (void *)entry < (void *)sfsw + len; entry++) {
		uuid_string_t   uuid_str;
		uuid_unparse_upper(entry->sfs_nx_uuid, uuid_str);
		os_log(OS_LOG_DEFAULT, "%s: %s\n", entry->sfs_if_name, uuid_str);
		__fsw_stats_print(&entry->sfs_fsws);
	}
}

void
skywalk_mptest_driver_SIGABRT_handler(int s)
{
	print_fsw_stats();
}

void
skywalk_mptest_driver_init(void)
{
	size_t len;

	assert(!skywalk_in_driver); // only call init once

	skywalk_in_driver = true;

	/* Query the kernel for available skywalk features */
	len = sizeof(skywalk_features);
	sysctlbyname("kern.skywalk.features", &skywalk_features, &len, NULL, 0);

	gettimeofday(&inittime, NULL);

	curr_test = NULL;

	signal(SIGINT, skywalk_mptest_driver_SIGINT_handler);
	signal(SIGABRT, skywalk_mptest_driver_SIGABRT_handler);
}


static struct skywalk_mptest_check *sk_checks[] = {
	&skt_filternative_check,
	&skt_filtercompat_check,
};

bool
skywalk_mptest_supported(const char *name)
{
	int i;

	for (i = 0; i < sizeof(sk_checks) / sizeof(sk_checks[0]); i++) {
		if (strcmp(name, sk_checks[i]->skt_testname) == 0) {
			return sk_checks[i]->skt_supported();
		}
	}
	return TRUE;
}

int
skywalk_mptest_driver_run(struct skywalk_mptest *skt, bool ignoreskip)
{
	posix_spawnattr_t attrs;
	int error;
	int childfail;
	bool childarg;

	skywalk_mptest_driver_init();

	/* Initialize posix_spawn attributes */
	posix_spawnattr_init(&attrs);

	if ((error = posix_spawnattr_setflags(&attrs, POSIX_SPAWN_SETEXEC)) != 0) {
		T_LOG("posix_spawnattr_setflags: %s", strerror(error));
		test_exit(1);
	}

	/* Run Tests */
	T_LOG("Test \"%s\":\t%s", skt->skt_testname, skt->skt_testdesc);

	if ((skt->skt_required_features & skywalk_features) !=
	    skt->skt_required_features) {
		T_LOG("Required features: 0x%llx, actual features 0x%llx",
		    skt->skt_required_features, skywalk_features);
		if (!ignoreskip) {
			T_LOG("Test Result: SKIPPED\n-------------------");
			return 0;
		} else {
			T_LOG("Proceeding with skipped test");
		}
	}
	if (!skywalk_mptest_supported(skt->skt_testname)) {
		T_LOG("Test is not supported on this device");
		if (!ignoreskip) {
			T_LOG("Test Result: SKIPPED\n-------------------");
			return 0;
		} else {
			T_LOG("Proceeding with skipped test");
		}
	}
	if (skt->skt_init) {
		skt->skt_init();
	}
	curr_test = skt;

	pid_t pids[skt->skt_nchildren];
	int fds[skt->skt_nchildren];

	/* If the child arg isn't set, then set it */
	childarg = !skt->skt_argv[3];

	T_LOG("Spawning %d children", skt->skt_nchildren);

	for (int j = 0; j < skt->skt_nchildren; j++) {
		int pfd[2];
		char argbuf[11];

		if (childarg) {
			snprintf(argbuf, sizeof(argbuf), "%d", j);
			skt->skt_argv[3] = "--child";
			skt->skt_argv[4] = argbuf;
		}

		//T_LOG("Spawning:");
		//for (int k = 0; skt->skt_argv[k]; k++) {
		//	T_LOG(" %s", skt->skt_argv[k]);
		//}
		//T_LOG("\n");

		if (socketpair(AF_LOCAL, SOCK_STREAM, 0, pfd) == -1) {
			SKT_LOG("socketpair: %s", strerror(errno));
			test_exit(1);
		}

		/* Fork and exec child */
		if ((pids[j] = fork()) == -1) {
			SKT_LOG("fork: %s", strerror(errno));
			test_exit(1);
		}

		if (pids[j] == 0) {
			/* XXX If a parent process test init expects to share an fd with a
			 * child process, care must be taken to make sure this doesn't
			 * accidentally close it.  So far, no test does this.  Another
			 * option would be to pass an fd number to the child's argv instead
			 * of assuming the hard coded fd number.
			 */
			error = close(pfd[1]);
			assert(!error);
			dup2(pfd[0], MPTEST_SEQ_FILENO);
			if (pfd[0] != MPTEST_SEQ_FILENO) {
				error = close(pfd[0]);
				assert(!error);
			}

			int argc = 0;  /* XXX */
			int ret = skt->skt_main(argc, skt->skt_argv);
			exit(ret);
			/* Not reached */
			abort();
		}

		error = close(pfd[0]);
		assert(!error);
		int fdflags = fcntl(pfd[1], F_GETFD);
		if (fdflags == -1) {
			SKT_LOG("fcntl(F_GETFD): %s", strerror(errno));
			test_exit(1);
		}
		error = fcntl(pfd[1], F_SETFD, fdflags | FD_CLOEXEC);
		assert(!error);
		fds[j] = pfd[1];
	}

	/* Wait for input from children */
	for (int j = 0; j < skt->skt_nchildren; j++) {
		ssize_t ret;
		char buf[1];
		if ((ret = read(fds[j], buf, sizeof(buf))) == -1) {
			SKT_LOG("read: %s", strerror(errno));
			test_exit(1);
		}
		assert(ret == 1);
	}

	// sleep for debug introspection
	//T_LOG("sleep 100\n");
	//sleep(100);

	/* Send output to children */
	for (int j = 0; j < skt->skt_nchildren; j++) {
		ssize_t ret;
		char buf[1] = { 0 };
		if ((ret = write(fds[j], buf, sizeof(buf))) == -1) {
			SKT_LOG("write: %s", strerror(errno));
			test_exit(1);
		}
		assert(ret == 1);

		error = close(fds[j]);
		assert(!error);
	}

	childfail = 0;
	for (int j = 0; j < skt->skt_nchildren; j++) {
		int child;
		pid_t childpid;
		int child_status;

		/* Wait for children and check results */
		if ((childpid = wait(&child_status)) == -1) {
			SKT_LOG("wait: %s", strerror(errno));
			test_exit(1);
		}

		/* Which child was this? */
		for (child = 0; child < skt->skt_nchildren; child++) {
			if (childpid == pids[child]) {
				pids[child] = 0;
				break;
			}
		}
		if (child == skt->skt_nchildren) {
			T_LOG("Received unexpected child status from pid %d\n", childpid);
			test_exit(1);
		}

		if (WIFEXITED(child_status)) {
			if (WEXITSTATUS(child_status)) {
				T_LOG("Child %d (pid %d) exited with status %d",
				    child, childpid, WEXITSTATUS(child_status));
			}
		}
		if (WIFSIGNALED(child_status)) {
			T_LOG("Child %d (pid %d) signaled with signal %d coredump %d",
			    child, childpid, WTERMSIG(child_status), WCOREDUMP(child_status));
		}

		if (!WIFEXITED(child_status) || WEXITSTATUS(child_status)) {
			childfail++;
			/* Wait for a second and then send sigterm to remaining children
			 * in case they may be stuck
			 */
			if (childfail == 1) {
				print_extra_stats(skt);
				sleep(1);
				for (int k = 0; k < skt->skt_nchildren; k++) {
					if (pids[k]) {
						error = kill(pids[k], SIGTERM);
						if (error == 0) {
							T_LOG("Delivered SIGTERM to child %d pid %d",
							    k, pids[k]);
						} else if (errno != ESRCH) {
							SKT_LOG("kill(%d, SIGTERM): %s", pids[k], strerror(errno));
							test_exit(1);
						}
					}
				}
			}
		}
	}

	if (skt->skt_fini) {
		skt->skt_fini();
	}
	curr_test = NULL;

	if (childfail) {
		T_FAIL("Test %s: %s", skt->skt_testname, skt->skt_testdesc);
		if ((skt->skt_required_features &
		    SK_FEATURE_NEXUS_KERNEL_PIPE) != 0 &&
		    geteuid() != 0) {
			T_LOG("%sPlease try running test as root%s",
			    BOLD, NORMAL);
		}
	} else {
		T_PASS("Test %s: %s", skt->skt_testname, skt->skt_testdesc);
	}

	posix_spawnattr_destroy(&attrs);

	return 0;
}
