package transfer_intent_hydrator

import (
	"context"
	"errors"
	"strings"
	"testing"

	pkgmodel "github.com/tech/sendico/pkg/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	pboundv1 "github.com/tech/sendico/pkg/proto/common/permission_bound/v1"
	storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1"
	endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
	methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"google.golang.org/grpc"
)

func TestHydrateOne_SuccessWithInlineMethods(t *testing.T) {
	h := New(nil, WithRefFactory(func() string { return "q-intent-1" }))
	tag := "12345"
	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
						WalletID: "mw-src-1",
					}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS,
					Data: mustMarshalBSON(t, pkgmodel.CryptoAddressPaymentData{
						Currency:       pkgmodel.CurrencyUSDT,
						Address:        "TXYZ",
						Network:        "TRON_MAINNET",
						DestinationTag: &tag,
					}),
				},
			},
		},
		Amount:  newMoney("10.25", "USDT"),
		Comment: "transfer note",
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: bson.NewObjectID().Hex(),
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("HydrateOne returned error: %v", err)
	}
	if got == nil {
		t.Fatalf("expected hydrated intent")
	}
	if got.Ref != "q-intent-1" {
		t.Fatalf("expected ref q-intent-1, got %q", got.Ref)
	}
	if got.Kind != QuoteIntentKindPayout {
		t.Fatalf("expected payout kind, got %s", got.Kind)
	}
	if got.Source.Type != QuoteEndpointTypeManagedWallet {
		t.Fatalf("expected managed wallet source, got %s", got.Source.Type)
	}
	if got.Source.ManagedWallet == nil || got.Source.ManagedWallet.ManagedWalletRef != "mw-src-1" {
		t.Fatalf("unexpected managed wallet source: %#v", got.Source.ManagedWallet)
	}
	if got.Destination.Type != QuoteEndpointTypeExternalChain {
		t.Fatalf("expected external chain destination, got %s", got.Destination.Type)
	}
	if got.Destination.ExternalChain == nil {
		t.Fatalf("expected external chain payload")
	}
	if got.Destination.ExternalChain.Asset == nil || got.Destination.ExternalChain.Asset.Chain != "TRON_MAINNET" {
		t.Fatalf("expected TRON_MAINNET chain, got %#v", got.Destination.ExternalChain.Asset)
	}
	if got.Destination.ExternalChain.Memo != tag {
		t.Fatalf("expected destination memo %q, got %q", tag, got.Destination.ExternalChain.Memo)
	}
	if got.SettlementCurrency != "USDT" {
		t.Fatalf("expected settlement currency USDT, got %q", got.SettlementCurrency)
	}
	if got.SettlementMode != QuoteSettlementModeFixSource {
		t.Fatalf("expected default settlement mode fix_source, got %s", got.SettlementMode)
	}
	if got.FeeTreatment != QuoteFeeTreatmentAddToSource {
		t.Fatalf("expected default fee treatment add_to_source, got %s", got.FeeTreatment)
	}
	if got.Amount == nil || got.Amount.Amount != "10.25" {
		t.Fatalf("unexpected amount: %#v", got.Amount)
	}
	if got.Comment != "transfer note" {
		t.Fatalf("unexpected comment: got=%q", got.Comment)
	}
	if _, ok := got.Attributes["comment"]; ok {
		t.Fatalf("comment must not be persisted in attributes")
	}
}

