package quote_computation_service

import (
	"context"
	"errors"
	"github.com/tech/sendico/pkg/discovery"
	"strings"
	"testing"
	"time"

	"github.com/tech/sendico/payments/quotation/internal/service/quotation/gateway_funding_profile"
	"github.com/tech/sendico/payments/quotation/internal/service/quotation/transfer_intent_hydrator"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestBuildPlan_BuildsStepsAndFundingGate(t *testing.T) {
	svc := New(nil, WithFundingProfileResolver(gateway_funding_profile.NewStaticFundingProfileResolver(gateway_funding_profile.StaticFundingProfileResolverInput{
		GatewayModes: map[string]model.FundingMode{
			paymenttypes.DefaultCardsGatewayID: model.FundingModeBalanceReserve,
		},
	})))

	orgID := bson.NewObjectID()
	intent := sampleCardQuoteIntent()
	intent.Attributes["ledger_block_account_ref"] = "ledger:block"

	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		PreviewOnly:        false,
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if planModel == nil || len(planModel.Items) != 1 {
		t.Fatalf("expected single plan item")
	}

	item := planModel.Items[0]
	if item == nil {
		t.Fatalf("expected plan item")
	}
	if item.IdempotencyKey != "idem-key" {
		t.Fatalf("expected item idempotency key idem-key, got %q", item.IdempotencyKey)
	}
	if item.QuoteInput.IdempotencyKey != "idem-key" {
		t.Fatalf("expected quote input idempotency key idem-key, got %q", item.QuoteInput.IdempotencyKey)
	}
	if len(item.Steps) != 2 {
		t.Fatalf("expected 2 steps, got %d", len(item.Steps))
	}
	if item.Steps[0].Operation != discovery.RailOperationMove {
		t.Fatalf("expected source operation MOVE, got %q", item.Steps[0].Operation)
	}
	if item.Steps[1].Operation != discovery.RailOperationSend {
		t.Fatalf("expected destination operation SEND, got %q", item.Steps[1].Operation)
	}
	if item.Funding == nil {
		t.Fatalf("expected funding gate")
	}
	if item.Funding.Mode != model.FundingModeBalanceReserve {
		t.Fatalf("expected funding mode balance_reserve, got %q", item.Funding.Mode)
	}
	if item.Route == nil {
		t.Fatalf("expected route specification")
	}
	if got := strings.TrimSpace(item.Route.GetRail()); got != "" {
		t.Fatalf("expected route rail header to be empty, got %q", got)
	}
	if got := item.Route.GetRouteRef(); got == "" {
		t.Fatalf("expected route_ref")
	}
	if got := item.Route.GetPricingProfileRef(); got == "" {
		t.Fatalf("expected pricing_profile_ref")
	}
	if got, want := len(item.Route.GetHops()), 2; got != want {
		t.Fatalf("unexpected route hops count: got=%d want=%d", got, want)
	}
	if item.Route.GetSettlement() == nil || item.Route.GetSettlement().GetAsset() == nil {
		t.Fatalf("expected route settlement asset")
	}
	if got, want := item.Route.GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USD"; got != want {
		t.Fatalf("unexpected settlement asset token: got=%q want=%q", got, want)
	}
	if item.ExecutionConditions == nil {
		t.Fatalf("expected execution conditions")
	}
	if !item.ExecutionConditions.GetPrefundingRequired() {
		t.Fatalf("expected prefunding required for balance reserve mode")
	}
}

func TestBuildPlan_RequiresFXAddsMiddleStep(t *testing.T) {
	svc := New(nil)
	orgID := bson.NewObjectID()
	intent := sampleCardQuoteIntent()
	intent.RequiresFX = true

	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		PreviewOnly:        false,
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(planModel.Items) != 1 || len(planModel.Items[0].Steps) != 3 {
		t.Fatalf("expected 3 steps for FX intent")
	}
	if got := planModel.Items[0].Steps[1].Operation; got != discovery.RailOperationFXConvert {
		t.Fatalf("expected middle step FX_CONVERT, got %q", got)
	}
	if planModel.Items[0].Route == nil {
		t.Fatalf("expected route specification")
	}
	if got, want := len(planModel.Items[0].Route.GetHops()), 3; got != want {
		t.Fatalf("unexpected route hops count: got=%d want=%d", got, want)
	}
	if got, want := planModel.Items[0].Route.GetHops()[1].GetRole(), quotationv2.RouteHopRole_ROUTE_HOP_ROLE_TRANSIT; got != want {
		t.Fatalf("unexpected middle hop role: got=%s want=%s", got.String(), want.String())
	}
}

