package orchestrator

import (
	"context"
	"strings"
	"testing"

	"github.com/tech/sendico/payments/orchestrator/storage/model"
	mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
	"github.com/tech/sendico/pkg/model/account_role"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
)

func TestDefaultPlanBuilder_BuildsPlanFromRoutes_CryptoToCard(t *testing.T) {
	ctx := context.Background()
	builder := newDefaultPlanBuilder(mloggerfactory.NewLogger(false))

	payment := &model.Payment{
		PaymentRef:     "pay-1",
		IdempotencyKey: "idem-1",
		Intent: model.PaymentIntent{
			Ref:  "ref-1",
			Kind: model.PaymentKindPayout,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-1",
					Asset: &paymenttypes.Asset{
						Chain:       "TRON_MAINNET",
						TokenSymbol: "USDT",
					},
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{MaskedPan: "4111"},
			},
			Amount:             &paymenttypes.Money{Currency: "USDT", Amount: "100"},
			SettlementCurrency: "USDT",
		},
		LastQuote: &model.PaymentQuoteSnapshot{
			ExpectedSettlementAmount: &paymenttypes.Money{Currency: "USDT", Amount: "95"},
			ExpectedFeeTotal:         &paymenttypes.Money{Currency: "USDT", Amount: "5"},
		},
	}

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

	routes := &stubRouteStore{
		routes: []*model.PaymentRoute{
			{FromRail: model.RailCrypto, ToRail: model.RailCardPayout, Network: "TRON_MAINNET", IsEnabled: true},
		},
	}

	templates := &stubPlanTemplateStore{
		templates: []*model.PaymentPlanTemplate{
			{
				FromRail:  model.RailCrypto,
				ToRail:    model.RailCardPayout,
				Network:   "TRON_MAINNET",
				IsEnabled: true,
				Steps: []model.OrchestrationStep{
					{StepID: "crypto_send", Rail: model.RailCrypto, Operation: "payout.crypto"},
					{StepID: "crypto_fee", Rail: model.RailCrypto, Operation: "fee.send", DependsOn: []string{"crypto_send"}},
					{StepID: "crypto_observe", Rail: model.RailCrypto, Operation: "observe.confirm", DependsOn: []string{"crypto_send"}},
					{StepID: "ledger_credit", Rail: model.RailLedger, Operation: "ledger.move", DependsOn: []string{"crypto_observe"}, FromRole: rolePtr(account_role.AccountRolePending), ToRole: rolePtr(account_role.AccountRoleOperating)},
					{StepID: "card_payout", Rail: model.RailCardPayout, Operation: "payout.card", DependsOn: []string{"ledger_credit"}},
					{StepID: "ledger_debit", Rail: model.RailLedger, Operation: "ledger.move", DependsOn: []string{"card_payout"}, CommitPolicy: model.CommitPolicyAfterSuccess, CommitAfter: []string{"card_payout"}, FromRole: rolePtr(account_role.AccountRoleOperating), ToRole: rolePtr(account_role.AccountRoleTransit)},
				},
			},
		},
	}

	registry := &stubGatewayRegistry{
		items: []*model.GatewayInstanceDescriptor{
			{
				ID:         "crypto-tron",
				InstanceID: "crypto-tron-1",
				Rail:       model.RailCrypto,
				Network:    "TRON_MAINNET",
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut:              true,
					CanSendFee:             true,
					RequiresObserveConfirm: true,
				},
				Limits:    model.Limits{MinAmount: "0", MaxAmount: "100000"},
				IsEnabled: true,
			},
			{
				ID:         "settlement",
				InstanceID: "settlement-1",
				Rail:       model.RailProviderSettlement,
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					RequiresObserveConfirm: true,
				},
				Limits:    model.Limits{MinAmount: "0", MaxAmount: "100000"},
				IsEnabled: true,
			},
			{
				ID:         "card",
				InstanceID: "card-1",
				Rail:       model.RailCardPayout,
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				Limits:    model.Limits{MinAmount: "0", MaxAmount: "100000"},
				IsEnabled: true,
			},
		},
	}

	plan, err := builder.Build(ctx, payment, quote, routes, templates, registry)
	if err != nil {
		t.Fatalf("expected plan, got error: %v", err)
	}
	if plan == nil {
		t.Fatal("expected plan")
	}
	if len(plan.Steps) != 6 {
		t.Fatalf("expected 6 steps, got %d", len(plan.Steps))
	}

	assertPlanStep(t, plan.Steps[0], "crypto_send", model.RailCrypto, model.RailOperationSend, "crypto-tron", "crypto-tron-1", "USDT", "95")
	assertPlanStep(t, plan.Steps[1], "crypto_fee", model.RailCrypto, model.RailOperationFee, "crypto-tron", "crypto-tron-1", "USDT", "5")
	assertPlanStep(t, plan.Steps[2], "crypto_observe", model.RailCrypto, model.RailOperationObserveConfirm, "crypto-tron", "crypto-tron-1", "", "")
	assertPlanStep(t, plan.Steps[3], "ledger_credit", model.RailLedger, model.RailOperationMove, "", "", "USDT", "95")
	assertPlanStep(t, plan.Steps[4], "card_payout", model.RailCardPayout, model.RailOperationSend, "card", "card-1", "USDT", "95")
	assertPlanStep(t, plan.Steps[5], "ledger_debit", model.RailLedger, model.RailOperationMove, "", "", "USDT", "95")
}