func TestHydrateOne_PropagatesFXIntent(t *testing.T) {
	h := New(nil, WithRefFactory(func() string { return "q-intent-fx-side" }))
	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{WalletID: "mw-src-1"}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
					Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
						Pan:      "4111111111111111",
						ExpMonth: "12",
						ExpYear:  "2030",
						Country:  "US",
					}),
				},
			},
		},
		Amount:             newMoney("10", "USDT"),
		SettlementCurrency: "RUB",
		Fx: &sharedv1.FXIntent{
			Pair: &fxv1.CurrencyPair{
				Base:  "USDT",
				Quote: "RUB",
			},
			Side:              fxv1.Side_BUY_BASE_SELL_QUOTE,
			Firm:              true,
			TtlMs:             12_000,
			PreferredProvider: "bestfx",
			MaxAgeMs:          1_000,
		},
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: bson.NewObjectID().Hex(),
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("HydrateOne returned error: %v", err)
	}
	if got == nil {
		t.Fatalf("expected hydrated intent")
	}
	if got.FX == nil || got.FX.Pair == nil {
		t.Fatalf("expected hydrated fx intent")
	}
	if got.FX.Side != paymenttypes.FXSideBuyBaseSellQuote {
		t.Fatalf("unexpected fx side: got=%q", got.FX.Side)
	}
	if got.FX.Pair.GetBase() != "USDT" || got.FX.Pair.GetQuote() != "RUB" {
		t.Fatalf("unexpected fx pair: got=%s/%s", got.FX.Pair.GetBase(), got.FX.Pair.GetQuote())
	}
	if !got.FX.Firm || got.FX.TTLms != 12_000 || got.FX.PreferredProvider != "bestfx" || got.FX.MaxAgeMs != 1_000 {
		t.Fatalf("unexpected fx extras: %#v", got.FX)
	}
}

func TestHydrateOne_RequiresFXWhenExplicitFXProvided(t *testing.T) {
	h := New(nil, WithRefFactory(func() string { return "q-intent-fx-required" }))
	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER,
					Data: mustMarshalBSON(t, map[string]string{"ledgerAccountRef": "ledger-src"}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
					Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
						Pan:      "4111111111111111",
						ExpMonth: "12",
						ExpYear:  "2030",
						Country:  "US",
					}),
				},
			},
		},
		Amount:             newMoney("10", "RUB"),
		SettlementCurrency: "RUB",
		Fx: &sharedv1.FXIntent{
			Pair: &fxv1.CurrencyPair{Base: "USDT", Quote: "RUB"},
			Side: fxv1.Side_SELL_BASE_BUY_QUOTE,
		},
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: bson.NewObjectID().Hex(),
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("HydrateOne returned error: %v", err)
	}
	if got == nil {
		t.Fatalf("expected hydrated intent")
	}
	if !got.RequiresFX {
		t.Fatalf("expected requires_fx=true when explicit fx is supplied")
	}
}

func TestHydrateOne_ResolvesPaymentMethodRefViaPrivateMethod(t *testing.T) {
	orgRef := bson.NewObjectID().Hex()
	methodRef := bson.NewObjectID().Hex()
	resolvedMethodRef := bson.NewObjectID().Hex()

	var getReq *methodsv1.GetPaymentMethodPrivateRequest
	h := New(&fakeMethodsClient{
		getPaymentMethodPrivateFn: func(
			_ context.Context,
			req *methodsv1.GetPaymentMethodPrivateRequest,
			_ ...grpc.CallOption,
		) (*methodsv1.GetPaymentMethodPrivateResponse, error) {
			getReq = req
			return &methodsv1.GetPaymentMethodPrivateResponse{
				PaymentMethodRecord: &endpointv1.PaymentMethodRecord{
					PermissionBound: &pboundv1.PermissionBound{
						Storable: &storablev1.Storable{Id: resolvedMethodRef},
					},
					PaymentMethod: &endpointv1.PaymentMethod{
						Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
						Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
							WalletID: "mw-source-ref",
						}),
					},
				},
			}, nil
		},
	})

	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{PaymentMethodRef: methodRef},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
					Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
						Pan:      "4111111111111111",
						ExpMonth: "12",
						ExpYear:  "2030",
						Country:  "US",
					}),
				},
			},
		},
		Amount: newMoney("1", "USD"),
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: orgRef,
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("HydrateOne returned error: %v", err)
	}
	if getReq == nil {
		t.Fatalf("expected GetPaymentMethodPrivate call")
	}
	if getReq.GetOrganizationRef() != orgRef {
		t.Fatalf("unexpected organization_ref in request: %#v", getReq)
	}
	if getReq.GetEndpoint() != methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_SOURCE {
		t.Fatalf("expected source endpoint role, got %s", getReq.GetEndpoint().String())
	}
	if getReq.GetPaymentMethodRef() != methodRef {
		t.Fatalf("unexpected payment_method_ref in request: %#v", getReq)
	}
	if got.Source.PaymentMethodRef != resolvedMethodRef {
		t.Fatalf("expected resolved payment_method_ref %q, got %q", resolvedMethodRef, got.Source.PaymentMethodRef)
	}
	if got.Source.ManagedWallet == nil || got.Source.ManagedWallet.ManagedWalletRef != "mw-source-ref" {
		t.Fatalf("unexpected resolved source: %#v", got.Source)
	}
}

