/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <inttypes.h>
#include <sched.h> /* IWYU pragma: keep */
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define UNIT_TESTING
#include <cmocka.h>

#include <isc/atomic.h>
#include <isc/commandline.h>
#include <isc/job.h>
#include <isc/lib.h>
#include <isc/loop.h>
#include <isc/mem.h>
#include <isc/os.h>
#include <isc/time.h>
#include <isc/timer.h>
#include <isc/util.h>

#include "timer.c"

#include <tests/isc.h>

/* Set to true (or use -v option) for verbose output */
static bool verbose = true;

#define FUDGE_SECONDS	  0	    /* in absence of clock_getres() */
#define FUDGE_NANOSECONDS 500000000 /* in absence of clock_getres() */

static isc_timer_t *timer = NULL;
static isc_time_t endtime;
static isc_mutex_t lasttime_mx;
static isc_time_t lasttime;
static int seconds;
static int nanoseconds;
static atomic_int_fast32_t eventcnt;
static atomic_uint_fast32_t errcnt;
static int nevents;

typedef struct setup_test_arg {
	isc_timertype_t timertype;
	isc_interval_t *interval;
	isc_job_cb action;
} setup_test_arg_t;

static void
setup_test_run(void *data) {
	isc_timertype_t timertype = ((setup_test_arg_t *)data)->timertype;
	isc_interval_t *interval = ((setup_test_arg_t *)data)->interval;
	isc_job_cb action = ((setup_test_arg_t *)data)->action;

	isc_mutex_lock(&lasttime_mx);
	lasttime = isc_time_now();
	UNLOCK(&lasttime_mx);

	isc_timer_create(isc_loop_main(), action, (void *)timertype, &timer);
	isc_timer_start(timer, timertype, interval);
}

static void
setup_test(isc_timertype_t timertype, isc_interval_t *interval,
	   isc_job_cb action) {
	setup_test_arg_t arg = { .timertype = timertype,
				 .interval = interval,
				 .action = action };

	isc_time_settoepoch(&endtime);
	atomic_init(&eventcnt, 0);

	isc_mutex_init(&lasttime_mx);

	atomic_store(&errcnt, ISC_R_SUCCESS);

	isc_loop_setup(isc_loop_main(), setup_test_run, &arg);
	isc_loopmgr_run();

	assert_int_equal(atomic_load(&errcnt), ISC_R_SUCCESS);

	isc_mutex_destroy(&lasttime_mx);
}

static void
set_global_error(isc_result_t result) {
	(void)atomic_compare_exchange_strong(
		&errcnt, &(uint_fast32_t){ ISC_R_SUCCESS }, result);
}

static void
subthread_assert_true(bool expected, const char *file, unsigned int line) {
	if (!expected) {
		printf("# %s:%u subthread_assert_true\n", file, line);
		set_global_error(ISC_R_UNEXPECTED);
	}
}
#define subthread_assert_true(expected) \
	subthread_assert_true(expected, __FILE__, __LINE__)

static void
subthread_assert_result_equal(isc_result_t result, isc_result_t expected,
			      const char *file, unsigned int line) {
	if (result != expected) {
		printf("# %s:%u subthread_assert_result_equal(%u != %u)\n",
		       file, line, (unsigned int)result,
		       (unsigned int)expected);
		set_global_error(result);
	}
}
#define subthread_assert_result_equal(observed, expected) \
	subthread_assert_result_equal(observed, expected, __FILE__, __LINE__)

