/*
 * 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_scheduler_new(void) {
	PurpleScheduler *scheduler = NULL;

	scheduler = purple_scheduler_new();
	birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER);
	g_assert_true(G_IS_LIST_MODEL(scheduler));

	g_assert_finalize_object(scheduler);
}

static void
test_purple_scheduler_properties(void) {
	PurpleScheduler *scheduler = NULL;
	GType item_type = G_TYPE_INVALID;
	guint n_items = 0;

	scheduler = g_object_new(
		PURPLE_TYPE_SCHEDULER,
		NULL);

	g_object_get(
		G_OBJECT(scheduler),
		"item-type", &item_type,
		"n-items", &n_items,
		NULL);

	g_assert_cmpuint(item_type, ==, PURPLE_TYPE_SCHEDULED_TASK);

	g_assert_cmpuint(n_items, ==, 0);

	g_assert_finalize_object(scheduler);
}

static void
test_purple_scheduler_add_remove(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	PurpleScheduler *scheduler = NULL;
	GDateTime *execute_at = NULL;
	GDateTime *now = NULL;
	GError *error = NULL;
	guint counter = 0;
	gboolean result = FALSE;

	scheduler = purple_scheduler_new();
	birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER);
	g_assert_true(G_IS_LIST_MODEL(scheduler));

	/* Create our execute_at time. */
	now = g_date_time_new_now_local();
	execute_at = g_date_time_add(now, 10 * G_TIME_SPAN_MINUTE);
	g_clear_pointer(&now, g_date_time_unref);

	/* Wire up our signals. */
	birb_count_list_model_items_changed(G_LIST_MODEL(scheduler), &counter);

	/* Create the task. */
	task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests",
	                                 TRUE);
	birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK);

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

	/* Add the task to the scheduler. */
	counter = 0;
	result = purple_scheduler_add_task(scheduler, task, execute_at, &error);

	g_assert_no_error(error);
	g_assert_true(result);

	/* Make sure items changed was called once and that we have 1 item in the
	 * list model.
	 */
	g_assert_cmpuint(counter, ==, 1);
	birb_assert_list_model_n_items(scheduler, 1);

	/* Make sure that the task got scheduled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Remove the task. */
	counter = 0;

	result = purple_scheduler_remove_task(scheduler,
	                                      purple_scheduled_task_get_id(task));
	g_assert_true(result);

	/* Make sure items changed was called once and that the model empty. */
	g_assert_cmpuint(counter, ==, 1);
	birb_assert_list_model_n_items(scheduler, 0);

	/* Make sure the that the task got cancelled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_CANCELLED);

	/* After removal from the scheduler, only our reference should exist. */
	g_assert_finalize_object(task);

	/* Clean up the scheduler. */
	g_assert_finalize_object(scheduler);

	g_clear_pointer(&execute_at, g_date_time_unref);
}

static void
test_purple_scheduler_add_already_scheduled(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduler *scheduler = NULL;
	GDateTime *original_execute_at = NULL;
	GDateTime *updated_execute_at = NULL;
	GError *error = NULL;
	gboolean result = FALSE;

	scheduler = purple_scheduler_new();

	/* Create the task. */
	task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests",
	                                 TRUE);

	/* Schedule the task and store the original execute_at. */
	result = purple_scheduled_task_schedule_relative(task,
	                                                 10 * G_TIME_SPAN_MILLISECOND,
	                                                 &error);
	g_assert_no_error(error);
	g_assert_true(result);

	original_execute_at = purple_scheduled_task_get_execute_at(task);
	if(original_execute_at != NULL) {
		g_date_time_ref(original_execute_at);
	}

	/* Add the task to the scheduler. */
	result = purple_scheduler_add_task_relative(scheduler,
	                                            task,
	                                            100 * G_TIME_SPAN_MILLISECOND,
	                                            &error);
	g_assert_no_error(error);
	g_assert_true(result);

	/* Get the execute time of the task and verify that it is not the same as
	 * the original time.
	 */
	updated_execute_at = purple_scheduled_task_get_execute_at(task);
	g_assert_false(birb_date_time_equal(original_execute_at,
	                                    updated_execute_at));

	/* Clean up everything. */
	g_assert_finalize_object(scheduler);
	g_assert_finalize_object(task);
	g_clear_pointer(&original_execute_at, g_date_time_unref);
}

