// Copyright Epic Games, Inc. All Rights Reserved.

#include "AgrarianEditorAutomationLibrary.h"

#include "AgrarianBuildingPlacementComponent.h"
#include "AgrarianCraftingComponent.h"
#include "AgrarianGameCharacter.h"
#include "AgrarianGameState.h"
#include "AgrarianInteractable.h"
#include "AgrarianInventoryComponent.h"
#include "AgrarianPersistentActorComponent.h"
#include "AgrarianPersistenceSubsystem.h"
#include "AgrarianRecipeDataAsset.h"
#include "AgrarianResourceNode.h"
#include "AgrarianSaveGame.h"
#include "AgrarianWildlifeBase.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/FileHelper.h"

#if WITH_EDITOR
#include "Editor.h"
#include "Landscape.h"
#endif

namespace
{
int32 GetIngredientQuantity(const UAgrarianRecipeDataAsset* RecipeAsset, const FName ItemId)
{
	if (!RecipeAsset)
	{
		return 0;
	}

	for (const FAgrarianItemStack& Ingredient : RecipeAsset->Recipe.Ingredients)
	{
		if (Ingredient.ItemId == ItemId)
		{
			return Ingredient.Quantity;
		}
	}

	return 0;
}

void AddNeededIngredient(TMap<FName, int32>& NeededItems, const FName ItemId, const int32 Quantity)
{
	if (ItemId == NAME_None || Quantity <= 0)
	{
		return;
	}

	NeededItems.FindOrAdd(ItemId) += Quantity;
}
}

AActor* UAgrarianEditorAutomationLibrary::SpawnActorInEditorWorld(TSubclassOf<AActor> ActorClass, const FVector& Location, const FRotator& Rotation, const FString& ActorLabel)
{
#if WITH_EDITOR
	if (!ActorClass)
	{
		return nullptr;
	}

	UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
	if (!EditorWorld)
	{
		return nullptr;
	}

	FActorSpawnParameters SpawnParameters;
	SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	SpawnParameters.ObjectFlags = RF_Transactional;

	AActor* SpawnedActor = EditorWorld->SpawnActor<AActor>(ActorClass, Location, Rotation, SpawnParameters);
	if (!SpawnedActor)
	{
		return nullptr;
	}

	if (!ActorLabel.IsEmpty())
	{
		SpawnedActor->SetActorLabel(ActorLabel, true);
	}

	SpawnedActor->MarkPackageDirty();
	EditorWorld->MarkPackageDirty();
	return SpawnedActor;
#else
	return nullptr;
#endif
}

