/*
 * Ibis - IRCv3 Library
 * Copyright (C) 2022-2024 Ibis Developers
 *
 * Ibis 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 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 <ibis.h>

#define TEST_IBIS_PARSER_DOMAIN (g_quark_from_static_string("test-ibis-parser"))

typedef struct {
	IbisTags *tags;
	char *source;
	char *command;
	guint n_params;
	const char * const params[16];
	IbisCTCPMessage *ctcp_message;
} TestIbisParserData;

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
test_ibis_parser(const char *raw, TestIbisParserData *d) {
	IbisMessage *message = NULL;
	GError *error = NULL;
	GStrv params = NULL;
	const char *command = NULL;
	char *upper_command = NULL;
	const char *raw_message = NULL;
	const char *source = NULL;

	message = ibis_message_parse(raw, &error);
	g_assert_no_error(error);
	g_assert_true(IBIS_IS_MESSAGE(message));

	raw_message = ibis_message_get_raw_message(message);
	g_assert_cmpstr(raw_message, ==, raw);

	if(d->command != NULL) {
		upper_command = g_utf8_strup(d->command, -1);
	}
	command = ibis_message_get_command(message);
	g_assert_cmpstr(command, ==, upper_command);
	g_clear_pointer(&upper_command, g_free);

	source = ibis_message_get_source(message);
	g_assert_cmpstr(source, ==, d->source);

	/* Make sure we have an expected tags hash table before checking them. */
	if(d->tags != NULL) {
		IbisTags *tags = NULL;

		tags = ibis_message_get_tags(message);

		g_assert_true(ibis_tags_equal(d->tags, tags));
	}

	/* Check the parameters to make sure they match. */
	params = ibis_message_get_params(message);
	g_assert_cmpstrv(params, d->params);

	if(IBIS_IS_CTCP_MESSAGE(d->ctcp_message)) {
		IbisCTCPMessage *ctcp_message = NULL;
		const char *command1 = NULL;
		const char *command2 = NULL;
		GStrv params1 = NULL;
		GStrv params2 = NULL;

		ctcp_message = ibis_message_get_ctcp_message(message);

		command1 = ibis_ctcp_message_get_command(ctcp_message);
		command2 = ibis_ctcp_message_get_command(d->ctcp_message);
		g_assert_cmpstr(command1, ==, command2);

		params1 = ibis_ctcp_message_get_params(ctcp_message);
		params2 = ibis_ctcp_message_get_params(d->ctcp_message);
		g_assert_cmpstrv(params1, params2);

		g_clear_object(&d->ctcp_message);
	}

	/* Cleanup everything the caller allocated. */
	g_clear_object(&d->tags);

	g_assert_finalize_object(message);
}

/******************************************************************************
 * CTCP Tests
 *****************************************************************************/
static void
test_ibis_parser_ctcp_privmsg(void) {
	TestIbisParserData data = {
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"#pidgin", "\001VERSION\001"},
	};

	data.ctcp_message = ibis_ctcp_message_new(IBIS_CTCP_VERSION);

	test_ibis_parser("PRIVMSG #pidgin :\001VERSION\001", &data);
}

static void
test_ibis_parser_ctcp_notice(void) {
	TestIbisParserData data = {
		.command = IBIS_MSG_NOTICE,
		.n_params = 2,
		.params = {"pidgy", "\001VERSION\001"},
	};

	data.ctcp_message = ibis_ctcp_message_new(IBIS_CTCP_VERSION);

	test_ibis_parser("NOTICE pidgy :\001VERSION\001", &data);
}

static void
test_ibis_parser_ctcp_other(void) {
	TestIbisParserData data = {
		.command = "OTHER",
		.n_params = 2,
		.params = {"#pidgin", "\001VERSION\001"},
	};

	test_ibis_parser("OTHER #pidgin :\001VERSION\001", &data);
}

