/*
 * Copyright (C) 2023 Hasl Developers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib.h>

#include <hasl.h>

/******************************************************************************
 * Tests
 *****************************************************************************/
static void
test_hasl_context_new(void) {
	HaslContext *ctx = NULL;

	ctx = hasl_context_new();

	g_assert_true(HASL_IS_CONTEXT(ctx));

	g_clear_object(&ctx);
}

static void
test_hasl_context_properties(void) {
	HaslContext *ctx = NULL;
	char *allowed_mechanisms = NULL;
	char *authzid = NULL;
	char *password = NULL;
	char *server_mechanisms = NULL;
	char *username = NULL;
	gboolean allow_clear_text = FALSE;
	gboolean tls = FALSE;

	ctx = g_object_new(
		HASL_TYPE_CONTEXT,
		"allow-clear-text", TRUE,
		"allowed-mechanisms", "PLAIN",
		"authzid", "authzid",
		"password", "password",
		"server-mechanisms", "EXTERNAL",
		"tls", TRUE,
		"username", "username",
		NULL);

	g_assert_true(HASL_IS_CONTEXT(ctx));

	g_object_get(
		G_OBJECT(ctx),
		"allow-clear-text", &allow_clear_text,
		"allowed-mechanisms", &allowed_mechanisms,
		"authzid", &authzid,
		"password", &password,
		"server-mechanisms", &server_mechanisms,
		"tls", &tls,
		"username", &username,
		NULL);

	g_assert_true(allow_clear_text);

	g_assert_cmpstr(allowed_mechanisms, ==, "PLAIN");
	g_clear_pointer(&allowed_mechanisms, g_free);

	g_assert_cmpstr(authzid, ==, "authzid");
	g_clear_pointer(&authzid, g_free);

	g_assert_cmpstr(password, ==, "password");
	g_clear_pointer(&password, g_free);

	g_assert_cmpstr(server_mechanisms, ==, "EXTERNAL");
	g_clear_pointer(&server_mechanisms, g_free);

	g_assert_true(tls);

	g_assert_cmpstr(username, ==, "username");
	g_clear_pointer(&username, g_free);

	g_clear_object(&ctx);
}

static void
test_hasl_context_add_mechanism_null(void) {
	if(g_test_subprocess()) {
		HaslContext *ctx = hasl_context_new();

		hasl_context_add_mechanism(ctx, NULL, HASL_TYPE_MECHANISM);

		g_clear_object(&ctx);
	}

	g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
	g_test_trap_assert_failed();
	g_test_trap_assert_stderr("*assertion*name*");
}

static void
test_hasl_context_add_mechanism_empty(void) {
	if(g_test_subprocess()) {
		HaslContext *ctx = hasl_context_new();

		hasl_context_add_mechanism(ctx, "", HASL_TYPE_MECHANISM);

		g_clear_object(&ctx);
	}

	g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
	g_test_trap_assert_failed();
	g_test_trap_assert_stderr("*assertion*name*");
}

static void
test_hasl_context_supported_mechanisms_default(void) {
	HaslContext *ctx = NULL;
	char *supported_mechanisms = NULL;

	ctx = hasl_context_new();
	supported_mechanisms = hasl_context_get_supported_mechanisms(ctx);

	g_assert_cmpstr(supported_mechanisms, ==, "ANONYMOUS EXTERNAL PLAIN");

	g_free(supported_mechanisms);
	g_clear_object(&ctx);
}

static void
test_hasl_context_supported_mechanisms_custom(void) {
	HaslContext *ctx = NULL;
	char *supported_mechanisms = NULL;
	gboolean r = FALSE;

	ctx = hasl_context_new();

	/* Add some custom mechanisms. */
	r = hasl_context_add_mechanism(ctx, "TEST", HASL_TYPE_MECHANISM);
	g_assert_true(r);

	r = hasl_context_add_mechanism(ctx, "DEBUG", HASL_TYPE_MECHANISM);
	g_assert_true(r);

	/* Now get the list and check it. */
	supported_mechanisms = hasl_context_get_supported_mechanisms(ctx);

	g_assert_cmpstr(supported_mechanisms, ==,
	                "ANONYMOUS DEBUG EXTERNAL PLAIN TEST");

	g_free(supported_mechanisms);
	g_clear_object(&ctx);
}

