// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

#include "Engine/GameInstance.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "UObject/UnrealType.h"

#if WITH_AUTOMATION_TESTS

namespace HyperTwistHandyPhase6RAUTestInternal
{
	FHyperTwistTrainingDeck MakeSpeechDeck()
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = TEXT("phase6r-au/handy-local-model-catalog-shell");
		Deck.Title = TEXT("Phase 6R-AU Handy Local Model Catalog Shell");
		Deck.DeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};

		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = TEXT("phase6r-au-case");
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Sequence;
		TrainingCase.PromptLabel = TEXT("Phase 6R-AU Coach Speech");
		TrainingCase.AllowedDeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};
		Deck.Cases = {TrainingCase};
		return Deck;
	}

	void ForceMockSpeechClient(UHyperTwistTrainingSubsystem* TrainingSubsystem)
	{
		if (TrainingSubsystem == nullptr)
		{
			return;
		}

		if (FStrProperty* SpeechClientKindProperty = FindFProperty<FStrProperty>(
			UHyperTwistTrainingSubsystem::StaticClass(),
			TEXT("CompanionSpeechClientKind")
		))
		{
			SpeechClientKindProperty->SetPropertyValue_InContainer(TrainingSubsystem, TEXT("mock"));
		}
	}

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

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

		ForceMockSpeechClient(TrainingSubsystem);
		const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck(
			MakeSpeechDeck(),
			TEXT("phase6r-au-user"),
			SessionId,
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		);
		return RunState.IsStructurallyValid() ? TrainingSubsystem : nullptr;
	}

	bool HasCapability(const TArray<FString>& Capabilities, const FString& ExpectedCapability)
	{
		return Capabilities.Contains(ExpectedCapability);
	}

	bool HasActionId(const TArray<FString>& ActionIds, const FString& ExpectedActionId)
	{
		return ActionIds.Contains(ExpectedActionId);
	}

	bool HasProfileActionBinding(
		const TArray<FHyperTwistSpeechShellActionBinding>& ActionBindings,
		const FString& ExpectedActionId
	)
	{
		return ActionBindings.ContainsByPredicate(
			[&ExpectedActionId](const FHyperTwistSpeechShellActionBinding& Binding)
			{
				return Binding.ActionId == ExpectedActionId;
			}
		);
	}

	const FHyperTwistSpeechExternalDictationModelCatalogEntry* FindCatalogEntry(
		const TArray<FHyperTwistSpeechExternalDictationModelCatalogEntry>& Entries,
		const FString& PayloadId
	)
	{
		return Entries.FindByPredicate(
			[&PayloadId](const FHyperTwistSpeechExternalDictationModelCatalogEntry& Entry)
			{
				return Entry.PayloadId == PayloadId;
			}
		);
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistHandyPhase6RAUSessionConfigTest,
	"HyperTwist.Permissive.Handy.Phase6R.AU.SessionConfig",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistHandyPhase6RAUSessionConfigTest::RunTest(const FString& Parameters)
{
	UHyperTwistTrainingSubsystem* TrainingSubsystem =
		HyperTwistHandyPhase6RAUTestInternal::MakeSpeechSubsystem(TEXT("phase6r-au-config-session"));
	TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-AU."), TrainingSubsystem);
	if (TrainingSubsystem == nullptr)
	{
		return false;
	}

	FString OpenError;
	TestTrue(
		TEXT("The companion speech session must open for the Handy local-model-catalog posture."),
		TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError)
	);
	TestTrue(TEXT("Opening the speech session must not report an error."), OpenError.IsEmpty());

	const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
		TrainingSubsystem->GetActiveCompanionSpeechSessionState();
	const FHyperTwistSpeechExternalDictationShellProfile& Profile =
		SessionState.SessionConfig.ExternalDictationShellProfileDefinition;

	TestTrue(TEXT("The external dictation shell profile must remain structurally valid."), Profile.IsStructurallyValid());
	TestEqual(
		TEXT("The local model catalog surface must stay bounded to review posture."),
		Profile.LocalModelCatalogSurfaceMode,
		TEXT("local-model-catalog-review")
	);
	TestEqual(
		TEXT("The local model integrity surface must stay bounded to payload-integrity review."),
		Profile.ModelIntegritySurfaceMode,
		TEXT("payload-integrity-review")
	);
	TestEqual(
		TEXT("The local model unload surface must stay bounded to sidecar unload-policy review."),
		Profile.ModelUnloadSurfaceMode,
		TEXT("sidecar-model-unload-policy")
	);
	TestTrue(
		TEXT("The shell profile must expose the local model catalog inspection action."),
		HyperTwistHandyPhase6RAUTestInternal::HasProfileActionBinding(
			Profile.ActionBindings,
			TEXT("inspect-local-model-catalog")
		)
	);
	TestTrue(
		TEXT("The shell profile must expose the payload-integrity review action."),
		HyperTwistHandyPhase6RAUTestInternal::HasProfileActionBinding(
			Profile.ActionBindings,
			TEXT("review-local-model-integrity")
		)
	);
	TestTrue(
		TEXT("The shell profile must expose the unload-policy review action."),
		HyperTwistHandyPhase6RAUTestInternal::HasProfileActionBinding(
			Profile.ActionBindings,
			TEXT("review-local-model-unload-policy")
		)
	);
	TestTrue(
		TEXT("The session config must preserve a structurally valid payload-custody profile."),
		SessionState.SessionConfig.PayloadCustodyProfileDefinition.IsStructurallyValid()
	);
	TestTrue(
		TEXT("Speech health must expose the local model catalog shell capability."),
		HyperTwistHandyPhase6RAUTestInternal::HasCapability(
			SessionState.ServiceHealth.Capabilities,
			TEXT("speechLocalModelCatalogShell")
		)
	);
	TestTrue(
		TEXT("Speech health must expose the model integrity shell capability."),
		HyperTwistHandyPhase6RAUTestInternal::HasCapability(
			SessionState.ServiceHealth.Capabilities,
			TEXT("speechModelIntegrityShell")
		)
	);
	TestTrue(
		TEXT("Speech health must expose the model unload-policy shell capability."),
		HyperTwistHandyPhase6RAUTestInternal::HasCapability(
			SessionState.ServiceHealth.Capabilities,
			TEXT("speechModelUnloadPolicyShell")
		)
	);
	TestTrue(
		TEXT("Speech health must preserve payload-custody capability."),
		HyperTwistHandyPhase6RAUTestInternal::HasCapability(
			SessionState.ServiceHealth.Capabilities,
			TEXT("downloadableModelPayloadCustody")
		)
	);
	TestTrue(
		TEXT("Speech health must preserve payload-integrity review capability."),
		HyperTwistHandyPhase6RAUTestInternal::HasCapability(
			SessionState.ServiceHealth.Capabilities,
			TEXT("payloadIntegrityReview")
		)
	);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistHandyPhase6RAUShellStateTest,
	"HyperTwist.Permissive.Handy.Phase6R.AU.LocalModelCatalogShellState",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistHandyPhase6RAUShellStateTest::RunTest(const FString& Parameters)
{
	UHyperTwistTrainingSubsystem* TrainingSubsystem =
		HyperTwistHandyPhase6RAUTestInternal::MakeSpeechSubsystem(TEXT("phase6r-au-shell-session"));
	TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-AU."), TrainingSubsystem);
	if (TrainingSubsystem == nullptr)
	{
		return false;
	}

	FString OpenError;
	TestTrue(
		TEXT("The companion speech session must open for local-model-catalog inspection."),
		TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError)
	);
	TestTrue(TEXT("Opening the speech session must not report an error."), OpenError.IsEmpty());

	const FHyperTwistSpeechExternalDictationShellState ShellState =
		TrainingSubsystem->GetActiveCompanionSpeechSessionState().ExternalDictationShellState;
	TestTrue(TEXT("The external dictation shell state must remain structurally valid."), ShellState.IsStructurallyValid());
	TestEqual(
		TEXT("The shell must preserve the payload-custody profile id."),
		ShellState.PayloadCustodyProfileId,
		TEXT("downloadable-model-payload-custody-v1")
	);
	TestEqual(
		TEXT("The shell must preserve the local model catalog surface id."),
		ShellState.LocalModelCatalogSurfaceModeId,
		TEXT("local-model-catalog-review")
	);
	TestEqual(
		TEXT("The shell must preserve the model integrity surface id."),
		ShellState.ModelIntegritySurfaceModeId,
		TEXT("payload-integrity-review")
	);
	TestEqual(
		TEXT("The shell must preserve the model unload surface id."),
		ShellState.ModelUnloadSurfaceModeId,
		TEXT("sidecar-model-unload-policy")
	);
	TestEqual(
		TEXT("The shell must retain two bounded catalog entries."),
		ShellState.ModelCatalogEntryCount,
		2
	);
	TestEqual(
		TEXT("The shell must retain one optional bounded catalog entry."),
		ShellState.OptionalModelCatalogEntryCount,
		1
	);
	TestEqual(
		TEXT("The shell must keep runtime download deferred for both catalog entries."),
		ShellState.DownloadDeferredModelEntryCount,
		2
	);
	TestEqual(
		TEXT("The shell must require integrity review for the primary whisper payload."),
		ShellState.IntegrityReviewRequiredEntryCount,
		1
	);
	TestEqual(
		TEXT("The shell must preserve the primary payload selection."),
		ShellState.SelectedPrimaryPayloadId,
		TEXT("speech-model/whisper.cpp/base.en")
	);
	TestTrue(TEXT("The shell must surface the local model catalog card."), ShellState.bLocalModelCatalogVisible);
	TestTrue(TEXT("The shell must surface the model integrity card."), ShellState.bModelIntegrityVisible);
	TestTrue(TEXT("The shell must surface the model unload-policy card."), ShellState.bModelUnloadPolicyVisible);
	TestTrue(TEXT("The shell must keep the bounded model catalog ready for review."), ShellState.bCatalogReady);
	TestTrue(TEXT("The shell must keep runtime download deferred."), ShellState.bRuntimeDownloadDeferred);
	TestTrue(TEXT("The shell must keep bundled payload redistribution deferred."), ShellState.bBundledPayloadRedistributionDeferred);
	TestTrue(TEXT("The shell must preserve user-managed payload support."), ShellState.bSupportsUserManagedPayloads);
	TestTrue(TEXT("The shell must preserve sidecar-managed payload support."), ShellState.bSupportsSidecarManagedPayloads);
	TestTrue(TEXT("The shell must preserve integrity-verification requirements."), ShellState.bIntegrityVerificationRequired);
	TestTrue(TEXT("The shell must keep unload actions deferred to sidecar/out-of-packet policy."), ShellState.bUnloadActionsDeferredToSidecar);
	TestTrue(
		TEXT("The shell must expose the local model catalog action."),
		HyperTwistHandyPhase6RAUTestInternal::HasActionId(
			ShellState.AvailableActionIds,
			TEXT("inspect-local-model-catalog")
		)
	);
	TestTrue(
		TEXT("The shell must expose the payload-integrity action."),
		HyperTwistHandyPhase6RAUTestInternal::HasActionId(
			ShellState.AvailableActionIds,
			TEXT("review-local-model-integrity")
		)
	);
	TestTrue(
		TEXT("The shell must expose the unload-policy action."),
		HyperTwistHandyPhase6RAUTestInternal::HasActionId(
			ShellState.AvailableActionIds,
			TEXT("review-local-model-unload-policy")
		)
	);

	const FHyperTwistSpeechExternalDictationModelCatalogEntry* PrimaryEntry =
		HyperTwistHandyPhase6RAUTestInternal::FindCatalogEntry(
			ShellState.ModelCatalogEntries,
			TEXT("speech-model/whisper.cpp/base.en")
		);
	TestNotNull(TEXT("The shell must keep the primary whisper model catalog entry."), PrimaryEntry);
	if (PrimaryEntry != nullptr)
	{
		TestTrue(TEXT("The primary catalog entry must remain selected."), PrimaryEntry->bSelectedPrimaryPayload);
		TestFalse(TEXT("The primary catalog entry must not be optional."), PrimaryEntry->bOptionalForSession);
		TestTrue(TEXT("The primary catalog entry must require integrity review."), PrimaryEntry->bIntegrityReviewRequired);
		TestTrue(TEXT("The primary catalog entry must keep runtime download deferred."), PrimaryEntry->bDownloadDeferredInCurrentPacket);
	}

	const FHyperTwistSpeechExternalDictationModelCatalogEntry* VadEntry =
		HyperTwistHandyPhase6RAUTestInternal::FindCatalogEntry(
			ShellState.ModelCatalogEntries,
			TEXT("speech-vad/whisper.cpp/silero-v6.2.0")
		);
	TestNotNull(TEXT("The shell must keep the optional VAD payload catalog entry."), VadEntry);
	if (VadEntry != nullptr)
	{
		TestTrue(TEXT("The VAD catalog entry must remain optional."), VadEntry->bOptionalForSession);
		TestFalse(TEXT("The VAD catalog entry must not become the selected primary payload."), VadEntry->bSelectedPrimaryPayload);
		TestFalse(TEXT("The VAD catalog entry must not force integrity review without a checksum."), VadEntry->bIntegrityReviewRequired);
	}

	return true;
}

#endif
