extern Zone *zone;

#include <cereal/archives/json.hpp>
#include <cereal/types/map.hpp>
#include "../../common/repositories/npc_types_repository.h"
#include "../../corpse.h"
#include "../../../common/repositories/respawn_times_repository.h"

inline void ClearState()
{
	ZoneStateSpawnsRepository::DeleteWhere(database, "zone_id = 32 and instance_id = 0");
}

inline std::vector<ZoneStateSpawnsRepository::ZoneStateSpawns> GetStateSpawns()
{
	return ZoneStateSpawnsRepository::GetWhere(database, "zone_id = 32 and instance_id = 0");
}

inline void PrintEntityCounts()
{
	std::cout << " NPC count: " << entity_list.GetNPCList().size() << std::endl;
	std::cout << " Corpse count: " << entity_list.GetCorpseList().size() << std::endl;
}

inline void PrintZoneNpcs()
{
	int       npc_count = 0;
	for (auto &npc: entity_list.GetNPCList()) {
		std::cout << npc.second->GetNPCTypeID() << " " << npc.second->GetCleanName() << std::endl;
		npc_count++;
	}

	std::cout << "Total spawned NPCs: " << npc_count << std::endl;
}

inline void SetupStateZone()
{
	SetupZone("soldungb");
	zone->Process();
	// depop the zone controller
	auto controller = entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID);
	if (controller != nullptr) {
		controller->Depop();
	}

	entity_list.MobProcess(); // process the depop
}

inline int GetStateSpawnSpawn2Count()
{
	int       count = 0;
	for (auto &e: GetStateSpawns()) {
		if (e.spawn2_id > 0) {
			count++;
		}
	}
	return count;
}

inline uint32_t SeedLootTable()
{
	const std::string table_name = "zone_state_test";

	auto entries = LoottableRepository::GetWhere(database, fmt::format("name = '{}'", table_name));
	if (!entries.empty()) {
		zone->LoadLootTable(entries[0].id);
		return entries[0].id;
	}

	// seed the loot table
	auto loot_table = LoottableRepository::NewEntity();
	loot_table.name = table_name;
	auto inserted = LoottableRepository::InsertOne(database, loot_table);

	auto loot_drop = LootdropRepository::NewEntity();
	loot_drop.name = table_name;
	auto inserted_loot_drop = LootdropRepository::InsertOne(database, loot_drop);

	auto loot_table_entries = LoottableEntriesRepository::NewEntity();
	loot_table_entries.lootdrop_id  = inserted_loot_drop.id;
	loot_table_entries.loottable_id = inserted.id;
	LoottableEntriesRepository::InsertOne(database, loot_table_entries);

	auto loot_drop_entries = LootdropEntriesRepository::NewEntity();
	loot_drop_entries.lootdrop_id = inserted_loot_drop.id;
	loot_drop_entries.item_id     = 11621; // cloak of flames
	loot_drop_entries.chance      = 100;
	LootdropEntriesRepository::InsertOne(database, loot_drop_entries);

	zone->LoadLootTable(inserted.id);

	return inserted.id;
}

inline std::map<std::string, std::string> GetVariablesDeserialized(const std::string &entity_variables)
{
	std::map<std::string, std::string> deserialized_map;

	if (entity_variables.empty()) {
		return deserialized_map;
	}

	if (!Strings::IsValidJson(entity_variables)) {
		LogZoneState("Invalid JSON data for entity variables");
		return deserialized_map;
	}

	try {
		std::stringstream ss;
		{
			ss << entity_variables;
			cereal::JSONInputArchive ar(ss);
			ar(deserialized_map);
		}
	} catch (const std::exception &e) {
		LogZoneState("Failed to load entity variables [{}]", e.what());
	}

	return deserialized_map;
}

