package orchestrator

import (
	"context"
	"strings"
	"testing"

	mntxclient "github.com/tech/sendico/gateway/mntx/client"
	ledgerclient "github.com/tech/sendico/ledger/client"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	mo "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/payments/rail"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.uber.org/zap"
)

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

	store := newStubPaymentsStore()
	repo := &stubRepository{store: store}

	transferRefs := []string{"send-1", "fee-1"}
	sendCalls := 0
	railGateway := &fakeRailGateway{
		rail: "CRYPTO",
		sendFn: func(ctx context.Context, req rail.TransferRequest) (rail.RailResult, error) {
			ref := transferRefs[sendCalls]
			sendCalls++
			return rail.RailResult{ReferenceID: ref, Status: rail.TransferStatusPending}, nil
		},
	}

	debitCalls := 0
	creditCalls := 0
	ledgerFake := &ledgerclient.Fake{
		CreateTransactionFn: func(ctx context.Context, tx rail.LedgerTx) (string, error) {
			if strings.EqualFold(tx.FromRail, "LEDGER") {
				debitCalls++
				return "debit-1", nil
			}
			if strings.EqualFold(tx.ToRail, "LEDGER") {
				creditCalls++
				return "credit-1", nil
			}
			return "", nil
		},
	}

	payoutCalls := 0
	mntxFake := &mntxclient.Fake{
		CreateCardPayoutFn: func(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
			payoutCalls++
			return &mntxv1.CardPayoutResponse{Payout: &mntxv1.CardPayoutState{PayoutId: "payout-1"}}, nil
		},
	}

	svc := &Service{
		logger:  zap.NewNop(),
		storage: repo,
		deps: serviceDependencies{
			railGateways: buildRailGatewayDependency(map[string]rail.RailGateway{
				"crypto-default": railGateway,
			}, nil, nil, nil, nil),
			ledger: ledgerDependency{
				client:   ledgerFake,
				internal: ledgerFake,
			},
			mntx: mntxDependency{client: mntxFake},
			cardRoutes: map[string]CardGatewayRoute{
				defaultCardGateway: {
					FundingAddress: "funding-address",
					FeeWalletRef:   "fee-wallet",
				},
			},
		},
	}

	executor := newPaymentExecutor(&svc.deps, svc.logger, svc)

	payment := &model.Payment{
		PaymentRef:     "pay-plan-1",
		IdempotencyKey: "pay-plan-1",
		OrganizationBoundBase: mo.OrganizationBoundBase{
			OrganizationRef: primitive.NewObjectID(),
		},
		Intent: model.PaymentIntent{
			Kind: model.PaymentKindPayout,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-src",
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{
					Pan:               "4111111111111111",
					Cardholder:        "Ada",
					CardholderSurname: "Lovelace",
					ExpMonth:          1,
					ExpYear:           2030,
					MaskedPan:         "4111",
				},
			},
			Attributes: map[string]string{
				"ledger_credit_account_ref": "ledger:credit",
				"ledger_debit_account_ref":  "ledger:debit",
			},
			Customer: &model.Customer{
				ID:        "cust-1",
				FirstName: "Ada",
				LastName:  "Lovelace",
				IP:        "1.2.3.4",
			},
		},
		PaymentPlan: &model.PaymentPlan{
			ID:             "pay-plan-1",
			IdempotencyKey: "pay-plan-1",
			Steps: []*model.PaymentStep{
				{StepID: "crypto_send", Rail: model.RailCrypto, Action: model.RailOperationSend, Amount: &paymenttypes.Money{Currency: "USDT", Amount: "100"}},
				{StepID: "crypto_fee", Rail: model.RailCrypto, Action: model.RailOperationFee, DependsOn: []string{"crypto_send"}, Amount: &paymenttypes.Money{Currency: "USDT", Amount: "5"}},
				{StepID: "crypto_observe", Rail: model.RailProviderSettlement, Action: model.RailOperationObserveConfirm, DependsOn: []string{"crypto_send"}},
				{StepID: "ledger_credit", Rail: model.RailLedger, Action: model.RailOperationCredit, DependsOn: []string{"crypto_observe"}, Amount: &paymenttypes.Money{Currency: "USDT", Amount: "95"}},
				{StepID: "card_payout", Rail: model.RailCardPayout, Action: model.RailOperationSend, DependsOn: []string{"ledger_credit"}, Amount: &paymenttypes.Money{Currency: "USDT", Amount: "95"}},
				{StepID: "ledger_debit", Rail: model.RailLedger, Action: model.RailOperationDebit, DependsOn: []string{"card_payout"}, CommitPolicy: model.CommitPolicyAfterSuccess, CommitAfter: []string{"card_payout"}, Amount: &paymenttypes.Money{Currency: "USDT", Amount: "95"}},
			},
		},
	}

	store.payments[payment.PaymentRef] = payment

	if err := executor.executePaymentPlan(ctx, store, payment, &orchestratorv1.PaymentQuote{}); err != nil {
		t.Fatalf("executePaymentPlan error: %v", err)
	}

	if sendCalls != 2 {
		t.Fatalf("expected 2 rail sends, got %d", sendCalls)
	}
	if debitCalls != 0 || creditCalls != 0 {
		t.Fatalf("unexpected ledger calls: debit=%d credit=%d", debitCalls, creditCalls)
	}
	if payoutCalls != 0 {
		t.Fatalf("expected no payout before source confirmation, got %d", payoutCalls)
	}
	if payment.State != model.PaymentStateSubmitted {
		t.Fatalf("expected submitted state, got %s", payment.State)
	}
	if payment.Execution == nil || payment.Execution.ChainTransferRef == "" || payment.Execution.FeeTransferRef == "" {
		t.Fatalf("expected chain and fee transfer refs set")
	}
	if payment.ExecutionPlan == nil || len(payment.ExecutionPlan.Steps) != 6 {
		t.Fatalf("expected execution plan with 6 steps")
	}
	if executionStepStatus(payment.ExecutionPlan.Steps[0]) != executionStepStatusSubmitted {
		t.Fatalf("expected send step submitted")
	}
	if executionStepStatus(payment.ExecutionPlan.Steps[1]) != executionStepStatusSubmitted {
		t.Fatalf("expected fee step submitted")
	}
	if executionStepStatus(payment.ExecutionPlan.Steps[2]) != executionStepStatusSubmitted {
		t.Fatalf("expected observe step submitted")
	}

	setExecutionStepStatus(payment.ExecutionPlan.Steps[0], executionStepStatusConfirmed)
	setExecutionStepStatus(payment.ExecutionPlan.Steps[1], executionStepStatusConfirmed)
	setExecutionStepStatus(payment.ExecutionPlan.Steps[2], executionStepStatusConfirmed)

	if err := executor.executePaymentPlan(ctx, store, payment, &orchestratorv1.PaymentQuote{}); err != nil {
		t.Fatalf("executePaymentPlan resume error: %v", err)
	}
	if debitCalls != 0 || creditCalls != 1 {
		t.Fatalf("expected ledger credit after source confirmation, debit=%d credit=%d", debitCalls, creditCalls)
	}
	if payoutCalls != 1 {
		t.Fatalf("expected card payout submitted, got %d", payoutCalls)
	}
	if payment.Execution == nil || payment.Execution.CardPayoutRef == "" {
		t.Fatalf("expected card payout ref set")
	}

	steps := executionStepsByCode(payment.ExecutionPlan)
	cardStep := steps["card_payout"]
	if cardStep == nil {
		t.Fatalf("expected card payout step in execution plan")
	}
	setExecutionStepStatus(cardStep, executionStepStatusConfirmed)

	if err := executor.executePaymentPlan(ctx, store, payment, &orchestratorv1.PaymentQuote{}); err != nil {
		t.Fatalf("executePaymentPlan finalize error: %v", err)
	}
	if debitCalls != 1 || creditCalls != 1 {
		t.Fatalf("expected ledger debit after payout confirmation, debit=%d credit=%d", debitCalls, creditCalls)
	}
}

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

	store := newStubPaymentsStore()
	repo := &stubRepository{store: store}

	blockCalls := 0
	var blockReq *ledgerv1.TransferRequest
	debitCalls := 0
	var debitTx rail.LedgerTx
	ledgerFake := &ledgerclient.Fake{
		TransferInternalFn: func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
			blockCalls++
			blockReq = req
			return &ledgerv1.PostResponse{JournalEntryRef: "hold-1"}, nil
		},
		CreateTransactionFn: func(ctx context.Context, tx rail.LedgerTx) (string, error) {
			debitCalls++
			debitTx = tx
			return "debit-1", nil
		},
	}

	mntxFake := &mntxclient.Fake{
		CreateCardPayoutFn: func(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
			return &mntxv1.CardPayoutResponse{Payout: &mntxv1.CardPayoutState{PayoutId: "payout-1"}}, nil
		},
	}

	svc := &Service{
		logger:  zap.NewNop(),
		storage: repo,
		deps: serviceDependencies{
			ledger: ledgerDependency{
				client:   ledgerFake,
				internal: ledgerFake,
			},
			mntx: mntxDependency{client: mntxFake},
			cardRoutes: map[string]CardGatewayRoute{
				defaultCardGateway: {
					FundingAddress: "funding-address",
					FeeWalletRef:   "fee-wallet",
				},
			},
		},
	}

	executor := newPaymentExecutor(&svc.deps, svc.logger, svc)

	payment := &model.Payment{
		PaymentRef:     "pay-block-1",
		IdempotencyKey: "pay-block-1",
		OrganizationBoundBase: mo.OrganizationBoundBase{
			OrganizationRef: primitive.NewObjectID(),
		},
		Intent: model.PaymentIntent{
			Kind: model.PaymentKindPayout,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-src",
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{
					Pan:               "4111111111111111",
					Cardholder:        "Ada",
					CardholderSurname: "Lovelace",
					ExpMonth:          1,
					ExpYear:           2030,
					MaskedPan:         "4111",
				},
			},
			Attributes: map[string]string{
				"ledger_debit_account_ref": "ledger:debit",
				"ledger_block_account_ref": "ledger:block",
			},
			Customer: &model.Customer{
				ID:        "cust-1",
				FirstName: "Ada",
				LastName:  "Lovelace",
				IP:        "1.2.3.4",
			},
		},
		PaymentPlan: &model.PaymentPlan{
			ID:             "pay-block-1",
			IdempotencyKey: "pay-block-1",
			Steps: []*model.PaymentStep{
				{StepID: "ledger_block", Rail: model.RailLedger, Action: model.RailOperationBlock, Amount: &paymenttypes.Money{Currency: "USD", Amount: "100"}},
				{StepID: "card_payout", Rail: model.RailCardPayout, Action: model.RailOperationSend, DependsOn: []string{"ledger_block"}, Amount: &paymenttypes.Money{Currency: "USD", Amount: "100"}},
				{StepID: "ledger_debit", Rail: model.RailLedger, Action: model.RailOperationDebit, DependsOn: []string{"card_payout"}, CommitPolicy: model.CommitPolicyAfterSuccess, CommitAfter: []string{"card_payout"}, Amount: &paymenttypes.Money{Currency: "USD", Amount: "100"}},
				{StepID: "ledger_release", Rail: model.RailLedger, Action: model.RailOperationRelease, DependsOn: []string{"card_payout"}, Amount: &paymenttypes.Money{Currency: "USD", Amount: "100"}},
			},
		},
	}

	store.payments[payment.PaymentRef] = payment

	if err := executor.executePaymentPlan(ctx, store, payment, &orchestratorv1.PaymentQuote{}); err != nil {
		t.Fatalf("executePaymentPlan error: %v", err)
	}

	if blockCalls != 1 || blockReq == nil {
		t.Fatalf("expected ledger block transfer, calls=%d", blockCalls)
	}
	if blockReq.GetFromLedgerAccountRef() != "ledger:debit" {
		t.Fatalf("unexpected block from account: %s", blockReq.GetFromLedgerAccountRef())
	}
	if blockReq.GetToLedgerAccountRef() != "ledger:block" {
		t.Fatalf("unexpected block to account: %s", blockReq.GetToLedgerAccountRef())
	}
	if debitCalls != 0 {
		t.Fatalf("expected no debit before payout confirmation, got %d", debitCalls)
	}
	if payment.State != model.PaymentStateFundsReserved {
		t.Fatalf("expected funds reserved state, got %s", payment.State)
	}

	steps := executionStepsByCode(payment.ExecutionPlan)
	cardStep := steps["card_payout"]
	if cardStep == nil {
		t.Fatalf("expected card payout step in execution plan")
	}
	setExecutionStepStatus(cardStep, executionStepStatusConfirmed)

	if err := executor.executePaymentPlan(ctx, store, payment, &orchestratorv1.PaymentQuote{}); err != nil {
		t.Fatalf("executePaymentPlan resume error: %v", err)
	}
	if debitCalls != 1 {
		t.Fatalf("expected ledger debit after payout confirmation, got %d", debitCalls)
	}
	if debitTx.LedgerAccountRef != "ledger:block" {
		t.Fatalf("expected debit from block account, got %s", debitTx.LedgerAccountRef)
	}
	if debitTx.ContraLedgerAccountRef != "" {
		t.Fatalf("expected contra to be cleared after block, got %s", debitTx.ContraLedgerAccountRef)
	}
	if payment.State != model.PaymentStateSettled {
		t.Fatalf("expected settled state, got %s", payment.State)
	}
}