FString UAgrarianEditorAutomationLibrary::RunPlayableLoopSmokeTest(TSubclassOf<AAgrarianGameCharacter> CharacterClass, AAgrarianResourceNode* ResourceNode, UAgrarianRecipeDataAsset* ShelterRecipe, TSubclassOf<AActor> ShelterClass)
{
#if WITH_EDITOR
	UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
	if (!EditorWorld)
	{
		return TEXT("FAIL: no editor world");
	}

	if (!CharacterClass || !ResourceNode || !ShelterRecipe || !ShelterClass)
	{
		return FString::Printf(
			TEXT("FAIL: missing input CharacterClass=%s ResourceNode=%s ShelterRecipe=%s ShelterClass=%s"),
			*GetNameSafe(CharacterClass.Get()),
			*GetNameSafe(ResourceNode),
			*GetNameSafe(ShelterRecipe),
			*GetNameSafe(ShelterClass.Get()));
	}

	FActorSpawnParameters CharacterSpawnParams;
	CharacterSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

	AAgrarianGameCharacter* TestCharacter = EditorWorld->SpawnActor<AAgrarianGameCharacter>(
		CharacterClass,
		FVector(700.0f, -450.0f, 180.0f),
		FRotator::ZeroRotator,
		CharacterSpawnParams);

	if (!TestCharacter)
	{
		return TEXT("FAIL: could not spawn test character");
	}

	TestCharacter->SetActorLabel(TEXT("AGR_AutomationLoopCharacter"), false);

	UAgrarianInventoryComponent* Inventory = TestCharacter->GetInventoryComponent();
	UAgrarianCraftingComponent* Crafting = TestCharacter->GetCraftingComponent();
	UAgrarianBuildingPlacementComponent* Placement = TestCharacter->GetBuildingPlacementComponent();

	if (!Inventory || !Crafting || !Placement)
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: spawned character is missing inventory, crafting, or placement component");
	}

	const int32 StartingWood = Inventory->GetItemCount(TEXT("wood"));
	if (!ResourceNode->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()))
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: resource node does not implement AgrarianInteractable");
	}

	if (!IAgrarianInteractable::Execute_CanInteract(ResourceNode, TestCharacter))
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: resource node cannot be gathered");
	}

	IAgrarianInteractable::Execute_Interact(ResourceNode, TestCharacter);

	const int32 WoodAfterGather = Inventory->GetItemCount(TEXT("wood"));
	if (WoodAfterGather <= StartingWood)
	{
		TestCharacter->Destroy();
		return FString::Printf(TEXT("FAIL: gather did not add wood, before=%d after=%d"), StartingWood, WoodAfterGather);
	}

	// The current shelter recipe depends on prototype structure parts that do not yet have world acquisition.
	// Seed only the missing recipe inputs so this smoke test can exercise the connected craft/place/save/load path.
	for (const FAgrarianItemStack& Ingredient : ShelterRecipe->Recipe.Ingredients)
	{
		const int32 CurrentCount = Inventory->GetItemCount(Ingredient.ItemId);
		if (CurrentCount < Ingredient.Quantity)
		{
			FAgrarianItemStack MissingStack = Ingredient;
			MissingStack.Quantity = Ingredient.Quantity - CurrentCount;
			if (!Inventory->AddItem(MissingStack))
			{
				TestCharacter->Destroy();
				return FString::Printf(TEXT("FAIL: could not seed missing ingredient %s"), *Ingredient.ItemId.ToString());
			}
		}
	}

	if (!Crafting->AddKnownRecipe(ShelterRecipe->Recipe))
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: could not add primitive shelter recipe to test character");
	}

	if (!Crafting->Craft(ShelterRecipe->Recipe.RecipeId))
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: primitive shelter craft failed");
	}

	if (Inventory->GetItemCount(TEXT("primitive_shelter")) < 1)
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: crafting did not add primitive_shelter to inventory");
	}

	TArray<FAgrarianItemStack> PlacementCost;
	FAgrarianItemStack ShelterCost = ShelterRecipe->Recipe.Result;
	ShelterCost.Quantity = 1;
	PlacementCost.Add(ShelterCost);
	Placement->SetActiveBuildable(ShelterClass, PlacementCost);
	Placement->PlacementDistance = 5000.0f;
	Placement->PlacementProbeRadius = 1.0f;

	const int32 ShelterCountBeforePlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
	int32 ShelterActorsBeforePlace = 0;
	for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
	{
		ShelterActorsBeforePlace++;
	}

	const FTransform PlacementTransform(FRotator::ZeroRotator, FVector(1500.0f, 550.0f, 300.0f));
	Placement->ServerPlaceBuildable_Implementation(ShelterClass, PlacementTransform);

	const int32 ShelterCountAfterPlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
	int32 ShelterActorsAfterPlace = 0;
	AActor* PlacedShelter = nullptr;
	for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
	{
		ShelterActorsAfterPlace++;
		PlacedShelter = *ActorIt;
	}

	if (ShelterActorsAfterPlace <= ShelterActorsBeforePlace)
	{
		TestCharacter->Destroy();
		return FString::Printf(TEXT("FAIL: placement did not spawn shelter, before=%d after=%d"), ShelterActorsBeforePlace, ShelterActorsAfterPlace);
	}

	if (ShelterCountAfterPlace >= ShelterCountBeforePlace)
	{
		TestCharacter->Destroy();
		return FString::Printf(TEXT("FAIL: placement did not consume shelter item, before=%d after=%d"), ShelterCountBeforePlace, ShelterCountAfterPlace);
	}

	if (!PlacedShelter || !PlacedShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: placed shelter is missing persistent actor component");
	}

	TArray<FAgrarianSavedWorldActor> SavedActors;
	for (TActorIterator<AActor> ActorIt(EditorWorld); ActorIt; ++ActorIt)
	{
		if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass<UAgrarianPersistentActorComponent>())
		{
			if (PersistentComponent->IsSaveable())
			{
				SavedActors.Add(PersistentComponent->CaptureSaveState());
			}
		}
	}

	if (SavedActors.IsEmpty())
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: save capture did not include any persistent world actors");
	}

	int32 RestoredActorCount = 0;
	for (const FAgrarianSavedWorldActor& SavedActor : SavedActors)
	{
		if (SavedActor.ActorTypeId != TEXT("primitive_shelter"))
		{
			continue;
		}

		AActor* RestoredActor = EditorWorld->SpawnActor<AActor>(ShelterClass, SavedActor.Transform);
		if (!RestoredActor)
		{
			continue;
		}

		if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass<UAgrarianPersistentActorComponent>())
		{
			PersistentComponent->ApplySaveState(SavedActor);
		}

		RestoredActorCount++;
	}

	if (RestoredActorCount <= 0)
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: restore did not spawn any saved primitive shelter actors");
	}

	TestCharacter->Destroy();
	return FString::Printf(
		TEXT("PASS: gathered wood %d->%d, crafted primitive_shelter, placed shelter, saved %d persistent actor(s), restored %d actor(s)"),
		StartingWood,
		WoodAfterGather,
		SavedActors.Num(),
		RestoredActorCount);
