/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */

#include "test-lib.h"
#include "str.h"
#include "strescape.h"
#include "event-filter.h"
#include "event-filter-private.h"

#define STRING1	"X"
#define STRING2	"Y"

/* dummy values, at least for now */
#define SOURCE_FILENAME "blah.c"
#define SOURCE_LINE 123

static void check_expr(const char *test_name,
		       struct event *event,
		       struct event_filter *filter,
		       enum event_filter_log_type log_type,
		       bool expected)
{
	struct event_filter_node *expr;
	unsigned int num_queries;
	bool got;

	/* get at the expr inside the filter */
	expr = event_filter_get_expr_for_testing(filter, &num_queries);
	test_out_quiet(t_strdup_printf("%s:num_queries==1", test_name),
		       num_queries == 1); /* should have only one query */

	got = event_filter_query_match_eval(filter, expr, event,
					    SOURCE_FILENAME, SOURCE_LINE,
					    log_type);
	test_out_quiet(t_strdup_printf("%s:got=expected", test_name),
		       got == expected);
}

static void do_test_expr(const char *filter_string, struct event *event,
			 enum event_filter_log_type log_type,
			 bool expected)
{
	const char *test_name, *error;

	test_name = t_strdup_printf(
		"%.*s log type + event {a=%s, b=%s} + filter '%s' (exp %s)",
		3, /* truncate the type name to avoid CI seeing 'warning' messages */
		event_filter_category_from_log_type(log_type),
		event_find_field_recursive_str(event, "a"),
		event_find_field_recursive_str(event, "b"),
		filter_string,
		expected ? "true" : "false");

	/* set up the filter expression */
	struct event_filter *filter = event_filter_create();
	test_out_quiet(t_strdup_printf("%s:event_filter_parse()", test_name),
		       event_filter_parse(filter_string, filter, &error) == 0);

	check_expr(test_name, event, filter, log_type, expected);

	event_filter_unref(&filter);
}

static void test_unary_expr(struct event *event,
			    const char *expr, bool truth,
			    enum event_filter_log_type log_type)
{
	/*
	 * The UNARY() macro checks:
	 *
	 * 1. expr
	 * 2. NOT expr
	 * 3. NOT (expr)
	 *
	 * Note that numbers 2 and 3 are equivalent.
	 *
	 * The truth argument specifies the expected truth-iness of the
	 * passed in expression.
	 */
#define UNARY()								\
	T_BEGIN {							\
		do_test_expr(expr,					\
			     event, log_type, truth);			\
		do_test_expr(t_strdup_printf("NOT %s", expr),		\
			     event, log_type, !truth);			\
		do_test_expr(t_strdup_printf("NOT (%s)", expr),		\
			     event, log_type, !truth);			\
	} T_END

	UNARY();
}

static void test_binary_expr(struct event *event,
			     const char *expr1, const char *expr2,
			     bool truth1, bool truth2,
			     enum event_filter_log_type log_type)
{
	/*
	 * The BINARY() macro checks:
	 *
	 * 1. expr1 op expr2
	 * 2. NOT expr1 op expr2
	 * 3. NOT (expr1) op expr2
	 * 4. (NOT expr1) op expr2
	 * 5. expr1 op NOT expr2
	 * 6. expr1 op NOT (expr2)
	 * 7. expr1 op (NOT expr2)
	 * 8. NOT (expr1 op expr2)
	 * 9. NOT expr1 op NOT expr2
	 * 10. NOT (expr1) op NOT (expr2)
	 * 11. (NOT expr1) op (NOT expr2)
	 *
	 * Where op is OR or AND.
	 *
	 * Note that:
	 *  - numbers 2, 3, and 4 are equivalent
	 *  - numbers 5, 6, and 7 are equivalent
	 *  - numbers 9, 10, and 11 are equivalent
	 *
	 * The truth arguments specify the expected truth-iness of the
	 * passed in expressions.
	 */
#define BINARY(opstr, op)						\
	T_BEGIN {							\
		do_test_expr(t_strdup_printf("%s %s %s", expr1, opstr, expr2),\
			     event, log_type,				\
			     (truth1) op (truth2));			\
		do_test_expr(t_strdup_printf("NOT %s %s %s", expr1, opstr, expr2),\
			     event, log_type,				\
			     !(truth1) op (truth2));			\
		do_test_expr(t_strdup_printf("NOT (%s) %s %s", expr1, opstr, expr2),\
			     event, log_type,				\
			     !(truth1) op (truth2));			\
		do_test_expr(t_strdup_printf("(NOT %s) %s %s", expr1, opstr, expr2),\
			     event, log_type,				\
			     !(truth1) op (truth2));			\
		do_test_expr(t_strdup_printf("%s %s NOT %s", expr1, opstr, expr2),\
			     event, log_type,				\
			     (truth1) op !(truth2));			\
		do_test_expr(t_strdup_printf("%s %s NOT (%s)", expr1, opstr, expr2),\
			     event, log_type,				\
			     (truth1) op !(truth2));			\
		do_test_expr(t_strdup_printf("%s %s (NOT %s)", expr1, opstr, expr2),\
			     event, log_type,				\
			     (truth1) op !(truth2));			\
		do_test_expr(t_strdup_printf("NOT (%s %s %s)", expr1, opstr, expr2),\
			     event, log_type,				\
			     !((truth1) op (truth2)));			\
		do_test_expr(t_strdup_printf("NOT %s %s NOT %s", expr1, opstr, expr2),\
			     event, log_type,				\
			     !(truth1) op !(truth2));			\
		do_test_expr(t_strdup_printf("NOT (%s) %s NOT (%s)", expr1, opstr, expr2),\
			     event, log_type,				\
			     !(truth1) op !(truth2));			\
		do_test_expr(t_strdup_printf("(NOT %s) %s (NOT %s)", expr1, opstr, expr2),\
			     event, log_type,				\
			     !(truth1) op !(truth2));			\
	} T_END