func TestDefaultPlanBuilder_ErrorsWhenRouteMissing(t *testing.T) {
	ctx := context.Background()
	builder := newDefaultPlanBuilder(mloggerfactory.NewLogger(false))

	payment := &model.Payment{
		PaymentRef:     "pay-1",
		IdempotencyKey: "idem-1",
		Intent: model.PaymentIntent{
			Ref:  "ref-1",
			Kind: model.PaymentKindPayout,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-1",
					Asset:            &paymenttypes.Asset{Chain: "TRON_MAINNET"},
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{MaskedPan: "4111"},
			},
			Amount: &paymenttypes.Money{Currency: "USDT", Amount: "10"},
		},
	}

	routes := &stubRouteStore{}
	templates := &stubPlanTemplateStore{}
	registry := &stubGatewayRegistry{}

	plan, err := builder.Build(ctx, payment, &orchestratorv1.PaymentQuote{}, routes, templates, registry)
	if err == nil {
		t.Fatalf("expected error, got plan: %#v", plan)
	}
}

func TestBuildPlanFromTemplate_ProviderSettlementUsesNetAmountWhenFixReceived(t *testing.T) {
	ctx := context.Background()
	builder := newDefaultPlanBuilder(mloggerfactory.NewLogger(false))

	payment := &model.Payment{
		PaymentRef:     "pay-settle-1",
		IdempotencyKey: "idem-settle-1",
		Intent: model.PaymentIntent{
			Ref:            "ref-1",
			Kind:           model.PaymentKindPayout,
			SettlementMode: model.SettlementModeFixReceived,
			Amount:         &paymenttypes.Money{Currency: "USDT", Amount: "100"},
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-1",
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{MaskedPan: "4111"},
			},
		},
	}

	quote := &orchestratorv1.PaymentQuote{
		DebitAmount:              &moneyv1.Money{Currency: "USDT", Amount: "105"},
		ExpectedSettlementAmount: &moneyv1.Money{Currency: "USDT", Amount: "100"},
		ExpectedFeeTotal:         &moneyv1.Money{Currency: "USDT", Amount: "5"},
	}

	template := &model.PaymentPlanTemplate{
		Steps: []model.OrchestrationStep{
			{StepID: "settle", Rail: model.RailProviderSettlement, Operation: "send"},
		},
	}

	registry := &stubGatewayRegistry{
		items: []*model.GatewayInstanceDescriptor{
			{
				ID:         "settlement",
				InstanceID: "settlement-1",
				Rail:       model.RailProviderSettlement,
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				Limits:    model.Limits{MinAmount: "0", MaxAmount: "100000"},
				IsEnabled: true,
			},
		},
	}

	plan, err := builder.buildPlanFromTemplate(ctx, payment, quote, template, model.RailCrypto, model.RailProviderSettlement, "TRON_MAINNET", "", registry)
	if err != nil {
		t.Fatalf("expected plan, got error: %v", err)
	}
	if len(plan.Steps) != 1 {
		t.Fatalf("expected 1 step, got %d", len(plan.Steps))
	}

	assertPlanStep(t, plan.Steps[0], "settle", model.RailProviderSettlement, model.RailOperationSend, "settlement", "settlement-1", "USDT", "95")
}

