/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "test-stats-common.h"
#include "array.h"

bool test_stats_callback(struct event *event,
			 enum event_callback_type type ATTR_UNUSED,
			 struct failure_context *ctx, const char *fmt ATTR_UNUSED,
			 va_list args ATTR_UNUSED)
{
	if (stats_metrics != NULL) {
		stats_metrics_event(stats_metrics, event, ctx);
		struct event_filter *filter =
			stats_metrics_get_event_filter(stats_metrics);
		return !event_filter_match(filter, event, ctx);
	}
	return TRUE;
}

static const char *const settings_blob_1[] = {
	"metric=test",
	"metric/test/metric_name=test",
	"metric/test/filter=event=test",
	NULL
};

static void test_stats_metrics(void)
{
	test_begin("stats metrics (event counting)");

	/* register some stats */
	test_stats_init(settings_blob_1);

	/* push event in */
	struct event *event = event_create(NULL);
	event_add_category(event, &test_category);
	event_set_name(event, "test");
	test_event_send(event);
	event_unref(&event);

	test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
	test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);

	test_stats_deinit();
	test_end();
}

static const char *const settings_blob_2[] = {
	"metric=test",
	"metric/test/metric_name=test",
	"metric/test/filter=(event=test AND test_field=value)",
	NULL
};

static void test_stats_metrics_filter(void)
{
	test_begin("stats metrics (filter)");

	test_stats_init(settings_blob_2);

	/* check filter */
	struct event_filter *filter =
		stats_metrics_get_event_filter(stats_metrics);
	string_t *str_filter = t_str_new(64);
	event_filter_export(filter, str_filter);
	test_assert_strcmp("((event=\"test\" AND \"test_field\"=\"value\"))",
			   str_c(str_filter));

	/* send event */
	struct event *event = event_create(NULL);
	event_add_category(event, &test_category);
	event_set_name(event, "test");
	event_add_str(event, "test_field", "value");
	test_event_send(event);
	event_unref(&event);

	test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
	test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);

	/* send another event */
	event = event_create(NULL);
	event_add_category(event, &test_category);
	event_set_name(event, "test");
	event_add_str(event, "test_field", "nother value");
	e_debug(event, "test");
	event_unref(&event);

	test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
	test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);

	test_stats_deinit();
	test_end();
}

static void test_stats_metrics_group_by_check_one(const struct metric *metric,
						  const char *sub_name,
						  unsigned int total_count,
						  unsigned int submetric_count,
						  unsigned int group_by_count,
						  enum stats_metric_group_by_func group_by_func,
						  const char *group_by_field,
						  enum metric_value_type value_type)
{
	test_assert_strcmp(metric->name, "test");

	if (sub_name != NULL)
		test_assert_strcmp(metric->sub_name, sub_name);
	else
		test_assert(metric->sub_name == NULL);

	test_assert(stats_dist_get_count(metric->duration_stats) == total_count);

	if (submetric_count > 0) {
		test_assert(array_is_created(&metric->sub_metrics));
		test_assert(array_count(&metric->sub_metrics) == submetric_count);
	} else {
		test_assert(!array_is_created(&metric->sub_metrics));
	}

	if (group_by_count > 0) {
		test_assert(metric->group_by_count == group_by_count);
		i_assert(metric->group_by != NULL);
		test_assert(metric->group_by[0].func == group_by_func);
		test_assert_strcmp(metric->group_by[0].field, group_by_field);
	} else {
		test_assert(metric->group_by_count == 0);
		test_assert(metric->group_by == NULL);
	}

	test_assert(metric->group_value.type == value_type);
}

#define DISCRETE_TEST_VAL_COUNT	3
struct discrete_test {
	const char *const *settings_blob;
	unsigned int num_values;
	const char *values_first[DISCRETE_TEST_VAL_COUNT];
	const char *values_second[DISCRETE_TEST_VAL_COUNT];
};

