/* Test implementation for osmo_tdef API. */
/*
 * (C) 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 *
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * Author: Neels Hofmeyr <neels@hofmeyr.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <stdio.h>
#include <errno.h>
#include <limits.h>

#include <osmocom/core/logging.h>
#include <osmocom/core/application.h>
#include <osmocom/core/fsm.h>

#include <osmocom/core/tdef.h>

static void *ctx = NULL;

static struct osmo_tdef tdefs[] = {
	{ .T=1, .default_val=100, .desc="100s" },
	{ .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="100ms" },
	{ .T=3, .default_val=50, .unit=OSMO_TDEF_M, .desc="50m" },
	{ .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="100 potatoes" },

	{ .T=-5, .default_val=100, .unit=OSMO_TDEF_MS, .desc="X-100ms" },
	{ .T=-6, .default_val=100, .unit=OSMO_TDEF_US, .desc="X-100us" },

	{ .T=7, .default_val=50, .desc="Water Boiling Timeout", .min_val=20, .max_val=800 },  // default is .unit=OSMO_TDEF_S == 0
	{ .T=8, .default_val=300, .desc="Tea brewing" },
	{ .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" },
	{ .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" },

	/* test conversions */
	{ .T=1000, .default_val=2*1000, .unit=OSMO_TDEF_MS, .desc="two seconds from ms" },
	{ .T=1001, .default_val=60*1000, .unit=OSMO_TDEF_MS, .desc="one minute from ms" },
	{ .T=1004, .default_val=1, .unit=OSMO_TDEF_MS, .desc="one ms" },
	{ .T=1005, .default_val=0, .unit=OSMO_TDEF_MS, .desc="zero ms" },
	{ .T=1006, .default_val=0, .unit=OSMO_TDEF_S, .desc="zero s" },
	{ .T=1007, .default_val=0, .unit=OSMO_TDEF_M, .desc="zero m" },
	{ .T=1008, .default_val=0, .unit=OSMO_TDEF_CUSTOM, .desc="zero" },
	{ .T=1009, .default_val=0, .unit=OSMO_TDEF_US, .desc="zero us" },

	{ .T=0, .default_val=1, .unit=OSMO_TDEF_CUSTOM, .desc="zero" },

	/* no desc */
	{ .T=123, .default_val=1 },

	{}  //  <-- important! last entry shall be zero
};

static struct osmo_tdef tdefs_range[] = {
	{ .T=1002, .default_val=(ULONG_MAX/60), .unit=OSMO_TDEF_M, .desc="almost too many seconds" },
	{ .T=1003, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="too many seconds" },