func TestHydrateOne_ResolvesPayeeRefViaPrivateMethod(t *testing.T) {
	orgRef := bson.NewObjectID().Hex()
	payeeRef := bson.NewObjectID().Hex()
	mainMethodRef := bson.NewObjectID().Hex()

	var getReq *methodsv1.GetPaymentMethodPrivateRequest
	h := New(&fakeMethodsClient{
		getPaymentMethodPrivateFn: func(
			_ context.Context,
			req *methodsv1.GetPaymentMethodPrivateRequest,
			_ ...grpc.CallOption,
		) (*methodsv1.GetPaymentMethodPrivateResponse, error) {
			getReq = req
			return &methodsv1.GetPaymentMethodPrivateResponse{
				PaymentMethodRecord: &endpointv1.PaymentMethodRecord{
					PermissionBound: &pboundv1.PermissionBound{
						Storable: &storablev1.Storable{Id: mainMethodRef},
					},
					PaymentMethod: &endpointv1.PaymentMethod{
						Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN,
						Data: mustMarshalBSON(t, pkgmodel.TokenPaymentData{
							Token:          "tok-1",
							CardholderName: "John",
							ExpMonth:       "1",
							ExpYear:        "2031",
							Country:        "US",
						}),
					},
				},
			}, nil
		},
	})

	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
						WalletID: "mw-src",
					}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PayeeRef{PayeeRef: payeeRef},
		},
		Amount: newMoney("7.5", "USD"),
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: orgRef,
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("HydrateOne returned error: %v", err)
	}
	if getReq == nil {
		t.Fatalf("expected GetPaymentMethodPrivate call")
	}
	if getReq.GetOrganizationRef() != orgRef || getReq.GetPayeeRef() != payeeRef {
		t.Fatalf("unexpected request: %#v", getReq)
	}
	if getReq.GetEndpoint() != methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_DESTINATION {
		t.Fatalf("expected destination endpoint role, got %s", getReq.GetEndpoint().String())
	}
	if got.Destination.Type != QuoteEndpointTypeCard {
		t.Fatalf("expected card destination, got %s", got.Destination.Type)
	}
	if got.Destination.Card == nil || got.Destination.Card.Token != "tok-1" {
		t.Fatalf("unexpected selected destination method: %#v", got.Destination)
	}
	if got.Destination.PaymentMethodRef != mainMethodRef {
		t.Fatalf("expected selected main method ref %q, got %q", mainMethodRef, got.Destination.PaymentMethodRef)
	}
}

