/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2020-2023 Brett Sheffield <bacs@librecast.net> */

#include "test.h"
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/resource.h>
#include <unistd.h>

#define DEFAULT_TERM_COLS 80
#define TESTID_WIDTH 16
#define SPINAL_TAP 11

int COLS = DEFAULT_TERM_COLS;
int MSGW;
int test_status = TEST_OK; /* test exit status (not a count of failures) */
int capreqd = 0;
int capfail = 0;
sem_t log_lock;

static int _vscprintf (const char * format, va_list argp)
{
	int r;
	va_list argc;
	va_copy(argc, argp);
	r = vsnprintf(NULL, 0, format, argc);
	va_end(argc);
	return r;
}

void vfail_msg(char *msg, va_list argp)
{
	char *b;
	b = malloc(_vscprintf(msg, argp) + 1);
	pthread_cleanup_push(free, b);
	vsprintf(b, msg, argp);
	printf("\n%-*s%-*s", SPINAL_TAP, " ", MSGW, b);
	pthread_cleanup_pop(1); /* free(b); */
	test_status = TEST_FAIL;
}

static int test_assert_va(int condition, int verbose, char *msg, va_list argp)
{
	char *b;
	va_list copy;
	va_copy(copy, argp);
	b = malloc(_vscprintf(msg, argp) + 6);
	pthread_cleanup_push(free, b);
	if (b) vsprintf(b, msg, copy);
	if (!condition) {
		vfail_msg(msg, argp);
		if (b) test_log("[FAIL] %s\n", b);
	}
	else if (b && verbose) {
		test_log("[ OK ] %s\n", b);
	}
	pthread_cleanup_pop(1); /* free(b); */
	va_end(argp);
	return condition;
}

int test_assert(int condition, char *msg, ...)
{
	va_list argp;
	va_start(argp, msg);
	return test_assert_va(condition, 1, msg, argp);
}

/* assert quietly - only log on failure */
int test_assert_q(int condition, char *msg, ...)
{
	va_list argp;
	va_start(argp, msg);
	return test_assert_va(condition, 0, msg, argp);
}

int test_strcmp(char *str1, char *str2, char *msg, ...)
{
	if (str1 == NULL || str2 == NULL || strcmp(str1, str2)) {
		va_list argp;
		va_start(argp, msg);
		vfail_msg(msg, argp);
		va_end(argp);
		return 0;
	}
	return 1;
}

int test_strncmp(char *str1, char *str2, size_t len, char *msg, ...)
{
	if (str1 == NULL || str2 == NULL || strncmp(str1, str2, len)) {
		va_list argp;
		va_start(argp, msg);
		vfail_msg(msg, argp);
		va_end(argp);
		return 0;
	}
	return 1;
}

int test_expect(char *expected, char *got)
{
	return test_strcmp(expected, got, "expected: '%s', got: '%s'", expected, got);
}

int test_expectn(char *expected, char *got, size_t len)
{
	return test_strncmp(expected, got, len, "expected: '%s', got: '%s'", expected, got);
}

void test_log(char *msg, ...)
{
	char *b;
	va_list argp;
	va_start(argp, msg);
	b = malloc(_vscprintf(msg, argp) + 1);
	pthread_cleanup_push(free, b);
	vsprintf(b, msg, argp);

	/* In order to keep the test logs clean we try to wait for other threads
	 * to finish before logging, but if we need to wait more than a second,
	 * give up and log anyway. A deadlock between two test threads is best
	 * avoided. */
	struct timespec ts;
	clock_gettime(CLOCK_REALTIME, &ts);
	ts.tv_sec++;
	sem_timedwait(&log_lock, &ts);

	fprintf(stderr, "%s", b);
	fflush(stderr);
	sem_post(&log_lock);
	va_end(argp);
	pthread_cleanup_pop(1); /* free(b); */
}

static void init_terminal(void)
{
#if HAVE_SYS_IOCTL_H
	if (isatty(fileno(stdout))) {
		/* get terminal size */
		struct winsize w;
		COLS = (ioctl(fileno(stdout), TIOCGWINSZ, &w) != -1) ? w.ws_col : DEFAULT_TERM_COLS;
	}
	else COLS = DEFAULT_TERM_COLS;
#endif
	MSGW = COLS - TESTID_WIDTH;
}

void test_init(void)
{
	static int runonce;
	if (runonce++) return;
	init_terminal();
	sem_init(&log_lock, 0, 1);
}

void test_name(char *str, ...)
{
	char *b;
	va_list argp;
	int do_exit = 1;

	test_init();
	va_start(argp, str);
	b = malloc(_vscprintf(str, argp) + 1);
	vsprintf(b, str, argp);
	test_log("  %s\n", b);
	test_log("\n");
	if (capfail) {
		printf("%-*s", MSGW, "----- requires capabilities (skipping) -----");
	}
	else if (!capreqd && geteuid() == 0) {
		printf("%-*s", MSGW, "----- does not require root (skipping) -----");
	}
	else if (test_status != TEST_OK) {
		printf("%-*s", MSGW, "----- (skipping) -----");
	}
	else {
		printf("%-*s", MSGW, b);
		do_exit ^= do_exit;
	}
	fflush(stdout); /* ensure the test name is displayed in case of SIGABORT */
	va_end(argp);
	free(b);
	if (do_exit) exit(test_status);
}

int test_skip(char *str, ...)
{
	char *b;
	va_list argp;

	init_terminal();
	sem_init(&log_lock, 0, 1);
	va_start(argp, str);
	b = malloc(_vscprintf(str, argp) + 1);
	vsprintf(b, str, argp);
	printf("(skipped) %-*s", MSGW - 10, b);
	test_log("  %s\n", b);
	test_log("\n");
	va_end(argp);
	free(b);
	return TEST_WARN;
}

void test_rusage(void)
{
	struct rusage ru = {0};
	if (getrusage(RUSAGE_SELF, &ru)) {
		perror("getrusage");
		return;
	}
	test_log("user  :   %lis.%li\n", ru.ru_utime.tv_sec, ru.ru_utime.tv_usec);
	test_log("system:   %lis.%li\n", ru.ru_stime.tv_sec, ru.ru_stime.tv_usec);
	test_log("maxrss:   %li\n", ru.ru_maxrss);
	test_log("ixrss:    %li\n", ru.ru_ixrss);
	test_log("idrss:    %li\n", ru.ru_idrss);
	test_log("isrss:    %li\n", ru.ru_isrss);
	test_log("minflt:   %li\n", ru.ru_minflt);
	test_log("majflt:   %li\n", ru.ru_majflt);
	test_log("nswap:    %li\n", ru.ru_nswap);
	test_log("inblock:  %li\n", ru.ru_inblock);
	test_log("oublock:  %li\n", ru.ru_oublock);
	test_log("msgsnd:   %li\n", ru.ru_msgsnd);
	test_log("msgrcv:   %li\n", ru.ru_msgrcv);
	test_log("nsignals: %li\n", ru.ru_nsignals);
	test_log("nvcsw:    %li\n", ru.ru_nvcsw);
	test_log("nivcsw:   %li\n", ru.ru_nivcsw);
}

void test_cap_require(int cap)
{
	(void) cap;
	// TODO check for capabilities on Linux
	if (geteuid()) capfail++;
	capreqd++;
}

void test_require_linux(void)
{
#ifndef __linux__
	init_terminal();
	printf("%-*s", MSGW, "----- linux only (skipping) -----");
	exit(test_status);
#endif
}