static void
test_hasl_context_current_mechanism(void) {
	HaslContext *context = NULL;
	const char *current = NULL;

	context = hasl_context_new();
	hasl_context_set_username(context, "homer");
	hasl_context_set_password(context, "0dy$$3y");
	hasl_context_set_tls(context, TRUE);

	/* Make sure NULL is returned if set_allowed_mechanisms hasn't been called
	 * yet.
	 */
	current = hasl_context_get_current_mechanism(context);
	g_assert_null(current);

	hasl_context_set_allowed_mechanisms(context, "PLAIN,EXTERNAL");

	/* Now make sure NULL is returned if next hasn't been called yet. */
	current = hasl_context_get_current_mechanism(context);
	g_assert_null(current);

	/* Call next until we exhausted it and make sure we get the values we're
	 * expecting.
	 */
	hasl_context_next(context);
	current = hasl_context_get_current_mechanism(context);
	g_assert_cmpstr(current, ==, "PLAIN");

	hasl_context_next(context);
	current = hasl_context_get_current_mechanism(context);
	g_assert_cmpstr(current, ==, "EXTERNAL");

	hasl_context_next(context);
	current = hasl_context_get_current_mechanism(context);
	g_assert_null(current);

	/* We do one extra to make sure we're not overflowing. */
	hasl_context_next(context);
	current = hasl_context_get_current_mechanism(context);
	g_assert_null(current);

	g_clear_object(&context);
}

static void
test_hasl_context_next_plain_and_external(void) {
	HaslContext *context = NULL;
	const char *next = NULL;

	context = hasl_context_new();
	hasl_context_set_username(context, "homer");
	hasl_context_set_password(context, "0dy$$3y");
	hasl_context_set_allowed_mechanisms(context, "PLAIN,EXTERNAL");
	hasl_context_set_allow_clear_text(context, TRUE);

	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "PLAIN");

	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "EXTERNAL");

	next = hasl_context_next(context);
	g_assert_null(next);

	/* Verify that we can't get past the end and get null on subsequent
	 * calls.
	 */
	next = hasl_context_next(context);
	g_assert_null(next);

	g_clear_object(&context);
}

static void
test_hasl_context_next_plain_not_possible(void) {
	HaslContext *context = NULL;
	const char *next = NULL;

	context = hasl_context_new();
	hasl_context_set_allowed_mechanisms(context, "PLAIN,EXTERNAL");

	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "EXTERNAL");

	next = hasl_context_next(context);
	g_assert_null(next);

	/* Verify that we can't get past the end and get null on subsequent
	 * calls.
	 */
	next = hasl_context_next(context);
	g_assert_null(next);

	g_clear_object(&context);
}

static void
test_hasl_context_next_plain_not_possible_only_mechanism(void) {
	HaslContext *context = NULL;
	const char *next = NULL;

	context = hasl_context_new();
	hasl_context_set_allowed_mechanisms(context, "PLAIN");

	next = hasl_context_next(context);
	g_assert_null(next);

	/* Verify that we can't get past the end and get null on subsequent
	 * calls.
	 */
	next = hasl_context_next(context);
	g_assert_null(next);

	g_clear_object(&context);
}

static void
test_hasl_context_next_uses_allowed_mechanisms(void) {
	HaslContext *context = NULL;
	const char *next = NULL;

	context = hasl_context_new();
	hasl_context_set_allowed_mechanisms(context, "EXTERNAL");

	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "EXTERNAL");

	g_assert_finalize_object(context);
}

static void
test_hasl_context_next_uses_supported(void) {
	HaslContext *context = NULL;
	const char *next = NULL;

	context = hasl_context_new();

	/* We don't want to hard code what mechanism should be first here so we
	 * just check that we got a mechanism.
	 */
	next = hasl_context_next(context);
	g_assert_nonnull(next);

	g_assert_finalize_object(context);
}

static void
test_hasl_context_next_uses_server_mechanisms(void) {
	HaslContext *context = NULL;
	const char *next = NULL;

	context = hasl_context_new();
	hasl_context_set_server_mechanisms(context, "EXTERNAL");

	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "EXTERNAL");

	g_assert_finalize_object(context);
}

static void
test_hasl_context_next_prefers_allowed_mechanisms(void) {
	HaslContext *context = NULL;
	const char *next = NULL;

	/* Build the context with server and allowed mechanisms. */
	context = hasl_context_new();
	hasl_context_set_username(context, "homer");
	hasl_context_set_password(context, "0dy$$3y");
	hasl_context_set_allow_clear_text(context, TRUE);
	hasl_context_set_allowed_mechanisms(context, "EXTERNAL");
	hasl_context_set_server_mechanisms(context, "ANONYMOUS");

	/* Verify that allowed are preferred. */
	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "EXTERNAL");

	/* Reset the context and remove allowed. */
	hasl_context_done(context);
	hasl_context_set_allowed_mechanisms(context, NULL);

	/* Verify that the server mechanisms are returned. */
	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "ANONYMOUS");

	/* Reset the context and change the allowed mechanisms. */
	hasl_context_done(context);
	hasl_context_set_allowed_mechanisms(context, "PLAIN");

	/* Verify that the allowed mechanisms are preferred again. */
	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "PLAIN");

	/* Reset the context and remove the server mechanisms. */
	hasl_context_done(context);
	hasl_context_set_server_mechanisms(context, NULL);

	/* Verify that the allowed mechanisms are still returned. */
	next = hasl_context_next(context);
	g_assert_cmpstr(next, ==, "PLAIN");

	g_assert_finalize_object(context);
}

