#include <psemek/test/test.hpp>
#include <psemek/util/pretty_print.hpp>
#include <psemek/util/exception.hpp>
#include <psemek/util/terminal_color.hpp>
#include <psemek/log/log.hpp>

#include <map>
#include <set>
#include <string>
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <cmath>
#include <chrono>
#include <cstdlib>

namespace psemek::test
{

	static std::map<std::string, void(*)(context &)> tests;

	static std::string normalize(std::string name)
	{
		std::replace(name.begin(), name.end(), '_', '/');

		for (std::size_t i = 0;;)
		{
			i = name.find("//", i);
			if (i == std::string::npos)
				break;
			name.replace(i, 2, "_");
			++i;
		}

		return name;
	}

	void add_test_case(char const * name, void(*f)(context &))
	{
		std::string pname = normalize(name);

		if (tests.count(pname))
		{
			std::cerr << "Test " << pname << " already registered" << std::endl;
			std::exit(EXIT_FAILURE);
		}

		tests[pname] = f;
	}

	struct sink
		: log::sink
	{
		std::vector<std::pair<log::level, std::string>> messages;

		void put_message(log::message const & msg) override
		{
			messages.push_back({msg.level, std::string(msg.message)});
		}
		
		void flush() override
		{}
	};

}

int main(int argc, char ** argv)
{
	std::string_view bold;
	std::string_view red;
	std::string_view green;
	std::string_view yellow;
	std::string_view cyan;
	std::string_view normal;

	if (psemek::util::terminal_color::supported())
	{
		bold = psemek::util::terminal_color::bold();
		red = psemek::util::terminal_color::red();
		green = psemek::util::terminal_color::green();
		yellow = psemek::util::terminal_color::yellow();
		cyan = psemek::util::terminal_color::cyan();
		normal = psemek::util::terminal_color::normal();
	}

	std::set<std::string> tests;
	if (argc == 1)
	{
		for (auto const & p : psemek::test::tests)
			tests.insert(p.first);
	}
	else
	{
		for (int a = 1; a < argc; ++a)
		{
			std::string pattern = std::string(argv[a]);
			if (pattern.empty())
			{
				std::cerr << "Empty test case pattern in argument list" << std::endl;
				std::exit(EXIT_FAILURE);
			}
			if (pattern.back() != '/')
				pattern += "/";
			for (auto const & p : psemek::test::tests)
			{
				if (p.first == argv[a] || p.first.starts_with(pattern))
					tests.insert(p.first);
			}
		}
	}

	std::size_t test_count = tests.size();

	std::size_t const test_index_len = std::ceil(std::log10(test_count + 1));

	std::string const indent = std::string(2 * test_index_len + 6, ' ');

	std::size_t max_name_length = 0;
	for (auto const & name : tests)
		max_name_length = std::max(max_name_length, name.size());

	std::size_t success = 0;

	using clock = std::chrono::high_resolution_clock;

	auto all_start = clock::now();

	std::unique_ptr<psemek::log::sink> sink = std::make_unique<psemek::test::sink>();
	auto sink_ptr = static_cast<psemek::test::sink *>(sink.get());

	psemek::log::register_thread("test");

	std::size_t i = 0;
	for (auto const & name : tests)
	{
		++i;

		std::cout
			<< bold << '[' << std::setfill(' ') << std::setw(test_index_len) << std::right << i << '/' << test_count << "] " << name
			<< normal << std::string(max_name_length + 5 - name.size(), '.') << std::flush;

		psemek::test::context ctx;

		psemek::log::add_sink(std::move(sink));

		bool failure = true;

		auto start = clock::now();
		try
		{
			psemek::test::tests[name](ctx);
			auto end = clock::now();
			std::cout << green << "ok " << normal << psemek::util::pretty(end - start, std::chrono::milliseconds{1});

			auto filter = [](auto const & m){ return m.first >= psemek::log::level::info; };
			if (std::count_if(sink_ptr->messages.begin(), sink_ptr->messages.end(), filter) == 1)
			{
				std::cout << "   " << std::find_if(sink_ptr->messages.begin(), sink_ptr->messages.end(), filter)->second;
				sink_ptr->messages.clear();
			}
			std::cout << std::endl;
			failure = false;
			++success;
		}
		catch (psemek::test::failure const & e)
		{
			auto end = clock::now();
			std::cout << red << "failure " << normal << psemek::util::pretty(end - start, std::chrono::milliseconds{1}) << std::endl;
			std::cout << indent << "Reason:   " << yellow << e.message() << normal << std::endl;
			std::cout << indent << "Location: " << cyan << e.location() << normal << std::endl;
		}
		catch (psemek::util::exception const & e)
		{
			auto end = clock::now();
			std::cout << red << "failure: " << e.what() << normal << " " << psemek::util::pretty(end - start, std::chrono::milliseconds{1}) << std::endl;
			std::cout << red << e.stacktrace();
		}
		catch (std::exception const & e)
		{
			auto end = clock::now();
			std::cout << red << "failure: " << e.what() << normal << " " << psemek::util::pretty(end - start, std::chrono::milliseconds{1}) << std::endl;
		}
		catch (...)
		{
			auto end = clock::now();
			std::cout << red << "failure: (unknown exception) " << normal << psemek::util::pretty(end - start, std::chrono::milliseconds{1}) << std::endl;
		}

		sink = psemek::log::remove_sink(sink_ptr);

		for (auto const & msg : sink_ptr->messages)
		{
			if (failure || msg.first >= psemek::log::level::info)
				std::cout << indent << msg.second << std::endl;
		}
		sink_ptr->messages.clear();

		std::size_t max_profile_name_len = 0;
		for (auto const & p : ctx.profile)
			max_profile_name_len = std::max(max_profile_name_len, p.name.size());

		for (auto const & p : ctx.profile)
		{
			std::cout << indent
				<< std::setfill(' ') << std::setw(max_profile_name_len + 5) << std::left << p.name
				<< psemek::util::pretty(p.duration, std::chrono::microseconds{1})
				<< (p.ended_with_exception ? " (exception)" : "")
				<< std::endl;
		}
	}

	auto all_end = clock::now();

	std::cout << std::endl;
	std::cout << (success == test_count ? green : red) << (success == test_count ? "Success: " : "Failure: ") << normal << success << '/' << test_count << " passed ("
		<< psemek::util::pretty(all_end - all_start, std::chrono::milliseconds{1}) << ")" << std::endl;

	if (success <= test_count)
		return EXIT_FAILURE;
}
