/*
 * 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/job.h>
#include <isc/lib.h>
#include <isc/loop.h>
#include <isc/ratelimiter.h>
#include <isc/time.h>

#include "ratelimiter.c"

#include <tests/isc.h>

isc_ratelimiter_t *rl = NULL;

typedef struct rlstat {
	isc_rlevent_t *event;
} rlstat_t;

ISC_LOOP_TEST_IMPL(ratelimiter_create) {
	assert_null(rl);
	expect_assert_failure(isc_ratelimiter_create(NULL, &rl));
	expect_assert_failure(isc_ratelimiter_create(isc_loop_main(), NULL));
	assert_null(rl);

	isc_ratelimiter_create(isc_loop_main(), &rl);
	isc_ratelimiter_shutdown(rl);
	isc_ratelimiter_detach(&rl);

	isc_loopmgr_shutdown();
}

ISC_LOOP_TEST_IMPL(ratelimiter_shutdown) {
	assert_null(rl);
	expect_assert_failure(isc_ratelimiter_shutdown(NULL));
	expect_assert_failure(isc_ratelimiter_shutdown(rl));

	isc_loopmgr_shutdown();
}

ISC_LOOP_TEST_IMPL(ratelimiter_detach) {
	assert_null(rl);

	expect_assert_failure(isc_ratelimiter_detach(NULL));
	expect_assert_failure(isc_ratelimiter_detach(&rl));

	isc_loopmgr_shutdown();
}

static int ticks = 0;
static isc_time_t start_time;
static isc_time_t tick_time;

static void
tick(void *arg) {
	rlstat_t *rlstat = (rlstat_t *)arg;

	isc_rlevent_free(&rlstat->event);
	isc_mem_put(isc_g_mctx, rlstat, sizeof(*rlstat));

	ticks++;

	tick_time = isc_time_now();

	isc_ratelimiter_shutdown(rl);
	isc_ratelimiter_detach(&rl);

	isc_loopmgr_shutdown();
}

ISC_LOOP_SETUP_IMPL(ratelimiter_common) {
	assert_null(rl);
	isc_time_set(&tick_time, 0, 0);
	start_time = isc_time_now();
	isc_ratelimiter_create(isc_loop_main(), &rl);
}

ISC_LOOP_SETUP_IMPL(ratelimiter_enqueue) {
	ticks = 0;
	setup_loop_ratelimiter_common(arg);
}

ISC_LOOP_TEARDOWN_IMPL(ratelimiter_enqueue) { assert_int_equal(ticks, 1); }

ISC_LOOP_TEST_SETUP_TEARDOWN_IMPL(ratelimiter_enqueue) {
	isc_result_t result;
	rlstat_t *rlstat = isc_mem_get(isc_g_mctx, sizeof(*rlstat));
	*rlstat = (rlstat_t){ 0 };

	result = isc_ratelimiter_enqueue(rl, isc_loop_main(), tick, rlstat,
					 &rlstat->event);
	assert_int_equal(result, ISC_R_SUCCESS);
	assert_non_null(rlstat->event);
}

ISC_LOOP_SETUP_IMPL(ratelimiter_enqueue_shutdown) {
	ticks = 0;
	setup_loop_ratelimiter_common(arg);
}

ISC_LOOP_TEARDOWN_IMPL(ratelimiter_enqueue_shutdown) {
	assert_int_equal(ticks, 1);
}

ISC_LOOP_TEST_SETUP_TEARDOWN_IMPL(ratelimiter_enqueue_shutdown) {
	isc_rlevent_t *event = NULL;
	rlstat_t *rlstat = isc_mem_get(isc_g_mctx, sizeof(*rlstat));
	*rlstat = (rlstat_t){ 0 };

	expect_assert_failure(isc_ratelimiter_enqueue(NULL, isc_loop_main(),
						      tick, NULL, &event));
	expect_assert_failure(
		isc_ratelimiter_enqueue(rl, NULL, tick, NULL, &event));
	expect_assert_failure(
		isc_ratelimiter_enqueue(rl, isc_loop_main(), tick, NULL, NULL));

	assert_int_equal(isc_ratelimiter_enqueue(rl, isc_loop_main(), tick,
						 rlstat, &rlstat->event),
			 ISC_R_SUCCESS);
	assert_non_null(rlstat->event);

	isc_ratelimiter_shutdown(rl);

	assert_int_equal(isc_ratelimiter_enqueue(rl, isc_loop_main(), tick,
						 NULL, &event),
			 ISC_R_SHUTTINGDOWN);
	assert_null(event);
}

ISC_LOOP_SETUP_IMPL(ratelimiter_dequeue) {
	ticks = 0;
	setup_loop_ratelimiter_common(arg);
}

ISC_LOOP_TEARDOWN_IMPL(ratelimiter_dequeue) { /* */
	assert_int_equal(ticks, 0);
}