func TestHydrateOne_ResolvesInlineAccountPaymentMethodViaPrivateMethod(t *testing.T) {
	orgRef := bson.NewObjectID().Hex()
	accountRecipientRef := bson.NewObjectID().Hex()
	childMethodRef := bson.NewObjectID().Hex()

	var getReq *methodsv1.GetPaymentMethodPrivateRequest
	h := New(&fakeMethodsClient{
		getPaymentMethodPrivateFn: func(
			_ context.Context,
			req *methodsv1.GetPaymentMethodPrivateRequest,
			_ ...grpc.CallOption,
		) (*methodsv1.GetPaymentMethodPrivateResponse, error) {
			getReq = req
			return &methodsv1.GetPaymentMethodPrivateResponse{
				PaymentMethodRecord: &endpointv1.PaymentMethodRecord{
					PermissionBound: &pboundv1.PermissionBound{
						Storable: &storablev1.Storable{Id: childMethodRef},
					},
					PaymentMethod: &endpointv1.PaymentMethod{
						Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
						Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
							WalletID: "mw-child-main",
						}),
					},
				},
			}, nil
		},
	})

	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
						WalletID: "mw-src",
					}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type:         endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_ACCOUNT,
					RecipientRef: accountRecipientRef,
				},
			},
		},
		Amount: newMoney("4.2", "USD"),
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: orgRef,
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("HydrateOne returned error: %v", err)
	}
	if getReq == nil {
		t.Fatalf("expected GetPaymentMethodPrivate call")
	}
	if getReq.GetOrganizationRef() != orgRef || getReq.GetPayeeRef() != accountRecipientRef {
		t.Fatalf("unexpected request: %#v", getReq)
	}
	if getReq.GetEndpoint() != methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_DESTINATION {
		t.Fatalf("expected destination endpoint role, got %s", getReq.GetEndpoint().String())
	}
	if got.Destination.Type != QuoteEndpointTypeManagedWallet {
		t.Fatalf("expected managed wallet destination, got %s", got.Destination.Type)
	}
	if got.Destination.ManagedWallet == nil || got.Destination.ManagedWallet.ManagedWalletRef != "mw-child-main" {
		t.Fatalf("unexpected resolved destination: %#v", got.Destination.ManagedWallet)
	}
	if got.Destination.PaymentMethodRef != childMethodRef {
		t.Fatalf("expected resolved child method ref %q, got %q", childMethodRef, got.Destination.PaymentMethodRef)
	}
}

func TestHydrateOne_ReferenceWithoutMethodsClientFails(t *testing.T) {
	h := New(nil)
	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{PaymentMethodRef: bson.NewObjectID().Hex()},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
						WalletID: "mw-dst",
					}),
				},
			},
		},
		Amount: newMoney("1", "USD"),
	}

	_, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: bson.NewObjectID().Hex(),
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if !errors.Is(err, ErrPaymentMethodsClientRequired) {
		t.Fatalf("expected ErrPaymentMethodsClientRequired, got %v", err)
	}
}

func TestHydrateMany_IndexesError(t *testing.T) {
	h := New(nil)
	intents := []*quotationv2.QuoteIntent{
		{
			Source: &endpointv1.PaymentEndpoint{
				Source: &endpointv1.PaymentEndpoint_PaymentMethod{
					PaymentMethod: &endpointv1.PaymentMethod{
						Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
						Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
							WalletID: "mw-src",
						}),
					},
				},
			},
			Destination: &endpointv1.PaymentEndpoint{
				Source: &endpointv1.PaymentEndpoint_PaymentMethod{
					PaymentMethod: &endpointv1.PaymentMethod{
						Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
						Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
							Pan:      "4111111111111111",
							ExpMonth: "12",
							ExpYear:  "2030",
						}),
					},
				},
			},
			Amount: newMoney("1", "USD"),
		},
		{
			Source: &endpointv1.PaymentEndpoint{
				Source: &endpointv1.PaymentEndpoint_PaymentMethod{
					PaymentMethod: &endpointv1.PaymentMethod{
						Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
						Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{
							WalletID: "mw-src-2",
						}),
					},
				},
			},
			Destination: &endpointv1.PaymentEndpoint{
				Source: &endpointv1.PaymentEndpoint_PaymentMethod{
					PaymentMethod: &endpointv1.PaymentMethod{
						Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
						Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
							Pan:      "4111111111111111",
							ExpMonth: "nope",
							ExpYear:  "2030",
						}),
					},
				},
			},
			Amount: newMoney("2", "USD"),
		},
	}

	_, err := h.HydrateMany(context.Background(), HydrateManyInput{
		OrganizationRef: bson.NewObjectID().Hex(),
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intents:         intents,
	})
	if err == nil {
		t.Fatalf("expected batch hydration error")
	}
	if !strings.Contains(err.Error(), "intents[1]") {
		t.Fatalf("expected indexed error, got %v", err)
	}
}