static const struct discrete_test discrete_tests[] = {
	{
		(const char *const []){
		  "metric/test/group_by=test_name,sub_name",
		  "metric/test/group_by/test_name/field=test_name",
		  "metric/test/group_by/sub_name/field=sub_name",
		  NULL },
		3,
		{ "eta", "kappa", "nu", },
		{ "upsilon", "pi", "epsilon", },
	},
	{
		(const char *const []){
		  "metric/test/group_by=test_name,sub_name",
		  "metric/test/group_by/test_name/field=test_name",
		  "metric/test/group_by/test_name/method=discrete",
		  "metric/test/group_by/test_name/method/discrete/method=discrete",
		  "metric/test/group_by/sub_name/field=sub_name",
		  "metric/test/group_by/sub_name/method=discrete",
		  "metric/test/group_by/sub_name/method/discrete/method=discrete",
		  NULL },
		3,
		{ "apple", "banana", "orange", },
		{ "pie", "yoghurt", "cobbler", },
	},
	{
		(const char *const []){
		  "metric/test/group_by=test_name,sub_name",
		  "metric/test/group_by/test_name/field=test_name",
		  "metric/test/group_by/test_name/method/discrete/method=discrete",
		  "metric/test/group_by/sub_name/field=sub_name",
		  "metric/test/group_by/sub_name/method=discrete",
		  "metric/test/group_by/sub_name/method/discrete/method=discrete",
		  NULL },
		3,
		{ "apollo", "gaia", "hermes", },
		{ "thor", "odin", "loki", },
	},
};

static void
test_stats_metrics_group_by_discrete_real(const struct discrete_test *test,
					  unsigned int group_idx)
{
	struct event *event;
	unsigned int i, j;

	test_begin(t_strdup_printf("stats metrics (discrete group by) - %u",
				   group_idx));

	const char *const base_settings[] = {
		"metric=test",
		"metric/test/metric_name=test",
		"metric/test/filter=event=test",
		NULL
	};
	const char *const *all_settings = t_strsplit(
		t_strconcat(t_strarray_join(base_settings, " "), " ",
			    t_strarray_join(test->settings_blob, " "), NULL), " ");
	test_stats_init(all_settings);

	for (i = 0; i < test->num_values; i++) {
		for (j = 0; j < test->num_values; j++) {
			event = event_create(NULL);
			event_add_category(event, &test_category);
			event_set_name(event, "test");
			event_add_str(event, "test_name", test->values_first[i]);
			event_add_str(event, "sub_name", test->values_second[j]);
			test_event_send(event);
			event_unref(&event);
		}
	}

	/* check total number of events */
	test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == test->num_values * test->num_values);

	/* analyze the structure */
	struct stats_metrics_iter *iter = stats_metrics_iterate_init(stats_metrics);
	const struct metric *root_metric = stats_metrics_iterate(iter);
	stats_metrics_iterate_deinit(&iter);

	test_stats_metrics_group_by_check_one(root_metric,
					      NULL,
					      test->num_values * test->num_values,
					      test->num_values,
					      2, STATS_METRIC_GROUPBY_DISCRETE,
					      "test_name", 0);

	struct metric *const *first = array_idx(&root_metric->sub_metrics, 0);

	/* examine each sub-metric */
	for (i = 0; i < test->num_values; i++) {
		test_stats_metrics_group_by_check_one(first[i],
						      test->values_first[i],
						      test->num_values,
						      test->num_values,
						      1, STATS_METRIC_GROUPBY_DISCRETE,
						      "sub_name",
						      METRIC_VALUE_TYPE_STR);

		struct metric *const *second = array_idx(&first[i]->sub_metrics, 0);

		/* examine each sub-sub-metric */
		for (j = 0; j < test->num_values; j++) {
			test_stats_metrics_group_by_check_one(second[j],
							      test->values_second[j],
							      1, 0, 0, 0, NULL,
							      METRIC_VALUE_TYPE_STR);
		}
	}

	test_stats_deinit();
	test_end();
}

static void test_stats_metrics_group_by_discrete(void)
{
	unsigned int i;

	for (i = 0; i < N_ELEMENTS(discrete_tests); i++)
		test_stats_metrics_group_by_discrete_real(&discrete_tests[i], i);
}

