// 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 HyperTwistWhisperCppPhase6RAETestInternal
{
	FHyperTwistTrainingDeck MakeSpeechDeck()
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = TEXT("phase6r-ae/provider-settlement-exception");
		Deck.Title = TEXT("Phase 6R-AE Provider Settlement Exception");
		Deck.DeliveryModes = {
			EHyperTwistTrainingDeliveryMode::CoachReviewed
		};

		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = TEXT("phase6r-ae-case");
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Sequence;
		TrainingCase.PromptLabel = TEXT("Phase 6R-AE 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-ae-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;
	}

	FHyperTwistSpeechUtteranceEnvelope MakeUtterance()
	{
		FHyperTwistSpeechUtteranceEnvelope Utterance;
		Utterance.AudioRef = TEXT("exception-coach");
		Utterance.SpeechStartMs = 120;
		Utterance.SpeechEndMs = 840;
		Utterance.SilenceGapMs = 240;
		return Utterance;
	}

	bool ContainsPanelId(
		const TArray<FHyperTwistSpeechShellPanelLayout>& Panels,
		const FString& ExpectedPanelId
	)
	{
		for (const FHyperTwistSpeechShellPanelLayout& Panel : Panels)
		{
			if (Panel.PanelId == ExpectedPanelId)
			{
				return true;
			}
		}

		return false;
	}

	bool ContainsActionId(
		const TArray<FHyperTwistSpeechShellActionBinding>& ActionBindings,
		const FString& ExpectedActionId
	)
	{
		for (const FHyperTwistSpeechShellActionBinding& ActionBinding : ActionBindings)
		{
			if (ActionBinding.ActionId == ExpectedActionId)
			{
				return true;
			}
		}

		return false;
	}
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RAEProviderSettlementExceptionSessionConfigTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.AE.ProviderSettlementExceptionSessionConfig",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

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

	TestTrue(TEXT("The settlement-exception session config must remain structurally valid."), Config.IsStructurallyValid());
	TestEqual(TEXT("The settlement-exception shell profile id must be present."), Config.ProviderSettlementExceptionShellProfileId, TEXT("speech-provider-settlement-exception-shell-v1"));
	TestTrue(TEXT("The settlement-exception shell profile must remain structurally valid."), Config.ProviderSettlementExceptionShellProfileDefinition.IsStructurallyValid());
	TestEqual(TEXT("The settlement-exception shell must preserve the operator-facing shell kind."), Config.ProviderSettlementExceptionShellProfileDefinition.ShellKind, TEXT("operator-provider-settlement-exception"));
	TestEqual(TEXT("The settlement-exception shell must preserve the exception inspect action id."), Config.ProviderSettlementExceptionShellProfileDefinition.ExceptionInspectActionId, TEXT("inspect-provider-settlement-exception"));
	TestEqual(TEXT("The settlement-exception shell must preserve the external handoff action id."), Config.ProviderSettlementExceptionShellProfileDefinition.ExternalPortalHandoffActionId, TEXT("review-provider-external-portal-handoff"));
	TestTrue(TEXT("The settlement-exception shell must include the external handoff panel."), HyperTwistWhisperCppPhase6RAETestInternal::ContainsPanelId(Config.ProviderSettlementExceptionShellProfileDefinition.Panels, TEXT("external-portal-handoff-card")));
	TestTrue(TEXT("The settlement-exception shell must include the exception inspect action."), HyperTwistWhisperCppPhase6RAETestInternal::ContainsActionId(Config.ProviderSettlementExceptionShellProfileDefinition.ActionBindings, TEXT("inspect-provider-settlement-exception")));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RAEProviderSettlementExceptionShellStateTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.AE.ProviderSettlementExceptionShellState",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

	const FHyperTwistSpeechTranscriptResult Result =
		TrainingSubsystem->SubmitActiveCompanionSpeechUtterance(
			HyperTwistWhisperCppPhase6RAETestInternal::MakeUtterance()
		);
	TestTrue(TEXT("The mock settlement-exception utterance must produce a final transcript."), Result.bIsFinal);
	TestTrue(TEXT("The mock settlement-exception utterance must remain structurally valid."), Result.IsStructurallyValid());

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

	TestTrue(TEXT("The service health must expose the settlement-exception shell."), Health.Capabilities.Contains(TEXT("providerSettlementExceptionShell")));
	TestTrue(TEXT("The service health must expose external portal handoff."), Health.Capabilities.Contains(TEXT("providerExternalPortalHandoff")));
	TestTrue(TEXT("The service health must expose settlement-exception review."), Health.Capabilities.Contains(TEXT("providerSettlementExceptionReview")));

	TestTrue(TEXT("The settlement-exception shell state must remain structurally valid."), ExceptionState.IsStructurallyValid());
	TestEqual(TEXT("The settlement-exception shell must preserve the mock provider profile."), ExceptionState.ActiveProviderProfileId, TEXT("speech-provider/mock-offline-profile-v1"));
	TestEqual(TEXT("The settlement-exception shell must retain exactly two bounded exception entries after open and submit."), ExceptionState.ExceptionEntryCount, 2);
	TestEqual(TEXT("The settlement-exception shell must preserve the latest source kind."), ExceptionState.LatestSourceKind, TEXT("settlement-derived-pending-charge"));
	TestTrue(TEXT("The settlement-exception shell must preserve summary readiness."), ExceptionState.bExceptionSummaryReady);
	TestFalse(TEXT("The settlement-exception shell must not mark external handoff ready before provider-posted charges or route exceptions appear."), ExceptionState.bExternalPortalHandoffReady);
	TestTrue(TEXT("The settlement-exception shell must preserve exception-review visibility."), ExceptionState.bSettlementExceptionReviewReady);
	TestTrue(TEXT("The settlement-exception shell must preserve the external handoff action."), ExceptionState.AvailableActionIds.Contains(TEXT("review-provider-external-portal-handoff")));
	TestTrue(TEXT("The settlement-exception shell must preserve the exception inspect action."), ExceptionState.AvailableActionIds.Contains(TEXT("inspect-provider-settlement-exception")));
	TestEqual(TEXT("The settlement-exception shell must preserve zero external-handoff-ready entries in the mock route."), ExceptionState.ExternalPortalHandoffReadyEntryCount, 0);
	TestEqual(TEXT("The settlement-exception shell must preserve the pending-charge entry count."), ExceptionState.PendingChargeEntryCount, 2);
	TestEqual(TEXT("The settlement-exception shell must preserve the manual-review entry count."), ExceptionState.ManualReviewEntryCount, 0);
	TestEqual(TEXT("The settlement-exception shell must preserve the blocked-settlement entry count."), ExceptionState.BlockedSettlementEntryCount, 2);
	TestTrue(TEXT("The settlement-exception shell must accumulate the mock estimated cost."), FMath::IsNearlyEqual(ExceptionState.TotalEstimatedCostUsd, 0.02f));
	TestTrue(TEXT("The settlement-exception shell must preserve zero settlement delta while provider charges are still pending."), FMath::IsNearlyEqual(ExceptionState.TotalSettlementDeltaUsd, 0.0f));
	TestEqual(TEXT("The settlement-exception shell must surface the pending provider-charge issue."), ExceptionState.ActiveIssue.IssueKind, TEXT("provider-charge-pending"));
	TestTrue(TEXT("The settlement-exception summary text must preserve the bounded pending-charge count."), ExceptionState.ExceptionSummaryText.Contains(TEXT("pendingChargeEntryCount=2")));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FHyperTwistWhisperCppPhase6RAEProviderBackedSettlementExceptionFallbackTest,
	"HyperTwist.Permissive.WhisperCpp.Phase6R.AE.ProviderBackedSettlementExceptionFallback",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

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

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

	HttpSpeechClient->ServiceBaseUrl.Reset();

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

	const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
		TrainingSubsystem->GetActiveCompanionSpeechSessionState();
	const FHyperTwistSpeechProviderSettlementExceptionShellState& ExceptionState =
		SessionState.ProviderSettlementExceptionShellState;

	TestTrue(TEXT("The provider-backed settlement-exception fallback must remain structurally valid."), ExceptionState.IsStructurallyValid());
	TestEqual(TEXT("The fallback settlement-exception shell must preserve the sidecar profile id."), ExceptionState.ActiveProviderProfileId, TEXT("speech-provider/local-http-sidecar-profile-v1"));
	TestEqual(TEXT("The fallback settlement-exception shell must preserve the fallback source kind."), ExceptionState.LatestSourceKind, TEXT("settlement-fallback-health"));
	TestEqual(TEXT("The fallback settlement-exception shell must preserve one exception entry."), ExceptionState.ExceptionEntryCount, 1);
	TestEqual(TEXT("The fallback settlement-exception shell must surface one active issue."), ExceptionState.ActiveIssueCount, 1);
	TestEqual(TEXT("The fallback settlement-exception shell must surface the route-degraded issue kind."), ExceptionState.ActiveIssue.IssueKind, TEXT("route-degraded"));
	TestTrue(TEXT("The fallback settlement-exception shell must still expose summary readiness for bounded inspection output."), ExceptionState.bExceptionSummaryReady);
	TestTrue(TEXT("The fallback settlement-exception shell must mark external handoff ready for route escalation posture."), ExceptionState.bExternalPortalHandoffReady);
	TestEqual(TEXT("The fallback settlement-exception shell must preserve one external-handoff-ready entry."), ExceptionState.ExternalPortalHandoffReadyEntryCount, 1);
	TestTrue(TEXT("The fallback settlement-exception summary text must preserve the missing-endpoint reason."), ExceptionState.ExceptionSummaryText.Contains(TEXT("service-base-url-missing")));
	return true;
}

#endif
