/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib.h>

#include <birb.h>

#include <purple.h>

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
test_purple_tags_counter_cb(G_GNUC_UNUSED PurpleTags *tags,
                            G_GNUC_UNUSED const char *tag,
                            G_GNUC_UNUSED const char *name,
                            G_GNUC_UNUSED const char *value,
                            gpointer data)
{
	guint *counter = data;

	/* Increment the counter so we know we were called. */
	*counter = *counter + 1;
}

/******************************************************************************
 * Tests
 *****************************************************************************/
static void
test_purple_tags_exists(void) {
	PurpleTags *tags = NULL;

	tags = purple_tags_new();

	purple_tags_add(tags, "foo");
	purple_tags_add(tags, "bar:1");
	purple_tags_add(tags, "baz");

	g_assert_true(purple_tags_exists(tags, "foo"));
	g_assert_true(purple_tags_exists(tags, "bar:1"));

	g_assert_false(purple_tags_exists(tags, "baz:"));
	g_assert_false(purple_tags_exists(tags, "qux"));

	g_assert_finalize_object(tags);
}

static void
test_purple_tags_lookup_exists(void) {
	PurpleTags *tags = NULL;
	gboolean found = FALSE;
	const char *value = NULL;
	guint counter = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &counter);

	purple_tags_add(tags, "foo");
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);
	g_assert_cmpuint(counter, ==, 1);
	value = purple_tags_lookup(tags, "foo", &found);
	g_assert_null(value);
	g_assert_true(found);

	purple_tags_add(tags, "bar:baz");
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 2);
	g_assert_cmpuint(counter, ==, 2);
	value = purple_tags_lookup(tags, "bar", &found);
	g_assert_cmpstr(value, ==, "baz");
	g_assert_true(found);

	/* make sure that a name of pur doesn't match a tag of purple */
	purple_tags_add(tags, "purple");
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 3);
	g_assert_cmpuint(counter, ==, 3);
	value = purple_tags_lookup(tags, "pur", &found);
	g_assert_null(value);
	g_assert_false(found);

	g_clear_object(&tags);
}

static void
test_purple_tags_lookup_non_existent(void) {
	PurpleTags *tags = purple_tags_new();
	gboolean found = FALSE;
	const char *value;

	value = purple_tags_lookup(tags, "foo", &found);
	g_assert_null(value);
	g_assert_false(found);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_remove_bare(void) {
	PurpleTags *tags = NULL;
	guint added = 0;
	guint removed = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &added);
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &removed);

	purple_tags_add(tags, "tag1");
	g_assert_cmpuint(added, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	purple_tags_remove(tags, "tag1");
	g_assert_cmpuint(removed, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_duplicate_bare(void) {
	PurpleTags *tags = NULL;
	guint added = 0;
	guint removed = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &added);
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &removed);

	purple_tags_add(tags, "tag1");
	g_assert_cmpuint(added, ==, 1);
	g_assert_cmpuint(removed, ==, 0);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	purple_tags_add(tags, "tag1");
	g_assert_cmpuint(added, ==, 2);
	g_assert_cmpuint(removed, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	g_clear_object(&tags);
}

static void
test_purple_tags_remove_non_existent_bare(void) {
	PurpleTags *tags = NULL;
	gboolean ret = FALSE;
	guint counter = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &counter);

	ret = purple_tags_remove(tags, "tag1");
	g_assert_cmpuint(counter, ==, 0);
	g_assert_false(ret);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_remove(void) {
	PurpleTags *tags = NULL;
	gboolean ret = FALSE;
	guint added = 0;
	guint removed = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &added);
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &removed);

	purple_tags_add(tags, "tag1:purple");
	g_assert_cmpuint(added, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	ret = purple_tags_remove(tags, "tag1:purple");
	g_assert_cmpuint(removed, ==, 1);
	g_assert_true(ret);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_remove_with_null_value(void) {
	PurpleTags *tags = NULL;
	gboolean ret = FALSE;
	guint added = 0;
	guint removed = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &added);
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &removed);

	purple_tags_add_with_value(tags, "tag1", NULL);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);
	g_assert_cmpuint(added, ==, 1);

	ret = purple_tags_remove_with_value(tags, "tag1", NULL);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);
	g_assert_cmpuint(removed, ==, 1);
	g_assert_true(ret);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_remove_with_value(void) {
	PurpleTags *tags = NULL;
	gboolean ret = FALSE;
	guint added = 0;
	guint removed = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &added);
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &removed);

	purple_tags_add_with_value(tags, "tag1", "purple");
	g_assert_cmpuint(added, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	ret = purple_tags_remove_with_value(tags, "tag1", "purple");
	g_assert_cmpuint(removed, ==, 1);
	g_assert_true(ret);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_duplicate_with_value(void) {
	PurpleTags *tags = NULL;
	guint added = 0;
	guint removed = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &added);
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &removed);

	purple_tags_add(tags, "tag1:purple");
	g_assert_cmpuint(added, ==, 1);
	g_assert_cmpuint(removed, ==, 0);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	purple_tags_add(tags, "tag1:purple");
	g_assert_cmpuint(added, ==, 2);
	g_assert_cmpuint(removed, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_with_value(void) {
	PurpleTags *tags = NULL;
	const char *value = NULL;
	gboolean found = FALSE;
	guint counter = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &counter);

	purple_tags_add_with_value(tags, "tag1", "purple");
	g_assert_cmpuint(counter, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	value = purple_tags_lookup(tags, "tag1", &found);
	g_assert_cmpstr(value, ==, "purple");
	g_assert_true(found);

	g_clear_object(&tags);
}

static void
test_purple_tags_add_with_value_null(void) {
	PurpleTags *tags = NULL;
	const char *value = NULL;
	gboolean found = FALSE;
	guint counter = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
	                 &counter);

	purple_tags_add_with_value(tags, "tag1", NULL);
	g_assert_cmpuint(counter, ==, 1);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);

	value = purple_tags_lookup(tags, "tag1", &found);
	g_assert_null(value);
	g_assert_true(found);

	g_clear_object(&tags);
}