func TestBuildPlan_RequiresFXUsesSettlementCurrencyForDestinationStep(t *testing.T) {
	svc := New(nil)
	orgID := bson.NewObjectID()
	intent := sampleCryptoToCardQuoteIntent()
	intent.RequiresFX = true
	intent.SettlementCurrency = "RUB"

	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		PreviewOnly:        false,
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(planModel.Items) != 1 {
		t.Fatalf("expected one plan item")
	}
	steps := planModel.Items[0].Steps
	if got, want := len(steps), 4; got != want {
		t.Fatalf("unexpected step count: got=%d want=%d", got, want)
	}
	last := steps[len(steps)-1]
	if last == nil || last.Amount == nil {
		t.Fatalf("expected destination step amount")
	}
	if got, want := strings.TrimSpace(last.Amount.GetCurrency()), "RUB"; got != want {
		t.Fatalf("unexpected destination step currency: got=%q want=%q", got, want)
	}
	if got := last.Operation; got != discovery.RailOperationSend {
		t.Fatalf("unexpected destination operation: got=%q want=%q", got, discovery.RailOperationSend)
	}
}

func TestBuildPlan_UsesSourceAssetCurrencyForSourceStep(t *testing.T) {
	svc := New(nil)
	orgID := bson.NewObjectID()
	intent := sampleCryptoToCardQuoteIntent()
	intent.SettlementMode = transfer_intent_hydrator.QuoteSettlementModeFixReceived
	intent.Amount = &paymenttypes.Money{
		Amount:   "5000",
		Currency: "RUB",
	}
	intent.SettlementCurrency = "RUB"
	intent.RequiresFX = false

	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		PreviewOnly:        false,
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(planModel.Items) != 1 {
		t.Fatalf("expected one plan item")
	}
	item := planModel.Items[0]
	if item == nil {
		t.Fatalf("expected plan item")
	}
	if !item.QuoteInput.Intent.RequiresFX {
		t.Fatalf("expected derived FX requirement for fix_received cross-currency flow")
	}
	if item.QuoteInput.Intent.FX == nil || item.QuoteInput.Intent.FX.Pair == nil {
		t.Fatalf("expected derived FX pair")
	}
	if got, want := strings.TrimSpace(item.QuoteInput.Intent.FX.Pair.GetBase()), "USDT"; got != want {
		t.Fatalf("unexpected derived FX base currency: got=%q want=%q", got, want)
	}
	if got, want := strings.TrimSpace(item.QuoteInput.Intent.FX.Pair.GetQuote()), "RUB"; got != want {
		t.Fatalf("unexpected derived FX quote currency: got=%q want=%q", got, want)
	}
	steps := item.Steps
	if got, want := len(steps), 4; got != want {
		t.Fatalf("unexpected step count: got=%d want=%d", got, want)
	}
	if steps[0] == nil || steps[0].Amount == nil {
		t.Fatalf("expected source step amount")
	}
	if got, want := strings.TrimSpace(steps[0].Amount.GetCurrency()), "USDT"; got != want {
		t.Fatalf("unexpected source step currency: got=%q want=%q", got, want)
	}
	last := steps[len(steps)-1]
	if last == nil || last.Amount == nil {
		t.Fatalf("expected destination step amount")
	}
	if got, want := strings.TrimSpace(last.Amount.GetCurrency()), "RUB"; got != want {
		t.Fatalf("unexpected destination step currency: got=%q want=%q", got, want)
	}
	if got := steps[1].Operation; got != discovery.RailOperationFXConvert {
		t.Fatalf("unexpected middle operation: got=%q want=%q", got, discovery.RailOperationFXConvert)
	}
}