	{ .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" },
	{ .T=INT_MAX-1, .default_val=ULONG_MAX-1, .unit=OSMO_TDEF_S, .desc="very large" },
	{ .T=INT_MAX-2, .default_val=LONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" },
	{ .T=INT_MAX-3, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="very large in minutes" },
	{ .T=INT_MIN, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="negative" },

	{}
};

#define print_tdef_get(TDEFS, T, AS_UNIT) do { \
		unsigned long val = osmo_tdef_get(TDEFS, T, AS_UNIT, 999); \
		printf("osmo_tdef_get(tdefs, %d, %s, 999)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \
	} while (0)

#define print_tdef_get_short(TDEFS, T, AS_UNIT) do { \
		unsigned long val = osmo_tdef_get(TDEFS, T, AS_UNIT, 999); \
		printf("osmo_tdef_get(%d, %s)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \
	} while (0)

void print_tdef_info(int T)
{
	const struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, T);
	if (!t) {
		printf(OSMO_T_FMT "=NULL", OSMO_T_FMT_ARGS(T));
		return;
	}
	printf(OSMO_T_FMT "=%lu%s",
	       OSMO_T_FMT_ARGS(T), t->val, osmo_tdef_unit_name(t->unit));
	if (t->val != t->default_val)
		printf("(def=%lu)", t->default_val);
	printf("\n");
}

static void test_tdef_get(bool test_range)
{
	int i;
	enum osmo_tdef_unit as_unit;

	printf("\n%s()\n", __func__);

	osmo_tdefs_reset(tdefs); // make all values the default

	for (i = 0; i < ARRAY_SIZE(tdefs)-1; i++) {
		int T = tdefs[i].T;
		print_tdef_info(T);
		for (as_unit = OSMO_TDEF_S; as_unit <= OSMO_TDEF_US; as_unit++) {
			print_tdef_get_short(tdefs, T, as_unit);
		}
	}

	if (!test_range)
		return;

	for (i = 0; i < ARRAY_SIZE(tdefs_range)-1; i++) {
		int T = tdefs_range[i].T;
		print_tdef_info(T);
		for (as_unit = OSMO_TDEF_S; as_unit <= OSMO_TDEF_US; as_unit++) {
			print_tdef_get_short(tdefs_range, T, as_unit);
		}
	}
}

static void test_tdef_get_nonexisting(void)
{
	printf("\n%s()\n", __func__);

	print_tdef_get(tdefs, 5, OSMO_TDEF_S);
	print_tdef_get(tdefs, 5, OSMO_TDEF_MS);
	print_tdef_get(tdefs, 5, OSMO_TDEF_M);
	print_tdef_get(tdefs, 5, OSMO_TDEF_CUSTOM);
	print_tdef_get(tdefs, 5, OSMO_TDEF_US);
}

static void test_tdef_set_and_get(void)
{
	struct osmo_tdef *t;
	printf("\n%s()\n", __func__);

	printf("setting 7 = 42\n");
	t = osmo_tdef_get_entry(tdefs, 7);
	OSMO_ASSERT(t != NULL);
	OSMO_ASSERT(osmo_tdef_val_in_range(t, 42));
	t->val = 42;
	print_tdef_info(7);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_MS);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_S);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_M);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_CUSTOM);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_US);

	printf("setting 7 = 420\n");
	OSMO_ASSERT(osmo_tdef_set(tdefs, 7, 420, OSMO_TDEF_S) == 0);
	print_tdef_info(7);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_MS);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_S);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_M);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_CUSTOM);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_US);

	printf("setting 7 = 10 (ERANGE)\n");
	OSMO_ASSERT(!osmo_tdef_val_in_range(t, 10));
	OSMO_ASSERT(osmo_tdef_set(tdefs, 7, 10, OSMO_TDEF_S) == -ERANGE);
	print_tdef_info(7);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_MS);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_S);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_M);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_CUSTOM);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_US);

	printf("setting 7 = 900 (ERANGE)\n");
	OSMO_ASSERT(!osmo_tdef_val_in_range(t, 900));
	OSMO_ASSERT(osmo_tdef_set(tdefs, 7, 900, OSMO_TDEF_S) == -ERANGE);
	print_tdef_info(7);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_MS);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_S);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_M);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_CUSTOM);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_US);

	printf("setting 23 = 50 (EEXIST)\n");
	OSMO_ASSERT(osmo_tdef_set(tdefs, 23, 50, OSMO_TDEF_S) == -EEXIST);

	printf("resetting\n");
	osmo_tdefs_reset(tdefs);
	print_tdef_info(7);
	print_tdef_get_short(tdefs, 7, OSMO_TDEF_S);
}

enum test_tdef_fsm_states {
	S_A = 0,
	S_B,
	S_C,
	S_D,
	S_E,
	S_F,
	S_G,
	S_H,
	S_I,
	S_J,
	S_K,
	S_L,
	S_M,
	S_N,
	S_O,
	/* ... gap ... */
	S_X,
	S_Y,
	S_Z,
};

static const struct osmo_tdef_state_timeout test_tdef_state_timeouts[32] = {
	[S_A] = { .T = 1 },
	[S_B] = { .T = 2 },
	[S_C] = { .T = 3 },
	[S_D] = { .T = 4 },

	[S_E] = { .T = -5 },
	[S_F] = { .T = -6 },

	[S_G] = { .T = 7 },
	[S_H] = { .T = 8 },
	[S_I] = { .T = 9 },
	[S_J] = { .T = 10 },

	/* keep_timer: adopt whichever T was running before and continue the timeout. */
	[S_K] = { .keep_timer = true },
	/* S_F defines an undefined T, but should continue previous state's timeout. */
	[S_L] = { .T = 123, .keep_timer = true },

	/* range */
	[S_M] = { .T = INT_MAX },
	[S_N] = { .T = INT_MIN },

	/* T0 is not addressable from osmo_tdef_state_timeout, since it is indistinguishable from an unset entry. Even
	 * though a timeout value is set for T=0, the transition to state S_O will show "no timer configured". */
	[S_O] = { .T = 0 },

	/* S_X undefined on purpose */
	/* S_Y defines a T that does not exist */
	[S_Y] = { .T = 666 },
	/* S_Z undefined on purpose */
};

#define S(x)	(1 << (x))

