#include <psemek/test/test.hpp>

#include <psemek/ui/tagged_text.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>

#include <memory>

using namespace psemek::ui;

static void compare(tagged_text const & result, tagged_text const & expected)
{
	expect_equal(result.tokens.size(), expected.tokens.size());
	for (std::size_t i = 0; i < result.tokens.size(); ++i)
	{
		expect_equal(result.tokens[i].index(), expected.tokens[i].index());
		if (auto token = std::get_if<std::string>(&result.tokens[i]))
		{
			expect_equal(*token, std::get<std::string>(expected.tokens[i]));
		}
		else if (auto token = std::get_if<tagged_text::opening_tag>(&result.tokens[i]))
		{
			expect_equal(token->type, std::get<tagged_text::opening_tag>(expected.tokens[i]).type);
			expect_equal(token->attribute, std::get<tagged_text::opening_tag>(expected.tokens[i]).attribute);
		}
		else
		{
			expect_equal(std::get<tagged_text::closing_tag>(result.tokens[i]).type, std::get<tagged_text::closing_tag>(expected.tokens[i]).type);
		}
	}
}

static void test(std::string_view text, tagged_text const & expected)
{
	compare(tagged_text::parse(text), expected);
}

static void test_throw(std::string_view text)
{
	expect_throw(tagged_text::parse(text), tagged_text::parse_error);
}

test_case(ui_tagged__text_empty)
{
	test("", {});
}

test_case(ui_tagged__text_notags)
{
	test("text", {{"text"}});
}

test_case(ui_tagged__text_whitespace)
{
	test("text text", {{"text text"}});
}

test_case(ui_tagged__text_newline)
{
	test("text\ntext", {{"text\ntext"}});
}

test_case(ui_tagged__text_tag__open)
{
	test("[tag]", {{tagged_text::opening_tag{"tag", std::nullopt}}});
}

test_case(ui_tagged__text_tag__close)
{
	test("[/tag]", {{tagged_text::closing_tag{"tag"}}});
}

test_case(ui_tagged__text_tag__open__close)
{
	test("[tag][/tag]", {{tagged_text::opening_tag{"tag", std::nullopt}, tagged_text::closing_tag{"tag"}}});
}

test_case(ui_tagged__text_tag__text)
{
	test("abc[tag][/tag]", {{"abc", tagged_text::opening_tag{"tag", std::nullopt}, tagged_text::closing_tag{"tag"}}});
	test("[tag]def[/tag]", {{tagged_text::opening_tag{"tag", std::nullopt}, "def", tagged_text::closing_tag{"tag"}}});
	test("abc[tag]def[/tag]", {{"abc", tagged_text::opening_tag{"tag", std::nullopt}, "def", tagged_text::closing_tag{"tag"}}});
	test("[tag][/tag]ghi", {{tagged_text::opening_tag{"tag", std::nullopt}, tagged_text::closing_tag{"tag"}, "ghi"}});
	test("abc[tag][/tag]ghi", {{"abc", tagged_text::opening_tag{"tag", std::nullopt}, tagged_text::closing_tag{"tag"}, "ghi"}});
	test("[tag]def[/tag]ghi", {{tagged_text::opening_tag{"tag", std::nullopt}, "def", tagged_text::closing_tag{"tag"}, "ghi"}});
	test("abc[tag]def[/tag]ghi", {{"abc", tagged_text::opening_tag{"tag", std::nullopt}, "def", tagged_text::closing_tag{"tag"}, "ghi"}});
}

test_case(ui_tagged__text_tag__attribute)
{
	test("[tag:attr][/tag]", {{tagged_text::opening_tag{"tag", "attr"}, tagged_text::closing_tag{"tag"}}});
}

test_case(ui_tagged__text_escape)
{
	test("\\[", {{"["}});
	test("\\]", {{"]"}});
	test("\\\\", {{"\\"}});
	test("\\[\\]", {{"[", "]"}});
	test(R"(text \[not \\ tag\] end)", {{"text ", "[not ", "\\ tag", "] end"}});
}

test_case(ui_tagged__text_error)
{
	test_throw("[[");
	test_throw("[ab[cd");
	test_throw("]");
	test_throw("][");
	test_throw("[]]");
	test_throw("[::]");
}

test_case(ui_tagged__text_random)
{
	using namespace psemek::random;

	generator rng;

	auto random_string = [&rng]() -> std::string {
		std::string result;
		result.resize(uniform<int>(rng, 1, 10));
		for (char & c : result)
			c = uniform<char>(rng, 'a', 'z');
		return result;
	};

	std::string text;
	tagged_text expected;

	for (int i = 0; i < 100; ++i)
	{
		auto roll = uniform<int>(rng, 0, 2);

		if (!expected.tokens.empty() && std::get_if<std::string>(&expected.tokens.back()) && roll == 0)
			roll = uniform<int>(rng, 1, 2);

		if (roll == 0)
		{
			auto str = random_string();
			text += str;
			expected.tokens.push_back(std::move(str));
		}
		else if (roll == 1)
		{
			bool const has_attr = uniform<bool>(rng);

			tagged_text::opening_tag token;

			auto tag_str = random_string();

			text += "[" + tag_str;
			token.type = std::move(tag_str);

			if (has_attr)
			{
				auto attr_str = random_string();
				text += ":" + attr_str;
				token.attribute = std::move(attr_str);
			}

			text += "]";

			expected.tokens.push_back(token);
		}
		else if (roll == 2)
		{
			tagged_text::closing_tag token;

			auto tag_str = random_string();

			text += "[/" + tag_str + "]";
			token.type = std::move(tag_str);

			expected.tokens.push_back(token);
		}
	}

	test(text, expected);
}