static void
ticktock(void *arg) {
	isc_result_t result;
	isc_time_t now;
	isc_time_t base;
	isc_time_t ulim;
	isc_time_t llim;
	isc_interval_t interval;
	int tick = atomic_fetch_add(&eventcnt, 1);

	UNUSED(arg);

	if (verbose) {
		print_message("# tick %d\n", tick);
	}

	now = isc_time_now();

	isc_interval_set(&interval, seconds, nanoseconds);
	isc_mutex_lock(&lasttime_mx);
	result = isc_time_add(&lasttime, &interval, &base);
	isc_mutex_unlock(&lasttime_mx);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	isc_interval_set(&interval, FUDGE_SECONDS, FUDGE_NANOSECONDS);
	result = isc_time_add(&base, &interval, &ulim);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	result = isc_time_subtract(&base, &interval, &llim);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	subthread_assert_true(isc_time_compare(&llim, &now) <= 0);
	subthread_assert_true(isc_time_compare(&ulim, &now) >= 0);

	isc_interval_set(&interval, 0, 0);
	isc_mutex_lock(&lasttime_mx);
	result = isc_time_add(&now, &interval, &lasttime);
	isc_mutex_unlock(&lasttime_mx);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	if (atomic_load(&eventcnt) == nevents) {
		endtime = isc_time_now();
		isc_timer_destroy(&timer);
		isc_loopmgr_shutdown();
	}
}

/*
 * Individual unit tests
 */

/* timer type ticker */
ISC_RUN_TEST_IMPL(ticker) {
	isc_interval_t interval;

	UNUSED(state);

	nevents = 12;
	seconds = 0;
	nanoseconds = 500000000;

	isc_interval_set(&interval, seconds, nanoseconds);

	setup_test(isc_timertype_ticker, &interval, ticktock);
}

static void
test_idle(void *arg) {
	isc_result_t result;
	isc_time_t now;
	isc_time_t base;
	isc_time_t ulim;
	isc_time_t llim;
	isc_interval_t interval;
	int tick = atomic_fetch_add(&eventcnt, 1);

	UNUSED(arg);

	if (verbose) {
		print_message("# tick %d\n", tick);
	}

	now = isc_time_now();

	isc_interval_set(&interval, seconds, nanoseconds);
	isc_mutex_lock(&lasttime_mx);
	result = isc_time_add(&lasttime, &interval, &base);
	isc_mutex_unlock(&lasttime_mx);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	isc_interval_set(&interval, FUDGE_SECONDS, FUDGE_NANOSECONDS);
	result = isc_time_add(&base, &interval, &ulim);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	result = isc_time_subtract(&base, &interval, &llim);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	subthread_assert_true(isc_time_compare(&llim, &now) <= 0);
	subthread_assert_true(isc_time_compare(&ulim, &now) >= 0);

	isc_interval_set(&interval, 0, 0);
	isc_mutex_lock(&lasttime_mx);
	isc_time_add(&now, &interval, &lasttime);
	isc_mutex_unlock(&lasttime_mx);

	isc_timer_destroy(&timer);
	isc_loopmgr_shutdown();
}

/* timer type once idles out */
ISC_RUN_TEST_IMPL(once_idle) {
	isc_interval_t interval;

	UNUSED(state);

	nevents = 1;
	seconds = 1;
	nanoseconds = 200000000;

	isc_interval_set(&interval, seconds, nanoseconds);

	setup_test(isc_timertype_once, &interval, test_idle);
}

/* timer reset */
static void
test_reset(void *arg) {
	isc_result_t result;
	isc_time_t now;
	isc_time_t base;
	isc_time_t ulim;
	isc_time_t llim;
	isc_interval_t interval;
	int tick = atomic_fetch_add(&eventcnt, 1);

	UNUSED(arg);

	if (verbose) {
		print_message("# tick %d\n", tick);
	}

	/*
	 * Check expired time.
	 */

	now = isc_time_now();

	isc_interval_set(&interval, seconds, nanoseconds);
	isc_mutex_lock(&lasttime_mx);
	result = isc_time_add(&lasttime, &interval, &base);
	isc_mutex_unlock(&lasttime_mx);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	isc_interval_set(&interval, FUDGE_SECONDS, FUDGE_NANOSECONDS);
	result = isc_time_add(&base, &interval, &ulim);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	result = isc_time_subtract(&base, &interval, &llim);
	subthread_assert_result_equal(result, ISC_R_SUCCESS);

	subthread_assert_true(isc_time_compare(&llim, &now) <= 0);
	subthread_assert_true(isc_time_compare(&ulim, &now) >= 0);

	isc_interval_set(&interval, 0, 0);
	isc_mutex_lock(&lasttime_mx);
	isc_time_add(&now, &interval, &lasttime);
	isc_mutex_unlock(&lasttime_mx);

	if (tick < 2) {
		if (tick == 1) {
			isc_interval_set(&interval, seconds, nanoseconds);
			isc_timer_start(timer, isc_timertype_once, &interval);
		}
	} else {
		isc_timer_destroy(&timer);
		isc_loopmgr_shutdown();
	}
}