static const struct osmo_fsm_state test_tdef_fsm_states[] = {
#define DEF_STATE(NAME) \
	[S_##NAME] = { \
		.name = #NAME, \
		.out_state_mask = 0xffffffff, \
	}

	DEF_STATE(A),
	DEF_STATE(B),
	DEF_STATE(C),
	DEF_STATE(D),
	DEF_STATE(E),
	DEF_STATE(F),
	DEF_STATE(G),
	DEF_STATE(H),
	DEF_STATE(I),
	DEF_STATE(J),

	DEF_STATE(K),
	DEF_STATE(L),

	DEF_STATE(M),
	DEF_STATE(N),
	DEF_STATE(O),

	DEF_STATE(X),
	DEF_STATE(Y),
	/* Z: test not being allowed to transition to other states. */
	[S_Z] = {
		.name = "Z",
		.out_state_mask = 0
			| S(S_A)
			,
	},
};

static const struct value_string test_tdef_fsm_event_names[] = { {} };

static struct osmo_fsm test_tdef_fsm = {
	.name = "tdef_test",
	.states = test_tdef_fsm_states,
	.event_names = test_tdef_fsm_event_names,
	.num_states = ARRAY_SIZE(test_tdef_fsm_states),
	.log_subsys = DLGLOBAL,
};

const struct timeval fake_time_start_time = { 123, 456 };

#define fake_time_passes(secs, usecs) do \
{ \
	struct timeval diff; \
	osmo_gettimeofday_override_add(secs, usecs); \
	osmo_clock_override_add(CLOCK_MONOTONIC, secs, usecs * 1000); \
	timersub(&osmo_gettimeofday_override_time, &fake_time_start_time, &diff); \
	printf("Time passes: %ld.%06ld s\n", (long)secs, (long)usecs); \
	osmo_timers_prepare(); \
	osmo_timers_update(); \
} while (0)

void fake_time_start(void)
{
	struct timespec *clock_override;

	osmo_gettimeofday_override_time = fake_time_start_time;
	osmo_gettimeofday_override = true;
	clock_override = osmo_clock_override_gettimespec(CLOCK_MONOTONIC);
	OSMO_ASSERT(clock_override);
	clock_override->tv_sec = fake_time_start_time.tv_sec;
	clock_override->tv_nsec = fake_time_start_time.tv_usec * 1000;
	osmo_clock_override_enable(CLOCK_MONOTONIC, true);
}

static void print_fsm_state(struct osmo_fsm_inst *fi)
{
	struct timeval remaining;
	printf("state=%s T=%d", osmo_fsm_inst_state_name(fi), fi->T);

	if (!osmo_timer_pending(&fi->timer)) {
		printf(", no timeout\n");
		return;
	}

	osmo_timer_remaining(&fi->timer, &osmo_gettimeofday_override_time, &remaining);
	printf(", %lu.%06lu s remaining\n", (unsigned int long) remaining.tv_sec,
	       (unsigned int long) remaining.tv_usec);
}


#define test_tdef_fsm_state_chg(tdefs, NEXT_STATE) do { \
		const struct osmo_tdef_state_timeout *st = osmo_tdef_get_state_timeout(NEXT_STATE, \
										       test_tdef_state_timeouts); \
		int rc = osmo_tdef_fsm_inst_state_chg(fi, NEXT_STATE, test_tdef_state_timeouts, tdefs, 999); \
		if (!st) { \
			printf(" --> %s (no timer configured for this state) rc=%d;\t", \
			       osmo_fsm_state_name(&test_tdef_fsm, NEXT_STATE), rc); \
		} else { \
			struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, st->T); \
			printf(" --> %s (configured as " OSMO_T_FMT "%s %lu %s) rc=%d;\t", \
			       osmo_fsm_state_name(&test_tdef_fsm, NEXT_STATE), \
			       OSMO_T_FMT_ARGS(st->T), st->keep_timer ? " (keep_timer)" : "", \
			       t? t->val : 0, t? osmo_tdef_unit_name(t->unit) : "-", \
			       rc); \
		} \
		print_fsm_state(fi); \
	} while(0)



