// 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 HyperTwistWhisperCppPhase6RZTestInternal
{
	FHyperTwistTrainingDeck MakeSpeechDeck()
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = TEXT("phase6r-z/usage-cost-event-model");
		Deck.Title = TEXT("Phase 6R-Z Usage Cost Event Model");
		Deck.DeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};

		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = TEXT("phase6r-z-case");
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Sequence;
		TrainingCase.PromptLabel = TEXT("Phase 6R-Z 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-z-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(
	FHyperTwistWhisperCppPhase6RZUsageCostSessionConfigTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.Z.UsageCostEventModelSessionConfig",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

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

	TestTrue(TEXT("The usage/cost session config must remain structurally valid."), Config.IsStructurallyValid());
	TestEqual(TEXT("The session config must expose the accounting profile id."), Config.UsageCostAccountingProfileId, TEXT("speech-usage-cost-accounting-v1"));
	TestTrue(TEXT("The accounting profile definition must remain structurally valid."), Config.UsageCostAccountingProfileDefinition.IsStructurallyValid());
	TestEqual(TEXT("The accounting profile must preserve normalized event ownership."), Config.UsageCostAccountingProfileDefinition.AccountingKind, TEXT("normalized-usage-cost-events"));
	TestEqual(TEXT("The accounting profile must preserve the health-surface quota visibility mode."), Config.UsageCostAccountingProfileDefinition.QuotaVisibilityMode, TEXT("first-party-health-surface"));
	TestTrue(TEXT("The accounting profile must preserve first-party ownership."), Config.UsageCostAccountingProfileDefinition.bPreservesFirstPartyAccountingOwnership);
	TestFalse(TEXT("The accounting profile must not allow donor-owned cost governance."), Config.UsageCostAccountingProfileDefinition.bAllowsDonorOwnedCostGovernance);
	TestTrue(TEXT("The accounting profile must normalize audio-seconds."), Config.UsageCostAccountingProfileDefinition.NormalizedMeterKinds.Contains(TEXT("audio-seconds")));
	TestTrue(TEXT("The accounting profile must normalize USD cost surfaces."), Config.UsageCostAccountingProfileDefinition.NormalizedCurrencyCodes.Contains(TEXT("USD")));

	TestTrue(TEXT("The usage event template must remain structurally valid."), Config.UsageEventTemplate.IsStructurallyValid());
	TestEqual(TEXT("The usage event template must preserve the current-session aggregation window."), Config.UsageEventTemplate.AggregationWindowId, TEXT("current-session"));
	TestEqual(TEXT("The usage event template must preserve the usage meter kind."), Config.UsageEventTemplate.MeterKind, TEXT("audio-seconds"));
	TestTrue(TEXT("The usage event template must remain estimate-only."), Config.UsageEventTemplate.bEstimateOnly);

	TestTrue(TEXT("The cost event template must remain structurally valid."), Config.CostEventTemplate.IsStructurallyValid());
	TestEqual(TEXT("The cost event template must preserve the bounded session cost summary kind."), Config.CostEventTemplate.EventKind, TEXT("bounded-session-cost-summary"));
	TestEqual(TEXT("The cost event template must preserve the pricing source."), Config.CostEventTemplate.PricingSourceId, TEXT("first-party-estimate"));
	TestTrue(TEXT("The cost event template must remain estimate-only."), Config.CostEventTemplate.bEstimateOnly);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RZUsageCostServiceHealthTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.Z.UsageCostEventModelServiceHealth",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

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

	TestTrue(TEXT("The usage/cost service health must remain structurally valid."), Health.IsStructurallyValid());
	TestTrue(TEXT("The service health must expose normalized usage events."), Health.Capabilities.Contains(TEXT("normalizedUsageEvents")));
	TestTrue(TEXT("The service health must expose normalized cost events."), Health.Capabilities.Contains(TEXT("normalizedCostEvents")));
	TestTrue(TEXT("The service health must expose quota/rate-limit state."), Health.Capabilities.Contains(TEXT("quotaRateLimitState")));
	TestEqual(TEXT("The health surface must preserve the accounting profile id."), Health.UsageCostAccountingProfileId, TEXT("speech-usage-cost-accounting-v1"));
	TestTrue(TEXT("The health accounting profile must remain structurally valid."), Health.UsageCostAccountingProfileDefinition.IsStructurallyValid());

	TestTrue(TEXT("The latest usage event must remain structurally valid."), Health.LatestUsageEvent.IsStructurallyValid());
	TestEqual(TEXT("The latest usage event must preserve the mock profile id."), Health.LatestUsageEvent.ProviderProfileId, TEXT("speech-provider/mock-offline-profile-v1"));
	TestEqual(TEXT("The latest usage event must preserve the service-health window."), Health.LatestUsageEvent.AggregationWindowId, TEXT("service-health-window"));
	TestTrue(TEXT("The latest usage event must remain estimate-only."), Health.LatestUsageEvent.bEstimateOnly);

	TestTrue(TEXT("The latest cost event must remain structurally valid."), Health.LatestCostEvent.IsStructurallyValid());
	TestEqual(TEXT("The latest cost event must preserve USD reporting."), Health.LatestCostEvent.CurrencyCode, TEXT("USD"));
	TestEqual(TEXT("The latest cost event must preserve the first-party estimate source."), Health.LatestCostEvent.PricingSourceId, TEXT("first-party-estimate"));
	TestTrue(TEXT("The latest cost event must remain estimate-only."), Health.LatestCostEvent.bEstimateOnly);

	TestTrue(TEXT("The quota/rate-limit state must remain structurally valid."), Health.QuotaRateLimitState.IsStructurallyValid());
	TestEqual(TEXT("The quota state must preserve the within-budget reason."), Health.QuotaRateLimitState.StateReason, TEXT("within-budget"));
	TestTrue(TEXT("The quota state must remain known for the mock route."), Health.QuotaRateLimitState.bQuotaKnown);
	TestFalse(TEXT("The mock route must not be rate-limited."), Health.QuotaRateLimitState.bRateLimited);
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RZProviderBackedUsageCostFallbackTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.Z.ProviderBackedUsageCostFallback",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

	HttpSpeechClient->ServiceBaseUrl.Reset();

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

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

	TestTrue(TEXT("The provider-backed usage/cost fallback health must remain structurally valid."), Health.IsStructurallyValid());
	TestTrue(TEXT("The provider-backed usage event must remain structurally valid."), Health.LatestUsageEvent.IsStructurallyValid());
	TestEqual(TEXT("The provider-backed usage event must switch into health-posture mode."), Health.LatestUsageEvent.EventKind, TEXT("provider-route-health-posture"));
	TestEqual(TEXT("The provider-backed usage event must carry the missing-endpoint reference."), Health.LatestUsageEvent.RawEventReferenceId, TEXT("service-base-url-missing"));
	TestEqual(TEXT("The provider-backed usage event must preserve the sidecar profile id."), Health.LatestUsageEvent.ProviderProfileId, TEXT("speech-provider/local-http-sidecar-profile-v1"));

	TestTrue(TEXT("The provider-backed cost event must remain structurally valid."), Health.LatestCostEvent.IsStructurallyValid());
	TestEqual(TEXT("The provider-backed cost event must switch into health-posture mode."), Health.LatestCostEvent.EventKind, TEXT("provider-route-health-posture"));
	TestTrue(TEXT("The provider-backed cost event must remain estimate-only."), Health.LatestCostEvent.bEstimateOnly);

	TestTrue(TEXT("The provider-backed quota state must remain structurally valid."), Health.QuotaRateLimitState.IsStructurallyValid());
	TestEqual(TEXT("The provider-backed quota state must expose the missing-endpoint reason."), Health.QuotaRateLimitState.StateReason, TEXT("service-base-url-missing"));
	TestFalse(TEXT("The provider-backed quota state must stay unknown while the endpoint is unavailable."), Health.QuotaRateLimitState.bQuotaKnown);
	TestEqual(TEXT("The provider-backed quota state must clear remaining request count in fallback posture."), Health.QuotaRateLimitState.RemainingRequestCount, 0);
	return true;
}

#endif