// MatchState compares the NPC to the state
// it should print what doesn't match when it returns false
inline bool MatchState(NPC *n, const ZoneStateSpawnsRepository::ZoneStateSpawns &state)
{
	if (n->GetNPCTypeID() != state.npc_id) {
		std::cout << "NPC ID mismatch: " << n->GetNPCTypeID() << " != " << state.npc_id << std::endl;
		return false;
	}

	if (n->GetX() != state.x) {
		std::cout << "X mismatch: " << n->GetX() << " != " << state.x << std::endl;
		return false;
	}

	if (n->GetY() != state.y) {
		std::cout << "Y mismatch: " << n->GetY() << " != " << state.y << std::endl;
		return false;
	}

	if (n->GetZ() != state.z) {
		std::cout << "Z mismatch: " << n->GetZ() << " != " << state.z << std::endl;
		return false;
	}

	if (n->GetHeading() != state.heading) {
		std::cout << "Heading mismatch: " << n->GetHeading() << " != " << state.heading << std::endl;
		return false;
	}

	if (n->GetHP() != state.hp) {
		std::cout << "HP mismatch: " << n->GetHP() << " != " << state.hp << std::endl;
		return false;
	}

	if (n->GetMana() != state.mana) {
		std::cout << "Mana mismatch: " << n->GetMana() << " != " << state.mana << std::endl;
		return false;
	}

	if (n->GetEndurance() != state.endurance) {
		std::cout << "Endurance mismatch: " << n->GetEndurance() << " != " << state.endurance << std::endl;
		return false;
	}

	auto entity_variables = GetVariablesDeserialized(state.entity_variables);

	for (const auto &[key, value]: entity_variables) {
		if (n->GetEntityVariable(key) != value) {
			std::cout << "Variable mismatch: " << key << " " << n->GetEntityVariable(key) << " != " << value
					  << std::endl;
			return false;
		}
	}

	return true;
}

// MatchFreshlySpawnedState compares the NPC to the state
// it should print what doesn't match when it returns false
inline bool MatchFreshlySpawnedState(NPC *n, const ZoneStateSpawnsRepository::ZoneStateSpawns &state)
{
	auto npc_type = content_db.LoadNPCTypesData(n->GetNPCTypeID());
	if (!npc_type) {
		RunTest("NPC type does not exist", false, true);
	}

	if (n->GetX() != state.x) {
		std::cout << "X mismatch: " << n->GetX() << " != " << state.x << std::endl;
		return false;
	}

	if (n->GetY() != state.y) {
		std::cout << "Y mismatch: " << n->GetY() << " != " << state.y << std::endl;
		return false;
	}

//	if (n->GetZ() != state.z) {
//		std::cout << "Z mismatch: " << n->GetZ() << " != " << state.z << std::endl;
//		return false;
//	}

	if (n->GetHeading() != state.heading) {
		std::cout << "Heading mismatch: " << n->GetHeading() << " != " << state.heading << std::endl;
		return false;
	}

	if (n->GetHP() != npc_type->max_hp) {
		std::cout << "HP mismatch: " << n->GetHP() << " != " << npc_type->max_hp << std::endl;
		return false;
	}

	if (n->GetLoottableID() != npc_type->loottable_id) {
		std::cout << "Loot table mismatch: " << n->GetLoottableID() << " != " << npc_type->loottable_id << std::endl;
		return false;
	}

	// not loaded from database
//	if (n->GetMana() != npc_type->Mana) {
//		std::cout << "Mana mismatch: " << n->GetMana() << " != " << npc_type->Mana << std::endl;
//		return false;
//	}

	// endurance not saved

	auto entity_variables = GetVariablesDeserialized(state.entity_variables);

	for (const auto &[key, value]: entity_variables) {
		if (n->GetEntityVariable(key) != value) {
			std::cout << "Variable mismatch: " << key << " " << n->GetEntityVariable(key) << " != " << value
					  << std::endl;
			return false;
		}
	}

	return true;
}