static void
test_ibis_parser_ctcp_no_trailing(void) {
	TestIbisParserData data = {
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"#pidgin", "\001VERSION"},
	};

	data.ctcp_message = ibis_ctcp_message_new(IBIS_CTCP_VERSION);

	test_ibis_parser("PRIVMSG #pidgin :\001VERSION", &data);
}

/******************************************************************************
 * Tests
 *****************************************************************************/
static void
test_ibis_parser_error(void) {
	IbisMessage *message = NULL;
	GError *error = NULL;

	message = ibis_message_parse("", &error);
	g_assert_error(error, IBIS_MESSAGE_PARSE_ERROR, 0);
	g_clear_error(&error);

	g_assert_null(message);
}

static void
test_ibis_parser_simple(void) {
	TestIbisParserData data = {
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", "asdf"},
	};

	test_ibis_parser("foo bar baz asdf", &data);
}

static void
test_ibis_parser_with_source(void) {
	TestIbisParserData data = {
		.source = "coolguy",
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", "asdf"},
	};

	test_ibis_parser(":coolguy foo bar baz asdf", &data);
}

static void
test_ibis_parser_with_trailing(void) {
	TestIbisParserData data = {
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", "asdf quux"},
	};

	test_ibis_parser("foo bar baz :asdf quux", &data);
}

static void
test_ibis_parser_with_empty_trailing(void) {
	TestIbisParserData data = {
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", ""},
	};

	test_ibis_parser("foo bar baz :", &data);
}

static void
test_ibis_parser_with_trailing_starting_colon(void) {
	TestIbisParserData data = {
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", ":asdf"},
	};

	test_ibis_parser("foo bar baz ::asdf", &data);
}

static void
test_ibis_parser_with_source_and_trailing(void) {
	TestIbisParserData data = {
		.source = "coolguy",
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", "asdf quux"},
	};

	test_ibis_parser(":coolguy foo bar baz :asdf quux", &data);
}

static void
test_ibis_parser_with_source_and_trailing_whitespace(void) {
	TestIbisParserData data = {
		.source = "coolguy",
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", "  asdf quux "},
	};

	test_ibis_parser(":coolguy foo bar baz :  asdf quux ", &data);
}

static void
test_ibis_parser_with_source_and_trailing_colon(void) {
	TestIbisParserData data = {
		.source = "coolguy",
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"bar", "lol :) "},
	};

	test_ibis_parser(":coolguy PRIVMSG bar :lol :) ", &data);
}

static void
test_ibis_parser_with_source_and_empty_trailing(void) {
	TestIbisParserData data = {
		.source = "coolguy",
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", ""},
	};

	test_ibis_parser(":coolguy foo bar baz :", &data);
}

static void
test_ibis_parser_with_source_and_trailing_only_whitespace(void) {
	TestIbisParserData data = {
		.source = "coolguy",
		.command = "foo",
		.n_params = 3,
		.params = {"bar", "baz", "  "},
	};

	test_ibis_parser(":coolguy foo bar baz :  ", &data);
}

static void
test_ibis_parser_with_tags(void) {
	TestIbisParserData data = {
		.command = "foo",
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "a", "b");
	ibis_tags_add(data.tags, "a", "b");
	ibis_tags_add(data.tags, "c", "32");
	ibis_tags_add(data.tags, "k", "");
	ibis_tags_add(data.tags, "rt", "ql7");

	test_ibis_parser("@a=b;c=32;k;rt=ql7 foo", &data);
}

static void
test_ibis_parser_with_escaped_tags(void) {
	TestIbisParserData data = {
		.command = "foo",
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "a", "b\\and\nk");
	ibis_tags_add(data.tags, "c", "72 45");
	ibis_tags_add(data.tags, "d", "gh;764");

	test_ibis_parser("@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo",
	                         &data);
}

static void
test_ibis_with_tags_and_source(void) {
	TestIbisParserData data = {
		.source = "quux",
		.command = "ab",
		.n_params = 1,
		.params = {"cd"},
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "c", "");
	ibis_tags_add(data.tags, "h", "");
	ibis_tags_add(data.tags, "a", "b");

	test_ibis_parser("@c;h=;a=b :quux ab cd", &data);
}