ISC_RUN_TEST_IMPL(reset) {
	isc_interval_t interval;

	UNUSED(state);

	nevents = 3;
	seconds = 0;
	nanoseconds = 750000000;

	isc_interval_set(&interval, seconds, nanoseconds);

	setup_test(isc_timertype_ticker, &interval, test_reset);
}

static atomic_bool startflag;
static isc_timer_t *tickertimer = NULL;
static isc_timer_t *oncetimer = NULL;

static void
tick_event(void *arg) {
	int tick;

	UNUSED(arg);

	if (!atomic_load(&startflag)) {
		if (verbose) {
			print_message("# tick_event %d\n", -1);
		}
		return;
	}

	tick = atomic_fetch_add(&eventcnt, 1);
	if (verbose) {
		print_message("# tick_event %d\n", tick);
	}

	/*
	 * On the first tick, purge all remaining tick events.
	 */
	if (tick == 0) {
		isc_timer_destroy(&tickertimer);
		isc_loopmgr_shutdown();
	}
}

static void
once_event(void *arg) {
	UNUSED(arg);

	if (verbose) {
		print_message("# once_event\n");
	}

	/*
	 * Allow task1 to start processing events.
	 */
	atomic_store(&startflag, true);

	isc_timer_destroy(&oncetimer);
}

ISC_LOOP_SETUP_IMPL(purge) {
	atomic_init(&eventcnt, 0);
	atomic_store(&errcnt, ISC_R_SUCCESS);
}

ISC_LOOP_TEARDOWN_IMPL(purge) {
	assert_int_equal(atomic_load(&errcnt), ISC_R_SUCCESS);
	assert_int_equal(atomic_load(&eventcnt), 1);
}

/* timer events purged */
ISC_LOOP_TEST_SETUP_TEARDOWN_IMPL(purge) {
	isc_interval_t interval;

	UNUSED(arg);

	if (verbose) {
		print_message("# purge_run\n");
	}

	atomic_init(&startflag, 0);
	seconds = 1;
	nanoseconds = 0;

	isc_interval_set(&interval, seconds, 0);

	tickertimer = NULL;
	isc_timer_create(isc_loop_main(), tick_event, NULL, &tickertimer);
	isc_timer_start(tickertimer, isc_timertype_ticker, &interval);

	oncetimer = NULL;

	isc_interval_set(&interval, (seconds * 2) + 1, 0);

	isc_timer_create(isc_loop_main(), once_event, NULL, &oncetimer);
	isc_timer_start(oncetimer, isc_timertype_once, &interval);
}

/*
 * Set of tests that check whether the rescheduling works as expected.
 */

isc_time_t timer_start;
isc_time_t timer_stop;
uint64_t timer_expect;
uint64_t timer_ticks;
isc_interval_t timer_interval;
isc_timertype_t timer_type;

ISC_LOOP_TEARDOWN_IMPL(timer_expect) {
	uint64_t diff = isc_time_microdiff(&timer_stop, &timer_start) / 1000000;
	assert_true(diff == timer_expect);
}

static void
timer_event(void *arg ISC_ATTR_UNUSED) {
	if (--timer_ticks == 0) {
		isc_timer_destroy(&timer);
		isc_loopmgr_shutdown();
		timer_stop = isc_loop_now(isc_loop());
	} else {
		isc_timer_start(timer, timer_type, &timer_interval);
	}
}

ISC_LOOP_SETUP_IMPL(reschedule_up) {
	timer_start = isc_loop_now(isc_loop());
	timer_expect = 1;
	timer_ticks = 1;
	timer_type = isc_timertype_once;
}

