// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

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

#if WITH_AUTOMATION_TESTS

namespace HyperTwistTwistyBound3AdapterBootstrapTestInternal
{
	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;
	}

	FHyperTwistReplayPacket MakeReplayPacket()
	{
		const FHyperTwistPuzzleState SampleState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();

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

		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");
		SecondMovePayload.Source = TEXT("unit-test");

		FHyperTwistReplayMovePayload ThirdMovePayload;
		ThirdMovePayload.Notation = TEXT("F'");
		ThirdMovePayload.TransformationRef = TEXT("classic/f-prime");
		ThirdMovePayload.Source = TEXT("unit-test");

		FHyperTwistReplayPacket ReplayPacket;
		ReplayPacket.ReplayId = TEXT("replay-bound3-001");
		ReplayPacket.SessionId = TEXT("session-bound3-001");
		ReplayPacket.PuzzleDefinition = SampleState.Definition;
		ReplayPacket.CaptureMode = EHyperTwistCaptureMode::Imported;
		ReplayPacket.Events = {
			MakeReplayEvent(
				TEXT("evt-001"),
				1,
				0,
				EHyperTwistReplayEventType::StateSnapshot,
				SerializeStructToJson(Snapshot)
			),
			MakeReplayEvent(
				TEXT("evt-002"),
				2,
				0,
				EHyperTwistReplayEventType::TimerStart,
				TEXT("")
			),
			MakeReplayEvent(
				TEXT("evt-003"),
				3,
				200,
				EHyperTwistReplayEventType::Move,
				SerializeStructToJson(FirstMovePayload)
			),
			MakeReplayEvent(
				TEXT("evt-004"),
				4,
				400,
				EHyperTwistReplayEventType::Move,
				SerializeStructToJson(SecondMovePayload)
			),
			MakeReplayEvent(
				TEXT("evt-005"),
				5,
				700,
				EHyperTwistReplayEventType::Move,
				SerializeStructToJson(ThirdMovePayload)
			),
			MakeReplayEvent(
				TEXT("evt-006"),
				6,
				1000,
				EHyperTwistReplayEventType::SolveEnd,
				TEXT("")
			)
		};

		return ReplayPacket;
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistTwistyBound3AdapterBoundaryTest,
	"HyperTwist.CleanRoom.TwistyJs.Bound3.AdapterBoundary",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistTwistyBound3AdapterBoundaryTest::RunTest(const FString& Parameters)
{
	UHyperTwistPuzzleViewerComponent* Viewer = NewObject<UHyperTwistPuzzleViewerComponent>();
	TestNotNull(TEXT("Viewer component instance must be created."), Viewer);
	TestTrue(
		TEXT("Replay packet load must succeed for the Bound 3 adapter test."),
		Viewer->LoadReplayPacket(HyperTwistTwistyBound3AdapterBootstrapTestInternal::MakeReplayPacket())
	);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest());
	TestTrue(TEXT("Projection test playback advance should move the cursor."), Viewer->AdvancePlaybackByMs(250));

	FHyperTwistViewerClassicCubingAdapterProjection Projection;
	TestTrue(
		TEXT("The viewer must build a consumer-side classic-cubing adapter projection."),
		Viewer->TryBuildClassicCubingAdapterProjection(Projection)
	);
	TestTrue(TEXT("The adapter projection must be structurally valid."), Projection.IsStructurallyValid());
	TestEqual(TEXT("The semantic contract id must come from the landed classic-cubing lane."), Projection.SemanticContractId, FString(TEXT("classic-cubing-semantics-stack")));
	TestEqual(TEXT("The geometry contract id must come from the landed classic-cubing lane."), Projection.GeometryContractId, FString(TEXT("classic-cubing-geometry-stack")));
	TestEqual(TEXT("The viewer adapter contract id must come from the landed classic-cubing lane."), Projection.ViewerAdapterContractId, FString(TEXT("classic-cubing-viewer-adapter-stack")));
	TestEqual(TEXT("The projection should preserve the viewer's scene context id."), Projection.SceneContextId, Viewer->GetSceneContext().SceneContextId);
	TestEqual(TEXT("The projection should preserve the viewer's total timeline time."), Projection.TotalTimelineTimeMs, 1000);
	TestEqual(TEXT("The projection should preserve the current display move index."), Projection.CurrentDisplayMoveIndex, 1);
	TestEqual(TEXT("The projection should preserve the current position snapshot time."), Projection.PositionSnapshot.CurrentTimeMs, 250);
	TestEqual(TEXT("The deterministic move projection should include all replay moves."), Projection.ProjectedMoves.Num(), 3);
	TestEqual(TEXT("The first projected step should start at 0ms."), Projection.ProjectedMoves[0].StartsAtMs, 0);
	TestEqual(TEXT("The first projected step should complete at 200ms."), Projection.ProjectedMoves[0].CompletesAtMs, 200);
	TestEqual(TEXT("The second projected step should start at 200ms."), Projection.ProjectedMoves[1].StartsAtMs, 200);
	TestEqual(TEXT("The second projected step should complete at 400ms."), Projection.ProjectedMoves[1].CompletesAtMs, 400);
	TestEqual(TEXT("The third projected step should start at 400ms."), Projection.ProjectedMoves[2].StartsAtMs, 400);
	TestEqual(TEXT("The third projected step should complete at 700ms."), Projection.ProjectedMoves[2].CompletesAtMs, 700);
	TestTrue(TEXT("The projection should surface classic-cubing viewer lifecycle hooks."), Projection.LifecycleHooks.Contains(TEXT("timeline scrubbed")));
	TestTrue(TEXT("The projection should surface classic-cubing viewer surface tags."), Projection.ViewerSurfaceTags.Contains(TEXT("TwistyPlayer embed")));

	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistTwistyBound3HostBootstrapTest,
	"HyperTwist.CleanRoom.TwistyJs.Bound3.HostBootstrap",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistTwistyBound3HostBootstrapTest::RunTest(const FString& Parameters)
{
	UHyperTwistPuzzleViewerComponent* Viewer = NewObject<UHyperTwistPuzzleViewerComponent>();
	TestNotNull(TEXT("Viewer component instance must be created."), Viewer);
	TestTrue(
		TEXT("Replay packet load must succeed for the Bound 3 bootstrap test."),
		Viewer->LoadReplayPacket(HyperTwistTwistyBound3AdapterBootstrapTestInternal::MakeReplayPacket())
	);

	FHyperTwistViewerEmbeddedHostBootstrapRequest Request;
	Request.HostInstanceId = TEXT("compact-host-01");
	Request.BindingMode = EHyperTwistViewerEmbeddedHostBindingMode::ScissoredView;
	Request.bBindTimelineSurface = true;
	Request.bBindScrubberSurface = true;
	Request.bBindStatusSurface = true;
	Request.bAllowPointerInput = true;

	FHyperTwistViewerEmbeddedHostBootstrapState BootstrapState;
	TestTrue(
		TEXT("The viewer must prepare a minimal embedded host bootstrap state."),
		Viewer->TryBootstrapEmbeddedClassicCubingHost(Request, BootstrapState)
	);
	TestTrue(TEXT("The bootstrap state must be structurally valid."), BootstrapState.IsStructurallyValid());
	TestEqual(TEXT("The bootstrap state should preserve the host instance id."), BootstrapState.HostInstanceId, Request.HostInstanceId);
	TestEqual(TEXT("The bootstrap state should reference the classic-cubing viewer adapter contract."), BootstrapState.ViewerAdapterContractId, FString(TEXT("classic-cubing-viewer-adapter-stack")));
	TestEqual(TEXT("The bootstrap state should reference the landed browser viewer embed contract."), BootstrapState.BrowserEmbedContractId, FString(TEXT("browser-viewer-embed")));
	TestEqual(TEXT("The bootstrap state should reference the landed browser spatial scene contract."), BootstrapState.SpatialSceneContractId, FString(TEXT("browser-spatial-scene-stack")));
	TestEqual(TEXT("The bootstrap state should preserve the selected binding mode."), BootstrapState.BindingMode, EHyperTwistViewerEmbeddedHostBindingMode::ScissoredView);
	TestTrue(TEXT("The bootstrap state should mark the embedded host ready."), BootstrapState.bReadyForEmbed);
	TestTrue(TEXT("The bootstrap state should bind the timeline surface when requested."), BootstrapState.bTimelineSurfaceBound);
	TestTrue(TEXT("The bootstrap state should bind the scrubber surface when requested."), BootstrapState.bScrubberSurfaceBound);
	TestTrue(TEXT("The bootstrap state should bind the status surface when requested."), BootstrapState.bStatusSurfaceBound);
	TestTrue(TEXT("The bootstrap state should keep pointer input enabled when requested."), BootstrapState.bPointerInputEnabled);
	TestTrue(TEXT("The bootstrap state should carry classic-cubing adapter lifecycle hooks."), BootstrapState.AdapterLifecycleHooks.Contains(TEXT("viewer ready")));
	TestTrue(TEXT("The bootstrap state should carry browser embed lifecycle hook ids."), BootstrapState.BrowserLifecycleHookIds.Contains(TEXT("hook/model-loaded")));
	TestTrue(TEXT("The bootstrap state should carry spatial scene lifecycle stages."), BootstrapState.SpatialLifecycleStages.Contains(TEXT("create canvas root")));
	TestTrue(TEXT("The bootstrap state should carry scissored-view event routing support."), BootstrapState.SpatialEventSurfaceTags.Contains(TEXT("scissored view embedding")));
	TestEqual(TEXT("The stored bootstrap state should match the returned bootstrap id."), Viewer->GetHostBootstrapState().BootstrapId, BootstrapState.BootstrapId);
	TestEqual(TEXT("The stored bootstrap state should preserve the projection id."), Viewer->GetHostBootstrapState().AdapterProjectionId, BootstrapState.AdapterProjectionId);

	return true;
}

#endif // WITH_AUTOMATION_TESTS
