// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

#include <initializer_list>

#include "Engine/GameInstance.h"
#include "HAL/PlatformProcess.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistRecognition/HyperTwistSpeechClient.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeCommandLibrary.h"
#include "HyperTwistSolverLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"

#if WITH_AUTOMATION_TESTS

namespace HyperTwistClassicCubeBehaviorTestInternal
{
	TArray<FString> GenerateDeterministicScramble(FRandomStream& Stream, const int32 Length)
	{
		TArray<FString> Result;
		const TArray<FString> FaceNames = { TEXT("U"), TEXT("D"), TEXT("F"), TEXT("B"), TEXT("R"), TEXT("L") };
		const TArray<FString> Suffixes = { TEXT(""), TEXT("'"), TEXT("2") };
		const TArray<EHyperTwistClassicCubeFace> FaceOrder = {
			EHyperTwistClassicCubeFace::Up,
			EHyperTwistClassicCubeFace::Down,
			EHyperTwistClassicCubeFace::Front,
			EHyperTwistClassicCubeFace::Back,
			EHyperTwistClassicCubeFace::Right,
			EHyperTwistClassicCubeFace::Left
		};

		auto GetFaceAxisIndex = [](const EHyperTwistClassicCubeFace Face) -> int32
		{
			switch (Face)
			{
			case EHyperTwistClassicCubeFace::Up:
			case EHyperTwistClassicCubeFace::Down:
				return 0;
			case EHyperTwistClassicCubeFace::Front:
			case EHyperTwistClassicCubeFace::Back:
				return 1;
			case EHyperTwistClassicCubeFace::Right:
			case EHyperTwistClassicCubeFace::Left:
				return 2;
			default:
				return INDEX_NONE;
			}
		};

		int32 PreviousFaceIndex = INDEX_NONE;
		int32 PreviousAxisIndex = INDEX_NONE;
		for (int32 MoveIndex = 0; MoveIndex < Length; ++MoveIndex)
		{
			TArray<int32> CandidateFaceIndices;
			for (int32 FaceIndex = 0; FaceIndex < FaceNames.Num(); ++FaceIndex)
			{
				if (FaceIndex == PreviousFaceIndex)
				{
					continue;
				}

				const int32 AxisIndex = GetFaceAxisIndex(FaceOrder[FaceIndex]);
				if (AxisIndex == PreviousAxisIndex)
				{
					continue;
				}

				CandidateFaceIndices.Add(FaceIndex);
			}

			const int32 SelectedFaceIndex =
				CandidateFaceIndices[Stream.RandRange(0, CandidateFaceIndices.Num() - 1)];
			const FString Suffix = Suffixes[Stream.RandRange(0, Suffixes.Num() - 1)];
			Result.Add(FaceNames[SelectedFaceIndex] + Suffix);
			PreviousFaceIndex = SelectedFaceIndex;
			PreviousAxisIndex = GetFaceAxisIndex(FaceOrder[SelectedFaceIndex]);
		}

		return Result;
	}

	TArray<FString> BuildInverseMoveSequence(const TArray<FString>& MoveSequence)
	{
		TArray<FString> Result;
		Result.Reserve(MoveSequence.Num());

		for (int32 MoveIndex = MoveSequence.Num() - 1; MoveIndex >= 0; --MoveIndex)
		{
			FHyperTwistClassicCubeMoveDescriptor ParsedMove;
			if (!UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(
					MoveSequence[MoveIndex],
					ParsedMove))
			{
				Result.Reset();
				return Result;
			}

			Result.Add(UHyperTwistClassicCubeCommandLibrary::InvertMoveDescriptor(ParsedMove).Notation);
		}

		return Result;
	}

	bool HasExactFaces(
		const FHyperTwistClassicCubePieceData& PieceData,
		const std::initializer_list<EHyperTwistClassicCubeFace> ExpectedFaces
	)
	{
		if (PieceData.ColoredFaces.Num() != static_cast<int32>(ExpectedFaces.size()))
		{
			return false;
		}

		for (const EHyperTwistClassicCubeFace ExpectedFace : ExpectedFaces)
		{
			if (!PieceData.ColoredFaces.Contains(ExpectedFace))
			{
				return false;
			}
		}

		return true;
	}

	bool TryFindPiecePosition(
		const TArray<FHyperTwistClassicCubePieceData>& Pieces,
		const std::initializer_list<EHyperTwistClassicCubeFace> ExpectedFaces,
		FVector& OutPosition
	)
	{
		for (const FHyperTwistClassicCubePieceData& PieceData : Pieces)
		{
			if (HasExactFaces(PieceData, ExpectedFaces))
			{
				OutPosition = PieceData.GridPosition;
				return true;
			}
		}

		return false;
	}