static void
test_purple_scheduler_double_add(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	PurpleScheduler *scheduler = NULL;
	GDateTime *execute_at = NULL;
	GDateTime *now = NULL;
	GError *error = NULL;
	guint counter = 0;
	gboolean result = FALSE;

	scheduler = purple_scheduler_new();
	birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER);
	g_assert_true(G_IS_LIST_MODEL(scheduler));

	/* Create our execute_at time. */
	now = g_date_time_new_now_local();
	execute_at = g_date_time_add(now, 10 * G_TIME_SPAN_MINUTE);
	g_clear_pointer(&now, g_date_time_unref);

	/* Wire up our signals. */
	birb_count_list_model_items_changed(G_LIST_MODEL(scheduler), &counter);

	/* Create the task. */
	task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests",
	                                 TRUE);
	birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK);

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

	/* Add the task to the scheduler. */
	counter = 0;
	result = purple_scheduler_add_task(scheduler, task, execute_at, &error);

	g_assert_no_error(error);
	g_assert_true(result);

	/* Make sure items changed was called once and that we have 1 item in the
	 * list model.
	 */
	g_assert_cmpuint(counter, ==, 1);
	birb_assert_list_model_n_items(scheduler, 1);

	/* Make sure that the task got scheduled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Now add the task again. */
	counter = 0;
	result = purple_scheduler_add_task(scheduler, task, execute_at, &error);
	g_assert_error(error,
	               PURPLE_SCHEDULER_ERROR,
	               PURPLE_SCHEDULER_ERROR_TASK_EXISTS);
	g_clear_error(&error);
	g_assert_false(result);

	/* Make sure the items-changed signal wasn't called and that we still only
	 * have one item in the list.
	 */
	g_assert_cmpuint(counter, ==, 0);
	birb_assert_list_model_n_items(scheduler, 1);

	/* Cleanup. We don't remove the task because we want to make sure the
	 * scheduler will cancel it when it shuts down. Also the task is last as
	 * it's still known to the scheduler.
	 */
	g_assert_finalize_object(scheduler);

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

	g_assert_finalize_object(task);

	g_clear_pointer(&execute_at, g_date_time_unref);
}

static void
test_purple_scheduler_double_remove(void) {
	PurpleScheduledTask *task = NULL;
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	PurpleScheduler *scheduler = NULL;
	GDateTime *execute_at = NULL;
	GDateTime *now = NULL;
	GError *error = NULL;
	guint counter = 0;
	gboolean result = FALSE;

	scheduler = purple_scheduler_new();
	birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER);
	g_assert_true(G_IS_LIST_MODEL(scheduler));

	/* Create our execute_at time. */
	now = g_date_time_new_now_local();
	execute_at = g_date_time_add(now, 10 * G_TIME_SPAN_MINUTE);
	g_clear_pointer(&now, g_date_time_unref);

	/* Wire up our signals. */
	birb_count_list_model_items_changed(G_LIST_MODEL(scheduler), &counter);

	/* Create the task. */
	task = purple_scheduled_task_new("test-scheduler", "Scheduler Tests",
	                                 TRUE);
	birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK);

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

	/* Add the task to the scheduler. */
	counter = 0;
	result = purple_scheduler_add_task(scheduler, task, execute_at, &error);

	g_assert_no_error(error);
	g_assert_true(result);

	/* Make sure items changed was called once and that we have 1 item in the
	 * list model.
	 */
	g_assert_cmpuint(counter, ==, 1);
	birb_assert_list_model_n_items(scheduler, 1);

	/* Make sure that the task got scheduled. */
	state = purple_scheduled_task_get_state(task);
	g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED);

	/* Remove the task. */
	counter = 0;
	result = purple_scheduler_remove_task(scheduler,
	                                      purple_scheduled_task_get_id(task));
	g_assert_true(result);

	/* Make sure the items-changed signal got called once and that we no longer
	 * have any items in the model.
	 */
	g_assert_cmpuint(counter, ==, 1);
	birb_assert_list_model_n_items(scheduler, 0);

	/* Attempt to remove the task again. */
	counter = 0;
	result = purple_scheduler_remove_task(scheduler,
	                                      purple_scheduled_task_get_id(task));
	g_assert_false(result);

	/* Make sure the items-changed signal wasn't called and that we still don't
	 * have any items in the model.
	 */
	g_assert_cmpuint(counter, ==, 0);
	birb_assert_list_model_n_items(scheduler, 0);

	/* Cleanup. We remove task first because the scheduler shouldn't know about
	 * it anymore.
	 */
	g_assert_finalize_object(task);
	g_assert_finalize_object(scheduler);

	g_clear_pointer(&execute_at, g_date_time_unref);
}

/******************************************************************************
 * Main
 *****************************************************************************/
