// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

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

#if WITH_AUTOMATION_TESTS

namespace HyperTwistWhisperCppPhase6RWTestInternal
{
	FHyperTwistTrainingDeck MakeSpeechDeck()
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = TEXT("phase6r-w/whispercpp-payload-custody");
		Deck.Title = TEXT("Phase 6R-W Whisper Payload Custody");
		Deck.DeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};

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

	void ForceSpeechClientKind(UHyperTwistTrainingSubsystem* TrainingSubsystem, const FString& ClientKind)
	{
		if (TrainingSubsystem == nullptr)
		{
			return;
		}

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

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

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

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

	const FHyperTwistSpeechModelPayloadDescriptor* FindPayload(
		const TArray<FHyperTwistSpeechModelPayloadDescriptor>& Payloads,
		const FString& PayloadId
	)
	{
		return Payloads.FindByPredicate(
			[&PayloadId](const FHyperTwistSpeechModelPayloadDescriptor& Candidate)
			{
				return Candidate.PayloadId == PayloadId;
			}
		);
	}

	UHyperTwistHttpSpeechClient* ResolveHttpSpeechClient(UHyperTwistTrainingSubsystem* TrainingSubsystem)
	{
		if (TrainingSubsystem == nullptr)
		{
			return nullptr;
		}

		FString CloseError;
		TrainingSubsystem->CloseActiveCompanionSpeechSession(CloseError);

		if (FObjectPropertyBase* ClientProperty = FindFProperty<FObjectPropertyBase>(
			UHyperTwistTrainingSubsystem::StaticClass(),
			TEXT("ActiveCompanionSpeechClientObject")
		))
		{
			return Cast<UHyperTwistHttpSpeechClient>(
				ClientProperty->GetObjectPropertyValue_InContainer(TrainingSubsystem)
			);
		}

		return nullptr;
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RWPayloadCustodySessionConfigTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.W.PayloadCustodySessionConfig",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

	FString OpenError;
	TestTrue(TEXT("The payload-custody route must open a speech session."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));
	TestTrue(TEXT("Opening the speech session must not report an error."), OpenError.IsEmpty());

	const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
		TrainingSubsystem->GetActiveCompanionSpeechSessionState();
	const FHyperTwistSpeechSessionConfig& Config = SessionState.SessionConfig;

	TestTrue(TEXT("The payload-custody session config must remain structurally valid."), Config.IsStructurallyValid());
	TestEqual(TEXT("The payload-custody route must preserve the custody profile id."), Config.PayloadCustodyProfileId, TEXT("downloadable-model-payload-custody-v1"));
	TestTrue(TEXT("The payload-custody route must expose a structurally valid custody profile."), Config.PayloadCustodyProfileDefinition.IsStructurallyValid());
	TestEqual(TEXT("The payload-custody route must retain two bounded payload descriptors."), Config.RequiredModelPayloads.Num(), 2);
	TestFalse(TEXT("The bounded custody route must keep runtime download deferred."), Config.PayloadCustodyProfileDefinition.bAllowsRuntimeDownloadInCurrentPacket);
	TestFalse(TEXT("The bounded custody route must keep bundled redistribution disabled."), Config.PayloadCustodyProfileDefinition.bAllowsBundledPayloadRedistribution);
	TestTrue(TEXT("The bounded custody route must preserve user-managed payload support."), Config.PayloadCustodyProfileDefinition.bSupportsUserManagedPayloads);
	TestTrue(TEXT("The bounded custody route must preserve sidecar-managed payload support."), Config.PayloadCustodyProfileDefinition.bSupportsSidecarManagedPayloads);

	const FHyperTwistSpeechModelPayloadDescriptor* PrimaryPayload =
		HyperTwistWhisperCppPhase6RWTestInternal::FindPayload(
			Config.RequiredModelPayloads,
			TEXT("speech-model/whisper.cpp/base.en")
		);
	TestNotNull(TEXT("The primary whisper model payload descriptor must be present."), PrimaryPayload);
	if (PrimaryPayload != nullptr)
	{
		TestEqual(TEXT("The primary whisper model must preserve the ggml artifact name."), PrimaryPayload->ArtifactName, TEXT("ggml-base.en.bin"));
		TestEqual(TEXT("The primary whisper model must preserve the payload source document."), PrimaryPayload->SourceDocumentPath, TEXT("whisper.cpp/models/README.md"));
		TestEqual(TEXT("The primary whisper model must preserve the SHA-backed integrity check kind."), PrimaryPayload->IntegrityCheckKind, TEXT("sha1"));
		TestEqual(TEXT("The primary whisper model must preserve the documented SHA value."), PrimaryPayload->IntegrityCheckValue, TEXT("137c40403d78fd54d454da0f9bd998f78703390c"));
		TestFalse(TEXT("The primary whisper model must not be optional for the bounded speech session."), PrimaryPayload->bOptionalForSession);
		TestTrue(TEXT("The bounded custody route must keep the primary payload user-supplied."), PrimaryPayload->bUserSuppliedOnly);
		TestFalse(TEXT("The bounded custody route must not mark the primary payload as bundled."), PrimaryPayload->bBundledWithCodeDistribution);
	}

	const FHyperTwistSpeechModelPayloadDescriptor* VadPayload =
		HyperTwistWhisperCppPhase6RWTestInternal::FindPayload(
			Config.RequiredModelPayloads,
			TEXT("speech-vad/whisper.cpp/silero-v6.2.0")
		);
	TestNotNull(TEXT("The bounded custody route must expose the optional VAD payload descriptor."), VadPayload);
	if (VadPayload != nullptr)
	{
		TestTrue(TEXT("The optional VAD payload must remain optional."), VadPayload->bOptionalForSession);
		TestEqual(TEXT("The optional VAD payload must preserve the ggml artifact name."), VadPayload->ArtifactName, TEXT("ggml-silero-v6.2.0.bin"));
	}

	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RWPayloadCustodyServiceHealthTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.W.PayloadCustodyServiceHealth",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistWhisperCppPhase6RWPayloadCustodyServiceHealthTest::RunTest(const FString& Parameters)
{
	UHyperTwistTrainingSubsystem* TrainingSubsystem =
		HyperTwistWhisperCppPhase6RWTestInternal::MakeSpeechSubsystem(
			TEXT("phase6r-w-health-session"),
			TEXT("mock")
		);
	TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-W."), TrainingSubsystem);
	if (TrainingSubsystem == nullptr)
	{
		return false;
	}

	FString OpenError;
	TestTrue(TEXT("The payload-custody route must open a speech session."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));
	TestTrue(TEXT("Opening the speech session must not report an error."), OpenError.IsEmpty());

	const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
		TrainingSubsystem->GetActiveCompanionSpeechSessionState();
	const FHyperTwistSpeechServiceHealth& Health = SessionState.ServiceHealth;

	TestTrue(TEXT("The payload-custody service health must remain structurally valid."), Health.IsStructurallyValid());
	TestEqual(TEXT("The service health must preserve the custody profile id."), Health.PayloadCustodyProfileId, TEXT("downloadable-model-payload-custody-v1"));
	TestEqual(TEXT("The service health must retain two bounded supported payload ids."), Health.SupportedModelPayloadIds.Num(), 2);
	TestEqual(TEXT("The service health must retain two bounded supported payload descriptors."), Health.SupportedModelPayloads.Num(), 2);
	TestTrue(TEXT("The service health must advertise payload-custody capability."), Health.Capabilities.Contains(TEXT("downloadableModelPayloadCustody")));
	TestTrue(TEXT("The service health must advertise payload-integrity review capability."), Health.Capabilities.Contains(TEXT("payloadIntegrityReview")));
	TestTrue(TEXT("The service health must retain the primary whisper model id."), Health.SupportedModelPayloadIds.Contains(TEXT("speech-model/whisper.cpp/base.en")));
	TestTrue(TEXT("The service health must retain the optional VAD payload id."), Health.SupportedModelPayloadIds.Contains(TEXT("speech-vad/whisper.cpp/silero-v6.2.0")));
	TestFalse(TEXT("The health custody profile must keep runtime download deferred."), Health.PayloadCustodyProfileDefinition.bAllowsRuntimeDownloadInCurrentPacket);
	TestFalse(TEXT("The health custody profile must keep bundled redistribution disabled."), Health.PayloadCustodyProfileDefinition.bAllowsBundledPayloadRedistribution);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RWProviderBackedPayloadCustodyFallbackTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.W.ProviderBackedPayloadCustodyFallback",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistWhisperCppPhase6RWProviderBackedPayloadCustodyFallbackTest::RunTest(const FString& Parameters)
{
	UHyperTwistTrainingSubsystem* TrainingSubsystem =
		HyperTwistWhisperCppPhase6RWTestInternal::MakeSpeechSubsystem(
			TEXT("phase6r-w-provider-session"),
			TEXT("http-sidecar")
		);
	TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-W."), TrainingSubsystem);
	if (TrainingSubsystem == nullptr)
	{
		return false;
	}

	UHyperTwistHttpSpeechClient* HttpSpeechClient =
		HyperTwistWhisperCppPhase6RWTestInternal::ResolveHttpSpeechClient(TrainingSubsystem);
	TestNotNull(TEXT("The provider-backed speech client must be available for the payload-custody fallback test."), HttpSpeechClient);
	if (HttpSpeechClient == nullptr)
	{
		return false;
	}

	HttpSpeechClient->ServiceBaseUrl.Reset();

	FString OpenError;
	TestFalse(TEXT("The provider-backed payload-custody route must fail cleanly when no provider endpoint is configured."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));
	TestEqual(TEXT("The provider-backed payload-custody route must report the missing service base URL."), OpenError, TEXT("service-base-url-missing"));

	const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
		TrainingSubsystem->GetActiveCompanionSpeechSessionState();
	const FHyperTwistSpeechServiceHealth& Health = SessionState.ServiceHealth;

	TestTrue(TEXT("The provider-backed fallback health must remain structurally valid."), Health.IsStructurallyValid());
	TestEqual(TEXT("The provider-backed fallback must keep the custody profile id."), Health.PayloadCustodyProfileId, TEXT("downloadable-model-payload-custody-v1"));
	TestTrue(TEXT("The provider-backed fallback must preserve payload-custody capability."), Health.Capabilities.Contains(TEXT("downloadableModelPayloadCustody")));
	TestTrue(TEXT("The provider-backed fallback must preserve payload-integrity review capability."), Health.Capabilities.Contains(TEXT("payloadIntegrityReview")));
	TestEqual(TEXT("The provider-backed fallback must preserve both payload ids."), Health.SupportedModelPayloadIds.Num(), 2);
	TestEqual(TEXT("The provider-backed fallback must preserve both payload descriptors."), Health.SupportedModelPayloads.Num(), 2);
	TestFalse(TEXT("The provider-backed fallback must keep runtime download deferred."), Health.PayloadCustodyProfileDefinition.bAllowsRuntimeDownloadInCurrentPacket);
	TestFalse(TEXT("The provider-backed fallback must keep bundled redistribution disabled."), Health.PayloadCustodyProfileDefinition.bAllowsBundledPayloadRedistribution);
	TestEqual(TEXT("The provider-backed fallback must keep the route error in the shell state."), SessionState.MicrophoneShellState.LastRouteError, TEXT("service-base-url-missing"));
	return true;
}

#endif
