package psvc

import (
	"context"
	"sort"
	"testing"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/discovery"
	pm "github.com/tech/sendico/pkg/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestExecuteBatchPayment_DefaultNoOptimization_DoesNotMergeSameDestination(t *testing.T) {
	env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	quote := newExecutableBatchQuote(env.orgID, "quote-batch-merge", []string{"intent-a", "intent-b"}, buildLedgerRoute())
	env.quotes.Put(quote)

	resp, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:             testMeta(env.orgID, "idem-batch-merge"),
		QuotationRef:     "quote-batch-merge",
		ClientPaymentRef: "client-batch-merge",
	})
	if err != nil {
		t.Fatalf("ExecuteBatchPayment returned error: %v", err)
	}
	if resp == nil {
		t.Fatal("expected response")
	}
	if got, want := len(resp.GetPayments()), 2; got != want {
		t.Fatalf("expected %d payment(s) with default no optimization, got=%d", want, got)
	}
	for i, p := range resp.GetPayments() {
		if got, want := p.GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED; got != want {
			t.Fatalf("payments[%d] state mismatch: got=%s want=%s", i, got, want)
		}
	}
}

func TestExecuteBatchPayment_DefaultNoOptimization_DoesNotMergeDifferentDestinations(t *testing.T) {
	env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	quote := newExecutableBatchQuoteDiffDest(env.orgID, "quote-batch-diff")
	env.quotes.Put(quote)

	resp, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:             testMeta(env.orgID, "idem-batch-diff"),
		QuotationRef:     "quote-batch-diff",
		ClientPaymentRef: "client-batch-diff",
	})
	if err != nil {
		t.Fatalf("ExecuteBatchPayment returned error: %v", err)
	}
	if got, want := len(resp.GetPayments()), 2; got != want {
		t.Fatalf("expected %d payments for batched execution without optimization, got=%d", want, got)
	}
	for i, p := range resp.GetPayments() {
		if got, want := p.GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED; got != want {
			t.Fatalf("payments[%d] state mismatch: got=%s want=%s", i, got, want)
		}
	}
}

func TestExecuteBatchPayment_CardBatchCreatesPerTargetPayoutStepsWithoutMerging(t *testing.T) {
	var (
		targetRefs []string
		amounts    []string
		comments   []string
	)
	env := newTestEnv(t, func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		if kind == "card_payout" {
			targetRefs = append(targetRefs, req.Step.Metadata[batchmeta.MetaPayoutTargetRef])
			amounts = append(amounts, req.Step.Metadata[batchmeta.MetaAmount])
			comments = append(comments, req.Step.Comment)
		}
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	quote := newExecutableBatchCardQuoteDiffDest(env.orgID, "quote-batch-card-diff")
	env.quotes.Put(quote)

	resp, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:             testMeta(env.orgID, "idem-batch-card-diff"),
		QuotationRef:     "quote-batch-card-diff",
		ClientPaymentRef: "client-batch-card-diff",
	})
	if err != nil {
		t.Fatalf("ExecuteBatchPayment returned error: %v", err)
	}
	if got, want := len(resp.GetPayments()), 2; got != want {
		t.Fatalf("expected %d payments for card batch without optimization, got=%d", want, got)
	}
	for i, payment := range resp.GetPayments() {
		if got, want := payment.GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED; got != want {
			t.Fatalf("payments[%d] state mismatch: got=%s want=%s", i, got, want)
		}
	}
	if got, want := len(targetRefs), 2; got != want {
		t.Fatalf("expected %d card payout send calls, got=%d", want, got)
	}
	if got, want := len(amounts), 2; got != want {
		t.Fatalf("expected %d card payout amounts, got=%d", want, got)
	}
	if got, want := len(comments), 2; got != want {
		t.Fatalf("expected %d card payout comments, got=%d", want, got)
	}

	sort.Strings(targetRefs)
	sort.Strings(amounts)
	sort.Strings(comments)
	if targetRefs[0] == targetRefs[1] {
		t.Fatalf("expected distinct target refs, got=%v", targetRefs)
	}
	if got, want := amounts, []string{"100", "150"}; got[0] != want[0] || got[1] != want[1] {
		t.Fatalf("amount overrides mismatch: got=%v want=%v", got, want)
	}
	if got, want := comments, []string{"batch-comment-a", "batch-comment-b"}; got[0] != want[0] || got[1] != want[1] {
		t.Fatalf("comment overrides mismatch: got=%v want=%v", got, want)
	}
}

