#include <psemek/test/test.hpp>

#include <psemek/util/function.hpp>
#include <psemek/util/unused.hpp>

#include <memory>
#include <array>

using namespace psemek::util;

test_case(util_function_empty_call)
{
	function<void()> f;
	expect_throw(f(), empty_function_error);
}

using heavy = std::array<int, 256>;

static void util_function_assign_test(bool light)
{
	function<void()> f;
	expect(!f);

	int counter = 0;
	if (light)
		f = [&counter]{ ++counter; };
	else
		f = [&counter, h = heavy{}]{ unused(h); ++counter; };
	expect(f);

	expect_equal(counter, 0);
	f();
	expect_equal(counter, 1);
	f();
	expect_equal(counter, 2);
}

static void util_function_move_test(bool light)
{
	function<void()> f;
	expect(!f);

	int counter = 0;
	if (light)
		f = [&counter]{ ++counter; };
	else
		f = [&counter, h = heavy{}]{ unused(h); ++counter; };
	expect(f);

	function<void()> g;
	expect(!g);

	g = std::move(f);
	expect(!f);
	expect(g);

	expect_equal(counter, 0);
	g();
	expect_equal(counter, 1);
	g();
	expect_equal(counter, 2);

	function<void()> h = std::move(g);
	expect(!f);
	expect(!g);
	expect(h);

	expect_equal(counter, 2);
	h();
	expect_equal(counter, 3);
	h();
	expect_equal(counter, 4);
}

static void util_function_destroy_test(bool light)
{
	std::shared_ptr<int> p = std::make_shared<int>(0);

	auto wp = std::weak_ptr(p);

	{
		function<void()> f, g;
		expect(!f);
		expect(!g);

		if (light)
		{
			f = [p]{ ++*p; };
			g = [p]{ --*p; };
		}
		else
		{
			f = [p, h = heavy{}]{ unused(h); ++*p; };
			g = [p, h = heavy{}]{ unused(h); --*p; };
		}
		expect(f);
		expect(g);
		expect(p);
		expect_equal(p.use_count(), 3);
		expect_equal(*p, 0);

		p.reset();
		expect(wp.lock());
		expect_equal(wp.use_count(), 2);

		expect_equal(*wp.lock(), 0);
		f();
		expect_equal(*wp.lock(), 1);
		f();
		expect_equal(*wp.lock(), 2);
		g();
		expect_equal(*wp.lock(), 1);
		g();
		expect_equal(*wp.lock(), 0);

		f.reset();
		expect(!f);
		expect(wp.lock());
		expect_equal(wp.use_count(), 1);
	}

	expect(!wp.lock());
	expect_equal(wp.use_count(), 0);
}

namespace
{

	struct fake_exception
		: std::exception
	{
		char const * what() const noexcept { return "fake"; }
	};

	template <typename Heavy>
	struct throw_on_move
	{
		Heavy heavy;

		throw_on_move() = default;
		throw_on_move(throw_on_move &&)
		{
			throw fake_exception{};
		}

		void operator()()
		{}
	};

}

static void util_function_guarantee_test(bool light)
{
	function<void()> f;
	expect(!f);

	int counter = 0;
	f = [&counter]{ ++counter; };
	expect(f);
	expect_equal(counter, 0);

	if (light)
		expect_throw(f = throw_on_move<int>{}, fake_exception);
	else
		expect_throw(f = throw_on_move<heavy>{}, fake_exception);

	expect(f);
	expect_equal(counter, 0);
	f();
	expect_equal(counter, 1);
}

test_case(util_function_light_assign)
{
	util_function_assign_test(true);
}

test_case(util_function_light_move)
{
	util_function_move_test(true);
}

test_case(util_function_light_destroy)
{
	util_function_destroy_test(true);
}

test_case(util_function_light_guarantee)
{
	util_function_guarantee_test(true);
}

test_case(util_function_heavy_assign)
{
	util_function_assign_test(false);
}

test_case(util_function_heavy_move)
{
	util_function_move_test(false);
}

test_case(util_function_heavy_destroy)
{
	util_function_destroy_test(false);
}

test_case(util_function_heavy_guarantee)
{
	util_function_guarantee_test(false);
}