func TestBuildPlan_ResolvesIndependentEconomicsKnobs(t *testing.T) {
	svc := New(nil)
	orgID := bson.NewObjectID()
	intent := sampleCardQuoteIntent()
	intent.SettlementMode = transfer_intent_hydrator.QuoteSettlementModeFixReceived
	intent.FeeTreatment = transfer_intent_hydrator.QuoteFeeTreatmentDeductFromDestination

	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if planModel == nil || len(planModel.Items) != 1 {
		t.Fatalf("expected single plan item")
	}
	item := planModel.Items[0]
	if item == nil {
		t.Fatalf("expected plan item")
	}
	if got, want := item.ResolvedSettlementMode, paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED; got != want {
		t.Fatalf("unexpected resolved settlement mode: got=%s want=%s", got.String(), want.String())
	}
	if got, want := item.ResolvedFeeTreatment, quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION; got != want {
		t.Fatalf("unexpected resolved fee treatment: got=%s want=%s", got.String(), want.String())
	}
}

func TestBuildPlan_DefaultsResolvedFeeTreatmentWhenUnspecified(t *testing.T) {
	svc := New(nil)
	orgID := bson.NewObjectID()
	intent := sampleCardQuoteIntent()
	intent.SettlementMode = transfer_intent_hydrator.QuoteSettlementModeFixReceived
	intent.FeeTreatment = transfer_intent_hydrator.QuoteFeeTreatmentUnspecified

	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if planModel == nil || len(planModel.Items) != 1 {
		t.Fatalf("expected single plan item")
	}
	item := planModel.Items[0]
	if item == nil {
		t.Fatalf("expected plan item")
	}
	if got, want := item.ResolvedSettlementMode, paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED; got != want {
		t.Fatalf("unexpected resolved settlement mode: got=%s want=%s", got.String(), want.String())
	}
	if got, want := item.ResolvedFeeTreatment, quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
		t.Fatalf("unexpected default resolved fee treatment: got=%s want=%s", got.String(), want.String())
	}
}