func TestExecuteBatchPayment_CryptoPolicyMergesByDestination(t *testing.T) {
	env := newTestEnvWithPolicy(t, BatchOptimizationPolicy{
		DefaultMode: BatchOptimizationModeNoOptimization,
		Rules: []BatchOptimizationRule{
			{
				ID:       "crypto-merge",
				Priority: 100,
				Mode:     BatchOptimizationModeMergeByDestination,
				Match: BatchOptimizationMatch{
					Rail: discovery.RailCrypto,
				},
			},
		},
	}, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	quote := newExecutableBatchCryptoQuoteSameDestination(env.orgID, "quote-batch-crypto-merge")
	env.quotes.Put(quote)

	resp, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:             testMeta(env.orgID, "idem-batch-crypto-merge"),
		QuotationRef:     "quote-batch-crypto-merge",
		ClientPaymentRef: "client-batch-crypto-merge",
	})
	if err != nil {
		t.Fatalf("ExecuteBatchPayment returned error: %v", err)
	}
	if got, want := len(resp.GetPayments()), 1; got != want {
		t.Fatalf("expected %d merged payment for crypto policy, got=%d", want, got)
	}
}

func TestExecuteBatchPayment_CryptoPolicyMergesSameRecipientEvenWithDifferentSourceAndQuoteRefs(t *testing.T) {
	env := newTestEnvWithPolicy(t, BatchOptimizationPolicy{
		DefaultMode: BatchOptimizationModeNoOptimization,
		Rules: []BatchOptimizationRule{
			{
				ID:       "crypto-merge",
				Priority: 100,
				Mode:     BatchOptimizationModeMergeByDestination,
				Match: BatchOptimizationMatch{
					Rail: discovery.RailCrypto,
				},
			},
		},
	}, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	quote := newExecutableBatchCryptoQuoteSameDestination(env.orgID, "quote-batch-crypto-merge-mixed")
	if len(quote.Items) != 2 {
		t.Fatalf("expected 2 quote items, got=%d", len(quote.Items))
	}
	quote.Items[0].Quote.Route.RouteRef = "route-a"
	quote.Items[1].Intent.Source = testLedgerEndpoint("ledger-src-2")
	quote.Items[1].Quote.Route.RouteRef = "route-b"
	env.quotes.Put(quote)

	resp, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:             testMeta(env.orgID, "idem-batch-crypto-merge-mixed"),
		QuotationRef:     "quote-batch-crypto-merge-mixed",
		ClientPaymentRef: "client-batch-crypto-merge-mixed",
	})
	if err != nil {
		t.Fatalf("ExecuteBatchPayment returned error: %v", err)
	}
	if got, want := len(resp.GetPayments()), 1; got != want {
		t.Fatalf("expected %d merged payment for crypto policy, got=%d", want, got)
	}
}

func TestExecuteBatchPayment_IdempotentRetry(t *testing.T) {
	env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	quote := newExecutableBatchQuote(env.orgID, "quote-batch-idem", []string{"intent-a", "intent-b"}, buildLedgerRoute())
	env.quotes.Put(quote)

	req := &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:             testMeta(env.orgID, "idem-batch-idem"),
		QuotationRef:     "quote-batch-idem",
		ClientPaymentRef: "client-batch-idem",
	}
	resp1, err := env.svc.ExecuteBatchPayment(context.Background(), req)
	if err != nil {
		t.Fatalf("first ExecuteBatchPayment returned error: %v", err)
	}

	resp2, err := env.svc.ExecuteBatchPayment(context.Background(), req)
	if err != nil {
		t.Fatalf("second ExecuteBatchPayment returned error: %v", err)
	}

	if got, want := len(resp2.GetPayments()), len(resp1.GetPayments()); got != want {
		t.Fatalf("expected same number of payments on retry: got=%d want=%d", got, want)
	}
	refs1 := make([]string, 0, len(resp1.GetPayments()))
	refs2 := make([]string, 0, len(resp2.GetPayments()))
	for i := range resp1.GetPayments() {
		refs1 = append(refs1, resp1.GetPayments()[i].GetPaymentRef())
	}
	for i := range resp2.GetPayments() {
		refs2 = append(refs2, resp2.GetPayments()[i].GetPaymentRef())
	}
	sort.Strings(refs1)
	sort.Strings(refs2)
	if len(refs1) != len(refs2) {
		t.Fatalf("payment refs count mismatch: got=%d want=%d", len(refs2), len(refs1))
	}
	for i := range refs1 {
		if refs1[i] != refs2[i] {
			t.Fatalf("payment_ref mismatch on retry at index %d: got=%q want=%q", i, refs2[i], refs1[i])
		}
	}
}

