#pragma once

#include <exception>
#include <chrono>
#include <vector>
#include <optional>
#include <iostream>
#include <utility>
#include <tuple>
#include <set>
#include <typeinfo>
#include <typeindex>

// Have to put it before including to_string.hpp due to how unqualified lookup works,
// see e.g. https://alexanderlobov.net/posts/2019-07-08-function-lookup-in-templates

namespace std
{

	template <typename T>
	ostream & operator << (ostream & s, optional<T> const & o)
	{
		if (o)
			s << *o;
		else
			s << "(empty)";
		return s;
	}

	template <typename T1, typename T2>
	ostream & operator << (ostream & s, pair<T1, T2> const & p)
	{
		s << '(' << p.first << ", " << p.second << ')';
		return s;
	}

	template <typename ... Ts>
	ostream & operator << (ostream & s, tuple<Ts...> const & t)
	{
		s << '(';
		[&]<size_t ... Is>(index_sequence<Is...>){
			((s << (Is == 0 ? "" : ", ") << get<Is>(t)), ...);
		}(make_index_sequence<sizeof...(Ts)>{});
		s << ')';
		return s;
	}

	template <typename T>
	ostream & operator << (ostream & s, vector<T> const & v)
	{
		s << "[";
		bool first = true;
		for (auto const & x : v)
		{
			if (!first)
				s << ", ";
			first = false;
			s << x;
		}
		s << "]";
		return s;
	}

	template <typename T>
	ostream & operator << (ostream & s, set<T> const & v)
	{
		s << "{";
		bool first = true;
		for (auto const & x : v)
		{
			if (!first)
				s << ", ";
			first = false;
			s << x;
		}
		s << "}";
		return s;
	}

}

namespace psemek::util
{

	std::string type_name(std::type_info const &);
	std::string type_name(std::type_index const &);

}

namespace std
{

	inline ostream & operator << (ostream & s, type_info const & type)
	{
		s << psemek::util::type_name(type);
		return s;
	}

	inline ostream & operator << (ostream & s, type_index const & type)
	{
		s << psemek::util::type_name(type);
		return s;
	}

}

#include <psemek/util/to_string.hpp>
#include <psemek/util/type_name.hpp>

namespace psemek::test
{

	struct context
	{
		struct profile_data
		{
			std::string name;
			std::chrono::high_resolution_clock::duration duration;
			bool ended_with_exception;
		};

		std::vector<profile_data> profile;
	};

	void add_test_case(char const * name, void(*f)(context &));

	struct failure
		: std::exception
	{
		failure(std::string message, std::string location)
			: message_(std::move(message))
			, location_(std::move(location))
		{}

		char const * what() const noexcept { return message_.data(); }

		std::string_view message() const { return message_; }
		std::string_view location() const { return location_; }

	private:
		std::string message_;
		std::string location_;
	};

	struct profiler
	{
		using clock = std::chrono::high_resolution_clock;

		profiler(std::string name, context & ctx)
			: name_{std::move(name)}
			, ctx_{ctx}
		{
			start_ = clock::now();
		}

		~profiler()
		{
			auto end = clock::now();
			ctx_.profile.push_back({name_, end - start_, std::uncaught_exceptions() > 0});
		}

	private:
		clock::time_point start_;
		std::string name_;
		context & ctx_;
	};

	template <typename T>
	bool check_relative_error(T const & x1, T const & x2, T const & error)
	{
		T m = std::min(std::abs(x1), std::abs(x2));
		T M = std::max(std::abs(x1), std::abs(x2));
		if (m == T{0})
			return M < error;
		return std::abs(x1 - x2) < M * error;
	}

}

#define test_case(name) \
void name ## _test_case (::psemek::test::context &); \
static const auto name ## _test_case_registrator = []{ ::psemek::test::add_test_case(#name, &(name ## _test_case)); return 0; }(); \
void name ## _test_case ([[maybe_unused]] ::psemek::test::context & _ctx)

#define psemek_test_fail(...) throw ::psemek::test::failure(::psemek::util::to_string(__VA_ARGS__), ::psemek::util::to_string(__FILE__, ":", __LINE__))

#define expect(cond) if (!static_cast<bool>(cond)) psemek_test_fail("!(" #cond ")")

#define expect_equal(expr1, expr2) if ((expr1) != (expr2)) psemek_test_fail(#expr1, " (", (expr1), ") != ", #expr2, " (", (expr2), ")")

#define expect_equal_ptr(expr1, expr2) if ((expr1) != (expr2)) psemek_test_fail(#expr1, " (", (void*)(expr1), ") != ", #expr2, " (", (void*)(expr2), ")")

#define expect_equal_deref(expr_ptr, expr_val) if (!(expr_ptr)) psemek_test_fail(#expr_ptr, " == nullptr"); else expect_equal(*expr_ptr, expr_val)

#define expect_different(expr1, expr2) if ((expr1) == (expr2)) psemek_test_fail(#expr1, " (", (expr1), ") == ", #expr2, " (", (expr2), ")")

#define expect_different_ptr(expr1, expr2) if ((expr1) == (expr2)) psemek_test_fail(#expr1, " (", (void*)(expr1), ") == ", #expr2, " (", (void*)(expr2), ")")

#define expect_null(expr) expect_equal(expr, nullptr)

#define expect_non_null(expr) expect_different(expr, nullptr)

#define expect_less(expr1, expr2) if ((expr1) >= (expr2)) psemek_test_fail(#expr1, " (", (expr1), ") >= ", #expr2, " (", (expr2), ")")
#define expect_lequal(expr1, expr2) if ((expr2) > (expr2)) psemek_test_fail(#expr1, " (", (expr1), ") > ", #expr2, " (", (expr2), ")")
#define expect_greater(expr1, expr2) if ((expr1) <= (expr2)) psemek_test_fail(#expr1, " (", (expr1), ") <= ", #expr2, " (", (expr2), ")")
#define expect_gequal(expr1, expr2) if ((expr1) < (expr2)) psemek_test_fail(#expr1, " (", (expr1), ") < ", #expr2, " (", (expr2), ")")

#define expect_small(expr1, expr2) if (std::abs((expr1)) > (expr2)) psemek_test_fail("abs(", #expr1, ") (", std::abs((expr1)), ") >= ", #expr2, " (", (expr2), ")")
#define expect_close(expr1, expr2, expr3) if (std::abs((expr1) - (expr2)) > (expr3)) psemek_test_fail(#expr1, " (", (expr1), ") and ", #expr2, " (", (expr2), ") differ by more than ", (expr3))
#define expect_close_rel(expr1, expr2, expr3) if (!::psemek::test::check_relative_error((expr1), (expr2), (expr3))) psemek_test_fail(#expr1, " (", (expr1), ") and ", #expr2, " (", (expr2), ") relative error is more than ", (expr3))

#define expect_throw(expr, type) do { bool thrown = false; try { (void)(expr); } catch (type const &) { thrown = true; } if (!thrown) psemek_test_fail(#expr, " didn't throw ", #type); } while (false)

#define expect_dynamic_type(expr, type) if (!dynamic_cast<type const *>(&expr)) psemek_test_fail("type(", #expr, ") = ", ::psemek::util::type_name(typeid(expr)), " != ", ::psemek::util::type_name<type>())

#define test_profile(name) \
	if (::psemek::test::profiler name ## _profiler(#name, _ctx); true)
