// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

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

#if WITH_AUTOMATION_TESTS

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

	bool DeserializeTransformEncoding(
		const FHyperTwistTransformation& Transformation,
		FHyperTwistMelinda2x2x2x2TransformEncoding& OutEncoding
	)
	{
		return FJsonObjectConverter::JsonObjectStringToUStruct(
			Transformation.TransformEncoding.PayloadJson,
			&OutEncoding,
			0,
			0
		);
	}

	bool HaveMatchingMelindaStatePayload(
		const FHyperTwistPuzzleState& LeftState,
		const FHyperTwistPuzzleState& RightState
	)
	{
		return LeftState.Definition.PuzzleId == RightState.Definition.PuzzleId
			&& LeftState.StateEncoding.EncodingProfile == RightState.StateEncoding.EncodingProfile
			&& LeftState.StateEncoding.PayloadJson == RightState.StateEncoding.PayloadJson
			&& LeftState.OrientationFrame.Reference == RightState.OrientationFrame.Reference
			&& LeftState.bIsSolved == RightState.bIsSolved;
	}

	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;
	}

	FHyperTwistTransformation MakeIdentityTransformation(const FHyperTwistPuzzleDefinitionRef& Definition)
	{
		TArray<int32> PositionPullMap;
		PositionPullMap.Reserve(16);
		for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
		{
			PositionPullMap.Add(PositionIndex);
		}

		TArray<int32> OrientationDeltaPerNewPosition;
		OrientationDeltaPerNewPosition.Init(0, 16);
		return MakeMelindaTransformation(
			Definition,
			TEXT("identity"),
			TEXT("identity"),
			PositionPullMap,
			OrientationDeltaPerNewPosition
		);
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistMelindaBound3MoveAlgebraTest,
	"HyperTwist.CleanRoom.HactarCE.Bound3.MoveAlgebra",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistMelindaBound3MoveAlgebraTest::RunTest(const FString& Parameters)
{
	const FHyperTwistPuzzleDefinitionRef Definition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
	const FHyperTwistRandomStateGenerationResult GeneratedBaseState =
		UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 2026);
	TestTrue(TEXT("Bound 3 move algebra should start from a solvable generated state."), GeneratedBaseState.bGenerated);

	const FHyperTwistPuzzleState BaseState = GeneratedBaseState.State;
	const FString SerializedBaseState = UHyperTwistContractLibrary::SerializePuzzleStateToJson(BaseState);

	const FHyperTwistTransformation TransformA =
		HyperTwistMelindaBound3CoreTestInternal::MakeMelindaTransformation(
			Definition,
			TEXT("bound3.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 FHyperTwistTransformation TransformB =
		HyperTwistMelindaBound3CoreTestInternal::MakeMelindaTransformation(
			Definition,
			TEXT("bound3.b"),
			TEXT("B"),
			{1, 2, 3, 0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
			{4, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
		);
	const FHyperTwistTransformation IdentityTransformation =
		HyperTwistMelindaBound3CoreTestInternal::MakeIdentityTransformation(Definition);

	const FHyperTwistTransformationInversionResult InverseA =
		UHyperTwistCoreLibrary::InvertTransformation(TransformA);
	TestTrue(TEXT("Bound 3 should invert Melinda transforms exactly."), InverseA.bInverted);
	TestTrue(TEXT("Bound 3 inverse surface should remain exact."), InverseA.bExactInverse);

	const FHyperTwistApplyTransformationResult AppliedA =
		UHyperTwistCoreLibrary::ApplyTransformation(BaseState, TransformA);
	TestTrue(TEXT("Transform A should apply exactly."), AppliedA.bApplied && AppliedA.bExactStateUpdate);

	const FHyperTwistApplyTransformationResult RoundTripA =
		UHyperTwistCoreLibrary::ApplyTransformation(AppliedA.State, InverseA.Transformation);
	TestTrue(TEXT("Applying a transform then its inverse should apply exactly."), RoundTripA.bApplied && RoundTripA.bExactStateUpdate);
	TestEqual(
		TEXT("Applying a transform then its inverse should return the original state."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(RoundTripA.State),
		SerializedBaseState
	);

	const FHyperTwistApplyTransformationResult SequentialAB =
		UHyperTwistCoreLibrary::ApplyTransformation(AppliedA.State, TransformB);
	TestTrue(TEXT("Sequential Melinda application should remain exact."), SequentialAB.bApplied && SequentialAB.bExactStateUpdate);

	const FHyperTwistTransformationCompositionResult ComposedAB =
		UHyperTwistCoreLibrary::ComposeTransformations(TransformA, TransformB);
	TestTrue(TEXT("Bound 3 should compose Melinda transforms exactly."), ComposedAB.bComposed);
	TestTrue(TEXT("Bound 3 composition surface should remain exact."), ComposedAB.bExactComposition);

	const FHyperTwistApplyTransformationResult AppliedComposedAB =
		UHyperTwistCoreLibrary::ApplyTransformation(BaseState, ComposedAB.Transformation);
	TestTrue(TEXT("Applying a composed Melinda transform should remain exact."), AppliedComposedAB.bApplied && AppliedComposedAB.bExactStateUpdate);
	TestEqual(
		TEXT("Applying Compose(A, B) once should match sequential application of A then B."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedComposedAB.State),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(SequentialAB.State)
	);

	const FHyperTwistTransformationCompositionResult IdentityThenA =
		UHyperTwistCoreLibrary::ComposeTransformations(IdentityTransformation, TransformA);
	TestTrue(TEXT("Compose(identity, A) should succeed."), IdentityThenA.bComposed && IdentityThenA.bExactComposition);
	const FHyperTwistApplyTransformationResult AppliedIdentityThenA =
		UHyperTwistCoreLibrary::ApplyTransformation(BaseState, IdentityThenA.Transformation);
	TestEqual(
		TEXT("Compose(identity, A) should preserve exact application behavior."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedIdentityThenA.State),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedA.State)
	);

	const FHyperTwistTransformationCompositionResult AThenIdentity =
		UHyperTwistCoreLibrary::ComposeTransformations(TransformA, IdentityTransformation);
	TestTrue(TEXT("Compose(A, identity) should succeed."), AThenIdentity.bComposed && AThenIdentity.bExactComposition);
	const FHyperTwistApplyTransformationResult AppliedAThenIdentity =
		UHyperTwistCoreLibrary::ApplyTransformation(BaseState, AThenIdentity.Transformation);
	TestEqual(
		TEXT("Compose(A, identity) should preserve exact application behavior."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedAThenIdentity.State),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(AppliedA.State)
	);

	const FHyperTwistTransformationInversionResult InverseB =
		UHyperTwistCoreLibrary::InvertTransformation(TransformB);
	TestTrue(TEXT("Transform B should also invert exactly."), InverseB.bInverted && InverseB.bExactInverse);

	const FHyperTwistTransformationInversionResult InverseComposedAB =
		UHyperTwistCoreLibrary::InvertTransformation(ComposedAB.Transformation);
	TestTrue(TEXT("Inverse(Compose(A, B)) should succeed."), InverseComposedAB.bInverted && InverseComposedAB.bExactInverse);

	const FHyperTwistTransformationCompositionResult ComposeInverseBInverseA =
		UHyperTwistCoreLibrary::ComposeTransformations(InverseB.Transformation, InverseA.Transformation);
	TestTrue(
		TEXT("Compose(Inverse(B), Inverse(A)) should succeed."),
		ComposeInverseBInverseA.bComposed && ComposeInverseBInverseA.bExactComposition
	);

	FHyperTwistMelinda2x2x2x2TransformEncoding InverseComposedEncoding;
	FHyperTwistMelinda2x2x2x2TransformEncoding ReverseOrderInverseEncoding;
	TestTrue(
		TEXT("Inverse(Compose(A, B)) payload should deserialize."),
		HyperTwistMelindaBound3CoreTestInternal::DeserializeTransformEncoding(
			InverseComposedAB.Transformation,
			InverseComposedEncoding
		)
	);
	TestTrue(
		TEXT("Compose(Inverse(B), Inverse(A)) payload should deserialize."),
		HyperTwistMelindaBound3CoreTestInternal::DeserializeTransformEncoding(
			ComposeInverseBInverseA.Transformation,
			ReverseOrderInverseEncoding
		)
	);
	TestTrue(
		TEXT("Inverse-of-compose should preserve the exact position pull map."),
		InverseComposedEncoding.PositionPullMap == ReverseOrderInverseEncoding.PositionPullMap
	);
	TestTrue(
		TEXT("Inverse-of-compose should preserve the exact orientation deltas."),
		InverseComposedEncoding.OrientationDeltaPerNewPosition
			== ReverseOrderInverseEncoding.OrientationDeltaPerNewPosition
	);

	const FHyperTwistApplyTransformationResult InverseComposeRoundTrip =
		UHyperTwistCoreLibrary::ApplyTransformation(SequentialAB.State, InverseComposedAB.Transformation);
	const FHyperTwistApplyTransformationResult ReverseOrderRoundTrip =
		UHyperTwistCoreLibrary::ApplyTransformation(SequentialAB.State, ComposeInverseBInverseA.Transformation);
	TestEqual(
		TEXT("Inverse(Compose(A, B)) should return the original state."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(InverseComposeRoundTrip.State),
		SerializedBaseState
	);
	TestEqual(
		TEXT("Compose(Inverse(B), Inverse(A)) should return the original state."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(ReverseOrderRoundTrip.State),
		SerializedBaseState
	);

	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistMelindaBound3ScramblePacketTest,
	"HyperTwist.CleanRoom.HactarCE.Bound3.ScramblePacket",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistMelindaBound3ScramblePacketTest::RunTest(const FString& Parameters)
{
	const FHyperTwistPuzzleDefinitionRef Definition = UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
	const FHyperTwistRandomStateGenerationResult GeneratedStateResult =
		UHyperTwistCoreLibrary::GenerateRandomPuzzleState(Definition, 77);
	TestTrue(TEXT("Generated-state scramble packets should start from a solvable state."), GeneratedStateResult.bGenerated);

	const FHyperTwistMelindaScramblePacketBuildResult GeneratedPacketResult =
		UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromGeneratedState(GeneratedStateResult);
	TestTrue(TEXT("Bound 3 should build a generated-state scramble packet."), GeneratedPacketResult.bBuilt);
	TestTrue(TEXT("Generated-state scramble packets should be structurally valid."), GeneratedPacketResult.Packet.IsStructurallyValid());
	TestTrue(
		TEXT("Generated-state scramble packets should preserve solvable target-state validation."),
		GeneratedPacketResult.TargetStateValidation.bIsSolvable
	);
	TestTrue(
		TEXT("Generated-state scramble packets should stay marked as generated-state packets."),
		GeneratedPacketResult.Packet.PacketSource == EHyperTwistMelindaScramblePacketSource::GeneratedState
	);
	TestTrue(TEXT("Generated-state scramble packets should carry a target state."), GeneratedPacketResult.Packet.bCarriesTargetState);
	TestFalse(
		TEXT("Generated-state scramble packets should not pretend to carry an exact transform sequence."),
		GeneratedPacketResult.Packet.bCarriesExactTransformSequence
	);
	TestEqual(
		TEXT("Generated-state scramble packets should preserve the generated target state exactly."),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(GeneratedPacketResult.Packet.TargetState),
		UHyperTwistContractLibrary::SerializePuzzleStateToJson(GeneratedStateResult.State)
	);
	TestEqual(
		TEXT("Generated-state scramble packets should not include a transform sequence."),
		GeneratedPacketResult.Packet.TransformSequence.Num(),
		0
	);

	const FHyperTwistTransformation TransformA =
		HyperTwistMelindaBound3CoreTestInternal::MakeMelindaTransformation(
			Definition,
			TEXT("bound3.packet.a"),
			TEXT("PA"),
			{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 FHyperTwistTransformationInversionResult InverseA =
		UHyperTwistCoreLibrary::InvertTransformation(TransformA);
	TestTrue(TEXT("Sequence-backed scramble packets should be able to use exact inverses."), InverseA.bInverted && InverseA.bExactInverse);

	TArray<FHyperTwistTransformation> TransformSequence;
	TransformSequence.Add(TransformA);
	TransformSequence.Add(InverseA.Transformation);
	const FHyperTwistMelindaScramblePacketBuildResult SequencePacketResult =
		UHyperTwistCoreLibrary::BuildMelindaScramblePacketFromTransformSequence(Definition, TransformSequence);
	TestTrue(TEXT("Bound 3 should build a transform-sequence scramble packet."), SequencePacketResult.bBuilt);
	TestTrue(TEXT("Transform-sequence scramble packets should be structurally valid."), SequencePacketResult.Packet.IsStructurallyValid());
	TestTrue(
		TEXT("Transform-sequence scramble packets should remain solvable at their target state."),
		SequencePacketResult.TargetStateValidation.bIsSolvable
	);
	TestTrue(
		TEXT("Transform-sequence scramble packets should stay marked as transform-sequence packets."),
		SequencePacketResult.Packet.PacketSource == EHyperTwistMelindaScramblePacketSource::TransformSequence
	);
	TestTrue(
		TEXT("Transform-sequence scramble packets should carry an exact transform sequence."),
		SequencePacketResult.Packet.bCarriesExactTransformSequence
	);
	TestEqual(
		TEXT("Transform-sequence scramble packets should preserve the exact sequence length."),
		SequencePacketResult.Packet.TransformSequence.Num(),
		2
	);

	const FHyperTwistPuzzleState SolvedState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
	const FHyperTwistApplyTransformationResult AppliedNetTransform =
		UHyperTwistCoreLibrary::ApplyTransformation(SolvedState, SequencePacketResult.Packet.NetTransform);
	TestTrue(TEXT("Packet net transform should apply exactly from solved state."), AppliedNetTransform.bApplied && AppliedNetTransform.bExactStateUpdate);
	TestTrue(
		TEXT("Applying the packet net transform should reproduce the packet target state."),
		HyperTwistMelindaBound3CoreTestInternal::HaveMatchingMelindaStatePayload(
			AppliedNetTransform.State,
			SequencePacketResult.Packet.TargetState
		)
	);
	TestTrue(
		TEXT("A transform and its inverse should collapse the packet target state back to solved."),
		HyperTwistMelindaBound3CoreTestInternal::HaveMatchingMelindaStatePayload(
			SequencePacketResult.Packet.TargetState,
			SolvedState
		)
	);

	return true;
}

#endif // WITH_AUTOMATION_TESTS
