/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * 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 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 <birb.h>

#include <purple.h>

/******************************************************************************
 * Tests
 *****************************************************************************/
static void
test_purple_scheduled_task_new(void) {
	PurpleScheduledTask *task = NULL;

	task = purple_scheduled_task_new("test-task", "A task for testing", TRUE);

	birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK);

	g_assert_finalize_object(task);
}

static void
test_purple_scheduled_task_properties(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	PurpleTags *tags = NULL;
	GDateTime *execute_at = NULL;
	gboolean cancellable = TRUE;
	gboolean persistent = FALSE;
	char *id = NULL;
	char *subtitle = NULL;
	char *task_type = NULL;
	char *title = NULL;

	task = g_object_new(
		PURPLE_TYPE_SCHEDULED_TASK,
		"cancellable", FALSE,
		"id", "0xabc123",
		"persistent", TRUE,
		"subtitle", "a task for testing",
		"task-type", "test",
		"title", "Test",
		NULL);

	g_assert_true(PURPLE_IS_SCHEDULED_TASK(task));

	g_object_get(
		G_OBJECT(task),
		"cancellable", &cancellable,
		"execute-at", &execute_at,
		"id", &id,
		"persistent", &persistent,
		"state", &state,
		"subtitle", &subtitle,
		"tags", &tags,
		"task-type", &task_type,
		"title", &title,
		NULL);

	g_assert_false(cancellable);

	/* The task hasn't been scheduled so it doesn't have an execution time. */
	g_assert_null(execute_at);

	g_assert_cmpstr(id, ==, "0xabc123");
	g_clear_pointer(&id, g_free);

	g_assert_true(persistent);

	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);

	g_assert_cmpstr(subtitle, ==, "a task for testing");
	g_clear_pointer(&subtitle, g_free);

	birb_assert_type(tags, PURPLE_TYPE_TAGS);
	g_clear_object(&tags);

	g_assert_cmpstr(task_type, ==, "test");
	g_clear_pointer(&task_type, g_free);

	g_assert_cmpstr(title, ==, "Test");
	g_clear_pointer(&title, g_free);

	g_assert_finalize_object(task);
}

/******************************************************************************
 * Schedule Tests
 *****************************************************************************/
static void
test_purple_scheduled_task_executed_counter_cb(PurpleScheduledTask *task,
                                               gpointer data)
{
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	guint *counter = data;

	birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTING);

	*counter = *counter + 1;
}

static void
test_purple_scheduled_task_executed_cancel_cb(PurpleScheduledTask *task,
                                              G_GNUC_UNUSED gpointer data)
{
	GError *error = NULL;
	gboolean result = FALSE;

	result = purple_scheduled_task_schedule_relative(task,
	                                                 10 * G_TIME_SPAN_MINUTE,
	                                                 &error);
	g_assert_error(error,
	               PURPLE_SCHEDULED_TASK_ERROR,
	               PURPLE_SCHEDULED_TASK_ERROR_RESCHEDULE_EXECUTING_TASK);
	g_clear_error(&error);
	g_assert_false(result);
}

static void
test_purple_scheduled_task_executed_quit_cb(PurpleScheduledTask *task,
                                            gpointer data)
{
	birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK);

	g_main_loop_quit(data);
}

static void
test_purple_scheduled_task_main_loop_timeout_cb(gpointer data) {
	g_main_loop_quit(data);

	g_assert_not_reached();
}

static void
test_purple_scheduled_task_schedule_normal(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	GDateTime *execute_at = NULL;
	GError *error = NULL;
	GMainLoop *loop = NULL;
	guint counter = 0;
	gboolean result = FALSE;

	task = purple_scheduled_task_new("test", "Test task", TRUE);
	g_signal_connect(task, "execute",
	                 G_CALLBACK(test_purple_scheduled_task_executed_counter_cb),
	                 &counter);

	/* Add another signal handler to verify that you can't reschedule an
	 * executing task.
	 */
	g_signal_connect(task, "execute",
	                 G_CALLBACK(test_purple_scheduled_task_executed_cancel_cb),
	                 NULL);

	/* Verify the default state is unscheduled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);

	/* Make sure that we don't currently have an execute-at. */
	execute_at = purple_scheduled_task_get_execute_at(task);
	g_assert_null(execute_at);

	/* Now schedule the task to execute 10 milliseconds from now. */
	result = purple_scheduled_task_schedule_relative(task,
	                                                 10 * G_TIME_SPAN_MILLISECOND,
	                                                 &error);
	g_assert_no_error(error);
	g_assert_true(result);

	/* Make sure the execute-at property got set. */
	execute_at = purple_scheduled_task_get_execute_at(task);
	g_assert_nonnull(execute_at);

	/* Make sure the state was set to scheduled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Create our main loop and add a timeout to avoid runaways. */
	loop = g_main_loop_new(NULL, FALSE);
	g_signal_connect(task, "execute",
	                 G_CALLBACK(test_purple_scheduled_task_executed_quit_cb),
	                 loop);

	g_timeout_add_seconds_once(10,
	                           test_purple_scheduled_task_main_loop_timeout_cb,
	                           loop);
	g_main_loop_run(loop);
	g_main_loop_unref(loop);

	/* Make sure that the execute signal was emitted. */
	g_assert_cmpuint(counter, ==, 1);

	/* Make sure the state was updated to executed. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTED);

	g_assert_finalize_object(task);
}

static void
test_purple_scheduled_task_schedule_cancelled(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	GError *error = NULL;
	gboolean result = FALSE;

	task = purple_scheduled_task_new("test", "Test task", TRUE);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);

	/* Cancelling a task that is not currently scheduled should do nothing. */
	purple_scheduled_task_cancel(task);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);

	/* Schedule the task. */
	result = purple_scheduled_task_schedule_relative(task,
	                                                 10 * G_TIME_SPAN_MINUTE,
	                                                 &error);
	g_assert_no_error(error);
	g_assert_true(result);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Cancel the task. */
	purple_scheduled_task_cancel(task);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_CANCELLED);

	g_assert_finalize_object(task);
}

