#include "Misc/AutomationTest.h"

#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "JsonObjectConverter.h"

namespace HyperTwistMelindaBound4CoreTestInternal
{
	template <typename TStruct>
	FString SerializeStructToJson(const TStruct& Value)
	{
		FString Json;
		FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
		return Json;
	}

	FHyperTwistTransformation MakeMelindaTransformation(
		const FHyperTwistPuzzleDefinitionRef& Definition,
		const FString& MoveId,
		const FString& Notation,
		const TArray<int32>& PositionPullMap,
		const TArray<int32>& OrientationDeltaPerNewPosition
	)
	{
		FHyperTwistMelinda2x2x2x2TransformEncoding Encoding;
		Encoding.MoveId = MoveId;
		Encoding.PositionPullMap = PositionPullMap;
		Encoding.OrientationDeltaPerNewPosition = OrientationDeltaPerNewPosition;

		FHyperTwistTransformation Transformation;
		Transformation.Definition = Definition;
		Transformation.TransformKind = EHyperTwistTransformKind::SingleMove;
		Transformation.Notation = Notation;
		Transformation.TransformEncoding.EncodingProfile = Encoding.EncodingProfile;
		Transformation.TransformEncoding.PayloadJson = SerializeStructToJson(Encoding);
		Transformation.OriginalNotation = Notation;
		return Transformation;
	}

	bool DeserializeMelindaStatePayload(
		const FHyperTwistPuzzleState& State,
		FHyperTwistMelinda2x2x2x2StateEncoding& OutEncoding
	)
	{
		return FJsonObjectConverter::JsonObjectStringToUStruct(
			State.StateEncoding.PayloadJson,
			&OutEncoding,
			0,
			0
		);
	}

	const FHyperTwistMelindaFlatTileProjection* FindTileByPosition(
		const FHyperTwistMelinda2x2x2x2FlatProjection& Projection,
		const int32 PositionIndex
	)
	{
		const TArray<FHyperTwistMelindaFlatTileProjection>& Tiles =
			PositionIndex < 8 ? Projection.FrontTiles : Projection.BackTiles;

		for (const FHyperTwistMelindaFlatTileProjection& Tile : Tiles)
		{
			if (Tile.PositionIndex == PositionIndex)
			{
				return &Tile;
			}
		}

		return nullptr;
	}

	bool ProjectionMatchesState(
		const FHyperTwistMelinda2x2x2x2FlatProjection& Projection,
		const FHyperTwistMelinda2x2x2x2StateEncoding& StateEncoding
	)
	{
		for (int32 PositionIndex = 0; PositionIndex < StateEncoding.PositionToPiece.Num(); ++PositionIndex)
		{
			const FHyperTwistMelindaFlatTileProjection* Tile = FindTileByPosition(Projection, PositionIndex);
			if (Tile == nullptr
				|| Tile->PieceId != StateEncoding.PositionToPiece[PositionIndex]
				|| Tile->bPieceInSolvedPosition != (Tile->PieceId == PositionIndex)
				|| !Tile->PanelSticker.IsStructurallyValid())
			{
				return false;
			}
		}

		return true;
	}