static void
test_ibis_last_param_no_colon(void) {
	TestIbisParserData data = {
		.source = "src",
		.command = IBIS_MSG_JOIN,
		.n_params = 1,
		.params = {"#chan"},
	};

	test_ibis_parser(":src JOIN #chan", &data);
}

static void
test_ibis_last_param_with_colon(void) {
	TestIbisParserData data = {
		.source = "src",
		.command = IBIS_MSG_JOIN,
		.n_params = 1,
		.params = {"#chan"},
	};

	test_ibis_parser(":src JOIN :#chan", &data);
}

static void
test_ibis_last_param_with_empty(void) {
	TestIbisParserData data = {
		.source = "src",
		.command = IBIS_MSG_JOIN,
		.n_params = 1,
		.params = {""},
	};

	test_ibis_parser(":src JOIN :", &data);
}

static void
test_ibis_without_last_param(void) {
	TestIbisParserData data = {
		.source = "src",
		.command = "AWAY",
	};

	test_ibis_parser(":src AWAY", &data);
}

static void
test_ibis_with_last_param(void) {
	TestIbisParserData data = {
		.source = "src",
		.command = "AWAY",
	};

	test_ibis_parser(":src AWAY ", &data);
}

static void
test_ibis_tab_is_not_space(void) {
	TestIbisParserData data = {
		.source = "cool\tguy",
		.command = "foo",
		.n_params = 2,
		.params = {"bar", "baz"},
	};

	test_ibis_parser(":cool\tguy foo bar baz", &data);
}

static void
test_ibis_source_control_characters_1(void) {
	/* Break each string after the hex escape as they are supposed to only be
	 * a single byte, but the c compiler will keep unescaping unless we break
	 * the string.
	 */
	TestIbisParserData data = {
		.source = "coolguy!ag@net\x03" "5w\x03" "ork.admin",
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"foo", "bar baz"},
	};
	const char *msg = NULL;

	msg = ":coolguy!ag@net\x03" "5w\x03" "ork.admin PRIVMSG foo :bar baz";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_source_control_characters_2(void) {
	/* Break each string after the hex escape as they are supposed to only be
	 * a single byte, but the c compiler will keep unescaping unless we break
	 * the string.
	 */
	TestIbisParserData data = {
		.source = "coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin",
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"foo", "bar baz"},
	};
	const char *msg = NULL;

	msg = ":coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin PRIVMSG foo :bar "
	      "baz";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_everything(void) {
	TestIbisParserData data = {
		.source = "irc.example.com",
		.command = "COMMAND",
		.n_params = 3,
		.params = {"param1", "param2", "param3 param3"},
	};
	const char *msg = NULL;

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "tag1", "value1");
	ibis_tags_add(data.tags, "tag2", "");
	ibis_tags_add(data.tags, "vendor1/tag3", "value2");
	ibis_tags_add(data.tags, "vendor2/tag4", "");

	msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= "
	      ":irc.example.com COMMAND param1 param2 :param3 param3";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_everything_but_tags(void) {
	TestIbisParserData data = {
		.source = "irc.example.com",
		.command = "COMMAND",
		.n_params = 3,
		.params = {"param1", "param2", "param3 param3"},
	};
	const char *msg = NULL;

	msg = ":irc.example.com COMMAND param1 param2 :param3 param3";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_everything_but_source(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
		.n_params = 3,
		.params = {"param1", "param2", "param3 param3"},
	};
	const char *msg = NULL;

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "tag1", "value1");
	ibis_tags_add(data.tags, "tag2", "");
	ibis_tags_add(data.tags, "vendor1/tag3", "value2");
	ibis_tags_add(data.tags, "vendor2/tag4", "");

	msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 "
	      "COMMAND param1 param2 :param3 param3";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_command_only(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
	};

	test_ibis_parser("COMMAND", &data);
}