inline bool MatchCorpseState(Corpse *c, const ZoneStateSpawnsRepository::ZoneStateSpawns &state)
{
	if (c->GetNPCTypeID() != state.npc_id) {
		std::cout << "Corpse NPC ID mismatch: " << c->GetNPCTypeID() << " != " << state.npc_id << std::endl;
		return false;
	}

	if (c->GetX() != state.x) {
		std::cout << "Corpse X mismatch: " << c->GetX() << " != " << state.x << std::endl;
		return false;
	}

	if (c->GetY() != state.y) {
		std::cout << "Corpse Y mismatch: " << c->GetY() << " != " << state.y << std::endl;
		return false;
	}

//	if (c->GetZ() != state.z) {
//		std::cout << "Corpse Z mismatch: " << c->GetZ() << " != " << state.z << std::endl;
//		return false;
//	}

	if (c->GetHeading() != state.heading) {
		std::cout << "Corpse Heading mismatch: " << c->GetHeading() << " != " << state.heading << std::endl;
		return false;
	}

	if (c->GetDecayTime() != state.decay_in_seconds * 1000) {
		std::cout << "Corpse Decay Time mismatch: " << c->GetDecayTime() << " != " << state.decay_in_seconds * 1000
				  << std::endl;
		return false;
	}

	auto entity_variables = GetVariablesDeserialized(state.entity_variables);

	for (const auto &[key, value]: entity_variables) {
		if (c->GetEntityVariable(key) != value) {
			std::cout << "Corpse Variable mismatch: " << key << " " << c->GetEntityVariable(key) << " != " << value
					  << std::endl;
			return false;
		}
	}

	return true;
}