func TestDefaultPlanBuilder_UsesSourceCurrencyForCryptoSendWithFX(t *testing.T) {
	ctx := context.Background()
	builder := newDefaultPlanBuilder(mloggerfactory.NewLogger(false))

	payment := &model.Payment{
		PaymentRef:     "pay-2",
		IdempotencyKey: "idem-2",
		Intent: model.PaymentIntent{
			Ref:        "ref-1",
			Kind:       model.PaymentKindPayout,
			RequiresFX: true,
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-2",
					Asset: &paymenttypes.Asset{
						Chain:       "TRON_MAINNET",
						TokenSymbol: "USDT",
					},
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{MaskedPan: "4111"},
			},
			Amount:             &paymenttypes.Money{Currency: "USDT", Amount: "1.4"},
			SettlementMode:     model.SettlementModeFixReceived,
			SettlementCurrency: "RUB",
		},
		LastQuote: &model.PaymentQuoteSnapshot{
			DebitAmount:              &paymenttypes.Money{Currency: "USDT", Amount: "1.498"},
			ExpectedSettlementAmount: &paymenttypes.Money{Currency: "RUB", Amount: "108.99"},
			ExpectedFeeTotal:         &paymenttypes.Money{Currency: "USDT", Amount: "0.098"},
		},
	}

	quote := &orchestratorv1.PaymentQuote{
		DebitAmount:              &moneyv1.Money{Currency: "USDT", Amount: "1.498"},
		ExpectedSettlementAmount: &moneyv1.Money{Currency: "RUB", Amount: "108.99"},
		ExpectedFeeTotal:         &moneyv1.Money{Currency: "USDT", Amount: "0.098"},
	}

	routes := &stubRouteStore{
		routes: []*model.PaymentRoute{
			{FromRail: model.RailCrypto, ToRail: model.RailCardPayout, Network: "TRON_MAINNET", IsEnabled: true},
		},
	}

	templates := &stubPlanTemplateStore{
		templates: []*model.PaymentPlanTemplate{
			{
				FromRail:  model.RailCrypto,
				ToRail:    model.RailCardPayout,
				Network:   "TRON_MAINNET",
				IsEnabled: true,
				Steps: []model.OrchestrationStep{
					{StepID: "crypto_send", Rail: model.RailCrypto, Operation: "payout.crypto"},
					{StepID: "crypto_fee", Rail: model.RailCrypto, Operation: "fee.send", DependsOn: []string{"crypto_send"}},
					{StepID: "crypto_observe", Rail: model.RailCrypto, Operation: "observe.confirm", DependsOn: []string{"crypto_send"}},
					{StepID: "ledger_credit", Rail: model.RailLedger, Operation: "ledger.move", DependsOn: []string{"crypto_observe"}, FromRole: rolePtr(account_role.AccountRolePending), ToRole: rolePtr(account_role.AccountRoleOperating)},
					{StepID: "card_payout", Rail: model.RailCardPayout, Operation: "payout.card", DependsOn: []string{"ledger_credit"}},
					{StepID: "ledger_debit", Rail: model.RailLedger, Operation: "ledger.move", DependsOn: []string{"card_payout"}, CommitPolicy: model.CommitPolicyAfterSuccess, CommitAfter: []string{"card_payout"}, FromRole: rolePtr(account_role.AccountRoleOperating), ToRole: rolePtr(account_role.AccountRoleTransit)},
				},
			},
		},
	}

	registry := &stubGatewayRegistry{
		items: []*model.GatewayInstanceDescriptor{
			{
				ID:         "crypto-tron",
				InstanceID: "crypto-tron-2",
				Rail:       model.RailCrypto,
				Network:    "TRON_MAINNET",
				Currencies: []string{"USDT"},
				Capabilities: model.RailCapabilities{
					CanPayOut:              true,
					CanSendFee:             true,
					RequiresObserveConfirm: true,
				},
				Limits:    model.Limits{MinAmount: "0", MaxAmount: "100000"},
				IsEnabled: true,
			},
			{
				ID:         "card",
				InstanceID: "card-2",
				Rail:       model.RailCardPayout,
				Currencies: []string{"RUB"},
				Capabilities: model.RailCapabilities{
					CanPayOut: true,
				},
				Limits:    model.Limits{MinAmount: "0", MaxAmount: "100000"},
				IsEnabled: true,
			},
		},
	}

	plan, err := builder.Build(ctx, payment, quote, routes, templates, registry)
	if err != nil {
		t.Fatalf("expected plan, got error: %v", err)
	}
	if plan == nil {
		t.Fatal("expected plan")
	}
	if len(plan.Steps) != 6 {
		t.Fatalf("expected 6 steps, got %d", len(plan.Steps))
	}

	assertPlanStep(t, plan.Steps[0], "crypto_send", model.RailCrypto, model.RailOperationSend, "crypto-tron", "crypto-tron-2", "USDT", "1.4")
	assertPlanStep(t, plan.Steps[1], "crypto_fee", model.RailCrypto, model.RailOperationFee, "crypto-tron", "crypto-tron-2", "USDT", "0.098")
	assertPlanStep(t, plan.Steps[2], "crypto_observe", model.RailCrypto, model.RailOperationObserveConfirm, "crypto-tron", "crypto-tron-2", "", "")
	assertPlanStep(t, plan.Steps[3], "ledger_credit", model.RailLedger, model.RailOperationMove, "", "", "RUB", "108.99")
	assertPlanStep(t, plan.Steps[4], "card_payout", model.RailCardPayout, model.RailOperationSend, "card", "card-2", "RUB", "108.99")
	assertPlanStep(t, plan.Steps[5], "ledger_debit", model.RailLedger, model.RailOperationMove, "", "", "RUB", "108.99")
}