ISC_LOOP_TEST_CUSTOM_IMPL(reschedule_up, setup_loop_reschedule_up,
			  teardown_loop_timer_expect) {
	isc_timer_create(isc_loop_main(), timer_event, NULL, &timer);

	/* Schedule the timer to fire immediately */
	isc_interval_set(&timer_interval, 0, 0);
	isc_timer_start(timer, timer_type, &timer_interval);

	/* And then reschedule it to 1 second */
	isc_interval_set(&timer_interval, 1, 0);
	isc_timer_start(timer, timer_type, &timer_interval);
}

ISC_LOOP_SETUP_IMPL(reschedule_down) {
	timer_start = isc_loop_now(isc_loop());
	timer_expect = 0;
	timer_ticks = 1;
	timer_type = isc_timertype_once;
}

ISC_LOOP_TEST_CUSTOM_IMPL(reschedule_down, setup_loop_reschedule_down,
			  teardown_loop_timer_expect) {
	isc_timer_create(isc_loop_main(), timer_event, NULL, &timer);

	/* Schedule the timer to fire at 10 seconds */
	isc_interval_set(&timer_interval, 10, 0);
	isc_timer_start(timer, timer_type, &timer_interval);

	/* And then reschedule it fire immediately */
	isc_interval_set(&timer_interval, 0, 0);
	isc_timer_start(timer, timer_type, &timer_interval);
}

ISC_LOOP_SETUP_IMPL(reschedule_from_callback) {
	timer_start = isc_loop_now(isc_loop());
	timer_expect = 1;
	timer_ticks = 2;
	timer_type = isc_timertype_once;
}

ISC_LOOP_TEST_CUSTOM_IMPL(reschedule_from_callback,
			  setup_loop_reschedule_from_callback,
			  teardown_loop_timer_expect) {
	isc_timer_create(isc_loop_main(), timer_event, NULL, &timer);

	isc_interval_set(&timer_interval, 0, NS_PER_SEC / 2);
	isc_timer_start(timer, timer_type, &timer_interval);
}

ISC_LOOP_SETUP_IMPL(zero) {
	timer_start = isc_loop_now(isc_loop());
	timer_expect = 0;
	timer_ticks = 1;
	timer_type = isc_timertype_once;
}

ISC_LOOP_TEST_CUSTOM_IMPL(zero, setup_loop_zero, teardown_loop_timer_expect) {
	isc_timer_create(isc_loop_main(), timer_event, NULL, &timer);

	/* Schedule the timer to fire immediately (in the next event loop) */
	isc_interval_set(&timer_interval, 0, 0);
	isc_timer_start(timer, timer_type, &timer_interval);
}

ISC_LOOP_SETUP_IMPL(reschedule_ticker) {
	timer_start = isc_loop_now(isc_loop());
	timer_expect = 1;
	timer_ticks = 5;
	timer_type = isc_timertype_ticker;
}

ISC_LOOP_TEST_CUSTOM_IMPL(reschedule_ticker, setup_loop_reschedule_ticker,
			  teardown_loop_timer_expect) {
	isc_timer_create(isc_loop_main(), timer_event, NULL, &timer);

	/* Schedule the timer to fire immediately (in the next event loop) */
	isc_interval_set(&timer_interval, 0, 0);
	isc_timer_start(timer, timer_type, &timer_interval);

	/* Then fire every 1/4 second */
	isc_interval_set(&timer_interval, 0, NS_PER_SEC / 4);
}

ISC_TEST_LIST_START

ISC_TEST_ENTRY_CUSTOM(ticker, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(once_idle, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(reset, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(purge, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(reschedule_up, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(reschedule_down, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(reschedule_from_callback, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(zero, setup_loopmgr, teardown_loopmgr)
ISC_TEST_ENTRY_CUSTOM(reschedule_ticker, setup_loopmgr, teardown_loopmgr)

ISC_TEST_LIST_END

ISC_TEST_MAIN