inline void TestSpawns()
{
	zone->Shutdown();
	ClearState();
	SetupStateZone();
	zone->Repop(true);
	entity_list.MobProcess();

//	PrintZoneNpcs();

	RunTest("Spawns > Ensure no state spawns exist before shutdown", 0, (int) GetStateSpawns().size());

	zone->Shutdown();

	auto entries = GetStateSpawns().size();
	RunTest(fmt::format("Spawns > State exists after shutdown, entries ({})", entries), true, entries > 0);

	SetupStateZone();

	entries = GetStateSpawns().size();
	RunTest(fmt::format("Spawns > State exists after bootup, entries ({})", entries), true, entries > 0);

	zone->Shutdown();
	SetupStateZone();

	entries = GetStateSpawns().size();
	RunTest(
		fmt::format("Spawns > State is the same after shutdown/bootup (2nd time), entries ({})", entries),
		true,
		entries > 0
	);

	// need to compare the state spawns to the actual spawns
	bool all_exist = true;

	for (auto &state_spawn: GetStateSpawns()) {
		auto npc = entity_list.GetNPCByNPCTypeID(state_spawn.npc_id);
		if (!npc) {
			all_exist = false;
			break;
		}
	}

	RunTest("Spawns > All state spawns (Spawn2) exist in entity list", true, all_exist);
	RunTest(
		fmt::format("Spawns > All state spawns (Spawn2) exist in entity list, count [{}]", GetStateSpawns().size()),
		true,
		GetStateSpawns().size() == entity_list.GetNPCList().size()
	);
	RunTest(
		fmt::format(
			"Spawns > Same count of state spawns and entity list | state spawns [{}] entity list [{}]",
			GetStateSpawns().size(),
			entity_list.GetNPCList().size()),
		true,
		GetStateSpawns().size() == entity_list.GetNPCList().size()
	);

	// kill all NPC's
	std::vector<NPC *> npcs_to_kill;

	// Collect NPCs first
	for (const auto &e: entity_list.GetNPCList()) {
		if (e.second) {
			npcs_to_kill.push_back(e.second);
		}
	}

	// Now safely process them
	for (auto *npc: npcs_to_kill) {
		npc->SetQueuedToCorpse();
		npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand);
	}

	bool condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == entries;
	RunTest(
		fmt::format("Spawns > All NPC's killed (0 NPCs) ([{}] Corpses)", entries),
		true,
		condition
	);

	std::vector<uint32_t> spawn2_ids = {};

	for (auto &e: GetStateSpawns()) {
		if (e.spawn2_id > 0) {
			spawn2_ids.push_back(e.spawn2_id);
		}
	}

	auto times = RespawnTimesRepository::GetWhere(
		database,
		fmt::format("id IN ({})", Strings::Join(spawn2_ids, ","))
	);

	for (auto &e: times) {
		e.duration = 1;
		e.start    = std::time(nullptr) + 1;
	}
	RespawnTimesRepository::ReplaceMany(database, times);

	zone->Shutdown();
	SetupStateZone();

	condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == entries;
	if (!condition) {
		PrintEntityCounts();
	}
	RunTest(
		fmt::format("Spawns > After restore (0 NPCs) ([{}] Corpses)", entries),
		true,
		condition
	);

	for (auto &e: entity_list.GetCorpseList()) {
		auto c = e.second;

		for (auto &s: GetStateSpawns()) {
			bool is_same_corpse =
					 c->GetNPCTypeID() == s.npc_id &&
					 c->GetX() == s.x &&
					 c->GetY() == s.y &&
					 c->GetHeading() == s.heading;

			if (is_same_corpse) {
				if (!MatchCorpseState(c, s)) {
					RunTest("Spawns > Corpse state matches state", true, false);
				}
			}
		}
	}

	Timer::RollForward(5);

	zone->Process();

	condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == entries;
	if (!condition) {
		PrintEntityCounts();
	}
	RunTest(
		fmt::format("Spawns > After respawn ([{}] NPCs) ([{}] Corpses)", entries, entries),
		true,
		condition
	);

	for (auto &c: entity_list.GetCorpseList()) {
		c.second->DepopNPCCorpse();
	}

	entity_list.CorpseProcess();

	condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == 0;
	if (!condition) {
		PrintEntityCounts();
	}
	RunTest(
		fmt::format("Spawns > After respawn ([{}] NPCs) (0 Corpses)", entries),
		true,
		condition
	);

	// lets set NPC's up with a predictable loottable for testing
	uint32_t loottable_id = SeedLootTable();

	for (auto &e: entity_list.GetNPCList()) {
		auto n = e.second;
		n->ClearLootItems();
		n->SetResumedFromZoneSuspend(false);
		n->AddLootTable(loottable_id);
		n->SetResumedFromZoneSuspend(true);
	}

	RespawnTimesRepository::DeleteWhere(database, fmt::format("id IN ({})", Strings::Join(spawn2_ids, ",")));

	zone->Shutdown();
	SetupStateZone();

	npcs_to_kill = {};

	// kill only 10 NPCs
	int i = 0;

	for (const auto &e: entity_list.GetNPCList()) {
		if (e.second) {
			npcs_to_kill.push_back(e.second);
			i++;
		}
		if (i == 10) {
			break;
		}
	}

	for (auto *npc: npcs_to_kill) {
		if (!npc) {
			continue;
		}
		npc->SetQueuedToCorpse();
		npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand);
	}

	condition = (int) entity_list.GetNPCList().size() == (entries - 10) && (int) entity_list.GetCorpseList().size() == 10;
	if (!condition) {
		PrintEntityCounts();
	}
	RunTest(
		fmt::format("Spawns > Kill 10 NPC's before save/restore ([{}] NPCs) (10 Corpses)", (entries - 10)),
		true,
		condition
	);

	zone->Shutdown();
	SetupStateZone();

	condition = (int) entity_list.GetNPCList().size() == (entries - 10) && (int) entity_list.GetCorpseList().size() == 10;
	if (!condition) {
		PrintEntityCounts();
	}
	RunTest(
		fmt::format("Spawns > After restore ([{}] NPCs) (10 Corpses)", (entries - 10)),
		true,
		condition
	);

	// validate that all corpses and npc's have cloak of flames
	bool      test_failed = false;
	for (auto &e: entity_list.GetCorpseList()) {
		auto corpse = e.second;
		if (!corpse) {
			continue;
		}

		bool has_item = corpse->HasItem(11621);
		if (!has_item || corpse->CountItem(11621) != 1) {
			std::cout << "Corpse does not have item" << std::endl;
			std::cout << " -- CountItem(11621) " << corpse->CountItem(11621) << std::endl;
			std::cout << " -- GetCleanName " << corpse->GetCleanName() << std::endl;
			RunTest(
				"Spawns > After restore > Corpse has item (cloak of flames) item count (1) (no dupes)",
				true,
				has_item
			);
		}
	}

	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (!npc) {
			continue;
		}

		bool has_item = npc->HasItem(11621);
		if (!has_item && npc->CountItem(11621) == 1) {
			std::cout << "NPC does not have item" << std::endl;
			std::cout << " -- CountItem(11621) " << npc->CountItem(11621) << std::endl;
			std::cout << " -- GetCleanName " << npc->GetCleanName() << std::endl;
			RunTest(
				"Spawns > After restore > NPC has item (cloak of flames) item count (1) (no dupes)",
				true,
				has_item
			);
		}
	}

	RunTest(
		"Spawns > After restore > All npcs/corpses have item (cloak of flames) item count (1) and no dupes",
		true,
		true
	);

	// test to make sure the entity variables match what was on state
	int npcs_matching_state = 0;

	for (auto &e: entity_list.GetNPCList()) {
		auto      npc = e.second;
		for (auto &s: GetStateSpawns()) {
			if (npc->GetSpawnGroupId() == s.spawngroup_id && npc->GetSpawn()->GetID() == s.spawn2_id) {
				if (!MatchState(npc, s)) {
					RunTest("Spawns > NPC state matches state", true, false);
				}
				npcs_matching_state++;
			}
		}
	}

	RunTest(
		fmt::format("Spawns > After restore > NPC(s) matching state | matching ({})", npcs_matching_state),
		false,
		false
	);

	// test to make sure the entity variables match what was on state
	int corpses_matching_state = 0;

	for (auto &e: entity_list.GetCorpseList()) {
		auto c = e.second;

		for (auto &s: GetStateSpawns()) {
			bool is_same_corpse =
					 c->GetNPCTypeID() == s.npc_id &&
					 c->GetX() == s.x &&
					 c->GetY() == s.y &&
					 c->GetHeading() == s.heading;

			if (is_same_corpse) {
				if (!MatchCorpseState(c, s)) {
					RunTest("Spawns > Corpse state matches state", true, false);
				}
				corpses_matching_state++;
			}
		}
	}

	RunTest(
		fmt::format("Spawns > After restore > Corpse(s) matching state | matching ({})", corpses_matching_state),
		false,
		false
	);

	int max_respawn = 0;
	const auto& l = RespawnTimesRepository::All(database);
	for (const auto& e : l) {
		if (e.duration > max_respawn) {
			max_respawn = e.duration;
		}
	}

	entity_list.MobProcess();

	zone->Process();
	zone->ClearSpawnTimers();
	entity_list.MobProcess();

	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (npc->GetSpawnGroupId() == 0) {
			continue;
		}

		npc->SetEntityVariable("previously_spawned", "true");
	}

	Timer::RollForward(max_respawn); // longest respawn time in zone
	zone->Process();
	entity_list.MobProcess(); // processing depops

	condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == 10;
	if (!condition) {
		PrintEntityCounts();
		PrintZoneNpcs();
	}
	RunTest(
		fmt::format("Spawns > After respawn, ensure we have expected entity counts ([{}] NPCs) (10 Corpses)", entries),
		true,
		condition
	);

	entity_list.MobProcess(); // processing depops

	npcs_matching_state = 0;
	int dropped_items = 0;

	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;

		if (npc->GetEntityVariable("previously_spawned") != "true") {
			for (auto &s: GetStateSpawns()) {
				if (npc->GetSpawnGroupId() == s.spawngroup_id && npc->GetSpawn()->GetID() == s.spawn2_id &&
					s.npc_id == 0) {
					if (!MatchFreshlySpawnedState(npc, s)) {
						RunTest("Spawns > NPC state matches state", true, false);
					}

					dropped_items += npc->GetLootList().size();
					npcs_matching_state++;
				}
			}
		}
	}

	RunTest(
		fmt::format(
			"Spawns > After restore, after respawn (10 killed) > ensure NPC(s) match state | matching ({})",
			npcs_matching_state
		),
		npcs_matching_state == 10,
		true
	);

	RunTest(
		fmt::format(
			"Spawns > After restore, after respawn (10 killed) ensure NPC's have loot assigned | dropped items ({})",
			dropped_items
		),
		dropped_items > 0,
		true
	);
}

