// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

#include "HyperTwistTraining/HyperTwistTrainingAnalyticsLibrary.h"

#if WITH_AUTOMATION_TESTS

namespace HyperTwistCubeDeskBound2SessionAnalyticsTestInternal
{
	const FHyperTwistTrainingAnalyticsPanelContract* FindPanelById(
		const FHyperTwistTrainingAnalyticsPanelContractBundle& Bundle,
		const FString& PanelId
	)
	{
		for (const FHyperTwistTrainingAnalyticsPanelContract& Panel : Bundle.Panels)
		{
			if (Panel.PanelId == PanelId)
			{
				return &Panel;
			}
		}

		return nullptr;
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistCubeDeskBound2SessionDomainTest,
	"HyperTwist.CleanRoom.CubeDesk.Bound2.SessionDomain",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistCubeDeskBound2SessionDomainTest::RunTest(const FString& Parameters)
{
	FHyperTwistTrainingSession Session;
	Session.TrainingSessionId = TEXT("bound2-session");
	Session.DeckId = TEXT("deck/pll");
	Session.UserId = TEXT("bound2-user");
	Session.Mode = EHyperTwistTrainingDeliveryMode::Timer;
	Session.SessionState = EHyperTwistTrainingSessionState::Active;
	Session.CurrentCaseId = TEXT("pll-aa");
	Session.AttemptCount = 1;

	FHyperTwistTrainingSessionSummary Summary;
	Summary.TrainingSessionId = Session.TrainingSessionId;
	Summary.DeckId = Session.DeckId;
	Summary.UserId = Session.UserId;
	Summary.SessionState = Session.SessionState;
	Summary.AttemptCount = 1;
	Summary.CompletedAttemptCount = 1;
	Summary.SuccessCount = 1;
	Summary.SuccessRate = 100.0f;
	Summary.LastCaseId = Session.CurrentCaseId;
	Summary.LastCompletedAtUtc = TEXT("2026-05-19T11:00:00Z");

	FHyperTwistTrainingAttempt SolveAttempt;
	SolveAttempt.AttemptId = TEXT("attempt-1");
	SolveAttempt.TrainingSessionId = Session.TrainingSessionId;
	SolveAttempt.CaseId = Session.CurrentCaseId;
	SolveAttempt.Result = EHyperTwistTrainingAttemptResult::Success;
	SolveAttempt.TotalTimeMs = 1789;
	SolveAttempt.ExecutionTimeMs = 1320;
	SolveAttempt.RecognitionTimeMs = 469;
	SolveAttempt.ReplayId = TEXT("replay-1");
	SolveAttempt.CompletedAtUtc = Summary.LastCompletedAtUtc;

	FHyperTwistTrainingCoachPanelState CoachPanelState;
	CoachPanelState.PreferredGuidanceLane = EHyperTwistTrainingCoachGuidanceLane::FollowUp;
	CoachPanelState.PreferredGuidanceSourceLabel = TEXT("follow-up-brief");

	FHyperTwistTrainingAnalyticsReportBundle ReportBundle;
	ReportBundle.PrimaryReportId = TEXT("analytics/session-outcome-balance");

	const FHyperTwistTrainingSessionDomainBundle FollowUpBundle =
		UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
			Session,
			Summary,
			SolveAttempt,
			FHyperTwistTrainingTemplateLaunchProvenance(),
			CoachPanelState,
			FHyperTwistTrainingMethodDrillRunState(),
			ReportBundle
		);

	TestTrue(TEXT("Follow-up bundle must resolve as a structurally valid session-domain bundle."), FollowUpBundle.IsStructurallyValid());
	TestEqual(
		TEXT("Active follow-up sessions must resolve the new owned session kind."),
		FollowUpBundle.SessionKind,
		EHyperTwistTrainingSessionKind::CoachFollowUp
	);
	TestEqual(
		TEXT("Follow-up game-session ids must stay deterministic from the training session id."),
		FollowUpBundle.GameSession.GameSessionId,
		TEXT("game-session/coach-follow-up/bound2-session")
	);
	TestTrue(TEXT("Follow-up bundle must expose the solve/session split explicitly."), FollowUpBundle.bHasSolveSession);
	TestEqual(
		TEXT("Solve-session attempt ids must survive the domain separation layer."),
		FollowUpBundle.SolveSession.AttemptId,
		SolveAttempt.AttemptId
	);
	TestEqual(
		TEXT("The primary analytics report id must stay attached to the derived game-session summary."),
		FollowUpBundle.GameSession.PrimaryAnalyticsReportId,
		ReportBundle.PrimaryReportId
	);

	FHyperTwistTrainingTemplateLaunchProvenance ReviewTemplate;
	ReviewTemplate.TemplateId = TEXT("template-review");
	ReviewTemplate.SourceReviewPlanId = TEXT("review-plan-1");
	ReviewTemplate.Title = TEXT("Review Template");
	ReviewTemplate.FocusDeckId = Session.DeckId;
	ReviewTemplate.SuggestedMode = Session.Mode;
	ReviewTemplate.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
	ReviewTemplate.RecommendedCaseCount = 5;
	ReviewTemplate.bPreferReviewRun = true;

	const FHyperTwistTrainingSessionDomainBundle ReviewBundle =
		UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
			Session,
			Summary,
			SolveAttempt,
			ReviewTemplate,
			FHyperTwistTrainingCoachPanelState(),
			FHyperTwistTrainingMethodDrillRunState(),
			ReportBundle
		);