	BINARY("OR", ||);
	BINARY("AND", &&);
}

static void test_event_filter_expr_fields(enum event_filter_log_type log_type)
{
	static const char *values[] = {
		NULL,
		"",
		STRING1,
		STRING2,
	};
	unsigned int a, b;

#define STR_IS_EMPTY(v) \
		(((v) == NULL) || (strcmp("", (v)) == 0))
#define STR_MATCHES(v, c) \
		(((v) != NULL) && (strcmp((c), (v)) == 0))

	/* unary */
	for (a = 0; a < N_ELEMENTS(values); a++) {
		/* set up the event to match against */
		struct event *event = event_create(NULL);
		event_add_str(event, "a", values[a]);

		test_unary_expr(event,
				"a=\"\"",
				STR_IS_EMPTY(values[a]),
				log_type);
		test_unary_expr(event,
				"a=" STRING1,
				STR_MATCHES(values[a], STRING1),
				log_type);

		event_unref(&event);
	}

	/* binary */
	for (a = 0; a < N_ELEMENTS(values); a++) {
		for (b = 0; b < N_ELEMENTS(values); b++) {
			/* set up the event to match against */
			struct event *event = event_create(NULL);
			event_add_str(event, "a", values[a]);
			event_add_str(event, "b", values[b]);

			test_binary_expr(event,
					 "a=\"\"",
					 "b=\"\"",
					 STR_IS_EMPTY(values[a]),
					 STR_IS_EMPTY(values[b]),
					 log_type);
			test_binary_expr(event,
					 "a=" STRING1,
					 "b=\"\"",
					 STR_MATCHES(values[a], STRING1),
					 STR_IS_EMPTY(values[b]),
					 log_type);
			test_binary_expr(event,
					 "a=\"\"",
					 "b=" STRING2,
					 STR_IS_EMPTY(values[a]),
					 STR_MATCHES(values[b], STRING2),
					 log_type);
			test_binary_expr(event,
					 "a=" STRING1,
					 "b=" STRING2,
					 STR_MATCHES(values[a], STRING1),
					 STR_MATCHES(values[b], STRING2),
					 log_type);

			event_unref(&event);
		}
	}
}

void test_event_filter_expr(void)
{
	static const enum event_filter_log_type log_types[] = {
		EVENT_FILTER_LOG_TYPE_DEBUG,
		EVENT_FILTER_LOG_TYPE_INFO,
		EVENT_FILTER_LOG_TYPE_WARNING,
		EVENT_FILTER_LOG_TYPE_ERROR,
		EVENT_FILTER_LOG_TYPE_FATAL,
		EVENT_FILTER_LOG_TYPE_PANIC,
	};
	unsigned int i;

	test_begin("event filter expressions");
	for (i = 0; i < N_ELEMENTS(log_types); i++)
		test_event_filter_expr_fields(log_types[i]);
	test_end();
}