inline void TestZoneVariables()
{
	std::vector<std::pair<std::string, std::string>> test_variables = {
		{"test_variable",  "test_value"},
		{"test_variable2", "test_value2"}
	};

	// Set variables
	for (const auto &[key, value]: test_variables) {
		zone->SetVariable(key, value);
		RunTest("Zone variables > (" + key + ") set", value, zone->GetVariable(key));
	}

	// Simulate shutdown and restart twice
	for (int i = 1; i <= 2; ++i) {
		zone->Shutdown();
		SetupStateZone();

		for (const auto &[key, value]: test_variables) {
			RunTest(
				"Zone variables > (" + key + ") persists after shutdown/bootup (" + std::to_string(i) + ")",
				value,
				zone->GetVariable(key)
			);
		}
	}

	// Delete one variable
	zone->DeleteVariable("test_variable");

	RunTest("Zone variables > (test_variable) delete is empty", "", zone->GetVariable("test_variable"));
	RunTest(
		"Zone variables > (test_variable2) delete second one still exists",
		"test_value2",
		zone->GetVariable("test_variable2")
	);

	// Final shutdown and restart check
	zone->Shutdown();
	SetupStateZone();

	for (const auto &[key, value]: test_variables) {
		std::string expected_value = (key == "test_variable") ? "" : value;
		RunTest("Zone variables > (" + key + ") after shutdown/bootup", expected_value, zone->GetVariable(key));
	}
}