	void CollectAllTiles(
		const FHyperTwistMelinda2x2x2x2FlatProjection& Projection,
		TArray<const FHyperTwistMelindaFlatTileProjection*>& OutTiles
	)
	{
		OutTiles.Reserve(Projection.FrontTiles.Num() + Projection.BackTiles.Num());
		for (const FHyperTwistMelindaFlatTileProjection& Tile : Projection.FrontTiles)
		{
			OutTiles.Add(&Tile);
		}

		for (const FHyperTwistMelindaFlatTileProjection& Tile : Projection.BackTiles)
		{
			OutTiles.Add(&Tile);
		}
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistMelindaBound4FlatProjectionTest,
	"HyperTwist.CleanRoom.HactarCE.Bound4.FlatProjection",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistMelindaBound4FlatProjectionTest::RunTest(const FString& Parameters)
{
	const FHyperTwistPuzzleDefinitionRef Definition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
	const FHyperTwistPuzzleState SolvedState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
	const FString SerializedSolvedState = UHyperTwistContractLibrary::SerializePuzzleStateToJson(SolvedState);

	FHyperTwistMelindaFlatProjectionOptions FullOptions;
	FullOptions.bIncludePieceLabels = true;
	FullOptions.bIncludeStickerPeeks = true;

	const FHyperTwistMelindaFlatProjectionBuildResult SolvedProjectionResult =
		UHyperTwistCoreLibrary::BuildMelindaFlatProjection(SolvedState, FullOptions);

	TestTrue(TEXT("Bound 4 should project the solved Melinda state."), SolvedProjectionResult.bProjected);
	TestTrue(TEXT("Solved projection should stay exact."), SolvedProjectionResult.bExactProjection);
	TestTrue(TEXT("Solved projection should remain structurally valid."), SolvedProjectionResult.Projection.IsStructurallyValid());
	TestTrue(TEXT("Solved projection should keep the source state solvable."), SolvedProjectionResult.SourceStateValidation.bIsSolvable);
	TestEqual(
		TEXT("Bound 4 projection must not mutate the solved input state."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(SolvedState),
		SerializedSolvedState
	);

	const FHyperTwistMelindaFlatTileProjection* FrontOriginTile =
		HyperTwistMelindaBound4CoreTestInternal::FindTileByPosition(
			SolvedProjectionResult.Projection,
			0
		);
	TestNotNull(TEXT("Solved front projection should expose position 0."), FrontOriginTile);
	if (FrontOriginTile != nullptr)
	{
		TestEqual(TEXT("Solved front origin tile should keep piece 0."), FrontOriginTile->PieceId, 0);
		TestEqual(TEXT("Solved front origin slot label should be stable."), FrontOriginTile->SlotLabel, FString(TEXT("-X-Y-Z-W")));
		TestEqual(TEXT("Solved front origin piece label should be derived from the canonical piece id."), FrontOriginTile->PieceLabel, FString(TEXT("-X-Y-Z-W")));
		TestEqual(
			TEXT("Solved front origin panel sticker should use the front W- color."),
			FrontOriginTile->PanelSticker.Color,
			EHyperTwistMelindaStickerColor::WNegative
		);
		TestEqual(TEXT("Solved front origin should expose three peek stickers when enabled."), FrontOriginTile->PeekStickers.Num(), 3);
	}

	const FHyperTwistMelindaFlatTileProjection* BackFarTile =
		HyperTwistMelindaBound4CoreTestInternal::FindTileByPosition(
			SolvedProjectionResult.Projection,
			15
		);
	TestNotNull(TEXT("Solved back projection should expose position 15."), BackFarTile);
	if (BackFarTile != nullptr)
	{
		TestEqual(TEXT("Solved back far tile should keep piece 15."), BackFarTile->PieceId, 15);
		TestEqual(
			TEXT("Solved back far tile should use the back W+ color."),
			BackFarTile->PanelSticker.Color,
			EHyperTwistMelindaStickerColor::WPositive
		);
	}

	const FHyperTwistTransformation TransformA =
		HyperTwistMelindaBound4CoreTestInternal::MakeMelindaTransformation(
			Definition,
			TEXT("bound4.a"),
			TEXT("A"),
			{3, 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
			{3, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
		);
	const FHyperTwistApplyTransformationResult AppliedTransform =
		UHyperTwistCoreLibrary::ApplyTransformation(SolvedState, TransformA);
	TestTrue(TEXT("Bound 4 transformed-state coverage needs an exact Melinda transform."), AppliedTransform.bApplied && AppliedTransform.bExactStateUpdate);

	const FString SerializedTransformedState =
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedTransform.State);
	const FHyperTwistMelindaFlatProjectionBuildResult TransformedProjectionResult =
		UHyperTwistCoreLibrary::BuildMelindaFlatProjection(AppliedTransform.State, FullOptions);
	TestTrue(TEXT("Bound 4 should project transformed Melinda states."), TransformedProjectionResult.bProjected);
	TestTrue(TEXT("Transformed projection should remain exact."), TransformedProjectionResult.bExactProjection);
	TestTrue(TEXT("Transformed projection should remain structurally valid."), TransformedProjectionResult.Projection.IsStructurallyValid());
	TestEqual(
		TEXT("Projection generation must not mutate the transformed canonical state."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedTransform.State),
		SerializedTransformedState
	);

	FHyperTwistMelinda2x2x2x2StateEncoding TransformedPayload;
	TestTrue(
		TEXT("Transformed Melinda payload should deserialize for projection coherence checks."),
		HyperTwistMelindaBound4CoreTestInternal::DeserializeMelindaStatePayload(
			AppliedTransform.State,
			TransformedPayload
		)
	);
	TestTrue(
		TEXT("Transformed projection should reflect the canonical piece occupancy exactly."),
		HyperTwistMelindaBound4CoreTestInternal::ProjectionMatchesState(
			TransformedProjectionResult.Projection,
			TransformedPayload
		)
	);

	const FHyperTwistMelindaFlatTileProjection* TransformedFrontOriginTile =
		HyperTwistMelindaBound4CoreTestInternal::FindTileByPosition(
			TransformedProjectionResult.Projection,
			0
		);
	TestNotNull(TEXT("Transformed projection should still expose position 0."), TransformedFrontOriginTile);
	if (TransformedFrontOriginTile != nullptr)
	{
		TestEqual(
			TEXT("The known Bound 4 transform should move piece 3 into position 0."),
			TransformedFrontOriginTile->PieceId,
			3
		);
		TestFalse(
			TEXT("The transformed front origin tile should no longer be in its solved position."),
			TransformedFrontOriginTile->bPieceInSolvedPosition
		);
	}

	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistMelindaBound4DebugOptionsTest,
	"HyperTwist.CleanRoom.HactarCE.Bound4.DebugOptions",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistMelindaBound4DebugOptionsTest::RunTest(const FString& Parameters)
{
	const FHyperTwistPuzzleDefinitionRef Definition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
	const FHyperTwistRandomStateGenerationResult GeneratedStateResult =
		UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 404);
	TestTrue(TEXT("Bound 4 random-state projection coverage should start from a solvable generated state."), GeneratedStateResult.bGenerated);

	const FString SerializedGeneratedState =
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(GeneratedStateResult.State);

	FHyperTwistMelindaFlatProjectionOptions MinimalOptions;
	MinimalOptions.bIncludePieceLabels = false;
	MinimalOptions.bIncludeStickerPeeks = false;

	FHyperTwistMelindaFlatProjectionOptions FullOptions;
	FullOptions.bIncludePieceLabels = true;
	FullOptions.bIncludeStickerPeeks = true;

	const FHyperTwistMelindaFlatProjectionBuildResult MinimalProjectionA =
		UHyperTwistCoreLibrary::BuildMelindaFlatProjection(GeneratedStateResult.State, MinimalOptions);
	const FHyperTwistMelindaFlatProjectionBuildResult MinimalProjectionB =
		UHyperTwistCoreLibrary::BuildMelindaFlatProjection(GeneratedStateResult.State, MinimalOptions);
	const FHyperTwistMelindaFlatProjectionBuildResult FullProjection =
		UHyperTwistCoreLibrary::BuildMelindaFlatProjection(GeneratedStateResult.State, FullOptions);

	TestTrue(TEXT("Minimal projection should succeed for generated Melinda states."), MinimalProjectionA.bProjected);
	TestTrue(TEXT("Repeated minimal projection should stay deterministic."), MinimalProjectionB.bProjected);
	TestTrue(TEXT("Full projection should succeed for generated Melinda states."), FullProjection.bProjected);
	TestTrue(TEXT("Generated-state projection should remain structurally valid."), FullProjection.Projection.IsStructurallyValid());
	TestTrue(TEXT("Projection validation should stay anchored to the existing solvable-state surface."), FullProjection.SourceStateValidation.bIsSolvable);
	TestEqual(
		TEXT("Projection generation must not mutate the generated canonical state."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(GeneratedStateResult.State),
		SerializedGeneratedState
	);
	TestEqual(
		TEXT("Repeated projection with the same state and options must be deterministic."),
		HyperTwistMelindaBound4CoreTestInternal::SerializeStructToJson(MinimalProjectionA.Projection),
		HyperTwistMelindaBound4CoreTestInternal::SerializeStructToJson(MinimalProjectionB.Projection)
	);

	TArray<const FHyperTwistMelindaFlatTileProjection*> MinimalTiles;
	HyperTwistMelindaBound4CoreTestInternal::CollectAllTiles(MinimalProjectionA.Projection, MinimalTiles);
	for (const FHyperTwistMelindaFlatTileProjection* Tile : MinimalTiles)
	{
		TestTrue(TEXT("Labels-off projection should omit piece labels."), Tile != nullptr && Tile->PieceLabel.IsEmpty());
		TestTrue(TEXT("Peeks-off projection should omit hidden-adjacent peeks."), Tile != nullptr && Tile->PeekStickers.Num() == 0);
		TestTrue(TEXT("Labels-off projection should still expose a panel sticker."), Tile != nullptr && Tile->PanelSticker.IsStructurallyValid());
	}

	TArray<const FHyperTwistMelindaFlatTileProjection*> FullTiles;
	HyperTwistMelindaBound4CoreTestInternal::CollectAllTiles(FullProjection.Projection, FullTiles);
	for (const FHyperTwistMelindaFlatTileProjection* Tile : FullTiles)
	{
		TestTrue(TEXT("Labels-on projection should expose derived piece labels."), Tile != nullptr && !Tile->PieceLabel.IsEmpty());
		TestTrue(TEXT("Peeks-on projection should expose three hidden-adjacent sticker cues."), Tile != nullptr && Tile->PeekStickers.Num() == 3);
	}

	return true;
}
