// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h"
#include "HyperTwistSimulation/HyperTwistViewerLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
#include "JsonObjectConverter.h"

#if WITH_AUTOMATION_TESTS

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

	FHyperTwistReplayEvent MakeReplayEvent(
		const FString& EventId,
		const int32 Sequence,
		const int32 TimeMs,
		const EHyperTwistReplayEventType EventType,
		const FString& PayloadJson
	)
	{
		FHyperTwistReplayEvent Event;
		Event.EventId = EventId;
		Event.Sequence = Sequence;
		Event.TimeMs = TimeMs;
		Event.EventType = EventType;
		Event.PayloadJson = PayloadJson;
		return Event;
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistTwistyBound1ReplayShellTest,
	"HyperTwist.CleanRoom.TwistyJs.Bound1.ReplayShell",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistTwistyBound1ReplayShellTest::RunTest(const FString& Parameters)
{
	const FHyperTwistPuzzleState SampleState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();

	FHyperTwistStateSnapshot Snapshot;
	Snapshot.SnapshotId = TEXT("snapshot-001");
	Snapshot.State = SampleState;
	Snapshot.DerivedHash = TEXT("snapshot-hash-001");

	FHyperTwistReplayMovePayload FirstMovePayload;
	FirstMovePayload.Notation = TEXT("R");
	FirstMovePayload.TransformationRef = TEXT("classic/r");
	FirstMovePayload.Source = TEXT("unit-test");

	FHyperTwistReplayMovePayload SecondMovePayload;
	SecondMovePayload.Notation = TEXT("U'");
	SecondMovePayload.TransformationRef = TEXT("classic/u-prime");
	SecondMovePayload.Source = TEXT("unit-test");

	FHyperTwistReplayPacket ReplayPacket;
	ReplayPacket.ReplayId = TEXT("replay-bound1-001");
	ReplayPacket.SessionId = TEXT("session-bound1-001");
	ReplayPacket.PuzzleDefinition = SampleState.Definition;
	ReplayPacket.CaptureMode = EHyperTwistCaptureMode::Imported;
	ReplayPacket.Events = {
		HyperTwistTwistyBound1ReplayShellTestInternal::MakeReplayEvent(
			TEXT("evt-001"),
			1,
			0,
			EHyperTwistReplayEventType::StateSnapshot,
			HyperTwistTwistyBound1ReplayShellTestInternal::SerializeStructToJson(Snapshot)
		),
		HyperTwistTwistyBound1ReplayShellTestInternal::MakeReplayEvent(
			TEXT("evt-002"),
			2,
			0,
			EHyperTwistReplayEventType::TimerStart,
			TEXT("")
		),
		HyperTwistTwistyBound1ReplayShellTestInternal::MakeReplayEvent(
			TEXT("evt-003"),
			3,
			250,
			EHyperTwistReplayEventType::Move,
			HyperTwistTwistyBound1ReplayShellTestInternal::SerializeStructToJson(FirstMovePayload)
		),
		HyperTwistTwistyBound1ReplayShellTestInternal::MakeReplayEvent(
			TEXT("evt-004"),
			4,
			500,
			EHyperTwistReplayEventType::Move,
			HyperTwistTwistyBound1ReplayShellTestInternal::SerializeStructToJson(SecondMovePayload)
		),
		HyperTwistTwistyBound1ReplayShellTestInternal::MakeReplayEvent(
			TEXT("evt-005"),
			5,
			900,
			EHyperTwistReplayEventType::SolveEnd,
			TEXT("")
		)
	};

	UHyperTwistPuzzleViewerComponent* Viewer = NewObject<UHyperTwistPuzzleViewerComponent>();
	TestNotNull(TEXT("Viewer component instance must be created."), Viewer);
	TestTrue(TEXT("Replay packet load must succeed for a structurally valid packet."), Viewer->LoadReplayPacket(ReplayPacket));
	TestTrue(TEXT("Viewer must report a loaded replay packet after replay load."), Viewer->HasLoadedReplayPacket());

	const FHyperTwistViewerPlaybackState InitialState = Viewer->GetPlaybackState();
	TestTrue(TEXT("Replay-backed playback state must stay structurally valid."), InitialState.IsStructurallyValid());
	TestEqual(TEXT("Replay load must expose the replay id through playback state."), InitialState.ReplayId, ReplayPacket.ReplayId);
	TestEqual(TEXT("Replay load must expose the puzzle id through playback state."), InitialState.PuzzleId, ReplayPacket.PuzzleDefinition.PuzzleId);
	TestEqual(TEXT("Replay load must derive the move count from replay events."), InitialState.TotalMoveCount, 2);
	TestEqual(TEXT("Replay load must initialize the playhead at the first move index."), InitialState.CurrentMoveIndex, 0);
	TestEqual(TEXT("Replay load must initialize current time to the start boundary."), InitialState.CurrentTimeMs, 0);
	TestEqual(TEXT("Replay load must derive total replay time from replay events."), InitialState.TotalTimeMs, 900);
	TestTrue(TEXT("Replay load should derive a scene context from a snapshot event when available."), InitialState.bSceneContextLoaded);
	TestEqual(TEXT("Initial replay-backed scrubber position must stay at the start boundary."), UHyperTwistViewerLibrary::GetScrubberNormalizedPosition(InitialState), 0.0f);
	TestEqual(TEXT("Timeline label must reflect replay-backed duration."), UHyperTwistViewerLibrary::GetTimelineLabel(InitialState), FString(TEXT("0.00s / 0.90s")));
	TestEqual(TEXT("Replay summary must count move events."), Viewer->GetReplaySummary().MoveCount, 2);
	TestEqual(TEXT("Replay summary must derive solve result state."), Viewer->GetReplaySummary().Result, FString(TEXT("completed")));
	TestTrue(
		TEXT("Replay-backed status label must include the completed replay result."),
		InitialState.StatusLabel.Contains(TEXT("completed"))
	);

	FHyperTwistViewerRuntimeSurface InitialRuntimeSurface;
	TestTrue(
		TEXT("Training runtime library must expose the replay-backed compact viewer surface."),
		UHyperTwistTrainingRuntimeLibrary::TryGetCompactReplayViewerSurface(Viewer, InitialRuntimeSurface)
	);
	TestTrue(TEXT("Runtime surface must stay structurally valid."), InitialRuntimeSurface.IsStructurallyValid());
	TestEqual(
		TEXT("Runtime surface must preserve the replay-backed playback state."),
		InitialRuntimeSurface.PlaybackState.ReplayId,
		ReplayPacket.ReplayId
	);
	TestEqual(
		TEXT("Runtime surface must preserve replay summary move count."),
		InitialRuntimeSurface.ReplaySummary.MoveCount,
		2
	);
	TestEqual(
		TEXT("Runtime surface must expose the start-boundary scrubber position."),
		InitialRuntimeSurface.ScrubberNormalizedPosition,
		0.0f
	);
	TestTrue(
		TEXT("Runtime surface control-bar readout must include the current move label."),
		InitialRuntimeSurface.ControlBarReadout.Contains(TEXT("R"))
	);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeSeekRequest(1));
	const FHyperTwistViewerPlaybackState SoughtState = Viewer->GetPlaybackState();
	TestEqual(TEXT("Seek must reposition the playhead within the replay-backed move list."), SoughtState.CurrentMoveIndex, 1);
	TestTrue(TEXT("Seeking to the last move must report the end boundary."), SoughtState.bAtEnd);
	TestEqual(TEXT("Seeking to the last move must advance current time to the replay end boundary."), SoughtState.CurrentTimeMs, 900);
	TestEqual(TEXT("Replay-backed scrubber should reach 1.0 at the end boundary."), UHyperTwistViewerLibrary::GetScrubberNormalizedPosition(SoughtState), 1.0f);

	FHyperTwistViewerRuntimeSurface SoughtRuntimeSurface;
	TestTrue(
		TEXT("Training runtime library must continue to expose the replay-backed surface after seek."),
		UHyperTwistTrainingRuntimeLibrary::TryGetCompactReplayViewerSurface(Viewer, SoughtRuntimeSurface)
	);
	TestEqual(
		TEXT("Runtime surface must expose the current move label after seek."),
		SoughtRuntimeSurface.CurrentMoveEntry.DisplayLabel,
		FString(TEXT("U'"))
	);
	TestEqual(
		TEXT("Runtime surface must expose the end-boundary scrubber position after seek."),
		SoughtRuntimeSurface.ScrubberNormalizedPosition,
		1.0f
	);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeStopRequest());
	const FHyperTwistViewerPlaybackState StoppedState = Viewer->GetPlaybackState();
	TestEqual(TEXT("Stop must rewind the replay-backed shell to the first move index."), StoppedState.CurrentMoveIndex, 0);
	TestEqual(TEXT("Stop must rewind replay-backed current time to the start boundary."), StoppedState.CurrentTimeMs, 0);
	TestFalse(TEXT("Stop should clear the end-boundary flag after rewind."), StoppedState.bAtEnd);

	return true;
}

#endif // WITH_AUTOMATION_TESTS