inline void TestHpManaEnd()
{
	std::vector<uint32_t> ids = {};

	for (auto &npc: entity_list.GetNPCList()) {
		ids.push_back(npc.second->GetNPCTypeID());
	}

	auto npc_types = NpcTypesRepository::GetWhere(database, fmt::format(" id IN ({})", Strings::Join(ids, ",")));

	// validate that hp / mana / end equal that of what's on the npc types row for all rows
	// dont run tests in the loop, just collect the data
	std::vector<std::pair<uint32_t, bool>> hp_mismatch   = {};
	std::vector<std::pair<uint32_t, bool>> mana_mismatch = {};

	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;

		for (auto &npc_type: npc_types) {
			if (npc->GetNPCTypeID() != npc_type.id) {
				continue;
			}

			// we need to make sure the values are not 0 because of auto scaling
			if (npc->GetHP() != npc_type.hp && npc_type.hp > 0) {
				hp_mismatch.emplace_back(npc->GetNPCTypeID(), true);
			}
			if (npc->GetMana() != npc_type.mana && npc_type.mana > 0) {
				mana_mismatch.emplace_back(npc->GetNPCTypeID(), true);
			}
		}
	}

	RunTest(
		"HP/Mana/End Save/Restore > Ensure default HP state matches data in npc_types row",
		0,
		(int) hp_mismatch.size()
	);

	RunTest(
		"HP/Mana/End Save/Restore > Ensure default Mana state matches data in npc_types row",
		0,
		(int) mana_mismatch.size()
	);

	// do damage to NPC's and make sure they restore to their original values
	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		npc->SetHP(1);
		npc->SetMana(1);
	}

	zone->Shutdown();
	SetupStateZone();

	hp_mismatch.clear();
	mana_mismatch.clear();

	// compare state values versus what's on the entity list NPC object
	for (auto &e: entity_list.GetNPCList()) {
		auto      npc = e.second;
		for (auto &state: GetStateSpawns()) {
			if (npc->GetNPCTypeID() != state.npc_id) {
				continue;
			}

			// we need to make sure the values are not 0 because of auto scaling
			if (npc->GetHP() != state.hp && state.hp > 0) {
				hp_mismatch.emplace_back(npc->GetNPCTypeID(), true);
			}
			if (npc->GetMana() != state.mana && state.mana > 0) {
				mana_mismatch.emplace_back(npc->GetNPCTypeID(), true);
			}
		}
	}

	RunTest(
		"HP/Mana/End Save/Restore > Ensure restored HP state matches data on the NPC object",
		0,
		(int) hp_mismatch.size()
	);
	RunTest(
		"HP/Mana/End Save/Restore > Ensure restored Mana state matches data on the NPC object",
		0,
		(int) mana_mismatch.size()
	);
}