#define QUANTIZED_TEST_VAL_COUNT	30
struct quantized_test {
	const char *const *settings_blob;
	unsigned int num_inputs;
	intmax_t input_vals[QUANTIZED_TEST_VAL_COUNT];

	unsigned int num_sub_metrics;

	unsigned int num_ranges;
	struct {
		struct stats_metric_settings_bucket_range range;
		intmax_t count;
	} ranges[QUANTIZED_TEST_VAL_COUNT];
};

static const struct quantized_test quantized_tests[] = {
	{
		(const char *const []){
		  "metric/test/group_by/foobar/method=linear",
		  "metric/test/group_by/foobar/method/linear/method=linear",
		  "metric/test/group_by/foobar/method/linear/min=100",
		  "metric/test/group_by/foobar/method/linear/max=1000",
		  "metric/test/group_by/foobar/method/linear/step=100",
		  NULL },
		13,
		{ 0, 50, 100, 101, 200, 201, 250, 301, 900, 901, 1000, 1001, 2000 },
		7,
		11,
		{ { { INTMAX_MIN, 100 }, 3 },
		  { { 100, 200 }, 2 },
		  { { 200, 300 }, 2 },
		  { { 300, 400 }, 1 },
		  { { 400, 500 }, 0 },
		  { { 500, 600 }, 0 },
		  { { 600, 700 }, 0 },
		  { { 700, 800 }, 0 },
		  { { 800, 900 }, 1 },
		  { { 900, 1000 }, 2 },
		  { { 1000, INTMAX_MAX }, 2 },
		}
	},
	{
		(const char *const []){
		  "metric/test/group_by/foobar/method=linear",
		  "metric/test/group_by/foobar/method/linear/method=linear",
		  "metric/test/group_by/foobar/method/linear/min=500",
		  "metric/test/group_by/foobar/method/linear/max=10000",
		  "metric/test/group_by/foobar/method/linear/step=500",
		  NULL },
		26,
		{
			0, 50, 100, 101, 200, 201, 250, 301, 900, 901, 1000, 1001, 1500,
			2000, 2001, 2200, 8000, 8001, 8200, 8500, 9000, 9001,
			10000, 10001, 10500, 15000,
		},
		11,
		21,
		{ { { INTMAX_MIN, 500 }, 8 },
		  { { 500, 1000 }, 3 },
		  { { 1000, 1500 }, 2 },
		  { { 1500, 2000 }, 1 },
		  { { 2000, 2500 }, 2 },
		  { { 2500, 3000 }, 0 },
		  { { 3000, 3500 }, 0 },
		  { { 3500, 4000 }, 0 },
		  { { 4000, 4500 }, 0 },
		  { { 4500, 5000 }, 0 },
		  { { 5000, 5500 }, 0 },
		  { { 5500, 6000 }, 0 },
		  { { 6000, 6500 }, 0 },
		  { { 6500, 7000 }, 0 },
		  { { 7000, 7500 }, 0 },
		  { { 7500, 8000 }, 1 },
		  { { 8000, 8500 }, 3 },
		  { { 8500, 9000 }, 1 },
		  { { 9000, 9500 }, 1 },
		  { { 9500, 10000 }, 1 },
		  { { 10000, INTMAX_MAX }, 3 },
		}
	},
	{
		(const char *const []){
		  "metric/test/group_by/foobar/method=linear",
		  "metric/test/group_by/foobar/method/linear/method=linear",
		  "metric/test/group_by/foobar/method/linear/min=500",
		  "metric/test/group_by/foobar/method/linear/max=10000",
		  "metric/test/group_by/foobar/method/linear/step=450",
		  NULL },
		26,
		{
			0, 50, 100, 101, 200, 201, 250, 301, 900, 901, 1000, 1001, 1500,
			2000, 2001, 2200, 8000, 8001, 8200, 8500, 9000, 9001,
			10000, 10001, 10500, 15000,
		},
		10,
		24,
		{ { { INTMAX_MIN, 500 }, 8 },
		  { { 500, 950 }, 2 },
		  { { 950, 1400 }, 2 },
		  { { 1400, 1850 }, 1 },
		  { { 1850, 2300 }, 3 },
		  { { 2300, 2750 }, 0 },
		  { { 2750, 3200 }, 0 },
		  { { 3200, 3650 }, 0 },
		  { { 3650, 4100 }, 0 },
		  { { 4100, 4550 }, 0 },
		  { { 4550, 5000 }, 0 },
		  { { 5000, 5450 }, 0 },
		  { { 5450, 5900 }, 0 },
		  { { 5900, 6350 }, 0 },
		  { { 6350, 6800 }, 0 },
		  { { 6800, 7250 }, 0 },
		  { { 7250, 7700 }, 0 },
		  { { 7700, 8150 }, 2 },
		  { { 8150, 8600 }, 2 },
		  { { 8600, 9050 }, 2 },
		  { { 9050, 9500 }, 0 },
		  { { 9500, 9950 }, 0 },
		  { { 9950, 10000 }, 1 },
		  { { 10000, INTMAX_MAX }, 3 },
		}
	},
	{
		/* start at 0 */
		(const char *const []){
		  "metric/test/group_by/foobar/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
		  NULL },
		12,
		{ 0, 5, 10, 11, 100, 101, 500, 1000, 1001, 1000000, 1000001, 2000000 },
		7,
		8,
		{ { { INTMAX_MIN, 1 }, 1 },
		  { { 1, 10 }, 2 },
		  { { 10, 100 }, 2 },
		  { { 100, 1000 }, 3 },
		  { { 1000, 10000 }, 1 },
		  { { 10000, 100000 }, 0 },
		  { { 100000, 1000000 }, 1 },
		  { { 1000000, INTMAX_MAX }, 2 },
		}
	},
	{
		/* start at 0 */
		(const char *const []){
		  "metric/test/group_by/foobar/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
		  "metric/test/group_by/foobar/method/exponential/base=2",
		  NULL },
		9,
		{ 0, 1, 2, 4, 5, 20, 64, 65, 100 },
		7,
		8,
		{ { { INTMAX_MIN, 1 }, 2 },
		  { { 1, 2 }, 1 },
		  { { 2, 4 }, 1 },
		  { { 4, 8 }, 1 },
		  { { 8, 16 }, 0 },
		  { { 16, 32 }, 1 },
		  { { 32, 64 }, 1 },
		  { { 64, INTMAX_MAX }, 2 },
		}
	},
	{
		/* start at >0 */
		(const char *const []){
		  "metric/test/group_by/foobar/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/min_magnitude=2",
		  "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
		  NULL },
		12,
		{ 0, 5, 10, 11, 100, 101, 500, 1000, 1001, 1000000, 1000001, 2000000 },
		5,
		6,
		{ { { INTMAX_MIN, 100 }, 5 },
		  { { 100, 1000 }, 3 },
		  { { 1000, 10000 }, 1 },
		  { { 10000, 100000 }, 0 },
		  { { 100000, 1000000 }, 1 },
		  { { 1000000, INTMAX_MAX }, 2 },
		}
	},
	{
		/* start at >0 */
		(const char *const []){
		  "metric/test/group_by/foobar/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/method=exponential",
		  "metric/test/group_by/foobar/method/exponential/min_magnitude=2",
		  "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
		  "metric/test/group_by/foobar/method/exponential/base=2",
		  NULL },
		9,
		{ 0, 1, 2, 4, 5, 20, 64, 65, 100 },
		5,
		6,
		{ { { INTMAX_MIN, 4 }, 4 },
		  { { 4, 8 }, 1 },
		  { { 8, 16 }, 0 },
		  { { 16, 32 }, 1 },
		  { { 32, 64 }, 1 },
		  { { 64, INTMAX_MAX }, 2 },
		}
	},
};

