// 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 HyperTwistTwistyBound2TimelineTestInternal
{
	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-bound2");
		Snapshot.State = SampleState;
		Snapshot.DerivedHash = TEXT("snapshot-bound2-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-bound2-001");
		ReplayPacket.SessionId = TEXT("session-bound2-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(
	FHyperTwistTwistyBound2ObserverModelTest,
	"HyperTwist.CleanRoom.TwistyJs.Bound2.ObserverModel",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistTwistyBound2ObserverModelTest::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 2 observer test."),
		Viewer->LoadReplayPacket(HyperTwistTwistyBound2TimelineTestInternal::MakeReplayPacket())
	);

	TArray<FHyperTwistViewerPositionSnapshot> PositionEvents;
	TArray<FHyperTwistViewerDirectionState> DirectionEvents;
	TArray<FHyperTwistViewerJumpEvent> JumpEvents;

	Viewer->OnPositionSnapshotUpdatedNative.AddLambda(
		[&PositionEvents](const FHyperTwistViewerPositionSnapshot& Snapshot)
		{
			PositionEvents.Add(Snapshot);
		}
	);
	Viewer->OnDirectionStateChangedNative.AddLambda(
		[&DirectionEvents](const FHyperTwistViewerDirectionState& State)
		{
			DirectionEvents.Add(State);
		}
	);
	Viewer->OnPlaybackJumpedNative.AddLambda(
		[&JumpEvents](const FHyperTwistViewerJumpEvent& JumpEvent)
		{
			JumpEvents.Add(JumpEvent);
		}
	);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest());
	TestEqual(TEXT("Play should emit one direction-state event."), DirectionEvents.Num(), 1);
	TestEqual(
		TEXT("Play should switch the direction-state channel to forward playback."),
		DirectionEvents.Last().Direction,
		EHyperTwistViewerPlaybackDirection::Forward
	);
	TestTrue(TEXT("Play should mark continuous playback active."), DirectionEvents.Last().bIsContinuousPlayback);
	TestEqual(TEXT("Play should not emit a jump event by itself."), JumpEvents.Num(), 0);

	TestTrue(TEXT("Advance should move the playing cursor."), Viewer->AdvancePlaybackByMs(100));
	TestEqual(TEXT("Advancing the playing cursor should emit one position update."), PositionEvents.Num(), 1);
	TestEqual(TEXT("The first position update should advance to 100ms."), PositionEvents.Last().CurrentTimeMs, 100);
	TestEqual(TEXT("The first position update should keep the base boundary at the start."), PositionEvents.Last().BaseBoundaryTimeMs, 0);
	TestEqual(TEXT("The first position update should expose one in-flight move."), PositionEvents.Last().InFlightMoves.Num(), 1);
	TestEqual(TEXT("The in-flight move should be the first move."), PositionEvents.Last().InFlightMoves[0].MoveIndex, 0);
	TestTrue(
		TEXT("The first in-flight move should be halfway complete at 100ms of a 200ms segment."),
		FMath::IsNearlyEqual(PositionEvents.Last().InFlightMoves[0].Fraction, 0.5f)
	);
	TestEqual(TEXT("Continuous position updates should stay off the jump channel."), JumpEvents.Num(), 0);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePauseRequest());
	TestEqual(TEXT("Pause should emit a second direction-state event."), DirectionEvents.Num(), 2);
	TestTrue(TEXT("Pause should mark the direction-state channel paused."), DirectionEvents.Last().bIsPaused);

	TestFalse(TEXT("Advancing while paused should not move the cursor."), Viewer->AdvancePlaybackByMs(50));
	TestEqual(TEXT("Advancing while paused should not emit another position update."), PositionEvents.Num(), 1);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeStepBackRequest());
	TestEqual(TEXT("Step back should emit a third direction-state event."), DirectionEvents.Num(), 3);
	TestEqual(
		TEXT("Step back should switch the direction-state channel to backward stepping."),
		DirectionEvents.Last().Direction,
		EHyperTwistViewerPlaybackDirection::Backward
	);
	TestTrue(TEXT("Step back should mark the direction-state channel stepping."), DirectionEvents.Last().bIsStepping);
	TestEqual(TEXT("Step back should emit one jump event."), JumpEvents.Num(), 1);
	TestEqual(TEXT("The first jump event should record step-back semantics."), JumpEvents.Last().JumpKind, EHyperTwistViewerJumpKind::StepBack);
	TestEqual(TEXT("Step back should jump from 100ms to 0ms."), JumpEvents.Last().PreviousTimeMs, 100);
	TestEqual(TEXT("Step back should land at the start boundary."), JumpEvents.Last().NewTimeMs, 0);
	TestEqual(TEXT("Step back should also emit a second position update."), PositionEvents.Num(), 2);
	TestTrue(TEXT("The step-back position update should be a boundary snapshot."), PositionEvents.Last().bAtBoundary);
	TestEqual(TEXT("The step-back position update should clear in-flight progress."), PositionEvents.Last().InFlightMoves.Num(), 0);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeSeekRequest(2));
	TestEqual(TEXT("Seek should emit a second jump event."), JumpEvents.Num(), 2);
	TestEqual(TEXT("The second jump event should record seek semantics."), JumpEvents.Last().JumpKind, EHyperTwistViewerJumpKind::Seek);
	TestEqual(TEXT("Seeking to the last move index should jump to the terminal replay time."), JumpEvents.Last().NewTimeMs, 1000);
	TestEqual(TEXT("Seek should emit a fourth direction-state event."), DirectionEvents.Num(), 4);
	TestEqual(TEXT("Seek should restore forward direction when seeking later."), DirectionEvents.Last().Direction, EHyperTwistViewerPlaybackDirection::Forward);

	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistTwistyBound2CursorTimelineTest,
	"HyperTwist.CleanRoom.TwistyJs.Bound2.CursorTimeline",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistTwistyBound2CursorTimelineTest::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 2 cursor test."),
		Viewer->LoadReplayPacket(HyperTwistTwistyBound2TimelineTestInternal::MakeReplayPacket())
	);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest());
	TestTrue(TEXT("Cursor advance should move the playhead during continuous playback."), Viewer->AdvancePlaybackByMs(100));

	FHyperTwistViewerPositionSnapshot Snapshot = Viewer->GetPositionSnapshot();
	TestEqual(TEXT("A 100ms cursor advance should expose the correct current time."), Snapshot.CurrentTimeMs, 100);
	TestEqual(TEXT("The base boundary should stay at the start before the first move completes."), Snapshot.BaseCompletedMoveIndex, INDEX_NONE);
	TestEqual(TEXT("The first partial segment should expose one in-flight move."), Snapshot.InFlightMoves.Num(), 1);
	TestEqual(TEXT("The in-flight move should be the first move."), Snapshot.InFlightMoves[0].MoveIndex, 0);
	TestTrue(TEXT("The first partial segment should report a 0.5 fraction."), FMath::IsNearlyEqual(Snapshot.InFlightMoves[0].Fraction, 0.5f));

	TestTrue(TEXT("A second cursor advance should continue through the timeline."), Viewer->AdvancePlaybackByMs(150));
	Snapshot = Viewer->GetPositionSnapshot();
	TestEqual(TEXT("The cursor should now be at 250ms."), Snapshot.CurrentTimeMs, 250);
	TestEqual(TEXT("The base completed move should now be move 0."), Snapshot.BaseCompletedMoveIndex, 0);
	TestEqual(TEXT("The cursor should now interpolate the second move."), Snapshot.InFlightMoves[0].MoveIndex, 1);
	TestTrue(TEXT("The second move should be 25% complete at 250ms."), FMath::IsNearlyEqual(Snapshot.InFlightMoves[0].Fraction, 0.25f));

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePauseRequest());
	const int32 PausedTimeMs = Viewer->GetPlaybackState().CurrentTimeMs;
	TestFalse(TEXT("The cursor must not advance while paused."), Viewer->AdvancePlaybackByMs(200));
	TestEqual(TEXT("Pause must preserve the current timeline position."), Viewer->GetPlaybackState().CurrentTimeMs, PausedTimeMs);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeStepForwardRequest());
	Snapshot = Viewer->GetPositionSnapshot();
	TestEqual(TEXT("Step forward should land on the next move boundary, not a partial continuous time."), Snapshot.CurrentTimeMs, 400);
	TestTrue(TEXT("A move-boundary step should land on a boundary snapshot."), Snapshot.bAtBoundary);
	TestEqual(TEXT("Step forward should expose no in-flight move at the boundary."), Snapshot.InFlightMoves.Num(), 0);
	TestEqual(TEXT("The base completed move should now be move 1."), Snapshot.BaseCompletedMoveIndex, 1);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeStepBackRequest());
	Snapshot = Viewer->GetPositionSnapshot();
	TestEqual(TEXT("Step back should return to the previous move boundary."), Snapshot.CurrentTimeMs, 200);
	TestEqual(TEXT("Step back should restore move 0 as the completed boundary anchor."), Snapshot.BaseCompletedMoveIndex, 0);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakePlayRequest());
	TestTrue(TEXT("Resume should continue from the paused move boundary."), Viewer->AdvancePlaybackByMs(100));
	TestEqual(TEXT("Resume should continue from the paused boundary instead of restarting."), Viewer->GetPlaybackState().CurrentTimeMs, 300);

	Viewer->ApplyControlRequest(UHyperTwistViewerLibrary::MakeSeekRequest(2));
	Snapshot = Viewer->GetPositionSnapshot();
	TestEqual(TEXT("Seeking to the last move index should land on the terminal replay time."), Snapshot.CurrentTimeMs, 1000);
	TestTrue(TEXT("Seeking to the last move index should land at the end state."), Snapshot.bAtEnd);
	TestEqual(TEXT("The final completed boundary anchor should remain the last move."), Snapshot.BaseCompletedMoveIndex, 2);
	TestEqual(TEXT("The scrubber should remain coherent at the terminal end state."), UHyperTwistViewerLibrary::GetScrubberNormalizedPosition(Viewer->GetPlaybackState()), 1.0f);

	return true;
}

#endif // WITH_AUTOMATION_TESTS