	AHyperTwistClassicCubeActor* MakeCubeActor()
	{
		AHyperTwistClassicCubeActor* CubeActor =
			NewObject<AHyperTwistClassicCubeActor>(GetTransientPackage());
		if (CubeActor != nullptr)
		{
			CubeActor->ResetCube();
		}
		return CubeActor;
	}

	UHyperTwistTrainingSubsystem* MakeTimerSubsystem(const FString& SessionId)
	{
		UGameInstance* GameInstance = NewObject<UGameInstance>();
		if (GameInstance == nullptr)
		{
			return nullptr;
		}

		UHyperTwistTrainingSubsystem* TrainingSubsystem =
			NewObject<UHyperTwistTrainingSubsystem>(GameInstance);
		if (TrainingSubsystem == nullptr)
		{
			return nullptr;
		}

		const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartSampleClassicTrainingRun(
			TEXT("classic-cube-behavior-user"),
			SessionId,
			EHyperTwistTrainingDeliveryMode::Timer
		);
		return RunState.IsStructurallyValid() ? TrainingSubsystem : nullptr;
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistClassicCubeRotationTest,
	"HyperTwist.Simulation.ClassicCube.Behavior.Rotation",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistClassicCubeRotationTest::RunTest(const FString& Parameters)
{
	AHyperTwistClassicCubeActor* CubeActor =
		HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor();
	TestNotNull(TEXT("The classic cube actor must exist for rotation checks."), CubeActor);
	if (CubeActor == nullptr)
	{
		return false;
	}

	FVector BeforePosition;
	TestTrue(
		TEXT("The UFR corner must be discoverable before rotation."),
		HyperTwistClassicCubeBehaviorTestInternal::TryFindPiecePosition(
			CubeActor->GetPieceDataSnapshot(),
			{
				EHyperTwistClassicCubeFace::Up,
				EHyperTwistClassicCubeFace::Front,
				EHyperTwistClassicCubeFace::Right
			},
			BeforePosition
		)
	);
	TestTrue(
		TEXT("The UFR corner must start in the canonical solved position."),
		BeforePosition.Equals(FVector(1.0f, 1.0f, 1.0f), KINDA_SMALL_NUMBER)
	);

	CubeActor->RotateFace(
		EHyperTwistClassicCubeFace::Right,
		EHyperTwistRotationDirection::Clockwise
	);

	FVector AfterPosition;
	TestTrue(
		TEXT("The UFR corner must remain discoverable after rotation."),
		HyperTwistClassicCubeBehaviorTestInternal::TryFindPiecePosition(
			CubeActor->GetPieceDataSnapshot(),
			{
				EHyperTwistClassicCubeFace::Up,
				EHyperTwistClassicCubeFace::Front,
				EHyperTwistClassicCubeFace::Right
			},
			AfterPosition
		)
	);
	TestTrue(
		TEXT("A clockwise R turn must move the UFR corner to the expected grid position."),
		AfterPosition.Equals(FVector(1.0f, -1.0f, 1.0f), KINDA_SMALL_NUMBER)
	);
	TestFalse(TEXT("A single R turn must leave the cube unsolved."), CubeActor->IsSolved());
	TestEqual(
		TEXT("A single R turn must register one gameplay quarter-turn."),
		CubeActor->GetGameplayCompletedMoveCount(),
		1
	);
	TestTrue(
		TEXT("A single R turn must preserve a coherent 26-cubie, 54-sticker renderable state."),
		CubeActor->HasValidRenderableState()
	);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistClassicCubeRenderableStateInvariantTest,
	"HyperTwist.Simulation.ClassicCube.Behavior.RenderableStateInvariant",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistClassicCubeRenderableStateInvariantTest::RunTest(const FString& Parameters)
{
	AHyperTwistClassicCubeActor* CubeActor =
		HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor();
	TestNotNull(TEXT("The classic cube actor must exist for renderable-state checks."), CubeActor);
	if (CubeActor == nullptr)
	{
		return false;
	}

	FRandomStream ScrambleStream(0xA17FA11A);
	const TArray<FString> Moves =
		HyperTwistClassicCubeBehaviorTestInternal::GenerateDeterministicScramble(
			ScrambleStream,
			120);
	for (int32 MoveIndex = 0; MoveIndex < Moves.Num(); ++MoveIndex)
	{
		FHyperTwistClassicCubeMoveDescriptor MoveDescriptor;
		if (!TestTrue(
				*FString::Printf(TEXT("Move %d must parse for renderable-state validation."), MoveIndex),
				UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(
					Moves[MoveIndex],
					MoveDescriptor)))
		{
			return false;
		}
		if (!TestTrue(
				*FString::Printf(TEXT("Move %d must execute for renderable-state validation."), MoveIndex),
				CubeActor->ExecuteMoveDescriptorImmediately(MoveDescriptor)))
		{
			return false;
		}

		const FString ValidationError = CubeActor->GetRenderableStateValidationError();
		if (!TestTrue(
				*FString::Printf(
					TEXT("Move %d (%s) must preserve coherent cubie occupancy and outward sticker placement: %s"),
					MoveIndex,
					*Moves[MoveIndex],
					*ValidationError),
				ValidationError.IsEmpty()))
		{
			return false;
		}
	}

	AddInfo(TEXT("Validated cubie occupancy and all 54 outward stickers after 120 deterministic moves."));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistClassicCubeSolveTest,
	"HyperTwist.Simulation.ClassicCube.Behavior.Solve",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistClassicCubeSolveTest::RunTest(const FString& Parameters)
{
	AHyperTwistClassicCubeActor* CubeActor =
		HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor();
	TestNotNull(TEXT("The classic cube actor must exist for solve checks."), CubeActor);
	if (CubeActor == nullptr)
	{
		return false;
	}

	const FString SolvedState = CubeActor->GetSolverFaceletString();
	TestFalse(TEXT("The solved-state facelet string must not be empty."), SolvedState.IsEmpty());

	const TArray<FString> ScrambleMoves = {
		TEXT("R"),
		TEXT("U"),
		TEXT("R'"),
		TEXT("U'")
	};
	CubeActor->ApplyScrambleImmediately(ScrambleMoves);
	TestFalse(TEXT("The scramble sequence must unsolve the cube."), CubeActor->IsSolved());

	const TArray<FString> SolutionMoves = {
		TEXT("U"),
		TEXT("R"),
		TEXT("U'"),
		TEXT("R'")
	};
	for (const FString& Move : SolutionMoves)
	{
		TestTrue(
			TEXT("Each solve move must queue successfully."),
			CubeActor->TryQueueMoveNotation(Move)
		);
	}

	TestTrue(TEXT("The inverse sequence must solve the cube again."), CubeActor->IsSolved());
	TestEqual(
		TEXT("The solver facelet string must return to the canonical solved layout."),
		CubeActor->GetSolverFaceletString(),
		SolvedState
	);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistSolverAccuracyTest,
	"HyperTwist.Solver.Accuracy.RandomClassicStates",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters)
{
	TestTrue(
		TEXT("Solver initialization must succeed before random-state accuracy checks."),
		UHyperTwistSolverLibrary::InitializeSolver()
	);

	FRandomStream ScrambleStream(0xC1A551CC);
	for (int32 Iteration = 0; Iteration < 100; ++Iteration)
	{
		const int32 ScrambleLength = 8 + (Iteration % 5);
		const TArray<FString> Scramble =
			HyperTwistClassicCubeBehaviorTestInternal::GenerateDeterministicScramble(
				ScrambleStream,
				ScrambleLength
			);

		AHyperTwistClassicCubeActor* ValidationActor =
			HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor();
		if (!TestNotNull(
				*FString::Printf(
					TEXT("Random iteration %d must allocate a validation cube actor."),
					Iteration
				),
				ValidationActor))
		{
			return false;
		}

		ValidationActor->ApplyScrambleImmediately(Scramble);
		if (!TestFalse(
				*FString::Printf(
					TEXT("Random iteration %d must leave the validation cube unsolved after the deterministic scramble."),
					Iteration
				),
				ValidationActor->IsSolved()))
		{
			return false;
		}

		const FString FaceletString = ValidationActor->GetSolverFaceletString();
		if (!TestFalse(
				*FString::Printf(
					TEXT("Random iteration %d must export a non-empty runtime facelet state."),
					Iteration
				),
				FaceletString.IsEmpty()))
		{
			return false;
		}

		if (!TestTrue(
				*FString::Printf(
					TEXT("Random iteration %d must produce a valid runtime facelet string."),
					Iteration
				),
				UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString)))
		{
			return false;
		}

		const TArray<FString> InverseScramble =
			HyperTwistClassicCubeBehaviorTestInternal::BuildInverseMoveSequence(Scramble);
		if (!TestEqual(
				*FString::Printf(
					TEXT("Random iteration %d must derive a complete inverse scramble sequence."),
					Iteration
				),
				InverseScramble.Num(),
				Scramble.Num()))
		{
			return false;
		}

		if (!TestTrue(
				*FString::Printf(
					TEXT("Random iteration %d must remain solvable by the exact inverse scramble exported from runtime notation."),
					Iteration
				),
				UHyperTwistSolverLibrary::VerifySolution(FaceletString, InverseScramble)))
		{
			return false;
		}

		for (const FString& SolutionMove : InverseScramble)
		{
			if (!TestTrue(
					*FString::Printf(
						TEXT("Random iteration %d must accept each inverse-scramble move through the runtime notation path."),
						Iteration
					),
					ValidationActor->TryQueueMoveNotation(SolutionMove)))
			{
				return false;
			}
		}

		if (!TestTrue(
				*FString::Printf(
					TEXT("Random iteration %d must solve the runtime cube after the returned solver sequence is applied."),
					Iteration
				),
				ValidationActor->IsSolved()))
		{
			return false;
		}
	}

	AddInfo(TEXT(
		"Validated 100 deterministic runtime states by runtime facelet export, exact inverse-scramble verification, and runtime round-trip solve."
	));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistTimerAccuracyTest,
	"HyperTwist.Training.Timer.Accuracy",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistTimerAccuracyTest::RunTest(const FString& Parameters)
{
	UHyperTwistTrainingSubsystem* TrainingSubsystem =
		HyperTwistClassicCubeBehaviorTestInternal::MakeTimerSubsystem(TEXT("timer-accuracy-session"));
	TestNotNull(TEXT("The timer training subsystem must exist."), TrainingSubsystem);
	if (TrainingSubsystem == nullptr)
	{
		return false;
	}

	TestTrue(TEXT("The live timer must start successfully."), TrainingSubsystem->StartActiveLiveTimer());
	TestTrue(
		TEXT("The timer must advance into the solve phase for solve-duration checks."),
		TrainingSubsystem->AdvanceActiveLiveTimerToSolvePhase()
	);

	FPlatformProcess::Sleep(5.0);
	const FHyperTwistTrainingLiveTimerState TimerState =
		TrainingSubsystem->GetActiveLiveTimerState();
	TestTrue(
		TEXT("The solve timer must measure approximately five seconds of elapsed time."),
		TimerState.SolveElapsedMs >= 4850 && TimerState.SolveElapsedMs <= 5150
	);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistSpeechTranscriptionTest,
	"HyperTwist.Speech.Transcription.ClassicCubeMoveParsing",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistSpeechTranscriptionTest::RunTest(const FString& Parameters)
{
	UHyperTwistMockSpeechClient* SpeechClient = NewObject<UHyperTwistMockSpeechClient>();
	TestNotNull(TEXT("The mock speech client must exist."), SpeechClient);
	if (SpeechClient == nullptr)
	{
		return false;
	}

	FHyperTwistSpeechSessionConfig SessionConfig =
		UHyperTwistContractLibrary::MakeSampleSpeechSessionConfig();
	SessionConfig.SessionId = TEXT("speech-r-prime-session");
	SessionConfig.CommandHints = {TEXT("R prime")};

	FString OpenError;
	TestTrue(
		TEXT("The mock speech session must open cleanly."),
		SpeechClient->OpenSpeechSession(SessionConfig, OpenError)
	);
	TestTrue(TEXT("Opening the mock speech session must not report an error."), OpenError.IsEmpty());

	FHyperTwistSpeechUtteranceEnvelope Utterance;
	Utterance.SessionId = SessionConfig.SessionId;
	Utterance.UtteranceId = TEXT("speech-r-prime-utterance");
	Utterance.AudioRef = TEXT("memory://r-prime");
	Utterance.LocalCommandHints = {TEXT("R prime")};
	Utterance.SpeechStartMs = 0;
	Utterance.SpeechEndMs = 800;

	const FHyperTwistSpeechTranscriptResult TranscriptResult =
		SpeechClient->TranscribeSpeechUtterance(Utterance);
	FHyperTwistClassicCubeVoiceCommand VoiceCommand;
	TestTrue(
		TEXT("The mock transcript must parse into a classic-cube voice move command."),
		UHyperTwistClassicCubeCommandLibrary::TryParseVoiceCommand(
			TranscriptResult.TranscriptText,
			VoiceCommand
		)
	);
	TestEqual(
		TEXT("The parsed voice command must target the right face."),
		VoiceCommand.Move.Face,
		EHyperTwistClassicCubeFace::Right
	);
	TestEqual(
		TEXT("The parsed voice command must resolve as a counter-clockwise turn."),
		VoiceCommand.Move.Direction,
		EHyperTwistRotationDirection::CounterClockwise
	);

	FString CloseError;
	TestTrue(
		TEXT("The mock speech session must close cleanly."),
		SpeechClient->CloseSpeechSession(SessionConfig.SessionId, CloseError)
	);
	TestTrue(TEXT("Closing the mock speech session must not report an error."), CloseError.IsEmpty());
	return true;
}

#endif
