package orchestrator

import (
	"context"
	"strings"
	"testing"

	chainclient "github.com/tech/sendico/gateway/chain/client"
	mntxclient "github.com/tech/sendico/gateway/mntx/client"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	mo "github.com/tech/sendico/pkg/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

func TestSubmitCardFundingTransfers_PlansTopUpAndFunding(t *testing.T) {
	ctx := context.Background()

	const (
		sourceWalletRef = "wallet-src"
		feeWalletRef    = "wallet-fee"
		fundingAddress  = "0xfunding"
	)

	var estimateCalls []*chainv1.EstimateTransferFeeRequest
	var computeCalls []*chainv1.ComputeGasTopUpRequest
	var ensureCalls []*chainv1.EnsureGasTopUpRequest
	var submitCalls []*chainv1.SubmitTransferRequest

	gateway := &chainclient.Fake{
		EstimateTransferFeeFn: func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
			estimateCalls = append(estimateCalls, req)
			dest := req.GetDestination()
			if req.GetSourceWalletRef() == feeWalletRef {
				return &chainv1.EstimateTransferFeeResponse{
					NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.005"},
				}, nil
			}
			if dest != nil && strings.TrimSpace(dest.GetExternalAddress()) != "" {
				return &chainv1.EstimateTransferFeeResponse{
					NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.01"},
				}, nil
			}
			return &chainv1.EstimateTransferFeeResponse{
				NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.02"},
			}, nil
		},
		ComputeGasTopUpFn: func(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
			computeCalls = append(computeCalls, req)
			return &chainv1.ComputeGasTopUpResponse{
				TopupAmount: &moneyv1.Money{Currency: "ETH", Amount: "0.025"},
			}, nil
		},
		EnsureGasTopUpFn: func(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
			ensureCalls = append(ensureCalls, req)
			return &chainv1.EnsureGasTopUpResponse{
				TopupAmount: &moneyv1.Money{Currency: "ETH", Amount: "0.025"},
				Transfer:    &chainv1.Transfer{TransferRef: req.GetIdempotencyKey()},
			}, nil
		},
		SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
			submitCalls = append(submitCalls, req)
			return &chainv1.SubmitTransferResponse{
				Transfer: &chainv1.Transfer{TransferRef: req.GetIdempotencyKey()},
			}, nil
		},
	}

	svc := &Service{
		logger: zap.NewNop(),
		deps: serviceDependencies{
			gateway: gatewayDependency{resolver: staticChainGatewayResolver{client: gateway}},
			cardRoutes: map[string]CardGatewayRoute{
				defaultCardGateway: {
					FundingAddress: fundingAddress,
					FeeWalletRef:   feeWalletRef,
				},
			},
		},
	}

	payment := &model.Payment{
		PaymentRef:            "pay-1",
		IdempotencyKey:        "pay-1",
		OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: bson.NewObjectID()},
		Intent: model.PaymentIntent{
			Ref:  "ref-1",
			Kind: model.PaymentKindPayout,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: sourceWalletRef,
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{
					MaskedPan: "4111",
				},
			},
			Amount: &paymenttypes.Money{Currency: "USDT", Amount: "5"},
		},
	}

	quote := &orchestratorv1.PaymentQuote{
		ExpectedFeeTotal: &moneyv1.Money{Currency: "USDT", Amount: "0.35"},
	}

	if err := svc.submitCardFundingTransfers(ctx, payment, quote); err != nil {
		t.Fatalf("submitCardFundingTransfers error: %v", err)
	}

	if len(estimateCalls) != 4 {
		t.Fatalf("expected 4 fee estimates, got %d", len(estimateCalls))
	}
	if len(computeCalls) != 1 {
		t.Fatalf("expected 1 gas top-up compute call, got %d", len(computeCalls))
	}
	if len(ensureCalls) != 1 {
		t.Fatalf("expected 1 gas top-up ensure call, got %d", len(ensureCalls))
	}
	if len(submitCalls) != 2 {
		t.Fatalf("expected 2 transfer submissions, got %d", len(submitCalls))
	}

	computeCall := computeCalls[0]
	if computeCall.GetWalletRef() != sourceWalletRef {
		t.Fatalf("gas top-up compute wallet mismatch: %s", computeCall.GetWalletRef())
	}
	if computeCall.GetEstimatedTotalFee().GetCurrency() != "ETH" || computeCall.GetEstimatedTotalFee().GetAmount() != "0.03" {
		t.Fatalf("gas top-up compute fee mismatch: %s %s", computeCall.GetEstimatedTotalFee().GetCurrency(), computeCall.GetEstimatedTotalFee().GetAmount())
	}

	ensureCall := ensureCalls[0]
	if ensureCall.GetSourceWalletRef() != feeWalletRef {
		t.Fatalf("gas top-up source wallet mismatch: %s", ensureCall.GetSourceWalletRef())
	}
	if ensureCall.GetTargetWalletRef() != sourceWalletRef {
		t.Fatalf("gas top-up destination mismatch: %s", ensureCall.GetTargetWalletRef())
	}
	if ensureCall.GetEstimatedTotalFee().GetCurrency() != "ETH" || ensureCall.GetEstimatedTotalFee().GetAmount() != "0.03" {
		t.Fatalf("gas top-up ensure fee mismatch: %s %s", ensureCall.GetEstimatedTotalFee().GetCurrency(), ensureCall.GetEstimatedTotalFee().GetAmount())
	}

	fundCall := findSubmitCall(t, submitCalls, "pay-1:card:fund")
	if fundCall.GetDestination().GetExternalAddress() != fundingAddress {
		t.Fatalf("funding destination mismatch: %s", fundCall.GetDestination().GetExternalAddress())
	}
	if fundCall.GetAmount().GetCurrency() != "USDT" || fundCall.GetAmount().GetAmount() != "5" {
		t.Fatalf("funding amount mismatch: %s %s", fundCall.GetAmount().GetCurrency(), fundCall.GetAmount().GetAmount())
	}

	feeCall := findSubmitCall(t, submitCalls, "pay-1:card:fee")
	if feeCall.GetDestination().GetManagedWalletRef() != feeWalletRef {
		t.Fatalf("fee destination mismatch: %s", feeCall.GetDestination().GetManagedWalletRef())
	}
	if feeCall.GetAmount().GetCurrency() != "USDT" || feeCall.GetAmount().GetAmount() != "0.35" {
		t.Fatalf("fee amount mismatch: %s %s", feeCall.GetAmount().GetCurrency(), feeCall.GetAmount().GetAmount())
	}

	if payment.Execution == nil || payment.Execution.ChainTransferRef != "pay-1:card:fund" || payment.Execution.FeeTransferRef != "pay-1:card:fee" {
		t.Fatalf("expected funding transfer ref recorded, got %v", payment.Execution)
	}

	plan := payment.ExecutionPlan
	if plan == nil {
		t.Fatal("expected execution plan to be populated")
	}
	gasStep := findExecutionStep(t, plan, stepCodeGasTopUp)
	if gasStep.Amount.GetAmount() != "0.025" || gasStep.Amount.GetCurrency() != "ETH" {
		t.Fatalf("gas step amount mismatch: %s %s", gasStep.Amount.GetCurrency(), gasStep.Amount.GetAmount())
	}
	if gasStep.NetworkFee.GetAmount() != "0.005" || gasStep.NetworkFee.GetCurrency() != "ETH" {
		t.Fatalf("gas step fee mismatch: %s %s", gasStep.NetworkFee.GetCurrency(), gasStep.NetworkFee.GetAmount())
	}
	if gasStep.TransferRef != "pay-1:card:gas" {
		t.Fatalf("expected gas step transfer ref to be set, got %s", gasStep.TransferRef)
	}

	fundStep := findExecutionStep(t, plan, stepCodeFundingTransfer)
	if fundStep.NetworkFee.GetAmount() != "0.01" || fundStep.NetworkFee.GetCurrency() != "ETH" {
		t.Fatalf("funding step fee mismatch: %s %s", fundStep.NetworkFee.GetCurrency(), fundStep.NetworkFee.GetAmount())
	}
	if fundStep.TransferRef != "pay-1:card:fund" {
		t.Fatalf("funding step transfer ref mismatch: %s", fundStep.TransferRef)
	}

	cardStep := findExecutionStep(t, plan, stepCodeCardPayout)
	if cardStep.Amount.GetAmount() != "5" || cardStep.Amount.GetCurrency() != "USDT" {
		t.Fatalf("card step amount mismatch: %s %s", cardStep.Amount.GetCurrency(), cardStep.Amount.GetAmount())
	}

	feeStep := findExecutionStep(t, plan, stepCodeFeeTransfer)
	if feeStep.Amount.GetAmount() != "0.35" || feeStep.Amount.GetCurrency() != "USDT" {
		t.Fatalf("fee step amount mismatch: %s %s", feeStep.Amount.GetCurrency(), feeStep.Amount.GetAmount())
	}
	if feeStep.NetworkFee.GetAmount() != "0.02" || feeStep.NetworkFee.GetCurrency() != "ETH" {
		t.Fatalf("fee step network fee mismatch: %s %s", feeStep.NetworkFee.GetCurrency(), feeStep.NetworkFee.GetAmount())
	}
	if feeStep.TransferRef != "pay-1:card:fee" {
		t.Fatalf("fee step transfer ref mismatch: %s", feeStep.TransferRef)
	}

	if plan.TotalNetworkFee == nil || plan.TotalNetworkFee.GetAmount() != "0.035" || plan.TotalNetworkFee.GetCurrency() != "ETH" {
		t.Fatalf("total network fee mismatch: %v", plan.TotalNetworkFee)
	}
}