ISC_LOOP_TEST_SETUP_TEARDOWN_IMPL(ratelimiter_dequeue) {
	isc_rlevent_t *fake = isc_mem_get(isc_g_mctx, sizeof(*fake));
	rlstat_t *rlstat = isc_mem_get(isc_g_mctx, sizeof(*rlstat));
	*rlstat = (rlstat_t){ 0 };

	assert_int_equal(isc_ratelimiter_enqueue(rl, isc_loop_main(), tick,
						 rlstat, &rlstat->event),
			 ISC_R_SUCCESS);
	assert_int_equal(isc_ratelimiter_dequeue(rl, &rlstat->event),
			 ISC_R_SUCCESS);
	isc_mem_put(isc_g_mctx, rlstat, sizeof(*rlstat));

	/* Set up a mock ratelimiter event that isn't actually scheduled */
	*fake = (isc_rlevent_t){ .link = ISC_LINK_INITIALIZER };
	isc_loop_attach(isc_loop_main(), &fake->loop);
	isc_ratelimiter_attach(rl, &fake->rl);
	assert_int_equal(isc_ratelimiter_dequeue(rl, &fake), ISC_R_NOTFOUND);
	isc_loop_detach(&fake->loop);
	isc_ratelimiter_detach(&fake->rl);
	isc_mem_put(isc_g_mctx, fake, sizeof(*fake));

	isc_ratelimiter_shutdown(rl);
	isc_ratelimiter_detach(&rl);

	isc_loopmgr_shutdown();
}

static isc_time_t tock_time;

static void
tock(void *arg) {
	rlstat_t *rlstat = (rlstat_t *)arg;

	isc_rlevent_free(&rlstat->event);
	isc_mem_put(isc_g_mctx, rlstat, sizeof(*rlstat));

	ticks++;
	tock_time = isc_time_now();
}

ISC_LOOP_SETUP_IMPL(ratelimiter_pertick_interval) {
	ticks = 0;
	isc_time_set(&tock_time, 0, 0);
	setup_loop_ratelimiter_common(arg);
}

ISC_LOOP_TEARDOWN_IMPL(ratelimiter_pertick_interval) {
	uint64_t t = isc_time_microdiff(&tick_time, &tock_time);
	assert_int_equal(ticks, 2);
	assert_true(t >= 1000000);

	t = isc_time_microdiff(&tock_time, &start_time);
	assert_true(t < 1000000);
}

ISC_LOOP_TEST_SETUP_TEARDOWN_IMPL(ratelimiter_pertick_interval) {
	rlstat_t *rlstat = NULL;
	isc_interval_t interval;

	isc_interval_set(&interval, 1, NS_PER_SEC / 10);

	expect_assert_failure(isc_ratelimiter_setinterval(NULL, &interval));
	expect_assert_failure(isc_ratelimiter_setinterval(rl, NULL));
	expect_assert_failure(isc_ratelimiter_setpertic(NULL, 1));
	expect_assert_failure(isc_ratelimiter_setpertic(rl, 0));
	expect_assert_failure(isc_ratelimiter_setpushpop(NULL, false));

	isc_ratelimiter_setinterval(rl, &interval);
	isc_ratelimiter_setpertic(rl, 1);
	isc_ratelimiter_setpushpop(rl, false);

	rlstat = isc_mem_get(isc_g_mctx, sizeof(*rlstat));
	*rlstat = (rlstat_t){ 0 };
	assert_int_equal(isc_ratelimiter_enqueue(rl, isc_loop_main(), tock,
						 rlstat, &rlstat->event),
			 ISC_R_SUCCESS);

	rlstat = isc_mem_get(isc_g_mctx, sizeof(*rlstat));
	*rlstat = (rlstat_t){ 0 };
	assert_int_equal(isc_ratelimiter_enqueue(rl, isc_loop_main(), tick,
						 rlstat, &rlstat->event),
			 ISC_R_SUCCESS);
}

ISC_LOOP_SETUP_IMPL(ratelimiter_pushpop) {
	ticks = 0;
	isc_time_set(&tock_time, 0, 0);
	setup_loop_ratelimiter_common(arg);
}

ISC_LOOP_TEARDOWN_IMPL(ratelimiter_pushpop) {
	uint64_t t = isc_time_microdiff(&tock_time, &tick_time);
	assert_int_equal(ticks, 2);
	assert_true(t < 1000000);
}

ISC_LOOP_TEST_SETUP_TEARDOWN_IMPL(ratelimiter_pushpop) {
	rlstat_t *rlstat = NULL;
	isc_interval_t interval;

	isc_interval_set(&interval, 1, NS_PER_SEC / 10);

	isc_ratelimiter_setinterval(rl, &interval);
	isc_ratelimiter_setpertic(rl, 2);
	isc_ratelimiter_setpushpop(rl, true);

	rlstat = isc_mem_get(isc_g_mctx, sizeof(*rlstat));
	*rlstat = (rlstat_t){ 0 };
	assert_int_equal(isc_ratelimiter_enqueue(rl, isc_loop_main(), tock,
						 rlstat, &rlstat->event),
			 ISC_R_SUCCESS);

	rlstat = isc_mem_get(isc_g_mctx, sizeof(*rlstat));
	*rlstat = (rlstat_t){ 0 };
	assert_int_equal(isc_ratelimiter_enqueue(rl, isc_loop_main(), tick,
						 rlstat, &rlstat->event),
			 ISC_R_SUCCESS);
}

static int
setup_test(void **state) {
	int r;

	r = setup_loopmgr(state);
	if (r != 0) {
		return r;
	}

	return 0;
}

static int
teardown_test(void **state) {
	int r;

	r = teardown_loopmgr(state);
	if (r != 0) {
		return r;
	}

	return 0;
}

ISC_TEST_LIST_START

ISC_TEST_ENTRY_CUSTOM(ratelimiter_create, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(ratelimiter_shutdown, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(ratelimiter_detach, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(ratelimiter_enqueue, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(ratelimiter_enqueue_shutdown, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(ratelimiter_dequeue, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(ratelimiter_pertick_interval, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(ratelimiter_pushpop, setup_test, teardown_test)

ISC_TEST_LIST_END

ISC_TEST_MAIN