func TestHydrateOne_AcceptsIndependentEconomicsKnobs(t *testing.T) {
	h := New(nil)
	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{WalletID: "mw-src"}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
					Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
						Pan:      "4111111111111111",
						ExpMonth: "12",
						ExpYear:  "2030",
					}),
				},
			},
		},
		Amount:         newMoney("1", "USD"),
		SettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED,
		FeeTreatment:   quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: bson.NewObjectID().Hex(),
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("expected independent economics knobs to be accepted, got %v", err)
	}
	if got.SettlementMode != QuoteSettlementModeFixReceived {
		t.Fatalf("unexpected settlement mode: got=%s", got.SettlementMode)
	}
	if got.FeeTreatment != QuoteFeeTreatmentAddToSource {
		t.Fatalf("unexpected fee treatment: got=%s", got.FeeTreatment)
	}
}

func TestHydrateOne_DefaultsSettlementModeWhenOnlyFeeTreatmentProvided(t *testing.T) {
	h := New(nil)
	intent := &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{WalletID: "mw-src"}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
					Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
						Pan:      "4111111111111111",
						ExpMonth: "12",
						ExpYear:  "2030",
					}),
				},
			},
		},
		Amount:         newMoney("1", "USD"),
		SettlementMode: paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED,
		FeeTreatment:   quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION,
	}

	got, err := h.HydrateOne(context.Background(), HydrateOneInput{
		OrganizationRef: bson.NewObjectID().Hex(),
		InitiatorRef:    bson.NewObjectID().Hex(),
		Intent:          intent,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got.SettlementMode != QuoteSettlementModeFixSource {
		t.Fatalf("unexpected default settlement mode: got=%s", got.SettlementMode)
	}
	if got.FeeTreatment != QuoteFeeTreatmentDeductFromDestination {
		t.Fatalf("unexpected resolved fee treatment: got=%s", got.FeeTreatment)
	}
}

func TestHydrateOne_ResolvesEconomicsMatrix(t *testing.T) {
	h := New(nil)
	cases := []struct {
		name     string
		mode     paymentv1.SettlementMode
		fee      quotationv2.FeeTreatment
		wantMode QuoteSettlementMode
		wantFee  QuoteFeeTreatment
	}{
		{
			name:     "defaults both when unspecified",
			mode:     paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED,
			fee:      quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED,
			wantMode: QuoteSettlementModeFixSource,
			wantFee:  QuoteFeeTreatmentAddToSource,
		},
		{
			name:     "preserves fix_source with add_to_source",
			mode:     paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
			fee:      quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
			wantMode: QuoteSettlementModeFixSource,
			wantFee:  QuoteFeeTreatmentAddToSource,
		},
		{
			name:     "preserves fix_source with deduct_from_destination",
			mode:     paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
			fee:      quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION,
			wantMode: QuoteSettlementModeFixSource,
			wantFee:  QuoteFeeTreatmentDeductFromDestination,
		},
		{
			name:     "preserves fix_received with add_to_source",
			mode:     paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED,
			fee:      quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
			wantMode: QuoteSettlementModeFixReceived,
			wantFee:  QuoteFeeTreatmentAddToSource,
		},
		{
			name:     "preserves fix_received with deduct_from_destination",
			mode:     paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED,
			fee:      quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION,
			wantMode: QuoteSettlementModeFixReceived,
			wantFee:  QuoteFeeTreatmentDeductFromDestination,
		},
		{
			name:     "defaults settlement when only fee provided",
			mode:     paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED,
			fee:      quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION,
			wantMode: QuoteSettlementModeFixSource,
			wantFee:  QuoteFeeTreatmentDeductFromDestination,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := h.HydrateOne(context.Background(), HydrateOneInput{
				OrganizationRef: bson.NewObjectID().Hex(),
				InitiatorRef:    bson.NewObjectID().Hex(),
				Intent:          economicsCardIntent(t, tc.mode, tc.fee),
			})
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if got.SettlementMode != tc.wantMode {
				t.Fatalf("unexpected settlement mode: got=%s want=%s", got.SettlementMode, tc.wantMode)
			}
			if got.FeeTreatment != tc.wantFee {
				t.Fatalf("unexpected fee treatment: got=%s want=%s", got.FeeTreatment, tc.wantFee)
			}
			if _, ok := got.Attributes["settlement_mode"]; ok {
				t.Fatalf("settlement_mode must not be persisted in attributes")
			}
			if _, ok := got.Attributes["fee_treatment"]; ok {
				t.Fatalf("fee_treatment must not be persisted in attributes")
			}
		})
	}
}

