/*
 * 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>

/******************************************************************************
 * Membership Tests
 *****************************************************************************/
static void
test_purple_conversation_members_membership_signal_cb(PurpleConversationMembers *members,
                                                      PurpleConversationMember *member,
                                                      gboolean announce,
                                                      const char *message,
                                                      gpointer data)
{
	guint *counter = data;

	g_assert_true(PURPLE_IS_CONVERSATION_MEMBERS(members));
	g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member));
	g_assert_true(announce);
	g_assert_cmpstr(message, ==, "announcement message");

	*counter = *counter + 1;
}

static void
test_purple_conversation_members_add_remove(void) {
	PurpleContactInfo *info = NULL;
	PurpleContactInfo *member_info = NULL;
	PurpleConversationMember *member = NULL;
	PurpleConversationMember *member1 = NULL;
	PurpleConversationMember *test_member = NULL;
	PurpleConversationMembers *members = NULL;
	gboolean removed = FALSE;
	guint added_counter = 0;
	guint removed_counter = 0;

	/* Create our instances. The id is just a uuid 4 to help us avoid a
	 * g_warning.
	 */
	members = purple_conversation_members_new();
	info = purple_contact_info_new("745c50ba-1189-48d9-827c-051783026c96");

	/* Connect our signals. */
	g_signal_connect(members, "member-added",
	                 G_CALLBACK(test_purple_conversation_members_membership_signal_cb),
	                 &added_counter);
	g_signal_connect(members, "member-removed",
	                 G_CALLBACK(test_purple_conversation_members_membership_signal_cb),
	                 &removed_counter);

	/* Add the member. */
	added_counter = 0;
	removed_counter = 0;
	member = purple_conversation_members_add_member(members, info, TRUE,
	                                                "announcement message");
	g_assert_cmpint(added_counter, ==, 1);
	g_assert_cmpint(removed_counter, ==, 0);
	g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member));

	test_member = purple_conversation_members_find_member(members, info);
	g_assert_true(member == test_member);

	/* Add our own reference to the returned member as we use it later to
	 * verify that double remove doesn't do anything.
	 */
	g_object_ref(member);

	/* Try to add the member again, which would just return the existing
	 * member and not emit the signal.
	 */
	added_counter = 0;
	removed_counter = 0;
	member1 = purple_conversation_members_add_member(members, info, TRUE,
	                                                 "announcement message");
	g_assert_cmpint(added_counter, ==, 0);
	g_assert_cmpint(removed_counter, ==, 0);
	g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member1));
	g_assert_true(member1 == member);

	/* Now remove the member and verify the signal was called. */
	added_counter = 0;
	removed_counter = 0;
	member_info = purple_conversation_member_get_contact_info(member);
	removed = purple_conversation_members_remove_member(members, member_info,
	                                                    TRUE,
	                                                    "announcement message");
	g_assert_true(removed);
	g_assert_cmpint(added_counter, ==, 0);
	g_assert_cmpint(removed_counter, ==, 1);

	test_member = purple_conversation_members_find_member(members, info);
	g_assert_null(test_member);

	/* Try to remove the member again and verify that nothing was removed and
	 * that the signal wasn't emitted.
	 */
	added_counter = 0;
	removed_counter = 0;
	member_info = purple_conversation_member_get_contact_info(member);
	removed = purple_conversation_members_remove_member(members, member_info,
	                                                    TRUE,
	                                                    "announcement message");
	g_assert_false(removed);
	g_assert_cmpint(added_counter, ==, 0);
	g_assert_cmpint(removed_counter, ==, 0);

	/* Clean up everything. */
	g_assert_finalize_object(members);
	g_assert_finalize_object(member);
	g_assert_finalize_object(info);
}

static void
test_purple_conversation_members_find_or_add_member(void) {
	PurpleConversationMembers *members = NULL;
	PurpleContactInfo *info = NULL;
	PurpleConversationMember *member1 = NULL;
	PurpleConversationMember *member2 = NULL;
	guint counter = 0;

	members = purple_conversation_members_new();
	birb_count_list_model_items_changed(G_LIST_MODEL(members), &counter);

	info = purple_contact_info_new(NULL);

	/* Create the member using find or create. */
	counter = 0;
	member1 = purple_conversation_members_find_or_add_member(members, info,
	                                                         FALSE, NULL);
	birb_assert_type(member1, PURPLE_TYPE_CONVERSATION_MEMBER);
	birb_assert_list_model_n_items(G_LIST_MODEL(members), 1);
	g_assert_cmpuint(counter, ==, 1);

	/* Find the member using find or create. */
	counter = 0;
	member2 = purple_conversation_members_find_or_add_member(members, info,
	                                                         FALSE, NULL);
	birb_assert_type(member2, PURPLE_TYPE_CONVERSATION_MEMBER);
	birb_assert_list_model_n_items(G_LIST_MODEL(members), 1);
	g_assert_cmpuint(counter, ==, 0);

	/* Now make sure the members are the same. */
	g_assert_true(purple_conversation_member_equal(member1, member2));
	g_assert_true(member1 == member2);

	g_assert_finalize_object(members);
	g_assert_finalize_object(info);
}