static void
test_hasl_context_done_new(void) {
	HaslContext *context = NULL;

	context = hasl_context_new();
	hasl_context_done(context);
	g_assert_finalize_object(context);
}

static void
test_hasl_context_done_used(void) {
	HaslContext *context = NULL;
	HaslMechanismResult result = HASL_MECHANISM_RESULT_ERROR;
	GError *error = NULL;
	const char *mechanism = NULL;
	guint8 *client_out = NULL;

	/* Create a context and allow PLAIN and EXTERNAL. */
	context = hasl_context_new();
	hasl_context_set_allowed_mechanisms(context, "PLAIN,EXTERNAL");
	hasl_context_set_username(context, "homer");
	hasl_context_set_password(context, "0dy$$3y");
	hasl_context_set_allow_clear_text(context, TRUE);

	mechanism = hasl_context_next(context);
	g_assert_cmpstr(mechanism, ==, "PLAIN");

	/* Step through to PLAIN, but ignore the output. */
	result = hasl_context_step(context, (guint8 *)"+", 1, &client_out, NULL, &error);
	g_assert_no_error(error);
	g_assert_cmpuint(result, ==, HASL_MECHANISM_RESULT_SUCCESS);
	g_free(client_out);

	/* Mark the context as done and verify that the current mechanism is NULL.
	 */
	hasl_context_done(context);

	mechanism = hasl_context_get_current_mechanism(context);
	g_assert_null(mechanism);

	/* And now we're done. */
	g_assert_finalize_object(context);
}

static void
test_hasl_context_allowed_mechanisms_null(void) {
	HaslContext *context = hasl_context_new();

	/* If NULL isn't allowed by a g_return_if_fail then a g_warning will be
	 * output which will cause the test to fail.
	 */
	hasl_context_set_allowed_mechanisms(context, NULL);

	g_assert_finalize_object(context);
}

static void
test_hasl_context_server_mechanisms_null(void) {
	HaslContext *context = hasl_context_new();

	/* If NULL isn't allowed by a g_return_if_fail then a g_warning will be
	 * output which will cause the test to fail.
	 */
	hasl_context_set_server_mechanisms(context, NULL);

	g_assert_finalize_object(context);
}

/******************************************************************************
 * Main
 *****************************************************************************/
int
main(int argc, char *argv[]) {
	g_test_init(&argc, &argv, NULL);

	g_test_add_func("/hasl/context/new", test_hasl_context_new);
	g_test_add_func("/hasl/context/properties", test_hasl_context_properties);

	g_test_add_func("/hasl/context/add-mechanism/null",
	                test_hasl_context_add_mechanism_null);
	g_test_add_func("/hasl/context/add-mechanism/empty",
	                test_hasl_context_add_mechanism_empty);

	g_test_add_func("/hasl/context/supported-mechanisms/default",
	                test_hasl_context_supported_mechanisms_default);
	g_test_add_func("/hasl/context/supported-mechanisms/custom",
	                test_hasl_context_supported_mechanisms_custom);

	g_test_add_func("/hasl/context/current-mechanism",
	                test_hasl_context_current_mechanism);

	g_test_add_func("/hasl/context/next/plain_and_external",
	                test_hasl_context_next_plain_and_external);
	g_test_add_func("/hasl/context/next/plain_not_possible",
	                test_hasl_context_next_plain_not_possible);
	g_test_add_func("/hasl/context/next/plain_not_possible_only_mechanism",
	                test_hasl_context_next_plain_not_possible_only_mechanism);
	g_test_add_func("/hasl/context/next/uses-allowed-mechanisms",
	                test_hasl_context_next_uses_allowed_mechanisms);

	g_test_add_func("/hasl/context/next/uses_supported",
	                test_hasl_context_next_uses_supported);
	g_test_add_func("/hasl/context/next/uses-server-mechanisms",
	                test_hasl_context_next_uses_server_mechanisms);
	g_test_add_func("/hasl/context/next/prefers-allowed-mechanisms",
	                test_hasl_context_next_prefers_allowed_mechanisms);

	g_test_add_func("/hasl/context/done/new", test_hasl_context_done_new);
	g_test_add_func("/hasl/context/done/used", test_hasl_context_done_used);

	g_test_add_func("/hasl/context/allowed-mechanisms/null",
	                test_hasl_context_allowed_mechanisms_null);
	g_test_add_func("/hasl/context/server-mechanisms/null",
	                test_hasl_context_server_mechanisms_null);

	return g_test_run();
}