func TestBuildPlan_SelectsGatewaysAndIgnoresIrrelevant(t *testing.T) {
	svc := New(nil, WithGatewayRegistry(staticGatewayRegistry{
		items: []*model.GatewayInstanceDescriptor{
			{
				ID:         "crypto-disabled",
				InstanceID: "crypto-disabled",
				Rail:       discovery.RailCrypto,
				Network:    "TRON",
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				IsEnabled: false,
			},
			{
				ID:         "crypto-network-mismatch",
				InstanceID: "crypto-network-mismatch",
				Rail:       discovery.RailCrypto,
				Network:    "ETH",
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				IsEnabled: true,
			},
			{
				ID:         "crypto-currency-mismatch",
				InstanceID: "crypto-currency-mismatch",
				Rail:       discovery.RailCrypto,
				Network:    "TRON",
				Currencies: []string{"EUR"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				IsEnabled: true,
			},
			{
				ID:         "crypto-gw-1",
				InstanceID: "crypto-gw-1",
				Rail:       discovery.RailCrypto,
				Network:    "TRON",
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				IsEnabled: true,
			},
			{
				ID:         "payout-disabled",
				InstanceID: "payout-disabled",
				Rail:       discovery.RailCardPayout,
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				IsEnabled: false,
			},
			{
				ID:         "payout-currency-mismatch",
				InstanceID: "payout-currency-mismatch",
				Rail:       discovery.RailCardPayout,
				Currencies: []string{"EUR"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				IsEnabled: true,
			},
			{
				ID:         "payout-gw-1",
				InstanceID: "payout-gw-1",
				Rail:       discovery.RailCardPayout,
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				IsEnabled: true,
			},
			{
				ID:         "provider-ignored",
				InstanceID: "provider-ignored",
				Rail:       discovery.RailProviderSettlement,
				Network:    "TRON",
				Currencies: []string{"USDT"},
				IsEnabled:  true,
			},
		},
	}))

	orgID := bson.NewObjectID()
	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		PreviewOnly:        false,
		Intents:            []*transfer_intent_hydrator.QuoteIntent{sampleCryptoToCardQuoteIntent()},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if planModel == nil || len(planModel.Items) != 1 {
		t.Fatalf("expected one plan item")
	}

	item := planModel.Items[0]
	if item == nil {
		t.Fatalf("expected non-nil plan item")
	}
	if got, want := len(item.Steps), 3; got != want {
		t.Fatalf("unexpected step count: got=%d want=%d", got, want)
	}
	if got, want := item.Steps[0].GatewayID, "crypto-gw-1"; got != want {
		t.Fatalf("unexpected source gateway: got=%q want=%q", got, want)
	}
	if got, want := item.Steps[1].GatewayID, "internal"; got != want {
		t.Fatalf("unexpected bridge gateway: got=%q want=%q", got, want)
	}
	if got, want := item.Steps[2].GatewayID, "payout-gw-1"; got != want {
		t.Fatalf("unexpected destination gateway: got=%q want=%q", got, want)
	}
	if item.Route == nil {
		t.Fatalf("expected route")
	}
	if got := strings.TrimSpace(item.Route.GetProvider()); got != "" {
		t.Fatalf("expected route provider header to be empty, got %q", got)
	}
	if got, want := len(item.Route.GetHops()), 3; got != want {
		t.Fatalf("unexpected route hop count: got=%d want=%d", got, want)
	}
	if got, want := item.Route.GetHops()[1].GetRail(), "LEDGER"; got != want {
		t.Fatalf("unexpected bridge rail: got=%q want=%q", got, want)
	}
	if got, want := item.Route.GetHops()[1].GetRole(), quotationv2.RouteHopRole_ROUTE_HOP_ROLE_TRANSIT; got != want {
		t.Fatalf("unexpected bridge role: got=%s want=%s", got.String(), want.String())
	}
	if item.Route.GetSettlement() == nil || item.Route.GetSettlement().GetAsset() == nil {
		t.Fatalf("expected route settlement asset")
	}
	if got, want := item.Route.GetSettlement().GetAsset().GetKey().GetChain(), "TRON"; got != want {
		t.Fatalf("unexpected settlement asset chain: got=%q want=%q", got, want)
	}
	if got, want := item.Route.GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
		t.Fatalf("unexpected settlement asset token: got=%q want=%q", got, want)
	}
	if got := item.Route.GetRouteRef(); got == "" {
		t.Fatalf("expected route_ref")
	}
	if got := item.Route.GetPricingProfileRef(); got == "" {
		t.Fatalf("expected pricing_profile_ref")
	}
}

func TestCompute_EnrichesRouteConditionsAndTotalCost(t *testing.T) {
	core := &fakeCore{
		quote: &ComputedQuote{
			DebitAmount: &moneyv1.Money{Amount: "100", Currency: "USD"},
			FeeLines: []*feesv1.DerivedPostingLine{
				{
					Money:    &moneyv1.Money{Amount: "1.50", Currency: "USD"},
					Side:     accountingv1.EntrySide_ENTRY_SIDE_DEBIT,
					LineType: accountingv1.PostingLineType_POSTING_LINE_FEE,
				},
			},
		},
		expiresAt: time.Unix(1000, 0),
	}
	svc := New(core, WithFundingProfileResolver(gateway_funding_profile.NewStaticFundingProfileResolver(gateway_funding_profile.StaticFundingProfileResolverInput{
		GatewayModes: map[string]model.FundingMode{
			paymenttypes.DefaultCardsGatewayID: model.FundingModeBalanceReserve,
		},
	})))

	orgID := bson.NewObjectID()
	output, err := svc.Compute(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		PreviewOnly:        false,
		Intents:            []*transfer_intent_hydrator.QuoteIntent{sampleCardQuoteIntent()},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if output == nil || len(output.Results) != 1 {
		t.Fatalf("expected single result")
	}
	result := output.Results[0]
	if result == nil || result.Quote == nil {
		t.Fatalf("expected result quote")
	}
	if result.Quote.Route == nil {
		t.Fatalf("expected route specification")
	}
	if got := strings.TrimSpace(result.Quote.Route.GetPayoutMethod()); got != "" {
		t.Fatalf("expected payout method header to be empty, got %q", got)
	}
	if got := result.Quote.Route.GetRouteRef(); got == "" {
		t.Fatalf("expected route_ref")
	}
	if got := result.Quote.Route.GetPricingProfileRef(); got == "" {
		t.Fatalf("expected pricing_profile_ref")
	}
	if got, want := len(result.Quote.Route.GetHops()), 2; got != want {
		t.Fatalf("unexpected route hops count: got=%d want=%d", got, want)
	}
	if result.Quote.Route.GetSettlement() == nil || result.Quote.Route.GetSettlement().GetAsset() == nil {
		t.Fatalf("expected route settlement asset")
	}
	if got, want := result.Quote.Route.GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USD"; got != want {
		t.Fatalf("unexpected route settlement token: got=%q want=%q", got, want)
	}
	if result.Quote.ExecutionConditions == nil {
		t.Fatalf("expected execution conditions")
	}
	if got := result.Quote.ExecutionConditions.GetReadiness(); got != quotationv2.QuoteExecutionReadiness_QUOTE_EXECUTION_READINESS_LIQUIDITY_OBTAINABLE {
		t.Fatalf("unexpected readiness: %s", got.String())
	}
	if result.Quote.TotalCost == nil {
		t.Fatalf("expected total cost")
	}
	if got, want := result.Quote.TotalCost.GetAmount(), "101.5"; got != want {
		t.Fatalf("unexpected total cost amount: got=%q want=%q", got, want)
	}
	if core.lastQuoteIn.Route == nil {
		t.Fatalf("expected selected route to be passed into build quote input")
	}
	hops := core.lastQuoteIn.Route.GetHops()
	if got, want := len(hops), 2; got != want {
		t.Fatalf("unexpected route hops in build input: got=%d want=%d", got, want)
	}
	if got, want := hops[1].GetGateway(), paymenttypes.DefaultCardsGatewayID; got != want {
		t.Fatalf("unexpected destination gateway in build input route: got=%q want=%q", got, want)
	}
	if core.lastQuoteIn.ExecutionConditions == nil {
		t.Fatalf("expected execution conditions to be passed into build quote input")
	}
	if got := core.lastQuoteIn.ExecutionConditions.GetPrefundingRequired(); !got {
		t.Fatalf("expected prefunding_required in build quote input for reserve mode")
	}
	if got, want := result.Quote.ResolvedSettlementMode, paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
		t.Fatalf("unexpected resolved settlement mode: got=%s want=%s", got.String(), want.String())
	}
	if got, want := result.Quote.ResolvedFeeTreatment, quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
		t.Fatalf("unexpected resolved fee treatment: got=%s want=%s", got.String(), want.String())
	}
}