static void
test_ibis_slashes_are_fun(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "foo", "\\\\;\\s \r\n");

	test_ibis_parser("@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND", &data);
}

static void
test_ibis_unreal_broken_1(void) {
	TestIbisParserData data = {
		.source = "gravel.mozilla.org",
		.command = "432",
		.n_params = 2,
		.params = {"#momo", "Erroneous Nickname: Illegal characters"},
	};
	const char *msg = NULL;

	msg = ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal "
	      "characters";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_unreal_broken_2(void) {
	TestIbisParserData data = {
		.source = "gravel.mozilla.org",
		.command = "MODE",
		.n_params = 2,
		.params = {"#tckk", "+n"},
	};

	test_ibis_parser(":gravel.mozilla.org MODE #tckk +n ", &data);
}

static void
test_ibis_unreal_broken_3(void) {
	TestIbisParserData data = {
		.source = "services.esper.net",
		.command = "MODE",
		.n_params = 3,
		.params = {"#foo-bar", "+o", "foobar"},
	};

	test_ibis_parser(":services.esper.net MODE #foo-bar +o foobar  ",
	                         &data);
}

static void
test_ibis_tag_escape_char_at_a_time(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "tag1", "value\\ntest");

	test_ibis_parser("@tag1=value\\\\ntest COMMAND", &data);
}

static void
test_ibis_tag_drop_unnecessary_escapes(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "tag1", "value1");

	test_ibis_parser("@tag1=value\\1 COMMAND", &data);
}

static void
test_ibis_tag_drop_trailing_slash(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "tag1", "value1");

	test_ibis_parser("@tag1=value1\\ COMMAND", &data);
}

static void
test_ibis_duplicate_tags(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
	};

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "tag1", "5");
	ibis_tags_add(data.tags, "tag2", "3");
	ibis_tags_add(data.tags, "tag3", "4");

	test_ibis_parser("@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND", &data);
}

static void
test_ibis_vendor_tags_are_namespaced(void) {
	TestIbisParserData data = {
		.command = "COMMAND",
	};
	const char *msg = NULL;

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "tag1", "5");
	ibis_tags_add(data.tags, "tag2", "3");
	ibis_tags_add(data.tags, "tag3", "4");
	ibis_tags_add(data.tags, "vendor/tag2", "8");

	msg = "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_special_mode_1(void) {
	TestIbisParserData data = {
		.source = "SomeOp",
		.command = "MODE",
		.n_params = 2,
		.params = {"#channel", "+i"},
	};

	test_ibis_parser(":SomeOp MODE #channel :+i", &data);
}

static void
test_ibis_special_mode_2(void) {
	TestIbisParserData data = {
		.source = "SomeOp",
		.command = "MODE",
		.n_params = 4,
		.params = {"#channel", "+oo", "SomeUser", "AnotherUser"},
	};

	test_ibis_parser(":SomeOp MODE #channel +oo SomeUser :AnotherUser",
	                         &data);
}

/******************************************************************************
 * Message tags examples
 *****************************************************************************/
