/*
 * 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_command_manager_new(void) {
	PurpleCommandManager *manager = NULL;

	manager = purple_command_manager_new();
	g_assert_true(PURPLE_IS_COMMAND_MANAGER(manager));

	g_assert_finalize_object(manager);
}

static void
test_purple_command_manager_add_remove(void) {
	PurpleCommand *command = NULL;
	PurpleCommandManager *manager = NULL;
	GListModel *model = NULL;
	gboolean ret = FALSE;
	guint counter = 0;

	manager = purple_command_manager_new();
	model = G_LIST_MODEL(manager);
	birb_count_list_model_items_changed(G_LIST_MODEL(manager), &counter);

	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);

	command = purple_command_new("test-command", "test", 0);
	/* We use command again in the next test. */
	g_object_ref(command);
	purple_command_manager_add(manager, command);
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
	g_assert_cmpuint(counter, ==, 1);

	/* Duplicate adds should be ignore. */
	counter = 0;
	purple_command_manager_add(manager, command);
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
	g_assert_cmpuint(counter, ==, 0);

	/* Duplicate command but as a new pointer and make sure it doesn't get
	 * added.
	 */
	counter = 0;
	command = purple_command_new("test-command", "test", 0);
	purple_command_manager_add(manager, command);
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
	g_assert_cmpuint(counter, ==, 0);

	/* Name and command must match. */
	counter = 0;
	ret = purple_command_manager_remove(manager, "unknown-command", "test");
	g_assert_false(ret);
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
	g_assert_cmpuint(counter, ==, 0);

	counter = 0;
	ret = purple_command_manager_remove(manager, "test-command", "unknown");
	g_assert_false(ret);
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
	g_assert_cmpuint(counter, ==, 0);

	/* Do the real remove. */
	counter = 0;
	ret = purple_command_manager_remove(manager, "test-command", "test");
	g_assert_true(ret);
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
	g_assert_cmpuint(counter, ==, 1);

	/* Duplicate removes should return false as the item is gone. */
	counter = 0;
	ret = purple_command_manager_remove(manager, "test-command", "test");
	g_assert_false(ret);
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
	g_assert_cmpuint(counter, ==, 0);

	g_assert_finalize_object(manager);
}

static void
test_purple_command_manager_remove_all_with_source(void) {
	PurpleCommand *command = NULL;
	PurpleCommandManager *manager = NULL;
	GListModel *model = NULL;
	guint counter = 0;

	manager = purple_command_manager_new();
	model = G_LIST_MODEL(manager);
	birb_count_list_model_items_changed(G_LIST_MODEL(manager), &counter);

	/* Make sure remove all works on an empty manager. */
	counter = 0;
	purple_command_manager_remove_all_with_source(manager, "test-1");
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
	g_assert_cmpuint(counter, ==, 0);

	/* Add 3 commands, two with the same source and another with a different
	 * source in between them.
	 */
	command = purple_command_new("privmsg", "test-1", 0);
	purple_command_manager_add(manager, command);

	command = purple_command_new("xyzzy", "test-2", 0);
	purple_command_manager_add(manager, command);

	command = purple_command_new("query", "test-1", 0);
	purple_command_manager_add(manager, command);

	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 3);

	/* Remove the commands with test-1 as a source and verify that the second
	 * command still exists.
	 */
	counter = 0;
	purple_command_manager_remove_all_with_source(manager, "test-1");
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
	g_assert_cmpuint(counter, ==, 2);

	command = purple_command_manager_find(manager, NULL, "xyzzy");
	g_assert_true(PURPLE_IS_COMMAND(command));

	/* Add another command with a source of test-2. */
	command = purple_command_new("quit", "test-2", 0);
	purple_command_manager_add(manager, command);

	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 2);

	/* Now remove all commands with a source of test-2. */
	counter = 0;
	purple_command_manager_remove_all_with_source(manager, "test-2");
	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
	g_assert_cmpuint(counter, ==, 2);

	g_assert_finalize_object(manager);
}