/******************************************************************************
 * Items Changed Tests
 *****************************************************************************/
static void
test_purple_conversation_members_item_changed_cb(GListModel *model,
                                                 guint position,
                                                 guint removed,
                                                 guint added,
                                                 gpointer data)
{
	guint *counter = data;

	g_assert_true(PURPLE_IS_CONVERSATION_MEMBERS(model));

	g_assert_cmpuint(position, ==, 1);
	g_assert_cmpuint(removed, ==, 1);
	g_assert_cmpuint(added, ==, 1);

	*counter = *counter + 1;
}

static void
test_purple_conversation_members_item_changed(void) {
	PurpleContactInfo *info = NULL;
	PurpleConversationMembers *members = NULL;
	PurpleConversationMember *member = NULL;
	guint counter = 0;

	members = purple_conversation_members_new();

	/* Add the first member, this is just so we can verify that position is non
	 * zero in the callback handler.
	 */
	info = purple_contact_info_new("first");
	purple_conversation_members_add_member(members, info, FALSE, NULL);
	g_clear_object(&info);

	/* Add the second member and hold on to their pointer as we're going to
	 * make changes to it, to verify that the items-changed signal gets called.
	 */
	info = purple_contact_info_new("second");
	member = purple_conversation_members_add_member(members, info, FALSE,
	                                                NULL);
	g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member));

	/* Connect the items-changed signal. We do this after everything is added
	 * to avoid having to deal with the additions which were already verified
	 * by the membership test.
	 */
	g_signal_connect(members, "items-changed",
	                 G_CALLBACK(test_purple_conversation_members_item_changed_cb),
	                 &counter);

	/* Change the alias on the contact info and verify that the signal got
	 * called.
	 */
	counter = 0;
	purple_contact_info_set_alias(info, "aliased");
	g_assert_cmpuint(counter, ==, 1);

	/* Change the nickname on the member. The callback only gets called once,
	 * because the members is only watching the name-for-display property.
	 */
	counter = 0;
	purple_conversation_member_set_nickname(member, "nickname");
	g_assert_cmpuint(counter, ==, 1);

	g_assert_finalize_object(members);
	g_assert_finalize_object(info);
}

/******************************************************************************
 * Active Typers Tests
 *****************************************************************************/
static void
test_purple_conversation_members_active_typers(void) {
	PurpleContactInfo *info = NULL;
	PurpleConversationMember *member = NULL;
	PurpleConversationMembers *members = NULL;
	GListModel *active_typers = NULL;
	guint counter = 0;

	members = purple_conversation_members_new();
	active_typers = purple_conversation_members_get_active_typers(members);
	birb_count_list_model_items_changed(G_LIST_MODEL(active_typers), &counter);

	info = purple_contact_info_new(NULL);
	member = purple_conversation_members_add_member(members, info, FALSE,
	                                                NULL);

	g_assert_cmpuint(counter, ==, 0);

	purple_conversation_member_set_typing_state(member,
	                                            PURPLE_TYPING_STATE_TYPING, 0);
	g_assert_cmpuint(counter, ==, 1);
	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 1);

	/* If the a user gets a typing refresh nothing should change. */
	counter = 0;
	purple_conversation_member_set_typing_state(member,
	                                            PURPLE_TYPING_STATE_TYPING, 0);
	g_assert_cmpuint(counter, ==, 0);
	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 1);

	/* Set the user's typing state to none. */
	counter = 0;
	purple_conversation_member_set_typing_state(member,
	                                            PURPLE_TYPING_STATE_NONE, 0);
	g_assert_cmpuint(counter, ==, 1);
	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 0);

	/* Set the user's state to none again and verify no changes. */
	counter = 0;
	purple_conversation_member_set_typing_state(member,
	                                            PURPLE_TYPING_STATE_NONE, 0);
	g_assert_cmpuint(counter, ==, 0);
	g_assert_cmpuint(g_list_model_get_n_items(active_typers), ==, 0);

	g_assert_finalize_object(members);
	g_assert_finalize_object(info);
}

/******************************************************************************
 * Extend Tests
 *****************************************************************************/
static void
test_purple_conversation_members_extend_existing_cb(G_GNUC_UNUSED GListModel *model,
                                                    guint position,
                                                    guint removed,
                                                    guint added,
                                                    G_GNUC_UNUSED gpointer data)
{
	g_assert_cmpuint(position, ==, 1);
	g_assert_cmpuint(removed, ==, 0);
	g_assert_cmpuint(added, ==, 3);
}

static void
test_purple_conversation_members_extend_new_cb(G_GNUC_UNUSED GListModel *model,
                                               guint position,
                                               guint removed,
                                               guint added,
                                               G_GNUC_UNUSED gpointer data)
{
	g_assert_cmpuint(position, ==, 0);
	g_assert_cmpuint(removed, ==, 3);
	g_assert_cmpuint(added, ==, 0);
}