static void
test_purple_tags_remove_non_existent_with_value(void) {
	PurpleTags *tags = NULL;
	guint counter = 0;

	tags = purple_tags_new();
	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
	                 &counter);

	purple_tags_remove(tags, "tag1:purple");
	g_assert_cmpuint(counter, ==, 0);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	g_clear_object(&tags);
}

static void
test_purple_tags_remove_all_empty(void) {
	PurpleTags *tags = NULL;
	guint counter = 0;

	tags = purple_tags_new();
	birb_count_list_model_items_changed(G_LIST_MODEL(tags), &counter);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	purple_tags_remove_all(tags);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);
	g_assert_cmpuint(counter, ==, 0);

	g_clear_object(&tags);
}

static void
test_purple_tags_remove_all_single(void) {
	PurpleTags *tags = NULL;
	guint counter = 0;

	tags = purple_tags_new();
	birb_count_list_model_items_changed(G_LIST_MODEL(tags), &counter);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	counter = 0;
	purple_tags_add(tags, "foo");
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);
	g_assert_cmpuint(counter, ==, 1);

	counter = 0;
	purple_tags_remove_all(tags);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);
	g_assert_cmpuint(counter, ==, 1);

	g_clear_object(&tags);
}

static void
test_purple_tags_remove_all_multiple(void) {
	PurpleTags *tags = NULL;
	guint counter = 0;

	tags = purple_tags_new();
	birb_count_list_model_items_changed(G_LIST_MODEL(tags), &counter);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);

	counter = 0;
	purple_tags_add(tags, "foo");
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 1);
	g_assert_cmpuint(counter, ==, 1);

	counter = 0;
	purple_tags_add(tags, "bar");
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 2);
	g_assert_cmpuint(counter, ==, 1);

	counter = 0;
	purple_tags_add(tags, "baz");
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 3);
	g_assert_cmpuint(counter, ==, 1);

	counter = 0;
	purple_tags_remove_all(tags);
	birb_assert_list_model_n_items(G_LIST_MODEL(tags), 0);
	g_assert_cmpuint(counter, ==, 1);

	g_clear_object(&tags);
}

static void
test_purple_tags_get_single(void) {
	PurpleTags *tags = purple_tags_new();
	const char *value = NULL;

	purple_tags_add(tags, "tag1:purple");
	value = purple_tags_get(tags, "tag1");

	g_assert_cmpstr(value, ==, "purple");

	g_clear_object(&tags);
}

static void
test_purple_tags_get_multiple(void) {
	PurpleTags *tags = purple_tags_new();
	const char *value = NULL;

	purple_tags_add(tags, "tag1:purple");
	purple_tags_add(tags, "tag1:pink");

	value = purple_tags_get(tags, "tag1");

	g_assert_cmpstr(value, ==, "purple");

	g_clear_object(&tags);
}