// --- test doubles ---

type stubRouteStore struct {
	routes []*model.PaymentRoute
}

func (s *stubRouteStore) List(_ context.Context, filter *model.PaymentRouteFilter) (*model.PaymentRouteList, error) {
	items := make([]*model.PaymentRoute, 0, len(s.routes))
	for _, route := range s.routes {
		if route == nil {
			continue
		}
		if filter != nil {
			if filter.FromRail != "" && route.FromRail != filter.FromRail {
				continue
			}
			if filter.ToRail != "" && route.ToRail != filter.ToRail {
				continue
			}
			if filter.Network != "" && !strings.EqualFold(route.Network, filter.Network) {
				continue
			}
		}
		if filter != nil && filter.IsEnabled != nil {
			if route.IsEnabled != *filter.IsEnabled {
				continue
			}
		}
		items = append(items, route)
	}
	return &model.PaymentRouteList{Items: items}, nil
}

type stubPlanTemplateStore struct {
	templates []*model.PaymentPlanTemplate
}

func (s *stubPlanTemplateStore) List(_ context.Context, filter *model.PaymentPlanTemplateFilter) (*model.PaymentPlanTemplateList, error) {
	items := make([]*model.PaymentPlanTemplate, 0, len(s.templates))
	for _, tpl := range s.templates {
		if tpl == nil {
			continue
		}
		if filter != nil {
			if filter.FromRail != "" && tpl.FromRail != filter.FromRail {
				continue
			}
			if filter.ToRail != "" && tpl.ToRail != filter.ToRail {
				continue
			}
			if filter.Network != "" && !strings.EqualFold(tpl.Network, filter.Network) {
				continue
			}
		}
		if filter != nil && filter.IsEnabled != nil {
			if tpl.IsEnabled != *filter.IsEnabled {
				continue
			}
		}
		items = append(items, tpl)
	}
	return &model.PaymentPlanTemplateList{Items: items}, nil
}

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

func (s *stubGatewayRegistry) List(_ context.Context) ([]*model.GatewayInstanceDescriptor, error) {
	return s.items, nil
}

func assertPlanStep(t *testing.T, step *model.PaymentStep, stepID string, rail model.Rail, action model.RailOperation, gatewayID, instanceID, currency, amount string) {
	t.Helper()
	if step == nil {
		t.Fatal("expected step")
	}
	if step.StepID != stepID {
		t.Fatalf("expected step id %q, got %q", stepID, step.StepID)
	}
	if step.Rail != rail {
		t.Fatalf("expected rail %s, got %s", rail, step.Rail)
	}
	if step.Action != action {
		t.Fatalf("expected action %s, got %s", action, step.Action)
	}
	if step.GatewayID != gatewayID {
		t.Fatalf("expected gateway %q, got %q", gatewayID, step.GatewayID)
	}
	if step.InstanceID != instanceID {
		t.Fatalf("expected instance %q, got %q", instanceID, step.InstanceID)
	}
	if currency == "" && amount == "" {
		if step.Amount != nil && step.Amount.Amount != "" {
			t.Fatalf("expected empty amount, got %v", step.Amount)
		}
		return
	}
	if step.Amount == nil {
		t.Fatalf("expected amount %s %s, got nil", currency, amount)
	}
	if step.Amount.GetCurrency() != currency || step.Amount.GetAmount() != amount {
		t.Fatalf("expected amount %s %s, got %s %s", currency, amount, step.Amount.GetCurrency(), step.Amount.GetAmount())
	}
}