#else
	return TEXT("FAIL: editor automation is only available in editor builds");
#endif
}

FString UAgrarianEditorAutomationLibrary::RunNaturalShelterLoopSmokeTest(
	TSubclassOf<AAgrarianGameCharacter> CharacterClass,
	AAgrarianResourceNode* WoodNode,
	AAgrarianResourceNode* FiberNode,
	AAgrarianWildlifeBase* Wildlife,
	UAgrarianRecipeDataAsset* FrameRecipe,
	UAgrarianRecipeDataAsset* WallPanelRecipe,
	UAgrarianRecipeDataAsset* RoofPanelRecipe,
	UAgrarianRecipeDataAsset* ShelterRecipe,
	TSubclassOf<AActor> ShelterClass)
{
#if WITH_EDITOR
	UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
	if (!EditorWorld)
	{
		return TEXT("FAIL: no editor world");
	}

	if (!CharacterClass || !WoodNode || !FiberNode || !Wildlife || !FrameRecipe || !WallPanelRecipe || !RoofPanelRecipe || !ShelterRecipe || !ShelterClass)
	{
		return FString::Printf(
			TEXT("FAIL: missing input CharacterClass=%s WoodNode=%s FiberNode=%s Wildlife=%s FrameRecipe=%s WallPanelRecipe=%s RoofPanelRecipe=%s ShelterRecipe=%s ShelterClass=%s"),
			*GetNameSafe(CharacterClass.Get()),
			*GetNameSafe(WoodNode),
			*GetNameSafe(FiberNode),
			*GetNameSafe(Wildlife),
			*GetNameSafe(FrameRecipe),
			*GetNameSafe(WallPanelRecipe),
			*GetNameSafe(RoofPanelRecipe),
			*GetNameSafe(ShelterRecipe),
			*GetNameSafe(ShelterClass.Get()));
	}

	FActorSpawnParameters CharacterSpawnParams;
	CharacterSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

	AAgrarianGameCharacter* TestCharacter = EditorWorld->SpawnActor<AAgrarianGameCharacter>(
		CharacterClass,
		FVector(720.0f, -420.0f, 180.0f),
		FRotator::ZeroRotator,
		CharacterSpawnParams);

	if (!TestCharacter)
	{
		return TEXT("FAIL: could not spawn test character");
	}

	TestCharacter->SetActorLabel(TEXT("AGR_AutomationNaturalShelterCharacter"), false);

	UAgrarianInventoryComponent* Inventory = TestCharacter->GetInventoryComponent();
	UAgrarianCraftingComponent* Crafting = TestCharacter->GetCraftingComponent();
	UAgrarianBuildingPlacementComponent* Placement = TestCharacter->GetBuildingPlacementComponent();

	if (!Inventory || !Crafting || !Placement)
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: spawned character is missing inventory, crafting, or placement component");
	}

	TMap<FName, int32> NeededItems;
	for (const FAgrarianItemStack& ShelterIngredient : ShelterRecipe->Recipe.Ingredients)
	{
		if (ShelterIngredient.ItemId == TEXT("primitive_frame"))
		{
			for (const FAgrarianItemStack& Ingredient : FrameRecipe->Recipe.Ingredients)
			{
				AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
			}
		}
		else if (ShelterIngredient.ItemId == TEXT("primitive_wall_panel"))
		{
			for (const FAgrarianItemStack& Ingredient : WallPanelRecipe->Recipe.Ingredients)
			{
				AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
			}
		}
		else if (ShelterIngredient.ItemId == TEXT("primitive_roof_panel"))
		{
			for (const FAgrarianItemStack& Ingredient : RoofPanelRecipe->Recipe.Ingredients)
			{
				AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
			}
		}
		else
		{
			AddNeededIngredient(NeededItems, ShelterIngredient.ItemId, ShelterIngredient.Quantity);
		}
	}

	const int32 NeededWood = NeededItems.FindRef(TEXT("wood"));
	const int32 NeededFiber = NeededItems.FindRef(TEXT("fiber"));
	const int32 NeededHide = NeededItems.FindRef(TEXT("hide"));

	auto GatherUntil = [TestCharacter, Inventory](AAgrarianResourceNode* Node, const FName ItemId, const int32 NeededCount) -> bool
	{
		while (Inventory->GetItemCount(ItemId) < NeededCount)
		{
			if (!Node || !Node->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()) || !IAgrarianInteractable::Execute_CanInteract(Node, TestCharacter))
			{
				return false;
			}

			IAgrarianInteractable::Execute_Interact(Node, TestCharacter);
		}

		return true;
	};

	if (!GatherUntil(WoodNode, TEXT("wood"), NeededWood))
	{
		const int32 WoodCount = Inventory->GetItemCount(TEXT("wood"));
		TestCharacter->Destroy();
		return FString::Printf(TEXT("FAIL: could not gather enough wood, needed=%d gathered=%d"), NeededWood, WoodCount);
	}

	if (!GatherUntil(FiberNode, TEXT("fiber"), NeededFiber))
	{
		const int32 FiberCount = Inventory->GetItemCount(TEXT("fiber"));
		TestCharacter->Destroy();
		return FString::Printf(TEXT("FAIL: could not gather enough fiber, needed=%d gathered=%d"), NeededFiber, FiberCount);
	}

	if (NeededHide > 0)
	{
		Wildlife->ApplyWildlifeDamage(Wildlife->MaxHealth + 10.0f, TestCharacter);
		if (!IAgrarianInteractable::Execute_CanInteract(Wildlife, TestCharacter))
		{
			TestCharacter->Destroy();
			return TEXT("FAIL: wildlife could not be harvested after lethal damage");
		}

		IAgrarianInteractable::Execute_Interact(Wildlife, TestCharacter);
		if (Inventory->GetItemCount(TEXT("hide")) < NeededHide)
		{
			const int32 HideCount = Inventory->GetItemCount(TEXT("hide"));
			TestCharacter->Destroy();
			return FString::Printf(TEXT("FAIL: could not harvest enough hide, needed=%d harvested=%d"), NeededHide, HideCount);
		}
	}

	if (!Crafting->AddKnownRecipe(FrameRecipe->Recipe) || !Crafting->AddKnownRecipe(WallPanelRecipe->Recipe) || !Crafting->AddKnownRecipe(RoofPanelRecipe->Recipe) || !Crafting->AddKnownRecipe(ShelterRecipe->Recipe))
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: could not add shelter recipes to test character");
	}

	auto CraftRepeated = [TestCharacter, Crafting](const UAgrarianRecipeDataAsset* RecipeAsset, const int32 Count) -> bool
	{
		for (int32 Index = 0; Index < Count; ++Index)
		{
			if (!RecipeAsset || !Crafting->Craft(RecipeAsset->Recipe.RecipeId))
			{
				TestCharacter->Destroy();
				return false;
			}
		}

		return true;
	};

	const int32 NeededFrames = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_frame"));
	const int32 NeededWallPanels = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_wall_panel"));
	const int32 NeededRoofPanels = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_roof_panel"));
	if (!CraftRepeated(FrameRecipe, NeededFrames) || !CraftRepeated(WallPanelRecipe, NeededWallPanels) || !CraftRepeated(RoofPanelRecipe, NeededRoofPanels))
	{
		return TEXT("FAIL: could not craft primitive shelter parts from gathered materials");
	}

	if (!Crafting->Craft(ShelterRecipe->Recipe.RecipeId))
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: primitive shelter craft failed after natural gathering");
	}

	if (Inventory->GetItemCount(TEXT("primitive_shelter")) < 1)
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: crafting did not add primitive_shelter to inventory");
	}

	TArray<FAgrarianItemStack> PlacementCost;
	FAgrarianItemStack ShelterCost = ShelterRecipe->Recipe.Result;
	ShelterCost.Quantity = 1;
	PlacementCost.Add(ShelterCost);
	Placement->SetActiveBuildable(ShelterClass, PlacementCost);
	Placement->PlacementDistance = 5000.0f;
	Placement->PlacementProbeRadius = 1.0f;

	const int32 ShelterCountBeforePlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
	int32 ShelterActorsBeforePlace = 0;
	for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
	{
		ShelterActorsBeforePlace++;
	}

	const FTransform PlacementTransform(FRotator::ZeroRotator, FVector(1500.0f, 550.0f, 300.0f));
	Placement->ServerPlaceBuildable_Implementation(ShelterClass, PlacementTransform);

	const int32 ShelterCountAfterPlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
	int32 ShelterActorsAfterPlace = 0;
	AActor* PlacedShelter = nullptr;
	for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
	{
		ShelterActorsAfterPlace++;
		PlacedShelter = *ActorIt;
	}

	if (ShelterActorsAfterPlace <= ShelterActorsBeforePlace)
	{
		TestCharacter->Destroy();
		return FString::Printf(TEXT("FAIL: placement did not spawn shelter, before=%d after=%d"), ShelterActorsBeforePlace, ShelterActorsAfterPlace);
	}

	if (ShelterCountAfterPlace >= ShelterCountBeforePlace)
	{
		TestCharacter->Destroy();
		return FString::Printf(TEXT("FAIL: placement did not consume shelter item, before=%d after=%d"), ShelterCountBeforePlace, ShelterCountAfterPlace);
	}

	if (!PlacedShelter || !PlacedShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: placed shelter is missing persistent actor component");
	}

	TArray<FAgrarianSavedWorldActor> SavedActors;
	for (TActorIterator<AActor> ActorIt(EditorWorld); ActorIt; ++ActorIt)
	{
		if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass<UAgrarianPersistentActorComponent>())
		{
			if (PersistentComponent->IsSaveable())
			{
				SavedActors.Add(PersistentComponent->CaptureSaveState());
			}
		}
	}

	if (SavedActors.IsEmpty())
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: save capture did not include any persistent world actors");
	}

	int32 RestoredActorCount = 0;
	for (const FAgrarianSavedWorldActor& SavedActor : SavedActors)
	{
		if (SavedActor.ActorTypeId != TEXT("primitive_shelter"))
		{
			continue;
		}

		AActor* RestoredActor = EditorWorld->SpawnActor<AActor>(ShelterClass, SavedActor.Transform);
		if (!RestoredActor)
		{
			continue;
		}

		if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass<UAgrarianPersistentActorComponent>())
		{
			PersistentComponent->ApplySaveState(SavedActor);
		}

		RestoredActorCount++;
	}

	if (RestoredActorCount <= 0)
	{
		TestCharacter->Destroy();
		return TEXT("FAIL: restore did not spawn any saved primitive shelter actors");
	}

	const int32 WoodGathered = Inventory->GetItemCount(TEXT("wood")) + NeededWood;
	const int32 FiberGathered = Inventory->GetItemCount(TEXT("fiber")) + NeededFiber;
	const int32 HideHarvested = Inventory->GetItemCount(TEXT("hide")) + NeededHide;
	TestCharacter->Destroy();
	return FString::Printf(
		TEXT("PASS: naturally gathered wood=%d fiber=%d hide=%d, crafted parts %d/%d/%d, crafted and placed primitive_shelter, saved %d persistent actor(s), restored %d actor(s)"),
		WoodGathered,
		FiberGathered,
		HideHarvested,
		NeededFrames,
		NeededWallPanels,
		NeededRoofPanels,
		SavedActors.Num(),
		RestoredActorCount);