static void test_tdef_state_timeout(bool test_range)
{
	struct osmo_fsm_inst *fi;
	unsigned long m_secs;
	printf("\n%s()\n", __func__);

	osmo_tdefs_reset(tdefs);

	fake_time_start();

	fi = osmo_fsm_inst_alloc(&test_tdef_fsm, ctx, NULL, LOGL_DEBUG, __func__);
	OSMO_ASSERT(fi);
	print_fsm_state(fi);

	test_tdef_fsm_state_chg(tdefs, S_A);
	test_tdef_fsm_state_chg(tdefs, S_B);
	test_tdef_fsm_state_chg(tdefs, S_C);
	test_tdef_fsm_state_chg(tdefs, S_D);
	test_tdef_fsm_state_chg(tdefs, S_E);
	test_tdef_fsm_state_chg(tdefs, S_F);
	test_tdef_fsm_state_chg(tdefs, S_G);
	test_tdef_fsm_state_chg(tdefs, S_H);
	test_tdef_fsm_state_chg(tdefs, S_I);
	test_tdef_fsm_state_chg(tdefs, S_J);

	printf("- test keep_timer:\n");
	fake_time_passes(123, 45678);
	print_fsm_state(fi);
	test_tdef_fsm_state_chg(tdefs, S_K);
	test_tdef_fsm_state_chg(tdefs, S_A);
	fake_time_passes(23, 45678);
	print_fsm_state(fi);
	test_tdef_fsm_state_chg(tdefs, S_K);

	test_tdef_fsm_state_chg(tdefs, S_A);
	fake_time_passes(23, 45678);
	print_fsm_state(fi);
	test_tdef_fsm_state_chg(tdefs, S_L);
	test_tdef_fsm_state_chg(tdefs, S_O);
	test_tdef_fsm_state_chg(tdefs, S_L);

	printf("- test T=0:\n");
	test_tdef_fsm_state_chg(tdefs, S_O);

	printf("- test no timer:\n");
	test_tdef_fsm_state_chg(tdefs, S_X);

	printf("- test undefined timer, using default_val arg of osmo_tdef_fsm_inst_state_chg(), here passed as 999:\n");
	test_tdef_fsm_state_chg(tdefs, S_Y);

	/* the range of unsigned long is architecture dependent. This test can be invoked manually to see whether
	 * clamping the timeout values works, but the output will be of varying lengths depending on the system's
	 * unsigned long range, and would cause differences in expected output. */
	if (test_range) {
		struct osmo_tdef *m;

		printf("- test large T:\n");
		test_tdef_fsm_state_chg(tdefs_range, S_M);

		printf("- test T<0:\n");
		test_tdef_fsm_state_chg(tdefs_range, S_N);

		printf("- test range:\n");
		test_tdef_fsm_state_chg(tdefs_range, S_M);

		m = osmo_tdef_get_entry(tdefs_range, INT_MAX);
		OSMO_ASSERT(m);

		/* sweep through all the bits, shifting in 0xfffff.. from the right. */
		m_secs = 0;
		do {
			m_secs = (m_secs << 1) + 1;
			switch (m_secs) {
			case 0x7fff:
				printf("--- int32_t max ---\n");
				break;
			case 0xffff:
				printf("--- uint32_t max ---\n");
				break;
			case 0x7fffffff:
				printf("--- int64_t max ---\n");
				break;
			case 0xffffffff:
				printf("--- uint64_t max ---\n");
				break;
			default:
				break;
			}

			m->val = m_secs - 1;
			test_tdef_fsm_state_chg(tdefs_range, S_M);
			m->val = m_secs;
			test_tdef_fsm_state_chg(tdefs_range, S_M);
			m->val = m_secs + 1;
			test_tdef_fsm_state_chg(tdefs_range, S_M);
		} while (m_secs < ULONG_MAX);
	}

	printf("- test disallowed transition:\n");
	test_tdef_fsm_state_chg(tdefs, S_Z);
	test_tdef_fsm_state_chg(tdefs, S_B);
	test_tdef_fsm_state_chg(tdefs, S_C);
	test_tdef_fsm_state_chg(tdefs, S_D);
}

int main(int argc, char **argv)
{
	ctx = talloc_named_const(NULL, 0, "tdef_test.c");
	osmo_init_logging2(ctx, NULL);

	log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_NONE);
	log_set_print_level(osmo_stderr_target, 1);
	log_set_print_category(osmo_stderr_target, 1);
	log_set_print_category_hex(osmo_stderr_target, 0);
	log_set_use_color(osmo_stderr_target, 0);

	osmo_fsm_log_addr(false);
	osmo_fsm_log_timeouts(true);

	log_set_category_filter(osmo_stderr_target, DLGLOBAL, 1, LOGL_DEBUG);

	OSMO_ASSERT(osmo_fsm_register(&test_tdef_fsm) == 0);

	test_tdef_get(argc > 1);
	test_tdef_get_nonexisting();
	test_tdef_set_and_get();
	/* Run range test iff any argument is passed on the cmdline. For the rationale, see the comment in
	 * test_tdef_state_timeout(). */
	test_tdef_state_timeout(argc > 1);

	return EXIT_SUCCESS;
}