static void
test_purple_conversation_members_extend(void) {
	PurpleContactInfo *info = NULL;
	PurpleConversationMember *member = NULL;
	PurpleConversationMembers *existing_members = NULL;
	PurpleConversationMembers *new_members = NULL;
	guint existing_items_changed_counter = 0;
	guint existing_n_items_counter = 0;
	guint new_items_changed_counter = 0;
	guint new_n_items_counter = 0;
	guint n_items = 0;

	existing_members = purple_conversation_members_new();
	birb_count_list_model_items_changed(G_LIST_MODEL(existing_members),
	                                    &existing_items_changed_counter);
	birb_count_property_changed(G_OBJECT(existing_members), "n-items",
	                            &existing_n_items_counter);

	new_members = purple_conversation_members_new();
	birb_count_list_model_items_changed(G_LIST_MODEL(new_members),
	                                    &new_items_changed_counter);
	birb_count_property_changed(G_OBJECT(new_members), "n-items",
	                            &new_n_items_counter);

	/* Add a member to the existing members. */
	existing_items_changed_counter = 0;
	existing_n_items_counter = 0;
	info = purple_contact_info_new(NULL);
	member = purple_conversation_members_add_member(existing_members, info,
	                                                FALSE, NULL);
	g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member));
	g_assert_cmpuint(existing_items_changed_counter, ==, 1);
	g_assert_cmpuint(existing_n_items_counter, ==, 1);
	n_items = g_list_model_get_n_items(G_LIST_MODEL(existing_members));
	g_assert_cmpuint(n_items, ==, 1);
	g_clear_object(&info);

	/* Add three members to the new members */
	for(guint i = 0; i < 3; i++) {
		char *id = g_uuid_string_random();

		new_items_changed_counter = 0;
		new_n_items_counter = 0;
		info = purple_contact_info_new(id);
		g_free(id);

		member = purple_conversation_members_add_member(new_members, info,
		                                                FALSE, NULL);
		g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member));
		n_items = g_list_model_get_n_items(G_LIST_MODEL(new_members));
		g_assert_cmpuint(n_items, ==, i + 1);
		g_assert_cmpuint(new_items_changed_counter, ==, 1);
		g_assert_cmpuint(new_n_items_counter, ==, 1);
		g_clear_object(&info);
	}

	existing_items_changed_counter = 0;
	existing_n_items_counter = 0;
	new_items_changed_counter = 0;
	new_n_items_counter = 0;

	/* Add some signal handlers that'll verify the items-changed signal was
	 * emitted properly.
	 */
	g_signal_connect(existing_members, "items-changed",
	                 G_CALLBACK(test_purple_conversation_members_extend_existing_cb),
	                 NULL);
	g_signal_connect(new_members, "items-changed",
	                 G_CALLBACK(test_purple_conversation_members_extend_new_cb),
	                 NULL);
	purple_conversation_members_extend(existing_members, new_members);
	g_assert_cmpuint(existing_items_changed_counter, ==, 1);
	g_assert_cmpuint(existing_n_items_counter, ==, 1);
	g_assert_cmpuint(new_items_changed_counter, ==, 1);
	g_assert_cmpuint(new_n_items_counter, ==, 1);

	g_assert_finalize_object(existing_members);
}

static void
test_purple_conversation_members_find_first_other(void) {
	PurpleConversationMember *member = NULL;
	PurpleConversationMembers *members = NULL;
	PurpleContactInfo *info1 = NULL;
	PurpleContactInfo *info2 = NULL;
	PurpleContactInfo *candidate = NULL;

	members = purple_conversation_members_new();

	info1 = purple_contact_info_new("one");
	info2 = purple_contact_info_new("two");

	/* Make sure an empty members returns NULL. */
	member = purple_conversation_members_find_first_other(members, info1);
	g_assert_null(member);

	/* Add the member and make sure it still returns NULL. */
	purple_conversation_members_add_member(members, info1, FALSE, NULL);
	member = purple_conversation_members_find_first_other(members, info1);
	g_assert_null(member);

	/* Add the second member and make sure it is returned. */
	purple_conversation_members_add_member(members, info2, FALSE, NULL);
	member = purple_conversation_members_find_first_other(members, info1);
	g_assert_nonnull(member);

	candidate = purple_conversation_member_get_contact_info(member);
	g_assert_true(purple_contact_info_equal(candidate, info2));

	/* Clean up. */
	g_assert_finalize_object(members);
	g_assert_finalize_object(info1);
	g_assert_finalize_object(info2);
}

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

	g_test_add_func("/conversation-members/add-remove",
	                test_purple_conversation_members_add_remove);
	g_test_add_func("/conversation-members/find-or-add-member",
	                test_purple_conversation_members_find_or_add_member);
	g_test_add_func("/conversation-members/items-changed",
	                test_purple_conversation_members_item_changed);
	g_test_add_func("/conversation-members/active-typers",
	                test_purple_conversation_members_active_typers);
	g_test_add_func("/conversation-members/extend",
	                test_purple_conversation_members_extend);

	g_test_add_func("/conversation-members/find-first-other",
	                test_purple_conversation_members_find_first_other);

	return g_test_run();
}