#else
	return TEXT("FAIL: editor automation is only available in editor builds");
#endif
}

FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubclassOf<AActor> ShelterClass, const FString& SlotName)
{
#if WITH_EDITOR
	UWorld* TestWorld = nullptr;
	if (GEditor)
	{
		TestWorld = GEditor->PlayWorld ? GEditor->PlayWorld.Get() : GEditor->GetEditorWorldContext().World();
	}

	if (!TestWorld)
	{
		return TEXT("FAIL: no editor or PIE world");
	}

	UGameInstance* GameInstance = TestWorld->GetGameInstance();
	if (!GameInstance)
	{
		return TEXT("FAIL: test world has no live GameInstance");
	}

	UAgrarianPersistenceSubsystem* Persistence = GameInstance->GetSubsystem<UAgrarianPersistenceSubsystem>();
	if (!Persistence)
	{
		return TEXT("FAIL: live GameInstance has no Agrarian persistence subsystem");
	}

	if (!ShelterClass)
	{
		return TEXT("FAIL: missing shelter class");
	}

	const FString PreviousSlotName = Persistence->DefaultSlotName;
	const int32 PreviousUserIndex = Persistence->UserIndex;
	const TMap<FName, TSubclassOf<AActor>> PreviousRegistry = Persistence->WorldActorClassRegistry;

	const FString EffectiveSlotName = SlotName.IsEmpty() ? TEXT("AgrarianAutomationPersistence") : SlotName;
	Persistence->DefaultSlotName = EffectiveSlotName;
	Persistence->UserIndex = 0;
	Persistence->WorldActorClassRegistry.Reset();
	Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), ShelterClass);
	UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);

	AAgrarianGameState* GameState = TestWorld->GetGameState<AAgrarianGameState>();
	if (!GameState)
	{
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: no Agrarian game state found for persistence test");
	}

	FAgrarianMappedWeatherInputs SavedWeatherInputs;
	SavedWeatherInputs.TileId = TEXT("automation_weather_tile");
	SavedWeatherInputs.Latitude = 37.5925f;
	SavedWeatherInputs.Longitude = -122.4995f;
	SavedWeatherInputs.TemperatureC = 9.5f;
	SavedWeatherInputs.DailyLowTemperatureC = 7.0f;
	SavedWeatherInputs.DailyHighTemperatureC = 13.0f;
	SavedWeatherInputs.PrecipitationMm = 4.0f;
	SavedWeatherInputs.WindSpeedKmh = 18.0f;
	SavedWeatherInputs.CloudCoverPercent = 85.0f;
	SavedWeatherInputs.RelativeHumidityPercent = 92.0f;
	SavedWeatherInputs.PressureMslHpa = 1007.0f;
	SavedWeatherInputs.VisibilityMeters = 6000.0f;
	SavedWeatherInputs.ProviderWeatherCode = 61;
	SavedWeatherInputs.MappedWeather = EAgrarianWeatherType::Rain;
	SavedWeatherInputs.Provider = TEXT("automation-weather");
	SavedWeatherInputs.ProviderTimestamp = TEXT("2026-05-16T08:00:00Z");
	SavedWeatherInputs.bHasProviderData = true;
	GameState->WorldHours = 14.25f;
	GameState->ApplyMappedWeatherInputs(SavedWeatherInputs);

	FActorSpawnParameters SpawnParams;
	SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	AActor* TestShelter = TestWorld->SpawnActor<AActor>(
		ShelterClass,
		FTransform(FRotator::ZeroRotator, FVector(1800.0f, 750.0f, 300.0f)),
		SpawnParams);

	if (!TestShelter)
	{
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: could not spawn persistence test shelter");
	}

	TestShelter->SetActorLabel(TEXT("AGR_AutomationPersistenceShelter"), false);

	if (!TestShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
	{
		TestShelter->Destroy();
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: test shelter is missing persistent actor component");
	}

	if (!Persistence->SaveCurrentWorld())
	{
		TestShelter->Destroy();
		UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: SaveCurrentWorld failed");
	}

	if (!Persistence->DoesSaveExist())
	{
		TestShelter->Destroy();
		UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: save slot was not written");
	}

	const UAgrarianSaveGame* LoadedSave = Persistence->LoadOrCreateSave();
	const int32 SavedActorCount = LoadedSave ? LoadedSave->WorldActors.Num() : 0;
	if (SavedActorCount <= 0)
	{
		TestShelter->Destroy();
		UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: loaded save did not include persistent world actors");
	}

	GameState->WorldHours = 3.5f;
	GameState->SetWeather(EAgrarianWeatherType::Storm);

	int32 RestoredPlayerCount = 0;
	int32 RestoredActorCount = 0;
	if (!Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount, true))
	{
		UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: LoadCurrentWorld failed to restore world state");
	}

	if (RestoredActorCount != SavedActorCount)
	{
		UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return FString::Printf(TEXT("FAIL: restored actor count mismatch, saved=%d restored=%d"), SavedActorCount, RestoredActorCount);
	}

	const FAgrarianWeatherDebugSnapshot RestoredWeather = GameState->GetWeatherDebugSnapshot();
	const bool bWeatherRestored =
		FMath::IsNearlyEqual(GameState->WorldHours, 14.25f, 0.01f) &&
		GameState->Weather == EAgrarianWeatherType::Rain &&
		RestoredWeather.TileId == SavedWeatherInputs.TileId &&
		RestoredWeather.Provider == SavedWeatherInputs.Provider &&
		RestoredWeather.ProviderTimestamp == SavedWeatherInputs.ProviderTimestamp &&
		RestoredWeather.AppliedWeather == EAgrarianWeatherType::Rain;
	if (!bWeatherRestored)
	{
		UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
		Persistence->DefaultSlotName = PreviousSlotName;
		Persistence->UserIndex = PreviousUserIndex;
		Persistence->WorldActorClassRegistry = PreviousRegistry;
		return TEXT("FAIL: weather state did not survive save/load");
	}

	int32 PersistentActorCountAfterRestore = 0;
	for (TActorIterator<AActor> ActorIt(TestWorld); ActorIt; ++ActorIt)
	{
		const AActor* Actor = *ActorIt;
		const UAgrarianPersistentActorComponent* PersistentComponent = Actor ? Actor->FindComponentByClass<UAgrarianPersistentActorComponent>() : nullptr;
		if (PersistentComponent && PersistentComponent->IsSaveable())
		{
			PersistentActorCountAfterRestore++;
		}
	}

	UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
	Persistence->DefaultSlotName = PreviousSlotName;
	Persistence->UserIndex = PreviousUserIndex;
	Persistence->WorldActorClassRegistry = PreviousRegistry;

	return FString::Printf(
		TEXT("PASS: live persistence subsystem saved %d actor(s), restored %d actor(s), restored %d player(s), restored weather %s from %s, world now has %d persistent actor(s)"),
		SavedActorCount,
		RestoredActorCount,
		RestoredPlayerCount,
		*UEnum::GetValueAsString(GameState->Weather),
		*RestoredWeather.Provider,
		PersistentActorCountAfterRestore);