	TestEqual(
		TEXT("Review-backed template launches must not collapse back into a generic training session."),
		ReviewBundle.SessionKind,
		EHyperTwistTrainingSessionKind::ReviewRun
	);
	TestTrue(
		TEXT("Review-backed template launches must mark the derived game session as review-backed."),
		ReviewBundle.GameSession.bReviewBacked
	);

	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistCubeDeskBound2AnalyticsPanelsTest,
	"HyperTwist.CleanRoom.CubeDesk.Bound2.AnalyticsPanels",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistCubeDeskBound2AnalyticsPanelsTest::RunTest(const FString& Parameters)
{
	FHyperTwistTrainingSession Session;
	Session.TrainingSessionId = TEXT("bound2-panel-session");
	Session.DeckId = TEXT("deck/oll");
	Session.UserId = TEXT("bound2-user");
	Session.Mode = EHyperTwistTrainingDeliveryMode::Timer;
	Session.SessionState = EHyperTwistTrainingSessionState::Completed;
	Session.CurrentCaseId = TEXT("oll-27");

	FHyperTwistTrainingSessionSummary Summary;
	Summary.TrainingSessionId = Session.TrainingSessionId;
	Summary.DeckId = Session.DeckId;
	Summary.UserId = Session.UserId;
	Summary.SessionState = Session.SessionState;
	Summary.AttemptCount = 8;
	Summary.CompletedAttemptCount = 8;
	Summary.SuccessCount = 6;
	Summary.FailureCount = 1;
	Summary.TimeoutCount = 1;
	Summary.TotalTimeMs = 18100;
	Summary.BestTotalTimeMs = 1950;
	Summary.AverageTotalTimeMs = 2262;
	Summary.SuccessRate = 75.0f;
	Summary.LastCaseId = Session.CurrentCaseId;
	Summary.LastCompletedAtUtc = TEXT("2026-05-19T11:05:00Z");

	FHyperTwistTrainingDeckHistorySummary DeckHistorySummary;
	DeckHistorySummary.UserId = Session.UserId;
	DeckHistorySummary.DeckId = Session.DeckId;
	DeckHistorySummary.SessionCount = 4;
	DeckHistorySummary.CompletedSessionCount = 3;
	DeckHistorySummary.TotalAttemptCount = 26;
	DeckHistorySummary.BestTotalTimeMs = 1890;
	DeckHistorySummary.BestRawSolveTimeMs = 1710;
	DeckHistorySummary.AverageTotalTimeMs = 2330;
	DeckHistorySummary.AverageRawSolveTimeMs = 2140;
	DeckHistorySummary.AverageInspectionTimeMs = 190;
	DeckHistorySummary.LastSessionId = Session.TrainingSessionId;
	DeckHistorySummary.LastCompletedAtUtc = Summary.LastCompletedAtUtc;

	FHyperTwistTrainingRollingWindowStat RollingWindow;
	RollingWindow.WindowLabel = TEXT("mo3");
	RollingWindow.WindowSize = 3;
	RollingWindow.SolveSampleCount = 3;
	RollingWindow.bWindowFilled = true;
	RollingWindow.CurrentAverageTotalTimeMs = 2180;
	RollingWindow.CurrentAverageRawSolveTimeMs = 2050;
	RollingWindow.BestAverageTotalTimeMs = 2090;
	RollingWindow.BestAverageRawSolveTimeMs = 1980;

	FHyperTwistTrainingTimingTrendSummary TimingTrendSummary;
	TimingTrendSummary.UserId = Session.UserId;
	TimingTrendSummary.DeckId = Session.DeckId;
	TimingTrendSummary.ReferenceUtc = Summary.LastCompletedAtUtc;
	TimingTrendSummary.SolveCount = 12;
	TimingTrendSummary.LastSolveTotalTimeMs = 2140;
	TimingTrendSummary.LastSolveRawSolveTimeMs = 2010;
	TimingTrendSummary.BestSingleTotalTimeMs = 1890;
	TimingTrendSummary.BestSingleRawSolveTimeMs = 1710;
	TimingTrendSummary.WorstSingleTotalTimeMs = 2780;
	TimingTrendSummary.WorstSingleRawSolveTimeMs = 2510;
	TimingTrendSummary.MeanTotalTimeMs = 2295;
	TimingTrendSummary.MeanRawSolveTimeMs = 2148;
	TimingTrendSummary.LastSolveAtUtc = Summary.LastCompletedAtUtc;
	TimingTrendSummary.RollingWindows.Add(RollingWindow);

	FHyperTwistTrainingPersonalBestSummary PersonalBestSummary;
	PersonalBestSummary.UserId = Session.UserId;
	PersonalBestSummary.DeckId = Session.DeckId;
	PersonalBestSummary.ReferenceUtc = Summary.LastCompletedAtUtc;
	PersonalBestSummary.BestSingleSessionId = Session.TrainingSessionId;
	PersonalBestSummary.BestSingleTotalTimeMs = 1890;
	PersonalBestSummary.BestSingleRawSolveTimeMs = 1710;
	PersonalBestSummary.BestAverageSessionId = Session.TrainingSessionId;
	PersonalBestSummary.BestSessionAverageTotalTimeMs = 2050;
	PersonalBestSummary.BestSessionAverageRawSolveTimeMs = 1930;
	PersonalBestSummary.LastPersonalBestAtUtc = Summary.LastCompletedAtUtc;
	PersonalBestSummary.bHasTimedPersonalBest = true;

	FHyperTwistTrainingDeckScopedStats DeckScopedStats;
	DeckScopedStats.UserId = Session.UserId;
	DeckScopedStats.DeckId = Session.DeckId;
	DeckScopedStats.ReferenceUtc = Summary.LastCompletedAtUtc;
	DeckScopedStats.SessionCount = 4;
	DeckScopedStats.CompletedRunCount = 3;
	DeckScopedStats.TrackedCaseCount = 21;
	DeckScopedStats.AttemptCount = 26;
	DeckScopedStats.SolveCount = 20;
	DeckScopedStats.SuccessCount = 17;
	DeckScopedStats.FailureCount = 5;
	DeckScopedStats.TimeoutCount = 2;
	DeckScopedStats.DNFCount = 1;
	DeckScopedStats.AverageTotalTimeMs = 2330;
	DeckScopedStats.AverageRawSolveTimeMs = 2140;
	DeckScopedStats.RecognitionAttemptCount = 7;
	DeckScopedStats.PrimaryDeliveryMode = Session.Mode;

	FHyperTwistTrainingCaseScopedStats CaseScopedStats;
	CaseScopedStats.UserId = Session.UserId;
	CaseScopedStats.DeckId = Session.DeckId;
	CaseScopedStats.CaseId = Session.CurrentCaseId;
	CaseScopedStats.ReferenceUtc = Summary.LastCompletedAtUtc;
	CaseScopedStats.AttemptCount = 3;
	CaseScopedStats.SolveCount = 3;
	CaseScopedStats.SuccessCount = 2;
	CaseScopedStats.FailureCount = 1;
	CaseScopedStats.AverageTotalTimeMs = 2410;
	CaseScopedStats.AverageRawSolveTimeMs = 2260;
	CaseScopedStats.RecognitionAttemptCount = 1;
	CaseScopedStats.PrimaryDeliveryMode = Session.Mode;
	CaseScopedStats.LastAttemptAtUtc = Summary.LastCompletedAtUtc;
	CaseScopedStats.LastSuccessAtUtc = Summary.LastCompletedAtUtc;

	FHyperTwistTrainingLiveTimerState LiveTimerState;
	LiveTimerState.TrainingSessionId = Session.TrainingSessionId;
	LiveTimerState.UserId = Session.UserId;
	LiveTimerState.DeckId = Session.DeckId;
	LiveTimerState.CaseId = Session.CurrentCaseId;
	LiveTimerState.LastUpdatedAtUtc = Summary.LastCompletedAtUtc;
	LiveTimerState.Phase = EHyperTwistTrainingLiveTimerPhase::Solving;
	LiveTimerState.SolveElapsedMs = 4120;
	FHyperTwistTrainingSplitCapture InspectionSplit;
	InspectionSplit.PhaseId = TEXT("inspection");
	InspectionSplit.Label = TEXT("Inspection");
	InspectionSplit.TimestampMs = 1200;
	LiveTimerState.SplitCaptures.Add(InspectionSplit);
	FHyperTwistTrainingSplitCapture ExecutionSplit;
	ExecutionSplit.PhaseId = TEXT("execution");
	ExecutionSplit.Label = TEXT("Execution");
	ExecutionSplit.TimestampMs = 2780;
	LiveTimerState.SplitCaptures.Add(ExecutionSplit);

	const FHyperTwistTrainingAnalyticsReportBundle ReportBundle =
		UHyperTwistTrainingAnalyticsLibrary::BuildActiveTrainingAnalyticsReportBundle(
			Summary,
			DeckHistorySummary,
			TimingTrendSummary,
			PersonalBestSummary,
			DeckScopedStats,
			CaseScopedStats,
			LiveTimerState,
			FHyperTwistReplayReviewAnalytics(),
			FHyperTwistRecognitionReplaySummary()
		);

	TestTrue(TEXT("The retained echarts-backed report bundle must stay structurally valid."), ReportBundle.IsStructurallyValid());
	TestEqual(
		TEXT("The primary report id must remain the bounded session-outcome owner."),
		ReportBundle.PrimaryReportId,
		TEXT("analytics/session-outcome-balance")
	);
	TestEqual(TEXT("All six bounded report contracts must still be emitted when their inputs are valid."), ReportBundle.Reports.Num(), 6);

	const FHyperTwistTrainingSessionDomainBundle SessionDomainBundle =
		UHyperTwistTrainingAnalyticsLibrary::BuildSessionDomainBundle(
			Session,
			Summary,
			FHyperTwistTrainingAttempt(),
			FHyperTwistTrainingTemplateLaunchProvenance(),
			FHyperTwistTrainingCoachPanelState(),
			FHyperTwistTrainingMethodDrillRunState(),
			ReportBundle
		);
	const FHyperTwistTrainingAnalyticsPanelContractBundle PanelBundle =
		UHyperTwistTrainingAnalyticsLibrary::BuildAnalyticsPanelContractBundle(
			SessionDomainBundle,
			ReportBundle
		);

	TestTrue(TEXT("Panel bundle must be structurally valid when report inputs are valid."), PanelBundle.IsStructurallyValid());
	TestEqual(
		TEXT("Primary panel ids must be stable first-party names over the existing report ids."),
		PanelBundle.PrimaryPanelId,
		TEXT("panel/training-session-overview")
	);
	TestEqual(TEXT("Panel contracts must stay one-to-one with the retained report contracts."), PanelBundle.Panels.Num(), 6);

	const FHyperTwistTrainingAnalyticsPanelContract* SessionOverviewPanel =
		HyperTwistCubeDeskBound2SessionAnalyticsTestInternal::FindPanelById(
			PanelBundle,
			TEXT("panel/training-session-overview")
		);
	TestNotNull(TEXT("Session overview panel contract must be discoverable by its stable panel id."), SessionOverviewPanel);
	if (SessionOverviewPanel != nullptr)
	{
		TestEqual(
			TEXT("Session overview panel contract must preserve the original report id."),
			SessionOverviewPanel->ReportId,
			TEXT("analytics/session-outcome-balance")
		);
		TestTrue(TEXT("Session overview panel must be marked as primary."), SessionOverviewPanel->bPrimaryPanel);
	}

	const FHyperTwistTrainingAnalyticsPanelContract* LiveSplitPanel =
		HyperTwistCubeDeskBound2SessionAnalyticsTestInternal::FindPanelById(
			PanelBundle,
			TEXT("panel/live-split-ladder")
		);
	TestNotNull(TEXT("Live split panel contract must be discoverable by its stable panel id."), LiveSplitPanel);
	if (LiveSplitPanel != nullptr)
	{
		TestEqual(
			TEXT("Live split panel contract must preserve the retained live split report id."),
			LiveSplitPanel->ReportId,
			TEXT("analytics/live-split-ladder")
		);
	}

	return true;
}

#endif