inline void TestBuffs()
{
	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (npc->GetNPCTypeID() == 0) {
			continue;
		}

		npc->CastSpell(6824, npc->GetID(), (EQ::spells::CastingSlot) 0, 0, 0);
	}

	zone->Shutdown();
	SetupStateZone();

	// Check buffs
	bool missing_buffs = false;

	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (npc->GetNPCTypeID() == 0) {
			continue;
		}

		if (!npc->FindBuff(6824)) {
			missing_buffs = true;
			break;
		}
	}

	RunTest("Buffs > Persist after shutdown/bootup", false, missing_buffs);
}

inline void TestZLocationDrift()
{
	zone->Shutdown();
	ClearState();
	SetupStateZone();

	auto b = GetStateSpawns();

	for (int i = 0; i < 10; ++i) {
		zone->Shutdown();
		SetupStateZone();
	}

	auto a = GetStateSpawns();

	// compare entries_before x/y/z to entries_after x/y/z
	bool locations_different = false;

	for (size_t i = 0; i < b.size(); ++i) {
		if (b[i].x != a[i].x || b[i].y != a[i].y || b[i].z != a[i].z) {
			locations_different = true;

			std::cout << "Location drift detected for NPC ID: " << b[i].npc_id << std::endl;
			std::cout << "Location drift detected for NPC ID: " << a[i].npc_id << std::endl;
			std::cout << "Before - X: " << b[i].x << ", Y: " << b[i].y << ", Z: " << b[i].z << std::endl;
			std::cout << "After  - X: " << a[i].x << ", Y: " << a[i].y << ", Z: " << a[i].z << std::endl;
			break;
		}
	}

	RunTest(
		"Z Location Drift > Ensure Z location does not drift after multiple shutdowns/bootups",
		false,
		locations_different
	);
}

inline void TestLocationChange()
{
	// Test that state spawns are where we moved them to if we moved them, move them slightly in a predictable way so
	// we can test for it after restore
	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (!npc) {
			continue;
		}

		// set all to -870	-1394	106.58 (nagafen area)
		npc->SetPosition(-870, -1394, 106);
	}

	zone->Shutdown();
	SetupStateZone();

	bool all_moved = true;

	for (auto &e: entity_list.GetNPCList()) {
		for (auto &state: GetStateSpawns()) {
			if (e.second->GetNPCTypeID() != state.npc_id) {
				continue;
			}

			auto n = e.second;
			if (n->GetSpawnGroupId() != state.spawngroup_id && n->GetSpawn()->GetID() != state.spawn2_id) {
				continue;
			}

			// z gets auto adjusted to the ground, so we dont need to check it
			if (n->GetX() != state.x || n->GetY() != state.y) {
				std::cout << "NPC ID: " << n->GetNPCTypeID() << " X: " << n->GetX() << " Y: " << n->GetY() << std::endl;
				std::cout << "State ID " << state.npc_id << " X: " << state.x << " Y: " << state.y << std::endl;
				std::cout << "-----------------------------------\n";
				all_moved = false;
				break;
			}
			else {
//				std::cout << "NPC ID: " << e.second->GetNPCTypeID() << " X: " << e.second->GetX() << " Y: " << e.second->GetY() << std::endl;
//				std::cout << "State ID " << state.npc_id << " X: " << state.x << " Y: " << state.y << std::endl;
//				std::cout << "-----------------------------------\n";
			}
		}
	}

	RunTest("Location > State spawns are where we moved them to after restore", true, all_moved);
}

inline void TestEntityVariables()
{
	std::map<std::string, std::string> test_entity_variables = {
		{"test_entity_variable",  "test_entity_value"},
		{"test_entity_variable2", "test_entity_value2"}
	};

	// Set entity variables
	for (const auto &[key, value]: test_entity_variables) {
		for (auto &e: entity_list.GetNPCList()) {
			e.second->SetEntityVariable(key, value);
		}
	}

	zone->Shutdown();
	SetupStateZone();

	// Check entity variables
	bool missing_entity_variables = false;
	for (const auto &[key, value]: test_entity_variables) {
		for (auto &e: entity_list.GetNPCList()) {
			if (e.second->GetEntityVariable(key) != value) {
				missing_entity_variables = true;
				break;
			}
		}
	}

	RunTest("Entity Variables > Persist after shutdown/bootup", false, missing_entity_variables);
}

