// 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 HyperTwistWhisperCppPhase6RYTestInternal
{
	FHyperTwistTrainingDeck MakeSpeechDeck()
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = TEXT("phase6r-y/provider-routing-policy");
		Deck.Title = TEXT("Phase 6R-Y Provider Routing And Policy");
		Deck.DeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};

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

	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(
	FHyperTwistWhisperCppPhase6RYProviderRoutingSessionConfigTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.Y.ProviderRoutingPolicySessionConfig",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

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

	TestTrue(TEXT("The provider-routing session config must remain structurally valid."), Config.IsStructurallyValid());
	TestEqual(TEXT("The mock route must preserve the mock routing policy id."), Config.ProviderRoutingPolicyId, TEXT("speech-provider-routing/local-offline-mock-first-v1"));
	TestTrue(TEXT("The routing policy definition must remain structurally valid."), Config.ProviderRoutingPolicyDefinition.IsStructurallyValid());
	TestEqual(TEXT("The mock route must keep the local-offline selection mode."), Config.ProviderRoutingPolicyDefinition.RouteSelectionMode, TEXT("local-offline-mock-first"));
	TestFalse(TEXT("The mock route must not request provider-backed routing as its primary posture."), Config.ProviderRoutingPolicyDefinition.bAllowProviderBackedRoutes);
	TestTrue(TEXT("The mock route must preserve mock verification routing."), Config.ProviderRoutingPolicyDefinition.bAllowMockVerificationRoutes);
	TestTrue(TEXT("The routing policy must preserve first-party ownership."), Config.ProviderRoutingPolicyDefinition.bPreservesFirstPartyRoutingOwnership);
	TestFalse(TEXT("The routing policy must not allow donor-owned routing language."), Config.ProviderRoutingPolicyDefinition.bAllowsDonorOwnedRoutingLanguage);

	TestTrue(TEXT("The mock route decision must remain structurally valid."), Config.ProviderRouteDecision.IsStructurallyValid());
	TestEqual(TEXT("The mock route decision must target the mock provider profile."), Config.ProviderRouteDecision.SelectedProviderProfileId, TEXT("speech-provider/mock-offline-profile-v1"));
	TestEqual(TEXT("The mock route decision must preserve the in-process transport."), Config.ProviderRouteDecision.SelectedTransportKind, TEXT("in-process"));
	TestEqual(TEXT("The mock route decision must keep the orchestration service lane."), Config.ProviderRouteDecision.SelectedServiceLaneId, TEXT("speech/python-service"));
	TestEqual(TEXT("The mock route decision must report a ready route."), Config.ProviderRouteDecision.RouteStateId, TEXT("route-ready"));
	TestEqual(TEXT("The mock route decision must expose the expected reason."), Config.ProviderRouteDecision.DecisionReason, TEXT("mock-verification-route-selected"));
	TestFalse(TEXT("The mock route decision must not request a provider-backed route."), Config.ProviderRouteDecision.bProviderBackedRouteRequested);
	TestFalse(TEXT("The mock route decision must not mark a provider-backed route as selected."), Config.ProviderRouteDecision.bProviderBackedRouteSelected);
	TestTrue(TEXT("The mock route decision must remain route-ready."), Config.ProviderRouteDecision.bRouteReady);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RYProviderRoutingServiceHealthTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.Y.ProviderRoutingPolicyServiceHealth",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

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

	TestTrue(TEXT("The provider-routing service health must remain structurally valid."), Health.IsStructurallyValid());
	TestTrue(TEXT("The service health must preserve provider routing policy capability."), Health.Capabilities.Contains(TEXT("providerRoutingPolicy")));
	TestTrue(TEXT("The service health must preserve provider route selection capability."), Health.Capabilities.Contains(TEXT("providerRouteSelection")));
	TestTrue(TEXT("The service health must preserve workflow-policy state capability."), Health.Capabilities.Contains(TEXT("workflowPolicyState")));
	TestEqual(TEXT("The service health must preserve the mock routing policy id."), Health.ProviderRoutingPolicyId, TEXT("speech-provider-routing/local-offline-mock-first-v1"));
	TestTrue(TEXT("The service health routing policy definition must remain structurally valid."), Health.ProviderRoutingPolicyDefinition.IsStructurallyValid());
	TestEqual(TEXT("The mock routing health must keep fail-closed fallback wording."), Health.ProviderRoutingPolicyDefinition.FallbackMode, TEXT("fail-closed"));
	TestTrue(TEXT("The mock routing policy must require manual transcript capability."), Health.ProviderRoutingPolicyDefinition.RequiredCapabilityFlags.Contains(TEXT("manualTranscriptCommit")));
	TestTrue(TEXT("The mock routing policy must support transcribe tasks."), Health.ProviderRoutingPolicyDefinition.SupportedTaskKinds.Contains(TEXT("transcribe")));

	TestTrue(TEXT("The mock routing health decision must remain structurally valid."), Health.ProviderRouteDecision.IsStructurallyValid());
	TestEqual(TEXT("The mock routing health decision must preserve the in-process endpoint class."), Health.ProviderRouteDecision.SelectedEndpointClass, TEXT("in-process-mock"));
	TestEqual(TEXT("The mock routing health decision must keep the ready route state."), Health.ProviderRouteDecision.RouteStateId, TEXT("route-ready"));
	TestFalse(TEXT("The mock routing health decision must not mark a provider-backed route as selected."), Health.ProviderRouteDecision.bProviderBackedRouteSelected);
	TestFalse(TEXT("The mock routing health decision must not fall back into health-posture mode."), Health.ProviderRouteDecision.bUsedFallbackHealthPosture);
	TestTrue(TEXT("The mock routing health decision must keep the route ready."), Health.ProviderRouteDecision.bRouteReady);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RYProviderBackedRoutingFallbackTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.Y.ProviderBackedRoutingFallback",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

	HttpSpeechClient->ServiceBaseUrl.Reset();

	FString OpenError;
	TestFalse(TEXT("The provider-backed routing path must fail cleanly when the configured provider endpoint is unavailable."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));
	TestEqual(TEXT("The provider-backed routing path must report a provider-backed transport failure once the provider profile endpoint is selected."), OpenError, TEXT("request-failed"));

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

	TestTrue(TEXT("The provider-backed routing fallback health must remain structurally valid."), Health.IsStructurallyValid());
	TestEqual(TEXT("The provider-backed routing fallback must keep the provider-backed routing policy id."), Health.ProviderRoutingPolicyId, TEXT("speech-provider-routing/provider-backed-sidecar-first-v1"));
	TestTrue(TEXT("The provider-backed routing policy definition must remain structurally valid."), Health.ProviderRoutingPolicyDefinition.IsStructurallyValid());
	TestEqual(TEXT("The provider-backed routing policy must preserve sidecar-first selection mode."), Health.ProviderRoutingPolicyDefinition.RouteSelectionMode, TEXT("provider-backed-sidecar-first"));
	TestEqual(TEXT("The provider-backed routing policy must preserve health-posture fallback wording."), Health.ProviderRoutingPolicyDefinition.FallbackMode, TEXT("health-posture-only"));
	TestTrue(TEXT("The provider-backed routing policy must allow provider-backed routes."), Health.ProviderRoutingPolicyDefinition.bAllowProviderBackedRoutes);
	TestTrue(TEXT("The provider-backed routing policy must preserve health-posture fallback."), Health.ProviderRoutingPolicyDefinition.bAllowHealthPostureFallback);

	TestTrue(TEXT("The provider-backed route decision must remain structurally valid."), Health.ProviderRouteDecision.IsStructurallyValid());
	TestEqual(TEXT("The provider-backed route decision must target the local HTTP provider profile."), Health.ProviderRouteDecision.SelectedProviderProfileId, TEXT("speech-provider/local-http-sidecar-profile-v1"));
	TestEqual(TEXT("The provider-backed route decision must preserve the local self-hosted endpoint class."), Health.ProviderRouteDecision.SelectedEndpointClass, TEXT("local-self-hosted-http"));
	TestEqual(TEXT("The provider-backed route decision must preserve HTTP transport."), Health.ProviderRouteDecision.SelectedTransportKind, TEXT("http"));
	TestEqual(TEXT("The provider-backed route decision must report the health-degraded state when the configured provider endpoint is unavailable."), Health.ProviderRouteDecision.RouteStateId, TEXT("route-health-degraded"));
	TestEqual(TEXT("The provider-backed route decision must expose the provider-backed health-degraded reason."), Health.ProviderRouteDecision.DecisionReason, TEXT("provider-backed-sidecar-health-degraded"));
	TestEqual(TEXT("The provider-backed route decision must expose the provider-backed transport failure."), Health.ProviderRouteDecision.FailureReason, TEXT("request-failed"));
	TestTrue(TEXT("The provider-backed route decision must mark a provider-backed route as requested."), Health.ProviderRouteDecision.bProviderBackedRouteRequested);
	TestTrue(TEXT("The provider-backed route decision must mark a provider-backed route as selected."), Health.ProviderRouteDecision.bProviderBackedRouteSelected);
	TestTrue(TEXT("The provider-backed route decision must preserve fallback health posture."), Health.ProviderRouteDecision.bUsedFallbackHealthPosture);
	TestFalse(TEXT("The provider-backed route decision must not report a ready route."), Health.ProviderRouteDecision.bRouteReady);
	TestEqual(TEXT("The shell state must preserve the provider-backed transport failure."), SessionState.MicrophoneShellState.LastRouteError, TEXT("request-failed"));
	return true;
}

#endif