static void
test_ibis_parser_message_tags_none(void) {
	TestIbisParserData data = {
		.source = "nick!ident@host.com",
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"me", "Hello"},
	};
	const char *msg = NULL;

	msg = ":nick!ident@host.com PRIVMSG me :Hello";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_parser_message_tags_3_tags(void) {
	TestIbisParserData data = {
		.source = "nick!ident@host.com",
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"me", "Hello"},
	};
	const char *msg = NULL;

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "aaa", "bbb");
	ibis_tags_add(data.tags, "ccc", "");
	ibis_tags_add(data.tags, "example.com/ddd", "eee");

	msg = "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me "
	      ":Hello";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_parser_message_tags_client_only(void) {
	TestIbisParserData data = {
		.source = "url_bot!bot@example.com",
		.command = IBIS_MSG_PRIVMSG,
		.n_params = 2,
		.params = {"#channel", "Example.com: A News Story"},
	};
	const char *msg = NULL;

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "+icon", "https://example.com/favicon.png");

	msg = "@+icon=https://example.com/favicon.png :url_bot!bot@example.com "
	      "PRIVMSG #channel :Example.com: A News Story";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_parser_message_tags_labeled_response(void) {
	TestIbisParserData data = {
		.source = "nick!user@example.com",
		.command = "TAGMSG",
		.n_params = 1,
		.params = {"#channel"},
	};
	const char *msg = NULL;

	data.tags = ibis_tags_new();
	ibis_tags_add(data.tags, "label", "123");
	ibis_tags_add(data.tags, "msgid", "abc");
	ibis_tags_add(data.tags, "+example-client-tag", "example-value");

	msg = "@label=123;msgid=abc;+example-client-tag=example-value "
	      ":nick!user@example.com TAGMSG #channel";

	test_ibis_parser(msg, &data);

}

/******************************************************************************
 * Overlong Encoding Tests
 *****************************************************************************/
/* This happened live in #libera. See IBIS-17 for more information. */
static void
test_ibis_parser_overlong_encoding_null(void) {
	/* \357\277\275 is the UTF-8 encoded unicode replacement character. In hex
	 * that's 0xEF 0xBF 0xBD but we use octal to make sure we don't
	 * accidentally match anything we're not expecting.
	 */
	TestIbisParserData data = {
		.source = "g!glguy@libera/staff/glguy",
		.command = "PRIVMSG",
		.n_params = 2,
		.params = {
			"#libera",
			"overlong encoded \\0 [\357\277\275\357\277\275]",
		},
	};
	const char *msg = NULL;

	msg = ":g!glguy@libera/staff/glguy PRIVMSG #libera :overlong encoded \\0 "
	      "[\300\200]";

	test_ibis_parser(msg, &data);
}

