/*
 * 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/.
 */

#undef NDEBUG

#include <iostream>
#include <map>
#include <ox/mc/mc.hpp>
#include <ox/model/model.hpp>
#include <ox/std/std.hpp>

union TestUnion {
	static constexpr auto TypeName = "TestUnion";
	static constexpr auto TypeVersion = 1;
	bool Bool;
	uint32_t Int;
	char *CString{};
};

struct TestStructNest {
	static constexpr auto TypeName = "TestStructNest";
	static constexpr auto TypeVersion = 1;
	bool Bool = false;
	uint32_t Int = 0;
	ox::IString<32> IString = "";
};

struct TestStruct {
	static constexpr auto TypeName = "TestStruct";
	static constexpr auto TypeVersion = 1;
	bool Bool = false;
	int32_t Int = 0;
	int32_t Int1 = 0;
	int32_t Int2 = 0;
	int32_t Int3 = 0;
	int32_t Int4 = 0;
	int32_t Int5 = 0;
	int32_t Int6 = 0;
	int32_t Int7 = 0;
	int32_t Int8 = 0;
	int32_t unionIdx = 1;
	TestUnion Union;
	ox::String String;
	ox::IString<32> IString = "";
	uint32_t List[4] = {0, 0, 0, 0};
	ox::Vector<uint32_t> Vector = {1, 2, 3, 4, 5};
	ox::Vector<uint32_t> Vector2 = {1, 2, 3, 4, 5};
	ox::HashMap<ox::String, int32_t> Map;
	TestStructNest EmptyStruct;
	TestStructNest Struct;
	constexpr ~TestStruct() noexcept {
		if (unionIdx == 2) {
			ox::safeDelete(Union.CString);
		}
	}
};

template<typename T>
constexpr ox::Error model(T *io, ox::CommonPtrWith<TestUnion> auto *obj) noexcept {
	OX_RETURN_ERROR(io->template setTypeInfo<TestUnion>());
	OX_RETURN_ERROR(io->field("Bool", &obj->Bool));
	OX_RETURN_ERROR(io->field("Int", &obj->Int));
	OX_RETURN_ERROR(io->fieldCString("CString", &obj->CString));
	return ox::Error(0);
}

OX_MODEL_BEGIN(TestStructNest)
	OX_MODEL_FIELD(Bool)
	OX_MODEL_FIELD(Int)
	OX_MODEL_FIELD(IString)
OX_MODEL_END()

template<typename T>
constexpr ox::Error model(T *io, ox::CommonPtrWith<TestStruct> auto *obj) noexcept {
	OX_RETURN_ERROR(io->template setTypeInfo<TestStruct>());
	OX_RETURN_ERROR(io->field("Bool", &obj->Bool));
	OX_RETURN_ERROR(io->field("Int", &obj->Int));
	OX_RETURN_ERROR(io->field("Int1", &obj->Int1));
	OX_RETURN_ERROR(io->field("Int2", &obj->Int2));
	OX_RETURN_ERROR(io->field("Int3", &obj->Int3));
	OX_RETURN_ERROR(io->field("Int4", &obj->Int4));
	OX_RETURN_ERROR(io->field("Int5", &obj->Int5));
	OX_RETURN_ERROR(io->field("Int6", &obj->Int6));
	OX_RETURN_ERROR(io->field("Int7", &obj->Int7));
	OX_RETURN_ERROR(io->field("Int8", &obj->Int8));
	OX_RETURN_ERROR(io->field("unionIdx", &obj->unionIdx));
	if constexpr(T::opType() == ox::OpType::Reflect) {
		OX_RETURN_ERROR(io->field("Union", ox::UnionView{&obj->Union, 0}));
	} else {
		OX_RETURN_ERROR(io->field("Union", ox::UnionView{&obj->Union, obj->unionIdx}));
	}
	OX_RETURN_ERROR(io->field("String", &obj->String));
	OX_RETURN_ERROR(io->field("IString", &obj->IString));
	OX_RETURN_ERROR(io->field("List", obj->List, 4));
	OX_RETURN_ERROR(io->field("Vector", &obj->Vector));
	OX_RETURN_ERROR(io->field("Vector2", &obj->Vector2));
	OX_RETURN_ERROR(io->field("Map", &obj->Map));
	OX_RETURN_ERROR(io->field("Struct", &obj->Struct));
	OX_RETURN_ERROR(io->field("EmptyStruct", &obj->EmptyStruct));
	return ox::Error(0);
}