func TestResolveEconomics_InvalidInputs(t *testing.T) {
	cases := []struct {
		name        string
		mode        paymentv1.SettlementMode
		fee         quotationv2.FeeTreatment
		wantErrPart string
	}{
		{
			name:        "invalid settlement mode rejected",
			mode:        paymentv1.SettlementMode(99),
			fee:         quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
			wantErrPart: "intent.settlement_mode is invalid",
		},
		{
			name:        "invalid fee treatment rejected",
			mode:        paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
			fee:         quotationv2.FeeTreatment(99),
			wantErrPart: "intent.fee_treatment is invalid",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, _, err := resolveEconomics(tc.mode, tc.fee)
			if err == nil {
				t.Fatalf("expected error")
			}
			if !strings.Contains(err.Error(), tc.wantErrPart) {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

func economicsCardIntent(t *testing.T, mode paymentv1.SettlementMode, fee quotationv2.FeeTreatment) *quotationv2.QuoteIntent {
	t.Helper()
	return &quotationv2.QuoteIntent{
		Source: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
					Data: mustMarshalBSON(t, pkgmodel.WalletPaymentData{WalletID: "mw-src"}),
				},
			},
		},
		Destination: &endpointv1.PaymentEndpoint{
			Source: &endpointv1.PaymentEndpoint_PaymentMethod{
				PaymentMethod: &endpointv1.PaymentMethod{
					Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
					Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
						Pan:      "4111111111111111",
						ExpMonth: "12",
						ExpYear:  "2030",
					}),
				},
			},
		},
		Amount:         newMoney("1", "USD"),
		SettlementMode: mode,
		FeeTreatment:   fee,
	}
}

type fakeMethodsClient struct {
	getPaymentMethodPrivateFn func(context.Context, *methodsv1.GetPaymentMethodPrivateRequest, ...grpc.CallOption) (*methodsv1.GetPaymentMethodPrivateResponse, error)
}

func (f *fakeMethodsClient) GetPaymentMethodPrivate(
	ctx context.Context,
	req *methodsv1.GetPaymentMethodPrivateRequest,
	opts ...grpc.CallOption,
) (*methodsv1.GetPaymentMethodPrivateResponse, error) {
	if f.getPaymentMethodPrivateFn == nil {
		return nil, errors.New("unexpected GetPaymentMethodPrivate call")
	}
	return f.getPaymentMethodPrivateFn(ctx, req, opts...)
}

func newMoney(amount, currency string) *moneyv1.Money {
	return &moneyv1.Money{
		Amount:   amount,
		Currency: currency,
	}
}

func mustMarshalBSON(t *testing.T, value any) []byte {
	t.Helper()
	data, err := bson.Marshal(value)
	if err != nil {
		t.Fatalf("marshal bson: %v", err)
	}
	return data
}