func TestSubmitCardPayout_UsesSettlementAmount(t *testing.T) {
	ctx := context.Background()

	const sourceWalletRef = "wallet-src"

	var payoutReq *mntxv1.CardPayoutRequest
	var submitCalls []*chainv1.SubmitTransferRequest

	gateway := &chainclient.Fake{
		SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
			submitCalls = append(submitCalls, req)
			return &chainv1.SubmitTransferResponse{
				Transfer: &chainv1.Transfer{TransferRef: "fee-transfer"},
			}, nil
		},
	}
	mntx := &mntxclient.Fake{
		CreateCardPayoutFn: func(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
			payoutReq = req
			return &mntxv1.CardPayoutResponse{
				Payout: &mntxv1.CardPayoutState{
					PayoutId: "payout-1",
					Status:   mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
				},
			}, nil
		},
	}

	svc := &Service{
		logger: zap.NewNop(),
		deps: serviceDependencies{
			gateway: gatewayDependency{resolver: staticChainGatewayResolver{client: gateway}},
			mntx:    mntxDependency{client: mntx},
		},
	}

	payment := &model.Payment{
		PaymentRef:            "pay-2",
		IdempotencyKey:        "pay-2",
		OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: bson.NewObjectID()},
		Intent: model.PaymentIntent{
			Ref:  "ref-2",
			Kind: model.PaymentKindPayout,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: sourceWalletRef,
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{
					Pan:        "5536913762657597",
					Cardholder: "Stephan",
				},
			},
			Amount: &paymenttypes.Money{Currency: "USDT", Amount: "5"},
			Customer: &model.Customer{
				ID:        "recipient-1",
				FirstName: "Stephan",
				LastName:  "Tester",
				IP:        "198.51.100.10",
			},
		},
		LastQuote: &model.PaymentQuoteSnapshot{
			ExpectedSettlementAmount: &paymenttypes.Money{Currency: "RUB", Amount: "392.30"},
			ExpectedFeeTotal:         &paymenttypes.Money{Currency: "USDT", Amount: "0.35"},
		},
	}

	if err := svc.submitCardPayout(ctx, "op-ref", payment); err != nil {
		t.Fatalf("submitCardPayout error: %v", err)
	}

	if payoutReq == nil {
		t.Fatal("expected card payout request to be sent")
	}
	if payoutReq.GetCurrency() != "RUB" || payoutReq.GetAmountMinor() != 39230 {
		t.Fatalf("payout request amount mismatch: %s %d", payoutReq.GetCurrency(), payoutReq.GetAmountMinor())
	}

	if payment.Execution == nil || payment.Execution.CardPayoutRef != "payout-1" {
		t.Fatalf("expected card payout ref recorded, got %v", payment.Execution)
	}

	if len(submitCalls) != 0 {
		t.Fatalf("expected 0 fee transfer submissions, got %d", len(submitCalls))
	}

	plan := payment.ExecutionPlan
	if plan == nil {
		t.Fatal("expected execution plan to be populated")
	}
	cardStep := findExecutionStep(t, plan, stepCodeCardPayout)
	if cardStep.TransferRef != "payout-1" {
		t.Fatalf("card step transfer ref mismatch: %s", cardStep.TransferRef)
	}
	if cardStep.Amount.GetAmount() != "392.30" || cardStep.Amount.GetCurrency() != "RUB" {
		t.Fatalf("card step amount mismatch: %s %s", cardStep.Amount.GetCurrency(), cardStep.Amount.GetAmount())
	}

}