static void
test_purple_scheduler_execute_task_cb(PurpleScheduler *scheduler,
                                      PurpleScheduledTask *task,
                                      const char *task_type,
                                      gpointer data)
{
	PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
	const char *actual_task_type = NULL;
	guint *counter = data;

	birb_assert_type(scheduler, PURPLE_TYPE_SCHEDULER);

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

	actual_task_type = purple_scheduled_task_get_task_type(task);
	g_assert_cmpstr(actual_task_type, ==, task_type);

	*counter = *counter + 1;
}

static void
test_purple_scheduler_signals_quit_cb(G_GNUC_UNUSED PurpleScheduler *scheduler,
                                      G_GNUC_UNUSED PurpleScheduledTask *task,
                                      G_GNUC_UNUSED const char *task_type,
                                      gpointer data)
{
	g_main_loop_quit(data);
}

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

	g_assert_not_reached();
}

static void
test_purple_scheduler_signals(void) {
	PurpleScheduledTask *task1 = NULL;
	PurpleScheduledTask *task2 = NULL;
	PurpleScheduler *scheduler = NULL;
	GError *error = NULL;
	GMainLoop *loop = NULL;
	guint all_counter = 0;
	guint detailed_counter = 0;
	gboolean result = FALSE;

	/* This test creates 2 tasks, one for 10ms from now and the second for 20ms
	 * from now. When the 20ms task is executed the main loop will be quit and
	 * allow the rest of the test to finish.
	 *
	 * There is a 2 second timeout to make sure we don't hang the unit tests if
	 * something unexpected happens. We use 2 seconds because internally the
	 * tasks are scheduled with g_timeout_add_seconds which tries to schedule
	 * timeouts together to avoid excessive CPU wake ups, so 2 seconds should
	 * cover that.
	 */

	/* Create the counter and add our signal handlers with and without the
	 * detail.
	 */
	scheduler = purple_scheduler_new();
	g_signal_connect(scheduler,
	                 "execute-task",
	                 G_CALLBACK(test_purple_scheduler_execute_task_cb),
	                 &all_counter);
	g_signal_connect(scheduler,
	                 "execute-task::scheduler-test-2",
	                 G_CALLBACK(test_purple_scheduler_execute_task_cb),
	                 &detailed_counter);

	/* Add the first task. */
	task1 = purple_scheduled_task_new("scheduler-test-1", "Scheduler Test 1",
	                                  TRUE);
	result = purple_scheduler_add_task_relative(scheduler,
	                                            task1,
	                                            10 * G_TIME_SPAN_MILLISECOND,
	                                            &error);
	g_assert_no_error(error);
	g_assert_true(result);

	/* Add the second task. */
	task2 = purple_scheduled_task_new("scheduler-test-2", "Scheduler Test 2",
	                                  FALSE);
	result = purple_scheduler_add_task_relative(scheduler,
	                                            task2,
	                                            20 * G_TIME_SPAN_MILLISECOND,
	                                            &error);
	g_assert_no_error(error);
	g_assert_true(result);

	/* Create the main loop to run the tasks. */
	loop = g_main_loop_new(NULL, FALSE);

	/* Add a handler to quit the main loop when the second task is executed. */
	g_signal_connect(scheduler,
	                 "execute-task::scheduler-test-2",
	                 G_CALLBACK(test_purple_scheduler_signals_quit_cb),
	                 loop);

	/* Add a timeout to avoid hangs on unexpected behavior. */
	g_timeout_add_seconds_once(2, test_purple_scheduler_signals_timeout_cb,
	                           loop);

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

	g_clear_pointer(&loop, g_main_loop_unref);

	/* Make sure the counters are correct. */
	g_assert_cmpuint(all_counter, ==, 2);
	g_assert_cmpuint(detailed_counter, ==, 1);

	/* Make sure the scheduler is empty. */
	birb_assert_list_model_n_items(scheduler, 0);

	g_assert_finalize_object(scheduler);
	g_assert_finalize_object(task1);
	g_assert_finalize_object(task2);
}

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

	g_test_add_func("/scheduler/new", test_purple_scheduler_new);
	g_test_add_func("/scheduler/properties", test_purple_scheduler_properties);

	g_test_add_func("/scheduler/add-remove", test_purple_scheduler_add_remove);
	g_test_add_func("/scheduler/add-already-scheduled",
	                test_purple_scheduler_add_already_scheduled);
	g_test_add_func("/scheduler/double-add", test_purple_scheduler_double_add);
	g_test_add_func("/scheduler/double-remove",
	                test_purple_scheduler_double_remove);

	g_test_add_func("/scheduler/signals", test_purple_scheduler_signals);

	return g_test_run();
}