func TestCompute_PropagatesIndependentResolvedEconomics(t *testing.T) {
	core := &fakeCore{
		quote: &ComputedQuote{
			DebitAmount: &moneyv1.Money{Amount: "100", Currency: "USD"},
		},
		expiresAt: time.Unix(1000, 0),
	}
	svc := New(core)

	orgID := bson.NewObjectID()
	intent := sampleCardQuoteIntent()
	intent.SettlementMode = transfer_intent_hydrator.QuoteSettlementModeFixReceived
	intent.FeeTreatment = transfer_intent_hydrator.QuoteFeeTreatmentDeductFromDestination

	output, err := svc.Compute(context.Background(), ComputeInput{
		OrganizationRef: orgID.Hex(),
		OrganizationID:  orgID,
		PreviewOnly:     true,
		Intents:         []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if output == nil || len(output.Results) != 1 {
		t.Fatalf("expected single result")
	}
	if output.Results[0].Quote == nil {
		t.Fatalf("expected quote")
	}
	if got, want := output.Results[0].Quote.ResolvedSettlementMode, paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED; got != want {
		t.Fatalf("unexpected resolved settlement mode: got=%s want=%s", got.String(), want.String())
	}
	if got, want := output.Results[0].Quote.ResolvedFeeTreatment, quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION; got != want {
		t.Fatalf("unexpected resolved fee treatment: got=%s want=%s", got.String(), want.String())
	}
}

func TestCompute_PreviewMarksIndicativeReadiness(t *testing.T) {
	core := &fakeCore{
		quote: &ComputedQuote{
			DebitAmount: &moneyv1.Money{Amount: "100", Currency: "USD"},
		},
		expiresAt: time.Unix(1000, 0),
	}
	svc := New(core)

	orgID := bson.NewObjectID()
	output, err := svc.Compute(context.Background(), ComputeInput{
		OrganizationRef: orgID.Hex(),
		OrganizationID:  orgID,
		PreviewOnly:     true,
		Intents:         []*transfer_intent_hydrator.QuoteIntent{sampleCardQuoteIntent()},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if output == nil || len(output.Results) != 1 {
		t.Fatalf("expected single result")
	}
	if output.Results[0].Quote == nil || output.Results[0].Quote.ExecutionConditions == nil {
		t.Fatalf("expected execution conditions")
	}
	if got := output.Results[0].Quote.ExecutionConditions.GetReadiness(); got != quotationv2.QuoteExecutionReadiness_QUOTE_EXECUTION_READINESS_INDICATIVE {
		t.Fatalf("unexpected readiness: %s", got.String())
	}
}

func TestCompute_FailsWhenCoreReturnsDifferentRoute(t *testing.T) {
	core := &fakeCore{
		quote: &ComputedQuote{
			DebitAmount: &moneyv1.Money{Amount: "100", Currency: "USD"},
			Route: &quotationv2.RouteSpecification{
				Rail:         "CARD",
				Provider:     "other-provider",
				PayoutMethod: "CARD",
			},
		},
		expiresAt: time.Unix(1000, 0),
	}
	svc := New(core)

	orgID := bson.NewObjectID()
	_, err := svc.Compute(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-key",
		PreviewOnly:        false,
		Intents:            []*transfer_intent_hydrator.QuoteIntent{sampleCardQuoteIntent()},
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for route mismatch, got %v", err)
	}
}

type fakeCore struct {
	quote       *ComputedQuote
	expiresAt   time.Time
	quoteErr    error
	quoteCalls  int
	lastQuoteIn BuildQuoteInput
}

func (f *fakeCore) BuildQuote(_ context.Context, in BuildQuoteInput) (*ComputedQuote, time.Time, error) {
	f.quoteCalls++
	f.lastQuoteIn = in
	if f.quoteErr != nil {
		return nil, time.Time{}, f.quoteErr
	}
	return f.quote, f.expiresAt, nil
}

func sampleCardQuoteIntent() *transfer_intent_hydrator.QuoteIntent {
	return &transfer_intent_hydrator.QuoteIntent{
		Ref:            "intent-1",
		Kind:           transfer_intent_hydrator.QuoteIntentKindPayout,
		SettlementMode: transfer_intent_hydrator.QuoteSettlementModeFixSource,
		Source: transfer_intent_hydrator.QuoteEndpoint{
			Type: transfer_intent_hydrator.QuoteEndpointTypeLedger,
			Ledger: &transfer_intent_hydrator.QuoteLedgerEndpoint{
				LedgerAccountRef:       "ledger:src",
				ContraLedgerAccountRef: "ledger:contra",
			},
		},
		Destination: transfer_intent_hydrator.QuoteEndpoint{
			Type: transfer_intent_hydrator.QuoteEndpointTypeCard,
			Card: &transfer_intent_hydrator.QuoteCardEndpoint{
				Token: "tok_1",
			},
		},
		Amount: &paymenttypes.Money{
			Amount:   "100",
			Currency: "USD",
		},
		SettlementCurrency: "USD",
		Attributes: map[string]string{
			"gateway": paymenttypes.DefaultCardsGatewayID,
		},
	}
}

func sampleCryptoToCardQuoteIntent() *transfer_intent_hydrator.QuoteIntent {
	return &transfer_intent_hydrator.QuoteIntent{
		Ref:            "intent-crypto-card",
		Kind:           transfer_intent_hydrator.QuoteIntentKindPayout,
		SettlementMode: transfer_intent_hydrator.QuoteSettlementModeFixSource,
		Source: transfer_intent_hydrator.QuoteEndpoint{
			Type: transfer_intent_hydrator.QuoteEndpointTypeManagedWallet,
			ManagedWallet: &transfer_intent_hydrator.QuoteManagedWalletEndpoint{
				ManagedWalletRef: "wallet-usdt-source",
				Asset: &paymenttypes.Asset{
					Chain:       "TRON",
					TokenSymbol: "USDT",
				},
			},
		},
		Destination: transfer_intent_hydrator.QuoteEndpoint{
			Type: transfer_intent_hydrator.QuoteEndpointTypeCard,
			Card: &transfer_intent_hydrator.QuoteCardEndpoint{
				Token: "tok_1",
			},
		},
		Amount: &paymenttypes.Money{
			Amount:   "100",
			Currency: "USDT",
		},
		SettlementCurrency: "USDT",
		Attributes:         map[string]string{},
	}
}

type staticGatewayRegistry struct {
	items []*model.GatewayInstanceDescriptor
}

func (r staticGatewayRegistry) List(context.Context) ([]*model.GatewayInstanceDescriptor, error) {
	if len(r.items) == 0 {
		return nil, nil
	}
	out := make([]*model.GatewayInstanceDescriptor, 0, len(r.items))
	for _, item := range r.items {
		if item == nil {
			continue
		}
		cloned := *item
		if item.Currencies != nil {
			cloned.Currencies = append([]string(nil), item.Currencies...)
		}
		out = append(out, &cloned)
	}
	return out, nil
}
