/*
 * Copyright 2015 - 2025 gary@drinkingtea.net
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#if __has_include(<chrono>)
#include<chrono>
#endif

#include <ox/std/def.hpp>
#undef NDEBUG

#include <map>
#include <ox/std/uuid.hpp>
#include <ox/std/std.hpp>

[[nodiscard]]
static uint64_t steadyNowMs() {
#if __has_include(<chrono>)
	using namespace std::chrono;
	return static_cast<uint64_t>(
			duration_cast<milliseconds>(
					steady_clock::now().time_since_epoch()).count());
#else
	return 0;
#endif
}

template<typename Map = ox::SmallMap<ox::String, ox::UUID>>
uint64_t timeMapStrToUuid(int const elemCnt, int const lookups, uint64_t seed = 4321) noexcept {
	ox::UUID::seedGenerator({1234, seed});
	Map map;
	// setup test map
	for (int i = 0; i < elemCnt; ++i) {
		auto const uuid = ox::UUID::generate().unwrap();
		map[uuid.toString()] = uuid;
	}
	auto const keys = map.keys();
	ox::Random rand;
	// start
	auto const startTime = steadyNowMs();
	for (int i = 0; i < lookups; ++i) {
		auto const &k = keys[static_cast<size_t>(rand.gen() % keys.size())];
		map[k];
		ox::expect(map[k], ox::UUID::fromString(k).unwrap());
	}
	return steadyNowMs() - startTime;
}

template<typename Map = ox::SmallMap<ox::UUID, ox::String>>
[[nodiscard]]
uint64_t timeMapUuidToStr(int const elemCnt, int const lookups, uint64_t seed = 4321) noexcept {
	ox::UUID::seedGenerator({1234, seed});
	Map map;
	// setup test map
	for (int i = 0; i < elemCnt; ++i) {
		auto const uuid = ox::UUID::generate().unwrap();
		map[uuid] = uuid.toString();
	}
	auto const keys = map.keys();
	ox::Random rand;
	// start
	auto const startTime = steadyNowMs();
	for (int i = 0; i < lookups; ++i) {
		auto const &k = keys[static_cast<size_t>(rand.gen() % keys.size())];
		ox::expect(map[k], k.toString());
	}
	return steadyNowMs() - startTime;
}

static ox::Error compareMaps(int const lookupCnt = 1'000'000) {
	auto const seed = steadyNowMs();
	uint64_t hashTime{};
	uint64_t smallTime{};
	int elemCnt = 1;
	oxOut("UUIDStr to UUID:\n\n");
	while (hashTime >= smallTime) {
		smallTime = timeMapStrToUuid<ox::SmallMap<ox::UUIDStr, ox::UUID>>(elemCnt, lookupCnt, seed);
		hashTime = timeMapStrToUuid<ox::HashMap<ox::UUIDStr, ox::UUID>>(elemCnt, lookupCnt, seed);
		oxOutf(
				"UUIDStr to UUID: elemCnt: {}, lookupCnt: {} - hash map time: {}ms, small map time: {}ms\n",
				elemCnt, lookupCnt, hashTime, smallTime);
		++elemCnt;
	}
	oxOut("\n\nString to UUID:\n\n");
	hashTime = 0;
	smallTime = 0;
	elemCnt = 1;
	while (hashTime >= smallTime) {
		smallTime = timeMapStrToUuid<ox::SmallMap<ox::String, ox::UUID>>(elemCnt, lookupCnt, seed);
		hashTime = timeMapStrToUuid<ox::HashMap<ox::String, ox::UUID>>(elemCnt, lookupCnt, seed);
		oxOutf(
				"String to UUID: elemCnt: {}, lookupCnt: {} - hash map time: {}ms, small map time: {}ms\n",
				elemCnt, lookupCnt, hashTime, smallTime);
		++elemCnt;
	}
	oxOut("\n\nUUID to UUIDStr:\n\n");
	hashTime = 0;
	smallTime = 0;
	elemCnt = 1;
	while (hashTime >= smallTime) {
		smallTime = timeMapUuidToStr<ox::SmallMap<ox::UUID, ox::UUIDStr>>(elemCnt, lookupCnt, seed);
		hashTime = timeMapUuidToStr<ox::HashMap<ox::UUID, ox::UUIDStr>>(elemCnt, lookupCnt, seed);
		oxOutf(
				"UUID to UUIDStr: elemCnt: {}, lookupCnt: {} - hash map time: {}ms, small map time: {}ms\n",
				elemCnt, lookupCnt, hashTime, smallTime);
		++elemCnt;
	}
	oxOut("\n");
	return {};
}

static std::map<ox::StringView, ox::Error(*)()> tests = {
	{
		"malloc",
		[] {
			ox::Buffer buff(ox::units::MB);
			ox::heapmgr::initHeap(&buff[0], &buff[buff.size()-1]);
			auto a1 = static_cast<char*>(ox::heapmgr::malloc(5));
			auto a2 = static_cast<char*>(ox::heapmgr::malloc(5));
			oxAssert(a1 >= buff.front().unwrap() && a1 < buff.back().unwrap(), "malloc is broken");
OX_CLANG_NOWARN_BEGIN(-Wunsafe-buffer-usage)
			oxAssert(a2 >= buff.front().unwrap() && a2 < buff.back().unwrap() && a2 > a1 + 5, "malloc is broken");
OX_CLANG_NOWARN_END
			ox::heapmgr::free(a1);
			ox::heapmgr::free(a2);
			return ox::Error{};
		}
	},
	{
		"itoa",
		[]() {
			ox::Array<char, 10> buff;
			ox::CharBuffWriter bw(buff);
			oxAssert(ox::writeItoa(5, bw), "ox::writeItoa returned Error");
			ox::expect(ox::StringView(buff.data()), ox::StringView("5"));
			OX_RETURN_ERROR(bw.seekp(0));
			oxAssert(ox::writeItoa(50, bw), "ox::writeItoa returned Error");
			ox::expect(ox::StringView(buff.data()), ox::StringView("50"));
			OX_RETURN_ERROR(bw.seekp(0));
			oxAssert(ox::writeItoa(500, bw), "ox::writeItoa returned Error");
			ox::expect(ox::StringView(buff.data()), ox::StringView("500"));
			return ox::Error{};
		}
	},
	OX_ALLOW_UNSAFE_BUFFERS_BEGIN
	{
		"ABCDEFG != HIJKLMN",
		[]() {
			return ox::Error(ox::memcmp("ABCDEFG", "HIJKLMN", 7) >= 0);
		}
	},
	{
		"HIJKLMN != ABCDEFG",
		[]() {
			return ox::Error(ox::memcmp("HIJKLMN", "ABCDEFG", 7) <= 0);
		}
	},
	{
		"ABCDEFG == ABCDEFG",
		[]() {
			return ox::Error(ox::memcmp("ABCDEFG", "ABCDEFG", 7) != 0);
		}
	},
	{
		"ABCDEFGHI == ABCDEFG",
		[]() {
			return ox::Error(ox::memcmp("ABCDEFGHI", "ABCDEFG", 7) != 0);
		}
	},
	OX_ALLOW_UNSAFE_BUFFERS_END
	{
		"IString",
		[]() {
			ox::IString<5> s;
			OX_RETURN_ERROR(s.append("A"));
			OX_RETURN_ERROR(s.append("B"));
			OX_RETURN_ERROR(s.append("9"));
			OX_RETURN_ERROR(s.append("C"));
			oxAssert(s == "AB9C", "IString append broken");
			s = "asdf";
			oxAssert(s == "asdf", "String assign broken");
			oxAssert(s != "aoeu", "String assign broken");
			oxAssert(s.size() == 4, "String assign broken");
			return ox::Error{};
		}
	},
	{
		"String",
		[]() {
			ox::String s;
			s += "A";
			s += "B";
			s += 9;
			s += "C";
			oxAssert(s == "AB9C", "String append broken");
			s = "asdf";
			oxAssert(s == "asdf", "String assign broken");
			s += "aoeu";
			oxAssert(s == "asdfaoeu", "String append broken");
			const ox::StringView str = "asdf";
			oxAssert(beginsWith(str, "as"), "String beginsWith is broken");
			oxAssert(beginsWith(str, "asd"), "String beginsWith is broken");
			oxAssert(beginsWith(str, "asdf"), "String beginsWith is broken");
			oxAssert(!beginsWith(str, "aa"), "String beginsWith is broken");
			oxAssert(!beginsWith(str, "aaaaaaa"), "String beginsWith is broken");
			oxAssert(!beginsWith(str, "li"), "String beginsWith is broken");
			oxAssert(!beginsWith(str, "afoif"), "String beginsWith is broken");
			oxAssert(endsWith(str, "df"), "String endsWith is broken");
			oxAssert(!endsWith(str, "awefawe"), "String endsWith is broken");
			oxAssert(!endsWith(str, "eu"), "String endsWith is broken");
			oxAssert(ox::StringView("Write") != ox::String(""), "String / StringView comparison broken");
			oxAssert(ox::String("Write") != ox::StringView(""), "String / StringView comparison broken");
			oxAssert(ox::String("Write") == ox::StringView("Write"), "String / StringView comparison broken");
			ox::expect(ox::String("asdf").substr(1, 3), "sd");
			oxAssert(
					ox::String(ox::StringView("Write")) == ox::StringView("Write"),
					"String / StringView comparison broken");
			return ox::Error{};
		}
	},
	{
		"Vector",
		[] {
			ox::Vector<int> v;
			oxAssert(v.size() == 0, "Initial Vector size not 0");
			oxAssert(v.empty(), "Vector::empty() is broken");
			auto insertTest = [&v](int const val, std::size_t const size) {
				v.push_back(val);
				OX_RETURN_ERROR(ox::Error(v.size() != size, "Vector size incorrect"));
				OX_RETURN_ERROR(ox::Error(v[v.size() - 1] != val, "Vector value wrong"));
				return ox::Error{};
			};
			oxAssert(insertTest(42, 1), "Vector insertion failed");
			oxAssert(insertTest(100, 2), "Vector insertion failed");
			oxAssert(insertTest(102, 3), "Vector insertion failed");
			return ox::Error{};
		}
	},
	{
		"Vector::shrink_to_fit",
		[] {
			{
				ox::Vector<ox::IString<8>> v;
				v.reserve(50);
				v.emplace_back("asdf");
				v.emplace_back("aoeu");
				auto const origData = v.data();
				v.shrink_to_fit();
				ox::expect(v[0], "asdf");
				ox::expect(v[1], "aoeu");
				ox::expect(v.capacity(), 2u);
				oxAssert(origData != v.data(), "shrink_to_fit did not create a new allocation");
			}
			{
				ox::Vector<ox::IString<8>> v;
				v.reserve(2);
				v.emplace_back("asdf");
				v.emplace_back("aoeu");
				auto const origData = v.data();
				v.shrink_to_fit();
				ox::expect(v[0], "asdf");
				ox::expect(v[1], "aoeu");
				ox::expect(v.capacity(), 2u);
				oxAssert(origData == v.data(), "shrink_to_fit inappropriately created a new allocation");
			}
			return ox::Error{};
		}
	},
	{
		"findIdx",
		[] {
			ox::Vector<ox::IString<8>> const v {"zero", "one", "two", "three", "four"};
			ox::expect(ox::findIdx(v.begin(), v.end(), "zero").or_value(5), 0u);
			ox::expect(ox::findIdx(v.begin(), v.end(), "one").or_value(5), 1u);
			ox::expect(ox::findIdx(v.begin(), v.end(), "two").or_value(5), 2u);
			ox::expect(ox::findIdx(v.begin(), v.end(), "three").or_value(5), 3u);
			ox::expect(ox::findIdx(v.begin(), v.end(), "four").or_value(5), 4u);
			ox::expect(ox::findIdx(v.begin(), v.end(), "five").or_value(5), 5u);
			ox::expect(ox::findIdx(v.begin(), v.end(), "six").or_value(6), 6u);
			return ox::Error{};
		}
	},
	{
		"SmallMap",
		[] {
			ox::SmallMap<ox::String, ox::String> map;
			map["asdf"] = "aoeu";
			ox::expect(map["asdf"], "aoeu");
			ox::expect(map.size(), 1u);
			ox::expect(map["aoeu"], "");
			ox::expect(map.size(), 2u);
			ox::SmallMap<ox::String, ox::String> cmap;
			cmap["asdf"] = "aoeu";
			auto constexpr constTest = [](ox::SmallMap<ox::String, ox::String> const &map) {
				OX_REQUIRE(asdf, map.at("asdf"));
				ox::expect(*asdf, "aoeu");
				ox::expect(map.size(), 1u);
				auto const aoeu = map.at("aoeu");
				ox::expect(aoeu.ok(), false);
				ox::expect(map.size(), 1u);
				return ox::Error{};
			};
			return constTest(cmap);
		}
	},
	{
		"SmallMap2",
		[] {
			ox::SmallMap<ox::String, int> si;
			si["asdf"] = 42;
			si["aoeu"] = 100;
			oxAssert(si["asdf"] == 42, "asdf != 42");
			oxAssert(si["aoeu"] == 100, "aoeu != 100");
			ox::SmallMap<int, int> ii;
			ii[4] = 42;
			ii[5] = 100;
			oxAssert(ii[4] == 42, "4 != 42");
			oxAssert(ii[5] == 100, "5 != 100");
			return ox::Error{};
		}
	},
	{
		"HashMap",
		[] {
			ox::HashMap<ox::String, int> si;
			si["asdf"] = 42;
			si["aoeu"] = 100;
			oxAssert(si["asdf"] == 42, "asdf != 42");
			oxAssert(si["aoeu"] == 100, "aoeu != 100");
			si.erase("asdf");
			oxAssert(!si.contains("asdf"), "wrongly contains asdf");
			oxAssert(si.contains("aoeu"), "does not contains aoeu");
			oxAssert(!si.at("asdf").ok(), "asdf != 0");
			ox::expect(si["asdf"], 0);
			oxAssert(si["aoeu"] == 100, "aoeu != 100");
			auto si2 = si;
			ox::expect(si2["asdf"], 0);
			oxAssert(si2["aoeu"] == 100, "aoeu != 100");
			ox::HashMap<int, int> ii;
			ii[4] = 42;
			ii[5] = 100;
			oxAssert(ii[4] == 42, "4 != 42");
			oxAssert(ii[5] == 100, "5 != 100");
			return ox::Error{};
		}
	},
	{
		"TimeSmallMapMillion",
		[] {
			timeMapStrToUuid<ox::SmallMap<ox::String, ox::UUID>>(1'000, 1'000'000);
			return ox::Error{};
		}
	},
	{
		"TimeHashMapMillion",
		[] {
			timeMapStrToUuid<ox::HashMap<ox::String, ox::UUID>>(1'000, 1'000'000);
			return ox::Error{};
		}
	},
	{
		"CompareMaps",
		[] {
			return compareMaps();
		}
	},
	{
		"Serialize-Int",
		[] {
			using BA = ox::Array<char, 4>;
			const auto actual = ox::serialize<uint32_t>(256).unwrap();
			oxOutf("[{}, {}, {}, {}]",
				   static_cast<int>(actual[0]),
				   static_cast<int>(actual[1]),
				   static_cast<int>(actual[2]),
				   static_cast<int>(actual[3]));
			ox::expect(ox::serialize<int32_t>(4).unwrap(), BA({4, 0, 0, 0}));
			ox::expect(ox::serialize<int32_t>(256).unwrap(), BA({0, 1, 0, 0}));
			ox::expect(ox::serialize<int32_t>(257).unwrap(), BA({1, 1, 0, 0}));
			ox::expect(ox::serialize<uint32_t>(4).unwrap(), BA({4, 0, 0, 0}));
			ox::expect(ox::serialize<uint32_t>(256).unwrap(), BA({0, 1, 0, 0}));
			ox::expect(ox::serialize<uint32_t>(257).unwrap(), BA({1, 1, 0, 0}));
			constexpr auto neg1 = static_cast<char>(-1); // ARM64 Linux assumes -1 literals are ints...
			ox::expect(ox::serialize<uint32_t>(0xffff'ffff).unwrap(), BA({neg1, neg1, neg1, neg1}));
			return ox::Error{};
		}
	},
	{
		"BufferWriter",
		[] {
			ox::Buffer b;
			ox::BufferWriter w(&b);
			oxAssert(w.write("asdf", 4), "write failed");
			ox::expect(b.size(), 4u);
			oxAssert(w.write("aoeu", 4), "write failed");
			ox::expect(b.size(), 8u);
			ox::expect(ox::StringView(b.data(), b.size()), "asdfaoeu");
			ox::StringView constexpr qwerty = "qwerty";
			oxAssert(w.write(qwerty.data(), qwerty.bytes()), "write failed");
			ox::expect(b.size(), 14u);
			ox::expect(ox::StringView(b.data(), b.size()), "asdfaoeuqwerty");
			return ox::Error{};
		}
	},
	{
		"FromHex",
		[] {
			ox::expect(ox::detail::fromHex("01").unwrap(), 0x01);
			ox::expect(ox::detail::fromHex("02").unwrap(), 0x02);
			ox::expect(ox::detail::fromHex("03").unwrap(), 0x03);
			ox::expect(ox::detail::fromHex("04").unwrap(), 0x04);
			ox::expect(ox::detail::fromHex("05").unwrap(), 0x05);
			ox::expect(ox::detail::fromHex("06").unwrap(), 0x06);
			ox::expect(ox::detail::fromHex("07").unwrap(), 0x07);
			ox::expect(ox::detail::fromHex("08").unwrap(), 0x08);
			ox::expect(ox::detail::fromHex("0d").unwrap(), 0x0d);
			ox::expect(ox::detail::fromHex("0e").unwrap(), 0x0e);
			ox::expect(ox::detail::fromHex("0f").unwrap(), 0x0f);
			ox::expect(ox::detail::fromHex("0F").unwrap(), 0x0f);
			ox::expect(ox::detail::fromHex("fF").unwrap(), 0xff);
			ox::expect(ox::detail::fromHex("ff").unwrap(), 0xff);
			ox::expect(ox::detail::fromHex("a0").unwrap(), 0xa0);
			ox::expect(ox::detail::fromHex("93").unwrap(), 0x93);
			ox::expect(ox::detail::fromHex("40").unwrap(), 0x40);
			return ox::Error{};
		}
	},
	{
		"ToHex",
		[] {
			ox::expect(ox::detail::toHex(0x01), "01");
			ox::expect(ox::detail::toHex(0x02), "02");
			ox::expect(ox::detail::toHex(0x03), "03");
			ox::expect(ox::detail::toHex(0x04), "04");
			ox::expect(ox::detail::toHex(0x05), "05");
			ox::expect(ox::detail::toHex(0x06), "06");
			ox::expect(ox::detail::toHex(0x07), "07");
			ox::expect(ox::detail::toHex(0x08), "08");
			ox::expect(ox::detail::toHex(0x0d), "0d");
			ox::expect(ox::detail::toHex(0x0e), "0e");
			ox::expect(ox::detail::toHex(0x0f), "0f");
			ox::expect(ox::detail::toHex(0x93), "93");
			ox::expect(ox::detail::toHex(0x40), "40");
			ox::expect(ox::detail::toHex(0xf0), "f0");
			return ox::Error{};
		}
	},
	{
		"UUID",
		[] {
			constexpr ox::StringView uuidStr = "8d814442-f46e-4cc3-8edc-ca3c01cc86db";
			OX_REQUIRE(uuid, ox::UUID::fromString(uuidStr));
			ox::expect(uuid.toString(), uuidStr);
			ox::expect(ox::UUID{}.isNull(), true);
			ox::expect(ox::UUID::fromString(uuidStr).value.isNull(), false);
			return ox::Error{};
		}
	},
	{
		"UUID::generate",
		[] {
			ox::UUID::seedGenerator({1234, 4321});
			ox::expect(ox::UUID::generate().unwrap().toString(), "5c3f4b5e-ccbf-4727-7f03-3053dedc8827");
			ox::expect(ox::UUID::generate().unwrap().toString(), "90d0274a-2774-4afa-88e5-0c1d60ba3abf");
			ox::expect(ox::UUID::generate().unwrap().toString(), "7df77910-841c-48ba-ea2e-44521ac47c2e");
			return ox::Error{};
		}
	},
	{
		"StringSplit",
		[] {
			ox::StringView sv = "ab.cd";
			auto list = ox::split(sv, ".");
			ox::expect(list.size(), 2u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			sv = "ab.cd.fg";
			list = ox::split(sv, ".");
			ox::expect(list.size(), 3u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			ox::expect(list[2], "fg");
			sv = "ab.cd.";
			list = ox::split(sv, ".");
			ox::expect(list.size(), 2u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			sv = ".ab.cd.";
			list = ox::split(sv, ".");
			ox::expect(list.size(), 2u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			sv = ".";
			list = ox::split(sv, ".");
			ox::expect(list.size(), 0u);
			sv = ".";
			list = ox::split(sv, ".");
			ox::expect(list.size(), 0u);
			sv = "";
			list = ox::split(sv, ".");
			ox::expect(list.size(), 0u);
			// split by single char
			sv = "ab.cd";
			list = ox::split(sv, '.');
			ox::expect(list.size(), 2u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			sv = "ab.cd.fg";
			list = ox::split(sv, '.');
			ox::expect(list.size(), 3u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			ox::expect(list[2], "fg");
			sv = "ab.cd.";
			list = ox::split(sv, '.');
			ox::expect(list.size(), 2u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			sv = ".ab.cd.";
			list = ox::split(sv, '.');
			ox::expect(list.size(), 2u);
			ox::expect(list[0], "ab");
			ox::expect(list[1], "cd");
			sv = ".";
			list = ox::split(sv, '.');
			ox::expect(list.size(), 0u);
			sv = ".";
			list = ox::split(sv, '.');
			ox::expect(list.size(), 0u);
			sv = "";
			list = ox::split(sv, '.');
			ox::expect(list.size(), 0u);
			return ox::Error{};
		}
	},
	{
		"intToStr",
		[] {
			ox::expect(ox::intToStr<uint8_t>(255u), "255");
			ox::expect(ox::intToStr<int8_t>(127), "127");
			ox::expect(ox::intToStr<int8_t>(-128), "-128");
			ox::expect(ox::intToStr<uint16_t>(65535u), "65535");
			ox::expect(ox::intToStr<int16_t>(32767), "32767");
			ox::expect(ox::intToStr<int16_t>(-32768), "-32768");
			ox::expect(ox::intToStr<uint32_t>(4294967295u), "4294967295");
			ox::expect(ox::intToStr<int32_t>(2147483647), "2147483647");
			ox::expect(ox::intToStr<int32_t>(-2147483648), "-2147483648");
			ox::expect(ox::intToStr<uint64_t>(18446744073709551615u), "18446744073709551615");
			ox::expect(ox::intToStr<int64_t>(9223372036854775807), "9223372036854775807");
			ox::expect(ox::intToStr<int64_t>(-9223372036854775807), "-9223372036854775807");
			ox::expect(ox::intToStr<uint64_t>(0), "0");
			ox::expect(ox::intToStr<uint64_t>(5), "5");
			ox::expect(ox::intToStr(0), "0");
			ox::expect(ox::intToStr(5), "5");
			ox::expect(ox::intToStr(5000), "5000");
			ox::expect(ox::intToStr(50000), "50000");
			ox::expect(ox::intToStr(500000), "500000");
			ox::expect(ox::intToStr(-5), "-5");
			ox::expect(ox::intToStr(-5000), "-5000");
			ox::expect(ox::intToStr(-50000), "-50000");
			ox::expect(ox::intToStr(-500000), "-500000");
			return ox::Error{};
		}
	},
};

int main(int const argc, const char **argv) {
	if (argc < 2) {
		oxError("Must specify test to run");
		return -1;
	}
	auto const args = ox::Span{argv, static_cast<size_t>(argc)};
	auto const testName = args[1];
	auto const func = tests.find(testName);
	if (func != tests.end()) {
		oxAssert(func->second(), "Test returned Error");
		return 0;
	}
	return -1;
}