static void
test_purple_scheduled_task_schedule_reschedule(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	GDateTime *now = NULL;
	GDateTime *later = NULL;
	GDateTime *execute_at = NULL;
	GError *error = NULL;
	gboolean result = FALSE;

	task = purple_scheduled_task_new("test", "Test task", TRUE);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);

	/* Schedule the task. */
	result = purple_scheduled_task_schedule_relative(task,
	                                                 10 * G_TIME_SPAN_MINUTE,
	                                                 &error);
	g_assert_no_error(error);
	g_assert_true(result);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Reschedule the task for an hour. */
	now = g_date_time_new_now_local();
	later = g_date_time_add(now, 1 * G_TIME_SPAN_HOUR);
	g_clear_pointer(&now, g_date_time_unref);

	result = purple_scheduled_task_schedule(task, later, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	/* Verify that the execute-at property is correct. */
	execute_at = purple_scheduled_task_get_execute_at(task);
	g_assert_true(birb_date_time_equal(execute_at, later));
	g_clear_pointer(&later, g_date_time_unref);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Cancel the task to clear the timeout and the reference it holds. */
	purple_scheduled_task_cancel(task);

	g_assert_finalize_object(task);
}

static void
test_purple_scheduled_task_schedule_past(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	GError *error = NULL;
	gboolean result = FALSE;

	task = purple_scheduled_task_new("test", "Test task", TRUE);

	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);

	/* Schedule the task for 10 minutes ago. */
	result = purple_scheduled_task_schedule_relative(task,
	                                                 -10 * G_TIME_SPAN_MINUTE,
	                                                 &error);
	g_assert_error(error,
	               PURPLE_SCHEDULED_TASK_ERROR,
	               PURPLE_SCHEDULED_TASK_ERROR_EXECUTE_AT_IN_PAST);
	g_clear_error(&error);
	g_assert_false(result);

	g_assert_finalize_object(task);
}

static void
test_purple_scheduled_task_schedule_reuse(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	GDateTime *execute_at = NULL;
	GError *error = NULL;
	GMainLoop *loop = NULL;
	guint counter = 0;
	gboolean result = FALSE;

	task = purple_scheduled_task_new("test", "Test task", TRUE);
	g_signal_connect(task, "execute",
	                 G_CALLBACK(test_purple_scheduled_task_executed_counter_cb),
	                 &counter);

	/* Verify the default state is unscheduled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);

	/* Make sure that we don't currently have an execute-at. */
	execute_at = purple_scheduled_task_get_execute_at(task);
	g_assert_null(execute_at);

	/* Now schedule the task to execute 10 milliseconds from now. */
	result = purple_scheduled_task_schedule_relative(task,
	                                                 10 * G_TIME_SPAN_MILLISECOND,
	                                                 &error);
	g_assert_no_error(error);
	g_assert_true(result);

	/* Make sure the execute-at property got set. */
	execute_at = purple_scheduled_task_get_execute_at(task);
	g_assert_nonnull(execute_at);

	/* Make sure the state was set to scheduled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Create our main loop and add a timeout to avoid runaways. */
	loop = g_main_loop_new(NULL, FALSE);
	g_signal_connect(task, "execute",
	                 G_CALLBACK(test_purple_scheduled_task_executed_quit_cb),
	                 loop);

	g_timeout_add_seconds_once(10,
	                           test_purple_scheduled_task_main_loop_timeout_cb,
	                           loop);
	g_main_loop_run(loop);

	/* Make sure that the execute signal was emitted. */
	g_assert_cmpuint(counter, ==, 1);

	/* Make sure the state was updated to executed. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTED);

	/* Now reset everything. */
	counter = 0;
	g_signal_connect(task, "execute",
	                 G_CALLBACK(test_purple_scheduled_task_executed_quit_cb),
	                 loop);
	g_timeout_add_seconds_once(10,
	                           test_purple_scheduled_task_main_loop_timeout_cb,
	                           loop);

	/* Now schedule the task to execute 10 milliseconds from now. */
	result = purple_scheduled_task_schedule_relative(task,
	                                                 10 * G_TIME_SPAN_MILLISECOND,
	                                                 &error);
	g_assert_no_error(error);
	g_assert_true(result);

	/* Run the loop. */
	g_main_loop_run(loop);

	/* Make sure that the execute signal was emitted. */
	g_assert_cmpuint(counter, ==, 1);

	/* Make sure the state was updated to executed. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTED);

	g_main_loop_unref(loop);

	g_assert_finalize_object(task);
}

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

	g_test_add_func("/scheduled-task/new", test_purple_scheduled_task_new);
	g_test_add_func("/scheduled-task/properties",
	                test_purple_scheduled_task_properties);

	g_test_add_func("/scheduled-task/schedule/normal",
	                test_purple_scheduled_task_schedule_normal);
	g_test_add_func("/scheduled-task/schedule/cancelled",
	                test_purple_scheduled_task_schedule_cancelled);
	g_test_add_func("/scheduled-task/schedule/reschedule",
	                test_purple_scheduled_task_schedule_reschedule);
	g_test_add_func("/scheduled-task/schedule/past",
	                test_purple_scheduled_task_schedule_past);
	g_test_add_func("/scheduled-task/schedule/reuse",
	                test_purple_scheduled_task_schedule_reuse);

	return g_test_run();
}