func TestSubmitCardFundingTransfers_RequiresFeeWalletRef(t *testing.T) {
	ctx := context.Background()

	gateway := &chainclient.Fake{
		EstimateTransferFeeFn: func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
			return &chainv1.EstimateTransferFeeResponse{
				NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.01"},
			}, nil
		},
	}

	svc := &Service{
		logger: zap.NewNop(),
		deps: serviceDependencies{
			gateway: gatewayDependency{resolver: staticChainGatewayResolver{client: gateway}},
			cardRoutes: map[string]CardGatewayRoute{
				defaultCardGateway: {
					FundingAddress: "0xfunding",
				},
			},
		},
	}

	payment := &model.Payment{
		PaymentRef:            "pay-3",
		IdempotencyKey:        "pay-3",
		OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: bson.NewObjectID()},
		Intent: model.PaymentIntent{
			Ref:  "ref-3",
			Kind: model.PaymentKindPayout,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-src",
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{
					MaskedPan: "4111",
				},
			},
			Amount: &paymenttypes.Money{Currency: "USDT", Amount: "5"},
		},
	}

	quote := &orchestratorv1.PaymentQuote{
		ExpectedFeeTotal: &moneyv1.Money{Currency: "USDT", Amount: "0.35"},
	}

	err := svc.submitCardFundingTransfers(ctx, payment, quote)
	if err == nil {
		t.Fatal("expected error for missing fee wallet ref")
	}
	if !strings.Contains(err.Error(), "fee wallet ref") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func findSubmitCall(t *testing.T, calls []*chainv1.SubmitTransferRequest, idempotencyKey string) *chainv1.SubmitTransferRequest {
	t.Helper()
	for _, call := range calls {
		if call.GetIdempotencyKey() == idempotencyKey {
			return call
		}
	}
	t.Fatalf("missing submit transfer call for %s", idempotencyKey)
	return nil
}

func findExecutionStep(t *testing.T, plan *model.ExecutionPlan, code string) *model.ExecutionStep {
	t.Helper()
	if plan == nil {
		t.Fatal("execution plan is nil")
	}
	for _, step := range plan.Steps {
		if step != nil && strings.EqualFold(step.Code, code) {
			return step
		}
	}
	t.Fatalf("missing execution step %s", code)
	return nil
}