static void
test_stats_metrics_group_by_quantized_real(const struct quantized_test *test,
					   unsigned int group_idx)
{
	unsigned int i;

	test_begin(t_strdup_printf("stats metrics (quantized group by) - %u",
				   group_idx));

	const char *const base_settings[] = {
		"metric=test",
		"metric/test/metric_name=test",
		"metric/test/filter=event=test",
		"metric/test/group_by=test_name,foobar",
		"metric/test/group_by/test_name/field=test_name",
		"metric/test/group_by/foobar/field=foobar",
		NULL
	};
	const char *const *all_settings = t_strsplit(
		t_strconcat(t_strarray_join(base_settings, " "), " ",
			    t_strarray_join(test->settings_blob, " "), NULL), " ");
	test_stats_init(all_settings);

	struct event *event;

	for (i = 0; i < test->num_inputs; i++) {
		event = event_create(NULL);
		event_add_category(event, &test_category);
		event_set_name(event, "test");
		event_add_str(event, "test_name", "alpha");
		event_add_int(event, "foobar", test->input_vals[i]);
		test_event_send(event);
		event_unref(&event);
	}

	/* check total number of events */
	test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == test->num_inputs);

	/* analyze the structure */
	struct stats_metrics_iter *iter = stats_metrics_iterate_init(stats_metrics);
	const struct metric *root_metric = stats_metrics_iterate(iter);
	stats_metrics_iterate_deinit(&iter);

	test_stats_metrics_group_by_check_one(root_metric, NULL, test->num_inputs,
					      1, 2, STATS_METRIC_GROUPBY_DISCRETE,
					      "test_name", 0);

	/* examine first level sub-metric */
	struct metric *const *first = array_idx(&root_metric->sub_metrics, 0);
	test_stats_metrics_group_by_check_one(first[0],
					      "alpha",
					      test->num_inputs,
					      test->num_sub_metrics,
					      1,
					      STATS_METRIC_GROUPBY_QUANTIZED,
					      "foobar",
					      METRIC_VALUE_TYPE_STR);

	/* check the ranges */
	test_assert(first[0]->group_by[0].num_ranges == test->num_ranges);
	for (i = 0; i < test->num_ranges; i++) {
		test_assert(first[0]->group_by[0].ranges[i].min == test->ranges[i].range.min);
		test_assert(first[0]->group_by[0].ranges[i].max == test->ranges[i].range.max);
	}

	/* examine second level sub-metrics */
	struct metric *const *second = array_idx(&first[0]->sub_metrics, 0);

	for (i = 0; i < test->num_sub_metrics; i++) {
		const char *sub_name;
		intmax_t range_idx;

		/* we check these first, before we use the value below */
		test_assert(second[i]->group_value.type == METRIC_VALUE_TYPE_BUCKET_INDEX);
		test_assert(second[i]->group_value.intmax < test->num_ranges);

		range_idx = second[i]->group_value.intmax;

		/* construct the expected sub-metric name */
		if (test->ranges[range_idx].range.min == INTMAX_MIN) {
			sub_name = t_strdup_printf("foobar_ninf_%jd",
						   test->ranges[range_idx].range.max);
		} else if (test->ranges[range_idx].range.max == INTMAX_MAX) {
			sub_name = t_strdup_printf("foobar_%jd_inf",
						   test->ranges[range_idx].range.min + 1);
		} else {
			sub_name = t_strdup_printf("foobar_%jd_%jd",
						   test->ranges[range_idx].range.min + 1,
						   test->ranges[range_idx].range.max);
		}

		test_stats_metrics_group_by_check_one(second[i],
						      sub_name,
						      test->ranges[second[i]->group_value.intmax].count,
						      0, 0, 0, NULL,
						      METRIC_VALUE_TYPE_BUCKET_INDEX);
	}

	test_stats_deinit();
	test_end();
}

static void test_stats_metrics_group_by_quantized(void)
{
	unsigned int i;

	for (i = 0; i < N_ELEMENTS(quantized_tests); i++)
		test_stats_metrics_group_by_quantized_real(&quantized_tests[i], i);
}

int main(void) {
	void (*const test_functions[])(void) = {
		test_stats_metrics,
		test_stats_metrics_filter,
		test_stats_metrics_group_by_discrete,
		test_stats_metrics_group_by_quantized,
		NULL
	};

	int ret = test_run(test_functions);

	return ret;
}
