// 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 HyperTwistWhisperCppPhase6RUTestInternal
{
	FHyperTwistTrainingDeck MakeSpeechDeck()
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = TEXT("phase6r-u/whispercpp-microphone-shell");
		Deck.Title = TEXT("Phase 6R-U Whisper Microphone Shell");
		Deck.DeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};

		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = TEXT("phase6r-u-case");
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Sequence;
		TrainingCase.PromptLabel = TEXT("Phase 6R-U 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-u-user"),
			SessionId,
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		);
		return RunState.IsStructurallyValid() ? TrainingSubsystem : nullptr;
	}

	const FHyperTwistSpeechShellPanelLayout* FindPanel(
		const FHyperTwistSpeechMicrophoneShellProfile& Profile,
		const FString& PanelId
	)
	{
		return Profile.Panels.FindByPredicate(
			[&PanelId](const FHyperTwistSpeechShellPanelLayout& Candidate)
			{
				return Candidate.PanelId == PanelId;
			}
		);
	}

	const FHyperTwistSpeechShellActionBinding* FindAction(
		const FHyperTwistSpeechMicrophoneShellProfile& Profile,
		const FString& ActionId
	)
	{
		return Profile.ActionBindings.FindByPredicate(
			[&ActionId](const FHyperTwistSpeechShellActionBinding& Candidate)
			{
				return Candidate.ActionId == ActionId;
			}
		);
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RUMicrophoneShellSessionConfigTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.U.MicrophoneShellSessionConfig",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

	FString OpenError;
	TestTrue(TEXT("The microphone shell 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 FHyperTwistSpeechMicrophoneShellProfile& Profile =
		SessionState.SessionConfig.MicrophoneShellProfileDefinition;

	TestEqual(
		TEXT("The microphone shell route must preserve the first-party shell profile id."),
		SessionState.SessionConfig.MicrophoneShellProfileId,
		TEXT("coach-command-microphone-shell-v1")
	);
	TestTrue(TEXT("The microphone shell route must expose a structurally valid shell profile."), Profile.IsStructurallyValid());
	TestEqual(TEXT("The microphone shell must retain four bounded panels."), Profile.Panels.Num(), 4);
	TestEqual(TEXT("The microphone shell must retain four bounded actions."), Profile.ActionBindings.Num(), 4);
	TestEqual(TEXT("The microphone shell must preserve the donor-backed step window."), Profile.StepWindowMs, 3000);
	TestEqual(TEXT("The microphone shell must preserve the donor-backed capture window."), Profile.CaptureWindowMs, 10000);
	TestEqual(TEXT("The microphone shell must preserve the donor-backed keep window."), Profile.KeepWindowMs, 200);
	TestEqual(TEXT("The microphone shell must preserve zero audio-context tokens in the bounded route."), Profile.AudioContextTokens, 0);
	TestTrue(TEXT("The microphone shell must preserve the donor-backed VAD threshold."), FMath::IsNearlyEqual(Profile.VadThreshold, 0.6f));
	TestTrue(TEXT("The microphone shell must preserve the donor-backed high-pass floor."), FMath::IsNearlyEqual(Profile.HighPassFrequencyHz, 100.0f));
	TestFalse(TEXT("The microphone shell must keep previous-text carryover disabled in the bounded route."), Profile.bKeepContextBetweenChunks);
	TestTrue(TEXT("The microphone shell must advertise streaming preview."), Profile.bSupportsStreamingPreview);
	TestTrue(TEXT("The microphone shell must advertise manual commit."), Profile.bSupportsManualCommit);

	const FHyperTwistSpeechShellPanelLayout* VadPanel =
		HyperTwistWhisperCppPhase6RUTestInternal::FindPanel(Profile, TEXT("vad-meter"));
	TestNotNull(TEXT("The VAD meter panel must be present."), VadPanel);
	if (VadPanel != nullptr)
	{
		TestEqual(TEXT("The VAD meter must stay anchored below the status readout."), VadPanel->AnchorId, TEXT("left-stack-below-status"));
	}

	const FHyperTwistSpeechShellActionBinding* CommitAction =
		HyperTwistWhisperCppPhase6RUTestInternal::FindAction(Profile, TEXT("commit-transcript"));
	TestNotNull(TEXT("The manual transcript commit action must be present."), CommitAction);
	if (CommitAction != nullptr)
	{
		TestEqual(TEXT("The manual transcript commit action must keep the Enter binding."), CommitAction->InputBinding, TEXT("Enter"));
	}

	TestTrue(
		TEXT("The speech service health must expose microphone-shell capability."),
		SessionState.ServiceHealth.Capabilities.Contains(TEXT("microphoneCaptureShell"))
	);
	TestTrue(
		TEXT("The speech service health must expose streaming-preview capability."),
		SessionState.ServiceHealth.Capabilities.Contains(TEXT("streamingPreview"))
	);
	TestTrue(
		TEXT("The speech service health must expose manual transcript commit capability."),
		SessionState.ServiceHealth.Capabilities.Contains(TEXT("manualTranscriptCommit"))
	);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RUMicrophoneShellOpenStateTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.U.MicrophoneShellOpenState",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistWhisperCppPhase6RUMicrophoneShellOpenStateTest::RunTest(const FString& Parameters)
{
	UHyperTwistTrainingSubsystem* TrainingSubsystem =
		HyperTwistWhisperCppPhase6RUTestInternal::MakeSpeechSubsystem(TEXT("phase6r-u-open-session"));
	TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-U."), TrainingSubsystem);
	if (TrainingSubsystem == nullptr)
	{
		return false;
	}

	FString OpenError;
	TestTrue(TEXT("The microphone shell 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 FHyperTwistSpeechMicrophoneShellState& ShellState = SessionState.MicrophoneShellState;

	TestTrue(TEXT("The microphone shell open state must be structurally valid."), ShellState.IsStructurallyValid());
	TestEqual(TEXT("The microphone shell state must preserve the first-party profile id."), ShellState.MicrophoneShellProfileId, TEXT("coach-command-microphone-shell-v1"));
	TestEqual(TEXT("The microphone shell state must preserve the listening contract."), ShellState.ListeningContractId, TEXT("listening-threshold-lifecycle"));
	TestEqual(TEXT("The microphone shell state must preserve the input route."), ShellState.InputRouteId, TEXT("queue/listening"));
	TestEqual(TEXT("The microphone shell state must preserve the donor-backed step window."), ShellState.StepWindowMs, 3000);
	TestEqual(TEXT("The microphone shell state must preserve the donor-backed capture window."), ShellState.CaptureWindowMs, 10000);
	TestEqual(TEXT("The microphone shell state must preserve the donor-backed keep window."), ShellState.KeepWindowMs, 200);
	TestTrue(TEXT("The microphone shell must report the session open."), ShellState.bSessionOpen);
	TestTrue(TEXT("The microphone shell must report the capture route ready."), ShellState.bCaptureRouteReady);
	TestTrue(TEXT("The microphone shell must start capture-active."), ShellState.bCaptureActive);
	TestTrue(TEXT("The microphone shell must start with VAD armed."), ShellState.bVadArmed);
	TestTrue(TEXT("The microphone shell must start awaiting speech."), ShellState.bAwaitingSpeech);
	TestFalse(TEXT("The microphone shell must not start with transcription in flight."), ShellState.bTranscriptionInFlight);
	TestFalse(TEXT("The microphone shell must not start commit-ready."), ShellState.bCommitReady);
	TestTrue(TEXT("The microphone shell must keep the status panel visible."), ShellState.bStatusPanelVisible);
	TestTrue(TEXT("The microphone shell must keep the VAD meter visible."), ShellState.bVadMeterVisible);
	TestTrue(TEXT("The microphone shell must keep the transcript preview visible."), ShellState.bTranscriptPreviewVisible);
	TestTrue(TEXT("The microphone shell must keep the command hints visible."), ShellState.bCommandHintsVisible);
	TestFalse(TEXT("The microphone shell must keep the permission prompt hidden."), ShellState.bPermissionPromptVisible);
	TestEqual(TEXT("The microphone shell must start with no submitted utterances."), ShellState.SubmittedUtteranceCount, 0);
	TestEqual(TEXT("The microphone shell must start with no final transcripts."), ShellState.FinalTranscriptCount, 0);
	TestEqual(TEXT("The microphone shell must expose four bounded shell actions."), ShellState.AvailableActionIds.Num(), 4);
	TestTrue(TEXT("The microphone shell must expose pause capture."), ShellState.AvailableActionIds.Contains(TEXT("pause-microphone-capture")));
	TestTrue(TEXT("The microphone shell must expose resume capture."), ShellState.AvailableActionIds.Contains(TEXT("resume-microphone-capture")));
	TestTrue(TEXT("The microphone shell must expose manual transcript commit."), ShellState.AvailableActionIds.Contains(TEXT("commit-transcript")));
	TestTrue(TEXT("The microphone shell must expose session close."), ShellState.AvailableActionIds.Contains(TEXT("close-speech-session")));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RUMicrophoneShellUtteranceStateTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.U.MicrophoneShellUtteranceState",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FHyperTwistWhisperCppPhase6RUMicrophoneShellUtteranceStateTest::RunTest(const FString& Parameters)
{
	UHyperTwistTrainingSubsystem* TrainingSubsystem =
		HyperTwistWhisperCppPhase6RUTestInternal::MakeSpeechSubsystem(TEXT("phase6r-u-utterance-session"));
	TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-U."), TrainingSubsystem);
	if (TrainingSubsystem == nullptr)
	{
		return false;
	}

	FHyperTwistSpeechUtteranceEnvelope Utterance;
	Utterance.AudioRef = TEXT("mock://coach/next-case");
	Utterance.SpeechStartMs = 140;
	Utterance.SpeechEndMs = 980;
	Utterance.SilenceGapMs = 240;
	Utterance.SampleCount = 13440;
	const FHyperTwistSpeechTranscriptResult Result =
		TrainingSubsystem->SubmitActiveCompanionSpeechUtterance(Utterance);

	TestTrue(TEXT("The microphone shell utterance route must return a structurally valid transcript result."), Result.IsStructurallyValid());
	TestEqual(TEXT("The bounded mock route must resolve the next-case transcript."), Result.TranscriptText, TEXT("next case"));

	const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
		TrainingSubsystem->GetActiveCompanionSpeechSessionState();
	const FHyperTwistSpeechMicrophoneShellState& ShellState = SessionState.MicrophoneShellState;

	TestTrue(TEXT("The microphone shell utterance state must be structurally valid."), ShellState.IsStructurallyValid());
	TestTrue(TEXT("The microphone shell must keep the speech session open after a final utterance."), ShellState.bSessionOpen);
	TestTrue(TEXT("The microphone shell must resume capture after a final utterance."), ShellState.bCaptureActive);
	TestTrue(TEXT("The microphone shell must resume awaiting speech after a final utterance."), ShellState.bAwaitingSpeech);
	TestFalse(TEXT("The microphone shell must clear transcription-in-flight after a final utterance."), ShellState.bTranscriptionInFlight);
	TestTrue(TEXT("The microphone shell must become commit-ready after a final utterance."), ShellState.bCommitReady);
	TestTrue(TEXT("The microphone shell must clear the active utterance id after a final utterance."), ShellState.ActiveUtteranceId.IsEmpty());
	TestEqual(TEXT("The microphone shell must retain the last committed utterance id."), ShellState.LastCommittedUtteranceId, Result.UtteranceId);
	TestEqual(TEXT("The microphone shell must retain the last transcript preview."), ShellState.LastTranscriptPreview, TEXT("next case"));
	TestEqual(TEXT("The microphone shell must retain the transcript language."), ShellState.LastLanguageCode, TEXT("en"));
	TestEqual(TEXT("The microphone shell must count one submitted utterance."), ShellState.SubmittedUtteranceCount, 1);
	TestEqual(TEXT("The microphone shell must count one final transcript."), ShellState.FinalTranscriptCount, 1);
	TestEqual(TEXT("The microphone shell must retain the detected speech start."), ShellState.DetectedSpeechStartMs, 140);
	TestEqual(TEXT("The microphone shell must retain the detected speech end."), ShellState.DetectedSpeechEndMs, 980);
	TestEqual(TEXT("The microphone shell must retain the last silence gap."), ShellState.LastSilenceGapMs, 240);
	return true;
}

#endif