static void
test_purple_tags_get_all(void) {
	PurpleTags *tags = purple_tags_new();
	GListModel *model = NULL;
	const char *values[] = {"foo", "bar", "baz", "qux", "quux", NULL};
	guint len = 0;
	guint n_items = 0;

	for(guint i = 0; values[i] != NULL; i++) {
		purple_tags_add(tags, values[i]);
		len++;
	}

	model = G_LIST_MODEL(tags);
	n_items = g_list_model_get_n_items(model);
	g_assert_cmpuint(n_items, ==, len);

	for(guint i = 0; i < n_items; i++) {
		BirbStringObject *obj = NULL;
		const char *value = NULL;

		obj = g_list_model_get_item(model, i);
		value = birb_string_object_get_string(obj);

		g_assert_cmpint(i, <, len);
		g_assert_cmpstr(value, ==, values[i]);

		i++;

		g_clear_object(&obj);
	}

	g_clear_object(&tags);
}

static void
test_purple_tags_get_all_with_name(void) {
	PurpleTags *tags = NULL;
	PurpleTags *filtered = NULL;
	PurpleTags *expected = NULL;

	tags = purple_tags_new();

	/* Make sure we get null back on no matches. */
	filtered = purple_tags_get_all_with_name(tags, "group");
	birb_assert_list_model_n_items(G_LIST_MODEL(filtered), 0);
	g_clear_object(&filtered);

	/* Add some tags and make sure we get the right ones back. */
	purple_tags_add(tags, "group");
	purple_tags_add(tags, "groups");
	purple_tags_add(tags, "group:");
	purple_tags_add(tags, "grouping");
	purple_tags_add(tags, "group:a");
	purple_tags_add(tags, "grouped");

	/* Do the look up and start asserting! */
	filtered = purple_tags_get_all_with_name(tags, "group");
	g_assert_nonnull(filtered);

	/* Setup our expected tags. */
	expected = purple_tags_new();
	purple_tags_add(expected, "group");
	purple_tags_add(expected, "group:");
	purple_tags_add(expected, "group:a");

	g_assert_true(purple_tags_contains(filtered, expected));
	g_clear_object(&expected);

	birb_assert_list_model_n_items(filtered, 3);

	g_clear_object(&filtered);
	g_clear_object(&tags);
}

static void
test_purple_tags_to_string_single(void) {
	PurpleTags *tags = purple_tags_new();
	char *value = NULL;

	purple_tags_add(tags, "foo");
	value = purple_tags_to_string(tags, NULL);

	g_assert_cmpstr(value, ==, "foo");

	g_free(value);

	g_clear_object(&tags);
}

static void
test_purple_tags_to_string_multiple_with_separator(void) {
	PurpleTags *tags = purple_tags_new();
	char *value = NULL;

	purple_tags_add(tags, "foo");
	purple_tags_add(tags, "bar");
	purple_tags_add(tags, "baz");
	value = purple_tags_to_string(tags, ", ");

	g_assert_cmpstr(value, ==, "foo, bar, baz");

	g_free(value);

	g_clear_object(&tags);
}

static void
test_purple_tags_to_string_multiple_with_null_separator(void) {
	PurpleTags *tags = purple_tags_new();
	char *value = NULL;

	purple_tags_add(tags, "foo");
	purple_tags_add(tags, "bar");
	purple_tags_add(tags, "baz");
	value = purple_tags_to_string(tags, NULL);

	g_assert_cmpstr(value, ==, "foobarbaz");

	g_free(value);

	g_clear_object(&tags);
}

/******************************************************************************
 * purple_tag_split Tests
 *****************************************************************************/
typedef struct {
	char *tag;
	char *name;
	char *value;
} TestPurpleTagSplitData;

static void
test_purple_tag_split(void) {
	TestPurpleTagSplitData data[] = {
		{"", "", NULL},
		{"foo", "foo", NULL},
		{"🐦", "🐦", NULL},
		{":", "", ""},
		{"foo:bar", "foo", "bar"},
		{"🐦:", "🐦", ""},
		{":🐦", "", "🐦"},
		{NULL, NULL, NULL},
	};

	for(int i = 0; data[i].tag != NULL; i++) {
		char *name = NULL;
		char *value = NULL;

		purple_tag_split(data[i].tag, &name, &value);

		g_assert_cmpstr(name, ==, data[i].name);
		g_assert_cmpstr(value, ==, data[i].value);

		g_free(name);
		g_free(value);
	}

	/* Finally make sure that we don't crash if neither optional argument was
	 * passed.
	 */
	purple_tag_split("", NULL, NULL);
}