static void
test_purple_command_manager_find(void) {
	PurpleCommand *command = NULL;
	PurpleCommandManager *manager = NULL;
	PurpleConversation *conversation = NULL;
	PurpleTags *tags = NULL;

	manager = purple_command_manager_new();

	/* We need at least one conversation to test filtering, so we just create
	 * one with a tag of test=true.
	 */
	conversation = g_object_new(PURPLE_TYPE_CONVERSATION, NULL);
	tags = purple_conversation_get_tags(conversation);
	purple_tags_add(tags, "test=true");

	/* Test that find works with nothing added. This is unlikely to happen in
	 * practice, but it's good to test for.
	 */
	command = purple_command_manager_find(manager, NULL, "unknown");
	g_assert_null(command);

	command = purple_command_manager_find(manager, conversation, "unknown");
	g_assert_null(command);

	/* Now add a test command and verify that we can find it. */
	command = purple_command_new("test", "test-1", 0);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "test=true");
	purple_command_manager_add(manager, command);

	command = purple_command_manager_find(manager, NULL, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));

	command = purple_command_manager_find(manager, conversation, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));

	/* Now add the command again but with a lower priority. */
	command = purple_command_new("test", "test-2", -100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "test=true");
	purple_command_manager_add(manager, command);

	command = purple_command_manager_find(manager, NULL, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_cmpuint(purple_command_get_priority(command), ==, 0);

	command = purple_command_manager_find(manager, conversation, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_cmpuint(purple_command_get_priority(command), ==, 0);

	/* Now add the command again but with a higher priority. */
	command = purple_command_new("test", "test-3", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "test=true");
	purple_command_manager_add(manager, command);

	command = purple_command_manager_find(manager, NULL, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_cmpuint(purple_command_get_priority(command), ==, 100);

	command = purple_command_manager_find(manager, conversation, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_cmpuint(purple_command_get_priority(command), ==, 100);

	/* Finally add the command again, but with a much higher priority and tags
	 * that won't match the conversation.
	 */
	command = purple_command_new("test", "test-4", 1337);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "test=false");
	purple_command_manager_add(manager, command);

	command = purple_command_manager_find(manager, NULL, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_cmpuint(purple_command_get_priority(command), ==, 1337);

	command = purple_command_manager_find(manager, conversation, "test");
	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_cmpuint(purple_command_get_priority(command), ==, 100);

	/* Cleanup. */
	g_assert_finalize_object(conversation);
	g_assert_finalize_object(manager);
}

static void
test_purple_command_manager_find_all(void) {
	PurpleCommand *command = NULL;
	PurpleCommandManager *manager = NULL;
	PurpleConversation *conversation = NULL;
	PurpleTags *tags = NULL;
	GListModel *commands = NULL;

	manager = purple_command_manager_new();

	/* We need at least one conversation to test filtering, so we just create
	 * one with a tag of test=true.
	 */
	conversation = g_object_new(PURPLE_TYPE_CONVERSATION, NULL);
	tags = purple_conversation_get_tags(conversation);
	purple_tags_add(tags, "test=true");

	/* Create and add our commands that all share a name. One with no tags,
	 * another with the same tags as the conversation, one without the
	 * conversation tags, and another with a different priority but the
	 * conversation tags.
	 */
	command = purple_command_new("test", "test-1", 0);
	purple_command_manager_add(manager, command);

	command = purple_command_new("test", "test-2", 0);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "test=true");
	purple_command_manager_add(manager, command);

	command = purple_command_new("test", "test-3", 0);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "test=false");
	purple_command_manager_add(manager, command);

	command = purple_command_new("test", "test-4", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "test=true");
	purple_command_manager_add(manager, command);

	/* Check an unknown command. */
	commands = purple_command_manager_find_all(manager, NULL, "unknown");
	g_assert_cmpuint(g_list_model_get_n_items(commands), ==, 0);
	g_clear_object(&commands);

	/* Check without the conversation. */
	commands = purple_command_manager_find_all(manager, NULL, "test");
	g_assert_cmpuint(g_list_model_get_n_items(commands), ==, 4);
	g_clear_object(&commands);

	/* Check with the conversation. */
	commands = purple_command_manager_find_all(manager, conversation, "test");
	g_assert_cmpuint(g_list_model_get_n_items(commands), ==, 3);
	g_clear_object(&commands);

	g_assert_finalize_object(conversation);
	g_assert_finalize_object(manager);
}

/******************************************************************************
 * Find and Execute tests
 *****************************************************************************/
static void
test_purple_command_manager_find_and_execute_counter(PurpleCommand *command,
                                                     PurpleConversation *conversation,
                                                     G_GNUC_UNUSED GStrv params,
                                                     gpointer data)
{
	guint *counter = data;

	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_true(PURPLE_IS_CONVERSATION(conversation));

	*counter = *counter + 1;
}

static void
test_purple_command_manager_find_and_execute_executed(PurpleCommand *command,
                                                      PurpleConversation *conversation,
                                                      GStrv params,
                                                      gpointer data)
{
	g_assert_true(PURPLE_IS_COMMAND(command));
	g_assert_true(PURPLE_IS_CONVERSATION(conversation));
	g_assert_cmpstrv(params, data);
}

static void
test_purple_command_manager_find_and_execute(void) {
	PurpleCommand *command = NULL;
	PurpleCommandManager *manager = NULL;
	PurpleConversation *conversation = NULL;
	gboolean result = FALSE;
	guint counter = 0;
	const char * const expected_args[] = {"arg1", "arg2", NULL};

	manager = purple_command_manager_new();

	conversation = g_object_new(PURPLE_TYPE_CONVERSATION, NULL);

	/* Make sure we can't find anything in an empty manager. */
	result = purple_command_manager_find_and_execute(manager, conversation,
	                                                 "unknown");
	g_assert_false(result);

	/* Again but with some arguments this time. */
	result = purple_command_manager_find_and_execute(manager, conversation,
	                                                 "unknown arg1 arg2");
	g_assert_false(result);

	/* Create and add the command. */
	command = purple_command_new("test", "test-1", 0);
	g_signal_connect(command, "executed",
	                 G_CALLBACK(test_purple_command_manager_find_and_execute_counter),
	                 &counter);
	g_signal_connect(command, "executed",
	                 G_CALLBACK(test_purple_command_manager_find_and_execute_executed),
	                 (gpointer)expected_args);
	purple_command_manager_add(manager, command);

	/* Try to execute it. */
	counter = 0;
	result = purple_command_manager_find_and_execute(manager, conversation,
	                                                 "test arg1 arg2");
	g_assert_true(result);
	g_assert_cmpuint(counter, ==, 1);

	g_assert_finalize_object(conversation);
	g_assert_finalize_object(manager);
}

static void
test_purple_command_manager_get_commands_for_conversation(void) {
	PurpleAccount *account = NULL;
	PurpleCommand *command = NULL;
	PurpleCommandManager *manager = NULL;
	PurpleConversation *conversation = NULL;
	PurpleTags *tags = NULL;
	GListModel *commands = NULL;

	/* This test will add a unique number of commands for each conversation
	 * type so we can use that number to determine that the correct commands
	 * were returned without having to explicitly check each command.
	 */

	account = purple_account_new("test", "test");

	/* Create the manager. */
	manager = purple_command_manager_new();

	/* Add a single command for dms. */
	command = purple_command_new("dm-1", "test", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "type:dm");
	purple_command_manager_add(manager, command);

	/* Add two commands for group dms. */
	command = purple_command_new("group-dm1", "test", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "type:group-dm");
	purple_command_manager_add(manager, command);

	command = purple_command_new("group-dm2", "test", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "type:group-dm");
	purple_command_manager_add(manager, command);

	/* Add three commands for channels. */
	command = purple_command_new("channel1", "test", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "type:channel");
	purple_command_manager_add(manager, command);

	command = purple_command_new("channel2", "test", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "type:channel");
	purple_command_manager_add(manager, command);

	command = purple_command_new("channel3", "test", 100);
	tags = purple_command_get_tags(command);
	purple_tags_add(tags, "type:channel");
	purple_command_manager_add(manager, command);

	/* Create a dm and verify that we got a list containing one command. */
	conversation = purple_conversation_new(account,
	                                       PURPLE_CONVERSATION_TYPE_DM,
	                                       "test");

	commands = purple_command_manager_get_commands_for_conversation(manager,
	                                                                conversation);

	birb_assert_list_model_n_items(commands, 1);

	g_assert_finalize_object(commands);
	g_assert_finalize_object(conversation);

	/* Create a group-dm and verify that we got a list containing two commands.
	 */
	conversation = purple_conversation_new(account,
	                                       PURPLE_CONVERSATION_TYPE_GROUP_DM,
	                                       "test");

	commands = purple_command_manager_get_commands_for_conversation(manager,
	                                                                conversation);

	birb_assert_list_model_n_items(commands, 2);

	g_assert_finalize_object(commands);
	g_assert_finalize_object(conversation);

	/* Create a channel and verify that we got a list containing three
	 * commands.
	 */
	conversation = purple_conversation_new(account,
	                                       PURPLE_CONVERSATION_TYPE_CHANNEL,
	                                       "test");
	commands = purple_command_manager_get_commands_for_conversation(manager,
	                                                                conversation);

	birb_assert_list_model_n_items(commands, 3);

	g_assert_finalize_object(commands);
	g_assert_finalize_object(conversation);

	/* Clean up. */
	g_assert_finalize_object(manager);
	g_assert_finalize_object(account);
}

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

	g_test_add_func("/command-manager/new", test_purple_command_manager_new);
	g_test_add_func("/command-manager/add-remove",
	                test_purple_command_manager_add_remove);

	g_test_add_func("/command-manager/remove-all-with-source",
	                test_purple_command_manager_remove_all_with_source);

	g_test_add_func("/command-manager/find", test_purple_command_manager_find);
	g_test_add_func("/command-manager/find-all",
	                test_purple_command_manager_find_all);
	g_test_add_func("/command-manager/find-and-execute",
	                test_purple_command_manager_find_and_execute);

	g_test_add_func("/command-manager/get-commands-for-conversation",
	                test_purple_command_manager_get_commands_for_conversation);

	return g_test_run();
}