inline void TestLoot()
{
	uint32_t table_id = SeedLootTable();

	for (auto &e: entity_list.GetNPCList()) {
		e.second->ClearLootItems();
		e.second->SetResumedFromZoneSuspend(false);
		e.second->AddLootTable(table_id);
		e.second->SetResumedFromZoneSuspend(true);
	}

	bool missing_loot = false;

	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (npc->GetNPCTypeID() == 0) {
			continue;
		}

		// cloak of flames
		if (npc->CountItem(11621) == 0) {
			missing_loot = true;
			break;
		}
	}

	RunTest("Loot > Cloak of Flames added to all NPC's via Loottable before shutdown", false, missing_loot);

	zone->Shutdown();
	SetupStateZone();

	missing_loot = false;
	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (npc->GetNPCTypeID() == 0) {
			continue;
		}

		// cloak of flames
		if (npc->CountItem(11621) == 0) {
			missing_loot = true;
			break;
		}
	}

	RunTest("Loot > Cloak of Flames added to all NPC's via Loottable after shutdown/bootup", false, missing_loot);

	// make sure no duplicates are added
	bool duplicates = false;

	for (auto &e: entity_list.GetNPCList()) {
		auto npc = e.second;
		if (npc->GetNPCTypeID() == 0) {
			continue;
		}

		if (npc->CountItem(11621) > 1) {
			duplicates = true;
			break;
		}
	}

	RunTest("Loot > No duplicates added when adding item to NPC", false, duplicates);

	// kill all NPC's
	std::vector<NPC *> npcs_to_kill;

	// Collect NPCs first
	for (const auto &e: entity_list.GetNPCList()) {
		if (e.second) {
			npcs_to_kill.push_back(e.second);
		}
	}

	// Now safely process them
	for (auto *npc: npcs_to_kill) {
		npc->SetQueuedToCorpse();
		npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand);
	}

	// make sure all of the corpses have "Cloak of Flames"
	bool missing_loot_corpse = false;

	for (auto &e: entity_list.GetCorpseList()) {
		auto corpse = e.second;
		if (corpse->GetNPCTypeID() == 0) {
			continue;
		}

		if (corpse->CountItem(11621) == 0) {
			missing_loot_corpse = true;
			break;
		}
	}

	RunTest(
		"Loot > Cloak of Flames added to all Corpse's via Loottable before shutdown/bootup",
		false,
		missing_loot_corpse
	);

	zone->Shutdown();
	SetupStateZone();

	missing_loot_corpse = false;
	for (auto &e: entity_list.GetCorpseList()) {
		auto corpse = e.second;
		if (corpse->GetNPCTypeID() == 0) {
			continue;
		}

		if (corpse->CountItem(11621) == 0) {
			missing_loot_corpse = true;
			break;
		}
	}

	RunTest(
		"Loot > Cloak of Flames added to all Corpse's via Loottable after shutdown/bootup",
		false,
		missing_loot_corpse
	);

	// make sure no duplicates are added
	bool duplicates_corpse = false;

	for (auto &e: entity_list.GetCorpseList()) {
		auto corpse = e.second;
		if (corpse->GetNPCTypeID() == 0) {
			continue;
		}

		if (corpse->CountItem(11621) > 1) {
			duplicates_corpse = true;
			break;
		}
	}

	RunTest("Loot > No duplicates added when adding item to Corpse", false, duplicates_corpse);
}

void ZoneCLI::TestZoneState(int argc, char **argv, argh::parser &cmd, std::string &description)
{
	if (cmd[{"-h", "--help"}]) {
		return;
	}

	ClearState(); // clean slate
	SetupStateZone();
	zone->Repop(true);

	std::cout << "===========================================\n";
	std::cout << "⚙\uFE0F> Running Zone State Tests... (soldungb)\n";
	std::cout << "===========================================\n\n";

	TestZoneVariables();
	TestHpManaEnd();
	TestBuffs();
	TestLocationChange();
	TestEntityVariables();
	TestLoot();
	TestSpawns();
	TestZLocationDrift();

	std::cout << "\n===========================================\n";
	std::cout << "✅ All Zone State Tests Completed!\n";
	std::cout << "===========================================\n";
}