static void
test_ibis_parser_colon_in_param(void) {
	TestIbisParserData data = {
		.command = "CMD",
		.n_params = 2,
		.params = {"x:y", "last"},
	};
	const char *msg = NULL;

	msg = "CMD x:y :last";

	test_ibis_parser(msg, &data);
}

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

	g_test_add_func("/ibis/parser/ctcp/privmsg",
	                test_ibis_parser_ctcp_privmsg);
	g_test_add_func("/ibis/parser/ctcp/notice", test_ibis_parser_ctcp_notice);
	g_test_add_func("/ibis/parser/ctcp/other", test_ibis_parser_ctcp_other);
	g_test_add_func("/ibis/parser/ctcp/no-trailing",
	                test_ibis_parser_ctcp_no_trailing);

	g_test_add_func("/ibis/parser/error", test_ibis_parser_error);

	/* These tests are based on the msg-split tests from
	 * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
	 */
	g_test_add_func("/ibis/parser/simple",
	                test_ibis_parser_simple);
	g_test_add_func("/ibis/parser/with-source",
	                test_ibis_parser_with_source);
	g_test_add_func("/ibis/parser/with-trailing",
	                test_ibis_parser_with_trailing);
	g_test_add_func("/ibis/parser/with-empty-trailing",
	                test_ibis_parser_with_empty_trailing);
	g_test_add_func("/ibis/parser/with-trailing-starting-colon",
	                test_ibis_parser_with_trailing_starting_colon);
	g_test_add_func("/ibis/parser/with-source-and-trailing",
	                test_ibis_parser_with_source_and_trailing);
	g_test_add_func("/ibis/parser/with-source-and-trailing-whitespace",
	                test_ibis_parser_with_source_and_trailing_whitespace);
	g_test_add_func("/ibis/parser/with-source-and-trailing-colon",
	                test_ibis_parser_with_source_and_trailing_colon);
	g_test_add_func("/ibis/parser/with-source-and-empty-trailing",
	                test_ibis_parser_with_source_and_empty_trailing);
	g_test_add_func("/ibis/parser/with-source-and-trailing-only-whitespace",
	                test_ibis_parser_with_source_and_trailing_only_whitespace);

	g_test_add_func("/ibis/parser/with-tags",
	                test_ibis_parser_with_tags);
	g_test_add_func("/ibis/parser/with-escaped-tags",
	                test_ibis_parser_with_escaped_tags);
	g_test_add_func("/ibis/parser/with-tags-and-source",
	                test_ibis_with_tags_and_source);

	g_test_add_func("/ibis/parser/last-param-no-colon",
	                test_ibis_last_param_no_colon);
	g_test_add_func("/ibis/parser/last-param-with-colon",
	                test_ibis_last_param_with_colon);
	g_test_add_func("/ibis/parser/last-param-empty",
	                test_ibis_last_param_with_empty);

	g_test_add_func("/ibis/parser/without-last-param",
	                test_ibis_without_last_param);
	g_test_add_func("/ibis/parser/with-last-parsm",
	                test_ibis_with_last_param);

	g_test_add_func("/ibis/parser/tab-is-not-space",
	                test_ibis_tab_is_not_space);

	g_test_add_func("/ibis/parser/source_control_characters_1",
	                test_ibis_source_control_characters_1);
	g_test_add_func("/ibis/parser/source_control_characters_2",
	                test_ibis_source_control_characters_2);

	g_test_add_func("/ibis/parser/everything",
	                test_ibis_everything);
	g_test_add_func("/ibis/parser/everything-but-tags",
	                test_ibis_everything_but_tags);
	g_test_add_func("/ibis/parser/everything-but-source",
	                test_ibis_everything_but_source);

	g_test_add_func("/ibis/parser/command-only",
	                test_ibis_command_only);

	g_test_add_func("/ibis/parser/slashes-are-fun",
	                test_ibis_slashes_are_fun);

	g_test_add_func("/ibis/parser/unreal-broken-1",
	                test_ibis_unreal_broken_1);
	g_test_add_func("/ibis/parser/unreal-broken-2",
	                test_ibis_unreal_broken_2);
	g_test_add_func("/ibis/parser/unreal-broken-3",
	                test_ibis_unreal_broken_3);

	g_test_add_func("/ibis/parser/tag-escape-char-at-a-time",
	                test_ibis_tag_escape_char_at_a_time);
	g_test_add_func("/ibis/parser/tag-drop-unnecessary-escapes",
	                test_ibis_tag_drop_unnecessary_escapes);
	g_test_add_func("/ibis/parser/tag-drop-trailing-slash",
	                test_ibis_tag_drop_trailing_slash);

	g_test_add_func("/ibis/parser/duplicate-tags",
	                test_ibis_duplicate_tags);
	g_test_add_func("/ibis/parser/vendor-tags-are-namespaced",
	                test_ibis_vendor_tags_are_namespaced);

	g_test_add_func("/ibis/parser/special-mode-1",
	                test_ibis_special_mode_1);
	g_test_add_func("/ibis/parser/special-mode-2",
	                test_ibis_special_mode_2);

	/* These tests are the examples from the message-tags specification,
	 * https://ircv3.net/specs/extensions/message-tags.html.
	 */
	g_test_add_func("/ibis/parser/message-tags/none",
	                test_ibis_parser_message_tags_none);
	g_test_add_func("/ibis/parser/message-tags/3-tags",
	                test_ibis_parser_message_tags_3_tags);
	g_test_add_func("/ibis/parser/message-tags/client-only",
	                test_ibis_parser_message_tags_client_only);
	g_test_add_func("/ibis/parser/message-tags/labeled-message",
	                test_ibis_parser_message_tags_labeled_response);

	/* Overlong encoding tests. */
	g_test_add_func("/ibis/parser/overlong-encoding/null",
	                test_ibis_parser_overlong_encoding_null);

	g_test_add_func("/ibis/parser/colon_in_param",
	                test_ibis_parser_colon_in_param);

	return g_test_run();
}