func TestExecuteBatchPayment_EmptyQuotationRefFails(t *testing.T) {
	env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	_, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:         testMeta(env.orgID, "idem-empty"),
		QuotationRef: "",
	})
	if err == nil {
		t.Fatal("expected error for empty quotation_ref")
	}
}

func TestExecuteBatchPayment_QuoteNotFoundFails(t *testing.T) {
	env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})

	_, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
		Meta:         testMeta(env.orgID, "idem-notfound"),
		QuotationRef: "nonexistent-quote",
	})
	if err == nil {
		t.Fatal("expected error for non-existent quote")
	}
}

func newExecutableBatchQuoteDiffDest(orgRef bson.ObjectID, quoteRef string) *model.PaymentQuoteRecord {
	now := time.Now().UTC()
	route := buildLedgerRoute()
	return &model.PaymentQuoteRecord{
		Base: modelBase(now),
		OrganizationBoundBase: pm.OrganizationBoundBase{
			OrganizationRef: orgRef,
		},
		QuoteRef:     quoteRef,
		RequestShape: model.QuoteRequestShapeBatch,
		Items: []*model.PaymentQuoteItemV2{
			{
				Intent: &model.PaymentIntent{
					Ref:                "intent-a",
					Kind:               model.PaymentKindPayout,
					Source:             testLedgerEndpoint("ledger-src"),
					Destination:        testLedgerEndpoint("ledger-dst-1"),
					Amount:             &paymenttypes.Money{Amount: "10", Currency: "USD"},
					SettlementCurrency: "USD",
				},
				Quote: &model.PaymentQuoteSnapshot{
					QuoteRef:    quoteRef,
					DebitAmount: &paymenttypes.Money{Amount: "10", Currency: "USD"},
					Route:       route,
				},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
			{
				Intent: &model.PaymentIntent{
					Ref:                "intent-b",
					Kind:               model.PaymentKindPayout,
					Source:             testLedgerEndpoint("ledger-src"),
					Destination:        testLedgerEndpoint("ledger-dst-2"),
					Amount:             &paymenttypes.Money{Amount: "15", Currency: "USD"},
					SettlementCurrency: "USD",
				},
				Quote: &model.PaymentQuoteSnapshot{
					QuoteRef:    quoteRef,
					DebitAmount: &paymenttypes.Money{Amount: "15", Currency: "USD"},
					Route:       route,
				},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
		},
		ExpiresAt: now.Add(1 * time.Hour),
	}
}

func newExecutableBatchCardQuoteDiffDest(orgRef bson.ObjectID, quoteRef string) *model.PaymentQuoteRecord {
	now := time.Now().UTC()
	return &model.PaymentQuoteRecord{
		Base: modelBase(now),
		OrganizationBoundBase: pm.OrganizationBoundBase{
			OrganizationRef: orgRef,
		},
		QuoteRef:     quoteRef,
		RequestShape: model.QuoteRequestShapeBatch,
		Items: []*model.PaymentQuoteItemV2{
			{
				Intent: &model.PaymentIntent{
					Ref:     "intent-a",
					Kind:    model.PaymentKindPayout,
					Comment: "batch-comment-a",
					Source:  testLedgerEndpoint("ledger-src"),
					Destination: model.PaymentEndpoint{
						Type: model.EndpointTypeCard,
						Card: &model.CardEndpoint{
							Pan:      "2200700142860161",
							ExpMonth: 3,
							ExpYear:  2030,
						},
					},
					Amount:             &paymenttypes.Money{Amount: "100", Currency: "RUB"},
					SettlementCurrency: "RUB",
				},
				Quote: &model.PaymentQuoteSnapshot{
					QuoteRef:                 quoteRef,
					DebitAmount:              &paymenttypes.Money{Amount: "1.2", Currency: "USDT"},
					ExpectedSettlementAmount: &paymenttypes.Money{Amount: "100", Currency: "RUB"},
					Route: &paymenttypes.QuoteRouteSpecification{
						Hops: []*paymenttypes.QuoteRouteHop{
							{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
							{Index: 20, Rail: "CARD", Gateway: paymenttypes.DefaultCardsGatewayID, Role: paymenttypes.QuoteRouteHopRoleDestination},
						},
					},
				},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
			{
				Intent: &model.PaymentIntent{
					Ref:     "intent-b",
					Kind:    model.PaymentKindPayout,
					Comment: "batch-comment-b",
					Source:  testLedgerEndpoint("ledger-src"),
					Destination: model.PaymentEndpoint{
						Type: model.EndpointTypeCard,
						Card: &model.CardEndpoint{
							Pan:      "2200700142860162",
							ExpMonth: 4,
							ExpYear:  2030,
						},
					},
					Amount:             &paymenttypes.Money{Amount: "150", Currency: "RUB"},
					SettlementCurrency: "RUB",
				},
				Quote: &model.PaymentQuoteSnapshot{
					QuoteRef:                 quoteRef,
					DebitAmount:              &paymenttypes.Money{Amount: "1.8", Currency: "USDT"},
					ExpectedSettlementAmount: &paymenttypes.Money{Amount: "150", Currency: "RUB"},
					Route: &paymenttypes.QuoteRouteSpecification{
						Hops: []*paymenttypes.QuoteRouteHop{
							{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
							{Index: 20, Rail: "CARD", Gateway: paymenttypes.DefaultCardsGatewayID, Role: paymenttypes.QuoteRouteHopRoleDestination},
						},
					},
				},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
		},
		ExpiresAt: now.Add(1 * time.Hour),
	}
}

func newExecutableBatchCryptoQuoteSameDestination(orgRef bson.ObjectID, quoteRef string) *model.PaymentQuoteRecord {
	now := time.Now().UTC()
	return &model.PaymentQuoteRecord{
		Base: modelBase(now),
		OrganizationBoundBase: pm.OrganizationBoundBase{
			OrganizationRef: orgRef,
		},
		QuoteRef:     quoteRef,
		RequestShape: model.QuoteRequestShapeBatch,
		Items: []*model.PaymentQuoteItemV2{
			{
				Intent: &model.PaymentIntent{
					Ref:                "intent-a",
					Kind:               model.PaymentKindPayout,
					Source:             testLedgerEndpoint("ledger-src"),
					Destination:        testExternalChainEndpoint("TQfCWxU4ERWstZZcMzEVB9MnnCSzE4agj8"),
					Amount:             &paymenttypes.Money{Amount: "12", Currency: "USDT"},
					SettlementCurrency: "USDT",
				},
				Quote: &model.PaymentQuoteSnapshot{
					QuoteRef:                 quoteRef,
					DebitAmount:              &paymenttypes.Money{Amount: "12", Currency: "USDT"},
					ExpectedSettlementAmount: &paymenttypes.Money{Amount: "12", Currency: "USDT"},
					Route: &paymenttypes.QuoteRouteSpecification{
						Rail:     "CRYPTO",
						Provider: "gw-crypto",
						Network:  "TRON_NILE",
						Hops: []*paymenttypes.QuoteRouteHop{
							{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
							{Index: 20, Rail: "CRYPTO", Gateway: "gw-crypto", Network: "TRON_NILE", Role: paymenttypes.QuoteRouteHopRoleDestination},
						},
					},
				},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
			{
				Intent: &model.PaymentIntent{
					Ref:                "intent-b",
					Kind:               model.PaymentKindPayout,
					Source:             testLedgerEndpoint("ledger-src"),
					Destination:        testExternalChainEndpoint("TQfCWxU4ERWstZZcMzEVB9MnnCSzE4agj8"),
					Amount:             &paymenttypes.Money{Amount: "7", Currency: "USDT"},
					SettlementCurrency: "USDT",
				},
				Quote: &model.PaymentQuoteSnapshot{
					QuoteRef:                 quoteRef,
					DebitAmount:              &paymenttypes.Money{Amount: "7", Currency: "USDT"},
					ExpectedSettlementAmount: &paymenttypes.Money{Amount: "7", Currency: "USDT"},
					Route: &paymenttypes.QuoteRouteSpecification{
						Rail:     "CRYPTO",
						Provider: "gw-crypto",
						Network:  "TRON_NILE",
						Hops: []*paymenttypes.QuoteRouteHop{
							{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
							{Index: 20, Rail: "CRYPTO", Gateway: "gw-crypto", Network: "TRON_NILE", Role: paymenttypes.QuoteRouteHopRoleDestination},
						},
					},
				},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
		},
		ExpiresAt: now.Add(1 * time.Hour),
	}
}

func testExternalChainEndpoint(address string) model.PaymentEndpoint {
	return model.PaymentEndpoint{
		Type: model.EndpointTypeExternalChain,
		ExternalChain: &model.ExternalChainEndpoint{
			Address: address,
			Asset: &paymenttypes.Asset{
				Chain:       "TRON",
				TokenSymbol: "USDT",
			},
		},
	}
}