#else
	return TEXT("FAIL: editor automation is only available in editor builds");
#endif
}

FString UAgrarianEditorAutomationLibrary::ImportLandscapeHeightmapIntoEditorWorld(
	const FString& HeightmapFilename,
	const int32 Width,
	const int32 Height,
	const float XScaleCm,
	const float YScaleCm,
	const float ZScaleCm,
	const FString& ActorLabel)
{
#if WITH_EDITOR
	if (Width <= 1 || Height <= 1 || Width != Height)
	{
		return FString::Printf(TEXT("FAIL: invalid landscape dimensions Width=%d Height=%d"), Width, Height);
	}

	if (FMath::IsNearlyZero(XScaleCm) || FMath::IsNearlyZero(YScaleCm) || FMath::IsNearlyZero(ZScaleCm))
	{
		return FString::Printf(TEXT("FAIL: invalid landscape scale X=%f Y=%f Z=%f"), XScaleCm, YScaleCm, ZScaleCm);
	}

	UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
	if (!EditorWorld)
	{
		return TEXT("FAIL: no editor world");
	}

	TArray<uint8> HeightmapBytes;
	if (!FFileHelper::LoadFileToArray(HeightmapBytes, *HeightmapFilename))
	{
		return FString::Printf(TEXT("FAIL: could not read heightmap %s"), *HeightmapFilename);
	}

	const int64 ExpectedByteCount = static_cast<int64>(Width) * static_cast<int64>(Height) * sizeof(uint16);
	if (HeightmapBytes.Num() != ExpectedByteCount)
	{
		return FString::Printf(
			TEXT("FAIL: heightmap byte count mismatch, expected=%lld actual=%d"),
			ExpectedByteCount,
			HeightmapBytes.Num());
	}

	TArray<uint16> HeightData;
	HeightData.SetNumUninitialized(Width * Height);
	FMemory::Memcpy(HeightData.GetData(), HeightmapBytes.GetData(), HeightmapBytes.Num());

	FActorSpawnParameters SpawnParameters;
	SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	SpawnParameters.ObjectFlags = RF_Transactional;

	const float WorldSizeX = static_cast<float>(Width - 1) * XScaleCm;
	const float WorldSizeY = static_cast<float>(Height - 1) * YScaleCm;
	const FVector LandscapeLocation(-WorldSizeX * 0.5f, -WorldSizeY * 0.5f, 0.0f);
	ALandscape* Landscape = EditorWorld->SpawnActor<ALandscape>(
		ALandscape::StaticClass(),
		LandscapeLocation,
		FRotator::ZeroRotator,
		SpawnParameters);

	if (!Landscape)
	{
		return TEXT("FAIL: could not spawn landscape actor");
	}

	Landscape->SetActorScale3D(FVector(XScaleCm, YScaleCm, ZScaleCm));
	if (!ActorLabel.IsEmpty())
	{
		Landscape->SetActorLabel(ActorLabel, true);
	}

	const FGuid LandscapeGuid = FGuid::NewGuid();
	const FGuid FinalLayerGuid;
	TMap<FGuid, TArray<uint16>> ImportHeightData;
	ImportHeightData.Add(FinalLayerGuid, MoveTemp(HeightData));

	TMap<FGuid, TArray<FLandscapeImportLayerInfo>> ImportMaterialLayerInfos;
	ImportMaterialLayerInfos.Add(FinalLayerGuid, TArray<FLandscapeImportLayerInfo>());
	TArray<FLandscapeLayer> ImportLayers;
	Landscape->Import(
		LandscapeGuid,
		0,
		0,
		Width - 1,
		Height - 1,
		1,
		63,
		ImportHeightData,
		*HeightmapFilename,
		ImportMaterialLayerInfos,
		ELandscapeImportAlphamapType::Additive,
		MakeArrayView(ImportLayers));

	Landscape->RegisterAllComponents();
	Landscape->PostEditChange();
	Landscape->MarkPackageDirty();
	EditorWorld->MarkPackageDirty();

	return FString::Printf(
		TEXT("PASS: imported landscape %s size=%dx%d scale=(%.6f,%.6f,%.6f) world_size_cm=(%.2f,%.2f)"),
		*GetNameSafe(Landscape),
		Width,
		Height,
		XScaleCm,
		YScaleCm,
		ZScaleCm,
		WorldSizeX,
		WorldSizeY);
#else
	return TEXT("FAIL: editor automation is only available in editor builds");
#endif
}
