// Copyright HyperTwist, Inc. All Rights Reserved.

#include "Misc/AutomationTest.h"

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

#if WITH_AUTOMATION_TESTS

namespace HyperTwistWhisperCppPhase6RAITestInternal
{
	const FString& GetCustomEndpointUrl()
	{
		static const FString EndpointUrl = TEXT("http://localhost:9/openai-compatible");
		return EndpointUrl;
	}

	const FString& GetCustomProviderProfileId()
	{
		static const FString ProviderProfileId =
			TEXT("speech-provider/custom-openai-endpoint-profile-v1");
		return ProviderProfileId;
	}

	const FString& GetCustomProviderLabel()
	{
		static const FString ProviderLabel = TEXT("Operator Custom Endpoint");
		return ProviderLabel;
	}

	FHyperTwistTrainingDeck MakeSpeechDeck()
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = TEXT("phase6r-ai/custom-endpoint-runtime");
		Deck.Title = TEXT("Phase 6R-AI Custom Endpoint Runtime");
		Deck.DeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};

		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = TEXT("phase6r-ai-case");
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Sequence;
		TrainingCase.PromptLabel = TEXT("Phase 6R-AI 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)
	{
		UGameInstance* GameInstance = NewObject<UGameInstance>();
		if (GameInstance == nullptr)
		{
			return nullptr;
		}

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

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

	FHyperTwistTrainingCompanionSpeechSessionState* ResolveActiveSpeechSessionState(
		UHyperTwistTrainingSubsystem* TrainingSubsystem
	)
	{
		if (TrainingSubsystem == nullptr)
		{
			return nullptr;
		}

		if (FStructProperty* SessionStateProperty = FindFProperty<FStructProperty>(
			UHyperTwistTrainingSubsystem::StaticClass(),
			TEXT("ActiveCompanionSpeechSessionState")
		))
		{
			return SessionStateProperty->ContainerPtrToValuePtr<FHyperTwistTrainingCompanionSpeechSessionState>(
				TrainingSubsystem
			);
		}

		return 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;
	}

	void ForceCustomEndpointSessionConfig(
		UHyperTwistTrainingSubsystem* TrainingSubsystem,
		const FString& SessionId
	)
	{
		if (FHyperTwistTrainingCompanionSpeechSessionState* SessionState =
				ResolveActiveSpeechSessionState(TrainingSubsystem))
		{
			SessionState->SessionConfig = UHyperTwistContractLibrary::MakeSampleSpeechSessionConfig();
			SessionState->SessionConfig.SessionId = SessionId;
			SessionState->SessionConfig.ProviderProfileId = GetCustomProviderProfileId();

			FHyperTwistSpeechProviderProfile& ProviderProfile =
				SessionState->SessionConfig.ProviderProfileDefinition;
			ProviderProfile.ProviderProfileId = GetCustomProviderProfileId();
			ProviderProfile.DisplayLabel = GetCustomProviderLabel();
			ProviderProfile.ProviderClass = TEXT("openai-compatible-custom-endpoint");
			ProviderProfile.EndpointClass = TEXT("openai-compatible-http-api");
			ProviderProfile.EndpointReferenceId = TEXT("speech/provider-profile/custom-openai-endpoint");
			ProviderProfile.EndpointBaseUrl = GetCustomEndpointUrl();
			ProviderProfile.AuthMaterialSource = TEXT("first-party-out-of-band-secret-reference");
			ProviderProfile.AuthReferenceId = TEXT("speech/byok/custom-openai-endpoint/default");
			ProviderProfile.ModelSelectionMode = TEXT("per-profile-model-map");
			ProviderProfile.bEnabled = true;
			ProviderProfile.bUserLabeled = true;
			ProviderProfile.bSupportsEnableDisable = true;
			ProviderProfile.bSupportsCustomBaseUrl = true;
			ProviderProfile.bPreservesFirstPartyCustody = true;

			SessionState->LastError.Reset();
			SessionState->ServiceHealth = FHyperTwistSpeechServiceHealth();
		}
	}

	void PrepareCustomEndpointRuntime(
		UHyperTwistTrainingSubsystem* TrainingSubsystem,
		UHyperTwistHttpSpeechClient*& OutHttpSpeechClient
	)
	{
		OutHttpSpeechClient = ResolveHttpSpeechClient(TrainingSubsystem);
		if (OutHttpSpeechClient != nullptr)
		{
			OutHttpSpeechClient->ServiceBaseUrl.Reset();
			OutHttpSpeechClient->RequestTimeoutSeconds = 0.25f;
		}

		ForceCustomEndpointSessionConfig(
			TrainingSubsystem,
			TEXT("phase6r-ai-custom-endpoint-session")
		);
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RAICustomEndpointSessionConfigTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.AI.CustomEndpointSessionConfig",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

	UHyperTwistHttpSpeechClient* HttpSpeechClient = nullptr;
	HyperTwistWhisperCppPhase6RAITestInternal::PrepareCustomEndpointRuntime(
		TrainingSubsystem,
		HttpSpeechClient
	);
	TestNotNull(TEXT("The provider-backed speech client must be available for Phase 6R-AI."), HttpSpeechClient);
	if (HttpSpeechClient == nullptr)
	{
		return false;
	}

	FString OpenError;
	TestFalse(TEXT("The custom endpoint route should fail cleanly without a live endpoint."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));
	TestNotEqual(TEXT("The custom endpoint route must not collapse back to a missing-base-URL failure."), OpenError, FString(TEXT("service-base-url-missing")));

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

	TestTrue(TEXT("The custom endpoint session config must remain structurally valid."), Config.IsStructurallyValid());
	TestEqual(TEXT("The custom endpoint session config must keep the provider profile id."), Config.ProviderProfileId, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomProviderProfileId());
	TestEqual(TEXT("The custom endpoint session config must keep the provider class."), Config.ProviderProfileDefinition.ProviderClass, TEXT("openai-compatible-custom-endpoint"));
	TestEqual(TEXT("The custom endpoint session config must keep the endpoint class."), Config.ProviderProfileDefinition.EndpointClass, TEXT("openai-compatible-http-api"));
	TestEqual(TEXT("The custom endpoint session config must keep the endpoint base URL."), Config.ProviderProfileDefinition.EndpointBaseUrl, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomEndpointUrl());
	TestTrue(TEXT("The provider route decision must mark the custom endpoint route as selected."), Config.ProviderRouteDecision.bSelectedCustomEndpointRoute);
	TestEqual(TEXT("The preflight route decision must stay ready when a custom endpoint is configured."), Config.ProviderRouteDecision.RouteStateId, TEXT("route-ready"));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RAICustomEndpointHealthFallbackTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.AI.CustomEndpointHealthFallback",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

	UHyperTwistHttpSpeechClient* HttpSpeechClient = nullptr;
	HyperTwistWhisperCppPhase6RAITestInternal::PrepareCustomEndpointRuntime(
		TrainingSubsystem,
		HttpSpeechClient
	);
	TestNotNull(TEXT("The provider-backed speech client must be available for the custom endpoint health test."), HttpSpeechClient);
	if (HttpSpeechClient == nullptr)
	{
		return false;
	}

	FString OpenError;
	TestFalse(TEXT("The custom endpoint route should fail cleanly without a live endpoint."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));

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

	TestTrue(TEXT("The fallback service health must remain structurally valid."), Health.IsStructurallyValid());
	TestEqual(TEXT("The fallback service health must keep the custom provider label."), Health.ProviderLabel, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomProviderLabel());
	TestEqual(TEXT("The fallback service health must keep the custom endpoint service URL."), Health.ServiceEndpoint, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomEndpointUrl());
	TestEqual(TEXT("The fallback provider profile must keep the custom provider profile id."), Health.ProviderProfileId, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomProviderProfileId());
	TestEqual(TEXT("The fallback provider profile must keep the custom endpoint base URL."), Health.ProviderProfileDefinition.EndpointBaseUrl, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomEndpointUrl());
	TestEqual(TEXT("The fallback route state must degrade cleanly instead of waiting for an endpoint."), Health.ProviderRouteDecision.RouteStateId, TEXT("route-health-degraded"));
	TestTrue(TEXT("The fallback route state must preserve the custom endpoint selection flag."), Health.ProviderRouteDecision.bSelectedCustomEndpointRoute);
	TestEqual(TEXT("The fallback route state must preserve the open-session failure."), Health.LastError, OpenError);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RAIDirectClientHealthReflectionTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.AI.DirectClientHealthReflection",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

	FString OpenError;
	TestFalse(TEXT("The custom endpoint route should fail cleanly without a live endpoint."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));

	const FHyperTwistSpeechServiceHealth DirectHealth = HttpSpeechClient->GetSpeechServiceHealth();
	TestTrue(TEXT("The direct client health must remain structurally valid."), DirectHealth.IsStructurallyValid());
	TestEqual(TEXT("The direct client health must keep the custom provider label."), DirectHealth.ProviderLabel, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomProviderLabel());
	TestEqual(TEXT("The direct client health must keep the custom endpoint service URL."), DirectHealth.ServiceEndpoint, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomEndpointUrl());
	TestEqual(TEXT("The direct client health must keep the custom endpoint base URL."), DirectHealth.ProviderProfileDefinition.EndpointBaseUrl, HyperTwistWhisperCppPhase6RAITestInternal::GetCustomEndpointUrl());
	TestTrue(TEXT("The direct client health must preserve the custom endpoint selection flag."), DirectHealth.ProviderRouteDecision.bSelectedCustomEndpointRoute);
	return true;
}

#endif