static void
test_purple_tag_contains_full(void) {
	PurpleTags *tags = NULL;
	PurpleTags *needle = NULL;

	tags = purple_tags_new();
	purple_tags_add(tags, "foo");
	purple_tags_add(tags, "bar:");
	purple_tags_add(tags, "baz:1");

	needle = purple_tags_new();
	purple_tags_add(needle, "foo");
	purple_tags_add(needle, "bar:");
	purple_tags_add(needle, "baz:1");

	g_assert_true(purple_tags_contains(tags, needle));

	g_assert_finalize_object(tags);
	g_assert_finalize_object(needle);
}

static void
test_purple_tag_contains_partial(void) {
	PurpleTags *tags = NULL;
	PurpleTags *needle = NULL;

	tags = purple_tags_new();
	purple_tags_add(tags, "foo");
	purple_tags_add(tags, "bar:");
	purple_tags_add(tags, "baz:1");

	needle = purple_tags_new();
	purple_tags_add(needle, "foo");
	purple_tags_add(needle, "baz:1");

	g_assert_true(purple_tags_contains(tags, needle));

	g_assert_finalize_object(tags);
	g_assert_finalize_object(needle);
}

static void
test_purple_tag_contains_none(void) {
	PurpleTags *tags = NULL;
	PurpleTags *needle = NULL;

	tags = purple_tags_new();
	purple_tags_add(tags, "foo");
	purple_tags_add(tags, "bar:1");

	needle = purple_tags_new();
	purple_tags_add(needle, "baz:qux");

	g_assert_false(purple_tags_contains(tags, needle));

	g_assert_finalize_object(tags);
	g_assert_finalize_object(needle);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
int
main(int argc, char **argv) {
	g_test_init(&argc, &argv, NULL);
	g_test_set_nonfatal_assertions();

	g_test_add_func("/tags/exists", test_purple_tags_exists);

	g_test_add_func("/tags/lookup-exists", test_purple_tags_lookup_exists);
	g_test_add_func("/tags/lookup-non-existent",
	                test_purple_tags_lookup_non_existent);

	g_test_add_func("/tags/add-remove-bare",
	                test_purple_tags_add_remove_bare);
	g_test_add_func("/tags/add-duplicate-bare",
	                test_purple_tags_add_duplicate_bare);
	g_test_add_func("/tags/remove-non-existent-bare",
	                test_purple_tags_remove_non_existent_bare);

	g_test_add_func("/tags/add-with-value",
	                test_purple_tags_add_with_value);
	g_test_add_func("/tags/add-with-value-null",
	                test_purple_tags_add_with_value_null);

	g_test_add_func("/tags/add-remove",
	                test_purple_tags_add_remove);
	g_test_add_func("/tags/add-remove-with-null-value",
	                test_purple_tags_add_remove_with_null_value);
	g_test_add_func("/tags/add-remove-with-value",
	                test_purple_tags_add_remove_with_value);
	g_test_add_func("/tags/add-duplicate-with-value",
	                test_purple_tags_add_duplicate_with_value);
	g_test_add_func("/tags/remove-non-existent-with-value",
	                test_purple_tags_remove_non_existent_with_value);
	g_test_add_func("/tags/remove-all-empty",
	                test_purple_tags_remove_all_empty);
	g_test_add_func("/tags/remove-all-single",
	                test_purple_tags_remove_all_single);
	g_test_add_func("/tags/remove-all-multiple",
	                test_purple_tags_remove_all_multiple);

	g_test_add_func("/tags/get-single", test_purple_tags_get_single);
	g_test_add_func("/tags/get-multiple", test_purple_tags_get_multiple);

	g_test_add_func("/tags/get-all", test_purple_tags_get_all);
	g_test_add_func("/tags/get-all-with-name",
	                test_purple_tags_get_all_with_name);

	g_test_add_func("/tags/to-string-single",
	                test_purple_tags_to_string_single);
	g_test_add_func("/tags/to-string-multiple-with-separator",
	                test_purple_tags_to_string_multiple_with_separator);
	g_test_add_func("/tags/to-string-multiple-with-null-separator",
	                test_purple_tags_to_string_multiple_with_null_separator);

	g_test_add_func("/tag/split", test_purple_tag_split);

	g_test_add_func("/tag/contains/full", test_purple_tag_contains_full);
	g_test_add_func("/tag/contains/partial", test_purple_tag_contains_partial);
	g_test_add_func("/tag/contains/none", test_purple_tag_contains_none);

	return g_test_run();
}