std::map<ox::StringView, ox::Error(*)()> tests = {
	{
		{
			"MetalClawWriter",
			[] {
				// This test doesn't confirm much, but it does show that the writer
				// doesn't segfault
				ox::Array<char, 1024> buff;
				TestStruct ts;
				OX_RETURN_ERROR(ox::writeMC(buff.data(), buff.size(), ts));
				OX_RETURN_ERROR(ox::writeMC(ts));
				return ox::Error(0);
			}
		},

		{
			"MetalClawReader",
			[] {
				// setup for tests
				TestStruct testIn, testOut;
				testIn.Bool = true;
				testIn.Int = 42;
				testIn.IString = "Test String 1";
				testIn.String = "Test String 2";
				testIn.Vector = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, };
				testIn.Vector2 = {};
				testIn.List[0] = 1;
				testIn.List[1] = 2;
				testIn.List[2] = 3;
				testIn.List[3] = 4;
				testIn.Struct.Bool = true;
				testIn.Struct.Int = 300;
				testIn.Struct.IString = "Test String 3";
				testIn.unionIdx = 1;
				testIn.Union.Int = 93;
				// run tests
				const auto [buff, err] = ox::writeMC(testIn);
				oxAssert(err, "writeMC failed");
				oxAssert(ox::readMC(buff, testOut), "readMC failed");
				//std::cout << testIn.Union.Int << "|" << testOut.Union.Int << "|\n";
				oxAssert(testIn.Bool                == testOut.Bool,                "Bool value mismatch");
				oxAssert(testIn.Int                 == testOut.Int,                 "Int value mismatch");
				oxAssert(testIn.Int1                == testOut.Int1,                "Int1 value mismatch");
				oxAssert(testIn.Int2                == testOut.Int2,                "Int2 value mismatch");
				oxAssert(testIn.Int3                == testOut.Int3,                "Int3 value mismatch");
				oxAssert(testIn.Int4                == testOut.Int4,                "Int4 value mismatch");
				oxAssert(testIn.Int5                == testOut.Int5,                "Int5 value mismatch");
				oxAssert(testIn.Int6                == testOut.Int6,                "Int6 value mismatch");
				oxAssert(testIn.Int7                == testOut.Int7,                "Int7 value mismatch");
				oxAssert(testIn.Int8                == testOut.Int8,                "Int8 value mismatch");
				oxAssert(testIn.Union.Int           == testOut.Union.Int,           "Union.Int value mismatch");
				oxAssert(testIn.String              == testOut.String,              "String value mismatch");
				ox::expect(testIn.IString,               testOut.IString);
				oxAssert(testIn.List[0]             == testOut.List[0],             "List[0] value mismatch");
				oxAssert(testIn.List[1]             == testOut.List[1],             "List[1] value mismatch");
				oxAssert(testIn.List[2]             == testOut.List[2],             "List[2] value mismatch");
				oxAssert(testIn.List[3]             == testOut.List[3],             "List[3] value mismatch");
				oxAssert(testIn.Vector.size()       == testOut.Vector.size(),       "Vector size mismatch");
				for (auto i = 0u; i < testIn.Vector.size(); ++i) {
					oxAssert(testIn.Vector[i]        == testOut.Vector[i],           ox::sfmt("Vector[{}] value mismatch", i));
				}
				oxAssert(testIn.Vector2.size()      == testOut.Vector2.size(),      "Vector2 size mismatch");
				oxAssert(testIn.Map["asdf"]         == testOut.Map["asdf"],         "Map[\"asdf\"] value mismatch");
				oxAssert(testIn.Map["aoeu"]         == testOut.Map["aoeu"],         "Map[\"aoeu\"] value mismatch");
				oxAssert(testIn.EmptyStruct.Bool    == testOut.EmptyStruct.Bool,    "EmptyStruct.Bool value mismatch");
				oxAssert(testIn.EmptyStruct.Int     == testOut.EmptyStruct.Int,     "EmptyStruct.Int value mismatch");
				oxAssert(testIn.EmptyStruct.IString == testOut.EmptyStruct.IString, "EmptyStruct.IString value mismatch");
				oxAssert(testIn.Struct.Int          == testOut.Struct.Int,          "Struct.Int value mismatch");
				oxAssert(testIn.Struct.IString      == testOut.Struct.IString,      "Struct.IString value mismatch");
				oxAssert(testIn.Struct.Bool         == testOut.Struct.Bool,         "Struct.Bool value mismatch");
				return ox::Error(0);
			}
		},

		{
			"encodeInteger",
			[] {
				using ox::MaxValue;
				using ox::mc::McInt;
				using ox::mc::encodeInteger;
				static constexpr auto check = [](McInt val, const ox::Vector<uint8_t, 9> &expected) {
					if (val.length != expected.size()) {
						std::cout << "val.length: " << val.length << ", expected: " << expected.size() << '\n';
						return ox::Error(1);
					}
					for (std::size_t i = 0; i < expected.size(); i++) {
						if (expected[i] != val.data[i]) {
							std::cout << "decoded: " << static_cast<uint32_t>(val.data[i]) << ", expected: " << static_cast<uint32_t>(expected[i]) << '\n';
							std::cout << "decoded: " << i << ": " << static_cast<uint32_t>(val.data[i]) << '\n';
							return ox::Error(1);
						}
					}
					return ox::Error(0);
				};
				constexpr auto check64 = [](McInt val, auto expected) {
					if (val.length != 9) {
						std::cout << "val.length: " << val.length << '\n';
						return ox::Error(1);
					}
					ox::LittleEndian<decltype(expected)> decoded;
					ox::memcpy(&decoded, &val.data[1], sizeof(decoded));
					if (expected != decoded) {
						std::cout << "decoded: " << decoded << ", expected: " << expected << '\n';
						return ox::Error(1);
					}
					return ox::Error(0);
				};
				// signed positive
				oxAssert(check(encodeInteger(int64_t(1)), {0b000'0001'0}), "Encode 1 fail");
				oxAssert(check(encodeInteger(int64_t(2)), {0b000'0010'0}), "Encode 2 fail");
				oxAssert(check(encodeInteger(int64_t(3)), {0b000'0011'0}), "Encode 3 fail");
				oxAssert(check(encodeInteger(int64_t(4)), {0b000'0100'0}), "Encode 4 fail");
				oxAssert(check(encodeInteger(int64_t(64)), {0b00'0000'01, 0b1}), "Encode 64 fail");
				oxAssert(check(encodeInteger(int64_t(128)), {0b00'0000'01, 0b10}), "Encode 128 fail");
				oxAssert(check(encodeInteger(int64_t(129)), {0b00'0001'01, 0b10}), "Encode 129 fail");
				oxAssert(check(encodeInteger(int64_t(130)), {0b00'0010'01, 0b10}), "Encode 130 fail");
				oxAssert(check(encodeInteger(int64_t(131)), {0b00'0011'01, 0b10}), "Encode 131 fail");
				// signed negative
				oxAssert(check(encodeInteger(  int64_t(-1)), {0b111'1111'0}), "Encode -1 fail");
				oxAssert(check(encodeInteger(  int64_t(-2)), {0b111'1110'0}), "Encode -2 fail");
				oxAssert(check(encodeInteger(  int64_t(-3)), {0b111'1101'0}), "Encode -3 fail");
				oxAssert(check(encodeInteger(  int64_t(-4)), {0b111'1100'0}), "Encode -4 fail");
				oxAssert(check(encodeInteger( int64_t(-64)), {0b100'0000'0}), "Encode -64 fail");
				oxAssert(check(encodeInteger(int64_t(-128)), {0b00'0000'01, 0b11'1111'10}), "Encode -128 fail");
				oxAssert(check(encodeInteger(int64_t(-129)), {0b11'1111'01, 0b11'1111'01}), "Encode -129 fail");
				oxAssert(check(encodeInteger(int64_t(-130)), {0b11'1110'01, 0b11'1111'01}), "Encode -130 fail");
				oxAssert(check(encodeInteger(int64_t(-131)), {0b11'1101'01, 0b11'1111'01}), "Encode -131 fail");
				// unsigned
				oxAssert(check(encodeInteger(uint32_t(0xffffffff)), {0b11101111, 255, 255, 255, 0b00011111}), "Encode 0xffffffff fail");
				oxAssert(check(encodeInteger(uint64_t(1)), {0b0010}), "Encode 1 fail");
				oxAssert(check(encodeInteger(uint64_t(2)), {0b0100}), "Encode 2 fail");
				oxAssert(check(encodeInteger(uint64_t(3)), {0b0110}), "Encode 3 fail");
				oxAssert(check(encodeInteger(uint64_t(4)), {0b1000}), "Encode 4 fail");
				oxAssert(check(encodeInteger(uint64_t(64)), {0b1000'000'0}), "Encode 4 fail");
				oxAssert(check(encodeInteger(uint64_t(128)), {0b0001, 0b10}), "Encode 128 fail");
				oxAssert(check(encodeInteger(uint64_t(129)), {0b0101, 0b10}), "Encode 129 fail");
				oxAssert(check(encodeInteger(uint64_t(130)), {0b1001, 0b10}), "Encode 130 fail");
				oxAssert(check(encodeInteger(uint64_t(131)), {0b1101, 0b10}), "Encode 131 fail");
				// Signed check needs lambda templates to run correctly without
				// code deduplication
				oxAssert(check64(encodeInteger(MaxValue<int64_t>), MaxValue<int64_t>), "Encode MaxValue<int64_t> fail");
				oxAssert(check64(encodeInteger(MaxValue<uint64_t>), MaxValue<uint64_t>), "Encode MaxValue<uint64_t> fail");
				return ox::Error(0);
			}
		},

		{
			"decodeInteger",
			[] {
				using ox::MaxValue;
				using ox::mc::McInt;
				using ox::mc::encodeInteger;
				using ox::mc::decodeInteger;
				static constexpr auto check = [](auto val) {
					auto result = decodeInteger<decltype(val)>(encodeInteger(val));
					OX_RETURN_ERROR(result.error);
					if (result.value != val) {
						std::cout << "Bad value: " << result.value << ", expected: " << val << '\n';
						return ox::Error(1);
					}
					return ox::Error(0);
				};
				oxAssert(check(uint32_t(14)), "Decode of 14 failed.");
				oxAssert(check(int8_t(-1)), "Decode of -1 failed.");
				oxAssert(check(int16_t(-1)), "Decode of -1 failed.");
				oxAssert(check(int32_t(-1)), "Decode of -1 failed.");
				oxAssert(check(int64_t(-1)), "Decode of -1 failed.");
				oxAssert(check(int64_t(-2)), "Decode of -2 failed.");
				oxAssert(check(int64_t(-127)), "Decode of -127 failed.");
				oxAssert(check(int64_t(-128)), "Decode of -128 failed.");
				oxAssert(check(int64_t(-129)), "Decode of -129 failed.");
				oxAssert(check(int64_t(-129000)), "Decode of -129000 failed.");
				oxAssert(check(int64_t(1)), "Decode of 1 failed.");
				oxAssert(check(int64_t(2)), "Decode of 2 failed.");
				oxAssert(check(int64_t(42)), "Decode of 42 failed.");
				oxAssert(check(int64_t(130)), "Decode of 130 failed.");
				oxAssert(check(int64_t(131)), "Decode of 131 failed.");
				oxAssert(check(int64_t(131000)), "Decode of 131000 failed.");
				oxAssert(check(uint64_t(1)), "Decode of 1 failed.");
				oxAssert(check(uint64_t(2)), "Decode of 2 failed.");
				oxAssert(check(uint64_t(42)), "Decode of 42 failed.");
				oxAssert(check(uint64_t(130)), "Decode of 130 failed.");
				oxAssert(check(uint64_t(131)), "Decode of 131 failed.");
				oxAssert(check(0xffffffff), "Decode of 0xffffffff failed.");
				oxAssert(check(0xffffffffffff), "Decode of 0xffffffffffff failed.");
				oxAssert(check(0xffffffffffffffff), "Decode of U64 max failed.");
				return ox::Error(0);
			}
		},


		{
			"MetalClawModelValue",
			[] {
				static constexpr size_t dataBuffLen = ox::units::MB;
				ox::Buffer dataBuff(dataBuffLen);
				TestStruct testIn;
				testIn.Bool = true;
				testIn.Int = 42;
				testIn.IString = "Test String 1";
				testIn.List[0] = 1;
				testIn.List[1] = 2;
				testIn.List[2] = 3;
				testIn.List[3] = 4;
				testIn.Struct.Bool = true;
				testIn.Struct.Int = 300;
				testIn.Struct.IString = "Test String 2";
				testIn.unionIdx = 1;
				testIn.Union.Int = 93;
				oxAssert(ox::writeMC(dataBuff.data(), dataBuff.size(), testIn), "Data generation failed");
				ox::TypeStore typeStore;
				const auto [type, typeErr] = ox::buildTypeDef(typeStore, testIn);
				oxAssert(typeErr, "Descriptor write failed");
				ox::ModelObject testOut;
				OX_RETURN_ERROR(testOut.setType(type));
				oxAssert(ox::readMC(dataBuff, testOut), "Data read failed");
				oxAssert(testOut.at("Int").unwrap()->get<int>() == testIn.Int, "testOut.Int failed");
				oxAssert(testOut.at("Bool").unwrap()->get<bool>() == testIn.Bool, "testOut.Bool failed");
				oxAssert(testOut.at("IString").unwrap()->get<ox::String>() == testIn.IString.c_str(), "testOut.String failed");
				oxAssert(testOut.at("String").unwrap()->get<ox::String>() == testIn.String, "testOut.String failed");
				auto &testOutStruct = testOut.at("Struct").unwrap()->get<ox::ModelObject>();
				auto &testOutUnion = testOut.at("Union").unwrap()->get<ox::ModelUnion>();
				auto &testOutList = testOut.at("List").unwrap()->get<ox::ModelValueVector>();
				auto testOutStructCopy = testOut.at("Struct").unwrap()->get<ox::ModelObject>();
				auto testOutUnionCopy = testOut.at("Union").unwrap()->get<ox::ModelUnion>();
				auto testOutListCopy = testOut.at("List").unwrap()->get<ox::ModelValueVector>();
				oxAssert(testOutStruct.typeName() == TestStructNest::TypeName, "ModelObject TypeName failed");
				oxAssert(testOutStruct.typeVersion() == TestStructNest::TypeVersion, "ModelObject TypeVersion failed");
				oxAssert(testOutStruct.at("Bool").unwrap()->get<bool>() == testIn.Struct.Bool, "testOut.Struct.Bool failed");
				oxAssert(testOutStruct.at("IString").unwrap()->get<ox::String>() == testIn.Struct.IString.c_str(), "testOut.Struct.IString failed");
				oxAssert(testOut.at("unionIdx").unwrap()->get<int>() == testIn.unionIdx, "testOut.unionIdx failed");
				oxAssert(testOutUnion.unionIdx() == testIn.unionIdx, "testOut.Union idx wrong");
				oxAssert(testOutUnion.at("Int").unwrap()->get<uint32_t>() == testIn.Union.Int, "testOut.Union.Int failed");
				oxAssert(testOutList[0].get<uint32_t>() == testIn.List[0], "testOut.List[0] failed");
				oxAssert(testOutList[1].get<uint32_t>() == testIn.List[1], "testOut.Struct.List[1] failed");
				oxAssert(testOutStructCopy.at("Bool").unwrap()->get<bool>() == testIn.Struct.Bool, "testOut.Struct.Bool (copy) failed");
				oxAssert(testOutStructCopy.at("IString").unwrap()->get<ox::String>() == testIn.Struct.IString.c_str(), "testOut.Struct.IString (copy) failed");
				oxAssert(testOutListCopy[0].get<uint32_t>() == testIn.List[0], "testOut.Struct.List[0] (copy) failed");
				oxAssert(testOutListCopy[1].get<uint32_t>() == testIn.List[1], "testOut.Struct.List[1] (copy) failed");
				return ox::Error(0);
			}
		},

		{
			"MetalClawDef",
			[] {
				 //constexpr size_t descBuffLen = 1024;
				 //uint8_t descBuff[descBuffLen];
				 static constexpr size_t dataBuffLen = ox::units::MB;
				 char dataBuff[dataBuffLen];
				 TestStruct testIn, testOut;
				 testIn.Bool = true;
				 testIn.Int = 42;
				 testIn.IString = "Test String 1";
				 testIn.List[0] = 1;
				 testIn.List[1] = 2;
				 testIn.List[2] = 3;
				 testIn.List[3] = 4;
				 testIn.Struct.Bool = false;
				 testIn.Struct.Int = 300;
				 testIn.Struct.IString = "Test String 2";
				 oxAssert(ox::writeMC(dataBuff, dataBuffLen, testIn), "Data generation failed");
				 ox::TypeStore typeStore;
				 const auto [type, typeErr] = ox::buildTypeDef(typeStore, testIn);
				 oxAssert(typeErr, "Descriptor write failed");
				 ox::BufferReader br({dataBuff, dataBuffLen});
				 return ox::walkModel<ox::MetalClawReader>(type, br,
					[](const ox::Vector<ox::FieldName>&, const ox::Vector<ox::String>&, const ox::DescriptorField &f, ox::MetalClawReader *rdr) -> ox::Error {
						//std::cout << f.fieldName.c_str() << '\n';
						auto fieldName = f.fieldName.c_str();
						switch (f.type->primitiveType) {
							case ox::PrimitiveType::UnsignedInteger:
							  std::cout << fieldName << ":\tuint" << f.type->length * 8 << "_t:\t";
							  switch (f.type->length) {
									case 1: {
										uint8_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
									case 2: {
										uint16_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
									case 4: {
										uint32_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
									case 8: {
										uint64_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
								}
								std::cout << '\n';
								break;
							case ox::PrimitiveType::SignedInteger:
								std::cout << fieldName << ":\tint" << f.type->length * 8 << "_t:\t";
								switch (f.type->length) {
									case 1: {
										int8_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
									case 2: {
										int16_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
									case 4: {
										int32_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
									case 8: {
										int64_t i = {};
										oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
										std::cout << i;
										break;
									}
								}
								std::cout << '\n';
								break;
							case ox::PrimitiveType::Bool: {
								bool i = {};
								oxAssert(rdr->field(fieldName, &i), "Walking model failed.");
								std::cout << fieldName << ":\t" << "bool:\t\t" << (i ? "true" : "false") << '\n';
								break;
							}
							case ox::PrimitiveType::String: {
								ox::String s;
								//std::cout << rdr->stringLength() << '\n';
								oxAssert(rdr->field(fieldName, &s), "Walking model failed.");
								oxOutf("{}:\tstring:\t\t{}\n", fieldName, s);
								break;
							}
							case ox::PrimitiveType::Struct:
								break;
							case ox::PrimitiveType::Union:
								break;
						}
						return ox::Error(0);
					}
				);
			}
		},
	}
};

int main(int argc, const char **args) {
	if (argc < 2) {
		oxError("Must specify test to run");
	}
	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;
}
