package orchestrator

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

	ledgerclient "github.com/tech/sendico/ledger/client"
	"github.com/tech/sendico/payments/orchestrator/storage"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	mo "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/payments/rail"
	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"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

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

	store := newStubPaymentsStore()
	repo := &stubRepository{store: store}
	svc := &Service{
		logger:  zap.NewNop(),
		clock:   testClock{now: time.Now()},
		storage: repo,
		deps: serviceDependencies{
			ledger: func() ledgerDependency {
				fake := &ledgerclient.Fake{
					ApplyFXWithChargesFn: func(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
						return &ledgerv1.PostResponse{JournalEntryRef: "fx-entry"}, nil
					},
				}
				return ledgerDependency{
					client:   fake,
					internal: fake,
				}
			}(),
		},
	}

	payment := &model.Payment{
		PaymentRef:            "fx-1",
		IdempotencyKey:        "fx-1",
		OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: bson.NewObjectID()},
		Intent: model.PaymentIntent{
			Ref:  "ref-1",
			Kind: model.PaymentKindFXConversion,
			Source: model.PaymentEndpoint{
				Type:   model.EndpointTypeLedger,
				Ledger: &model.LedgerEndpoint{LedgerAccountRef: "ledger:source"},
			},
			Destination: model.PaymentEndpoint{
				Type:   model.EndpointTypeLedger,
				Ledger: &model.LedgerEndpoint{LedgerAccountRef: "ledger:dest"},
			},
			Amount: &paymenttypes.Money{Currency: "USD", Amount: "100"},
		},
	}
	store.payments[payment.PaymentRef] = payment

	quote := &orchestratorv1.PaymentQuote{
		FxQuote: &oraclev1.Quote{
			QuoteRef:    "quote-1",
			BaseAmount:  &moneyv1.Money{Currency: "USD", Amount: "100"},
			QuoteAmount: &moneyv1.Money{Currency: "EUR", Amount: "90"},
			Price:       &moneyv1.Decimal{Value: "0.9"},
		},
	}
	if err := svc.executePayment(ctx, store, payment, quote); err != nil {
		t.Fatalf("executePayment returned error: %v", err)
	}

	if payment.State != model.PaymentStateSettled {
		t.Fatalf("expected payment settled, got %s", payment.State)
	}

	if payment.Execution == nil || payment.Execution.FXEntryRef == "" {
		t.Fatal("expected FX entry ref set on payment execution")
	}
}

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

	store := newStubPaymentsStore()
	routes := &stubRoutesStore{
		routes: []*model.PaymentRoute{
			{FromRail: model.RailCrypto, ToRail: model.RailLedger, Network: "TRON_MAINNET", IsEnabled: true},
		},
	}
	plans := &stubPlanTemplatesStore{
		templates: []*model.PaymentPlanTemplate{
			{
				FromRail:  model.RailCrypto,
				ToRail:    model.RailLedger,
				Network:   "TRON_MAINNET",
				IsEnabled: true,
				Steps: []model.OrchestrationStep{
					{StepID: "crypto_send", Rail: model.RailCrypto, Operation: "payout.crypto"},
					{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)},
				},
			},
		},
	}
	repo := &stubRepository{store: store, routes: routes, plans: plans}
	svc := &Service{
		logger:  zap.NewNop(),
		clock:   testClock{now: time.Now()},
		storage: repo,
		deps: serviceDependencies{
			railGateways: buildRailGatewayDependency(map[string]rail.RailGateway{
				"crypto-tron": &fakeRailGateway{
					rail: "CRYPTO",
					sendFn: func(ctx context.Context, req rail.TransferRequest) (rail.RailResult, error) {
						return rail.RailResult{}, errors.New("chain failure")
					},
				},
			}, nil, nil, nil, nil),
			gatewayRegistry: &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,
							RequiresObserveConfirm: true,
						},
						Limits:    model.Limits{MinAmount: "0", MaxAmount: "100000"},
						IsEnabled: true,
					},
				},
			},
			cardRoutes: map[string]CardGatewayRoute{
				defaultCardGateway: {
					FundingAddress: "funding-address",
				},
			},
		},
	}

	payment := &model.Payment{
		PaymentRef:            "chain-1",
		IdempotencyKey:        "chain-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: "wallet-src",
					Asset: &paymenttypes.Asset{
						Chain:       "TRON_MAINNET",
						TokenSymbol: "USDT",
					},
				},
			},
			Destination: model.PaymentEndpoint{
				Type:   model.EndpointTypeLedger,
				Ledger: &model.LedgerEndpoint{LedgerAccountRef: "ledger:dest"},
			},
			Amount: &paymenttypes.Money{Currency: "USDT", Amount: "50"},
		},
	}
	store.payments[payment.PaymentRef] = payment

	err := svc.executePayment(ctx, store, payment, &orchestratorv1.PaymentQuote{})
	if err == nil || err.Error() != "chain failure" {
		t.Fatalf("expected chain failure error, got %v", err)
	}
	if payment.State != model.PaymentStateFailed {
		t.Fatalf("expected payment failed, got %s", payment.State)
	}
	if payment.FailureCode != model.PaymentFailureCodeChain {
		t.Fatalf("expected failure code chain, got %s", payment.FailureCode)
	}
}

func TestProcessTransferUpdateHandler_Settled(t *testing.T) {
	ctx := context.Background()
	payment := &model.Payment{
		PaymentRef: "pay-1",
		State:      model.PaymentStateSubmitted,
		Execution:  &model.ExecutionRefs{ChainTransferRef: "transfer-1"},
	}
	store := newStubPaymentsStore()
	store.payments[payment.PaymentRef] = payment
	store.byChain["transfer-1"] = payment

	svc := &Service{
		logger:  zap.NewNop(),
		clock:   testClock{now: time.Now()},
		storage: &stubRepository{store: store},
	}
	svc.h.events = newPaymentEventHandler(svc.storage, svc.ensureRepository, svc.logger, nil, nil, nil)

	req := &orchestratorv1.ProcessTransferUpdateRequest{
		Event: &chainv1.TransferStatusChangedEvent{
			Transfer: &chainv1.Transfer{
				TransferRef: "transfer-1",
				Status:      chainv1.TransferStatus_TRANSFER_SUCCESS,
			},
		},
	}

	reSP, err := gsresponse.Execute(ctx, svc.h.events.processTransferUpdate(ctx, req))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if reSP.GetPayment().GetState() != orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED {
		t.Fatalf("expected settled state, got %s", reSP.GetPayment().GetState())
	}
}

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

	payment := &model.Payment{
		PaymentRef: "pay-card",
		State:      model.PaymentStateSubmitted,
		Intent: model.PaymentIntent{
			Ref: "ref-1",
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{MaskedPan: "4111"},
			},
		},
		Execution: &model.ExecutionRefs{ChainTransferRef: "fund-1"},
	}

	plan := ensureExecutionPlan(payment)
	fundStep := ensureExecutionStep(plan, stepCodeFundingTransfer)
	fundStep.TransferRef = "fund-1"
	setExecutionStepRole(fundStep, executionStepRoleSource)
	setExecutionStepStatus(fundStep, model.OperationStateWaiting)

	feeStep := ensureExecutionStep(plan, stepCodeFeeTransfer)
	feeStep.TransferRef = "fee-1"
	setExecutionStepRole(feeStep, executionStepRoleSource)
	setExecutionStepStatus(feeStep, model.OperationStateWaiting)

	cardStep := ensureExecutionStep(plan, stepCodeCardPayout)
	setExecutionStepRole(cardStep, executionStepRoleConsumer)
	setExecutionStepStatus(cardStep, model.OperationStatePlanned)

	store := newStubPaymentsStore()
	store.payments[payment.PaymentRef] = payment
	store.indexTransfers(payment)

	svc := &Service{
		logger:  zap.NewNop(),
		clock:   testClock{now: time.Now()},
		storage: &stubRepository{store: store},
	}

	payoutCalls := 0
	submit := func(ctx context.Context, operationRef string, payment *model.Payment) error {
		payoutCalls++
		if payment.Execution == nil {
			payment.Execution = &model.ExecutionRefs{}
		}
		payment.Execution.CardPayoutRef = "payout-1"
		plan := ensureExecutionPlan(payment)
		step := ensureExecutionStep(plan, stepCodeCardPayout)
		setExecutionStepRole(step, executionStepRoleConsumer)
		step.TransferRef = "payout-1"
		step.OperationRef = operationRef
		setExecutionStepStatus(step, model.OperationStateWaiting)
		return nil
	}
	svc.h.events = newPaymentEventHandler(svc.storage, svc.ensureRepository, svc.logger, submit, nil, nil)

	req := &orchestratorv1.ProcessTransferUpdateRequest{
		Event: &chainv1.TransferStatusChangedEvent{
			Transfer: &chainv1.Transfer{
				TransferRef: "fund-1",
				Status:      chainv1.TransferStatus_TRANSFER_SUCCESS,
			},
		},
	}
	resp, err := gsresponse.Execute(ctx, svc.h.events.processTransferUpdate(ctx, req))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if payoutCalls != 0 {
		t.Fatalf("expected no payout on first confirmation, got %d", payoutCalls)
	}
	if fundStep.State != model.OperationStateSuccess {
		t.Fatalf("expected funding step confirmed, got %s", feeStep.State)
	}
	if resp.GetPayment().GetState() != orchestratorv1.PaymentState_PAYMENT_STATE_SUBMITTED {
		t.Fatalf("expected submitted state, got %s", resp.GetPayment().GetState())
	}

	req = &orchestratorv1.ProcessTransferUpdateRequest{
		Event: &chainv1.TransferStatusChangedEvent{
			Transfer: &chainv1.Transfer{
				TransferRef: "fee-1",
				Status:      chainv1.TransferStatus_TRANSFER_SUCCESS,
			},
		},
	}
	resp, err = gsresponse.Execute(ctx, svc.h.events.processTransferUpdate(ctx, req))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if payoutCalls != 1 {
		t.Fatalf("expected payout after all sources confirmed, got %d", payoutCalls)
	}
	if feeStep.State != model.OperationStateSuccess {
		t.Fatalf("expected fee step confirmed, got %s", string(model.OperationStateSuccess))
	}
	if resp.GetPayment().GetExecution().GetCardPayoutRef() != "payout-1" {
		t.Fatalf("expected card payout ref set, got %s", resp.GetPayment().GetExecution().GetCardPayoutRef())
	}
}

func TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) {
	ctx := context.Background()
	payment := &model.Payment{
		PaymentRef: "pay-2",
		State:      model.PaymentStateSubmitted,
		Intent: model.PaymentIntent{
			Ref: "ref-2",
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-dst",
				},
			},
			Amount: &paymenttypes.Money{Currency: "USD", Amount: "40"},
		},
	}
	store := newStubPaymentsStore()
	store.listResp = &model.PaymentList{Items: []*model.Payment{payment}}
	store.payments[payment.PaymentRef] = payment

	svc := &Service{
		logger:  zap.NewNop(),
		clock:   testClock{now: time.Now()},
		storage: &stubRepository{store: store},
	}
	svc.h.events = newPaymentEventHandler(svc.storage, svc.ensureRepository, svc.logger, nil, nil, nil)

	req := &orchestratorv1.ProcessDepositObservedRequest{
		Event: &chainv1.WalletDepositObservedEvent{
			WalletRef: "wallet-dst",
			Amount:    &moneyv1.Money{Currency: "USD", Amount: "40"},
		},
	}

	reSP, err := gsresponse.Execute(ctx, svc.h.events.processDepositObserved(ctx, req))
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}
	if reSP.GetPayment().GetState() != orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED {
		t.Fatalf("expected settled state, got %s", reSP.GetPayment().GetState())
	}
}

// ----------------------------------------------------------------------

type stubRepository struct {
	store  *stubPaymentsStore
	quotes storage.QuotesStore
	routes storage.RoutesStore
	plans  storage.PlanTemplatesStore
}

func (r *stubRepository) Ping(context.Context) error      { return nil }
func (r *stubRepository) Payments() storage.PaymentsStore { return r.store }
func (r *stubRepository) Quotes() storage.QuotesStore {
	if r.quotes != nil {
		return r.quotes
	}
	return &stubQuotesStore{}
}
func (r *stubRepository) Routes() storage.RoutesStore {
	if r.routes != nil {
		return r.routes
	}
	return &stubRoutesStore{}
}

func (r *stubRepository) PlanTemplates() storage.PlanTemplatesStore {
	if r.plans != nil {
		return r.plans
	}
	return &stubPlanTemplatesStore{}
}

type stubQuotesStore struct {
	quotes map[string]*model.PaymentQuoteRecord
}

func (s *stubQuotesStore) Create(ctx context.Context, quote *model.PaymentQuoteRecord) error {
	if quote == nil {
		return merrors.InvalidArgument("nil quote")
	}
	if s.quotes == nil {
		s.quotes = map[string]*model.PaymentQuoteRecord{}
	}
	s.quotes[strings.TrimSpace(quote.QuoteRef)] = quote
	return nil
}

func (s *stubQuotesStore) GetByRef(ctx context.Context, orgRef bson.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
	if s.quotes == nil {
		return nil, storage.ErrQuoteNotFound
	}
	if q, ok := s.quotes[strings.TrimSpace(quoteRef)]; ok {
		return q, nil
	}
	return nil, storage.ErrQuoteNotFound
}

func (s *stubQuotesStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
	if s.quotes == nil {
		return nil, storage.ErrQuoteNotFound
	}
	for _, q := range s.quotes {
		if q.OrganizationRef != orgRef {
			continue
		}
		if q.IdempotencyKey == idempotencyKey {
			return q, nil
		}
	}
	return nil, storage.ErrQuoteNotFound
}

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

func (s *stubRoutesStore) Create(ctx context.Context, route *model.PaymentRoute) error {
	return merrors.InvalidArgument("routes store not implemented")
}

func (s *stubRoutesStore) Update(ctx context.Context, route *model.PaymentRoute) error {
	return merrors.InvalidArgument("routes store not implemented")
}

func (s *stubRoutesStore) GetByID(ctx context.Context, id bson.ObjectID) (*model.PaymentRoute, error) {
	return nil, storage.ErrRouteNotFound
}

func (s *stubRoutesStore) List(ctx 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 stubPlanTemplatesStore struct {
	templates []*model.PaymentPlanTemplate
}

func (s *stubPlanTemplatesStore) Create(ctx context.Context, template *model.PaymentPlanTemplate) error {
	return merrors.InvalidArgument("plan templates store not implemented")
}

func (s *stubPlanTemplatesStore) Update(ctx context.Context, template *model.PaymentPlanTemplate) error {
	return merrors.InvalidArgument("plan templates store not implemented")
}

func (s *stubPlanTemplatesStore) GetByID(ctx context.Context, id bson.ObjectID) (*model.PaymentPlanTemplate, error) {
	return nil, storage.ErrPlanTemplateNotFound
}

func (s *stubPlanTemplatesStore) List(ctx 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 stubPaymentsStore struct {
	payments map[string]*model.Payment
	byChain  map[string]*model.Payment
	listResp *model.PaymentList
}

func newStubPaymentsStore() *stubPaymentsStore {
	return &stubPaymentsStore{
		payments: map[string]*model.Payment{},
		byChain:  map[string]*model.Payment{},
	}
}

func (s *stubPaymentsStore) Create(ctx context.Context, payment *model.Payment) error {
	if _, exists := s.payments[payment.PaymentRef]; exists {
		return storage.ErrDuplicatePayment
	}
	s.payments[payment.PaymentRef] = payment
	s.indexTransfers(payment)
	return nil
}

func (s *stubPaymentsStore) Update(ctx context.Context, payment *model.Payment) error {
	if _, exists := s.payments[payment.PaymentRef]; !exists {
		return storage.ErrPaymentNotFound
	}
	s.payments[payment.PaymentRef] = payment
	s.indexTransfers(payment)
	return nil
}

func (s *stubPaymentsStore) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.Payment, error) {
	if p, ok := s.payments[paymentRef]; ok {
		return p, nil
	}
	return nil, storage.ErrPaymentNotFound
}

func (s *stubPaymentsStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, key string) (*model.Payment, error) {
	for _, p := range s.payments {
		if p.OrganizationRef == orgRef && strings.TrimSpace(p.IdempotencyKey) == key {
			return p, nil
		}
	}
	return nil, storage.ErrPaymentNotFound
}

func (s *stubPaymentsStore) GetByChainTransferRef(ctx context.Context, transferRef string) (*model.Payment, error) {
	if p, ok := s.byChain[transferRef]; ok {
		return p, nil
	}
	return nil, storage.ErrPaymentNotFound
}

func (s *stubPaymentsStore) List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error) {
	if s.listResp != nil {
		return s.listResp, nil
	}
	return &model.PaymentList{}, nil
}

func (s *stubPaymentsStore) indexTransfers(payment *model.Payment) {
	if payment == nil {
		return
	}
	if payment.Execution != nil && payment.Execution.ChainTransferRef != "" {
		s.byChain[payment.Execution.ChainTransferRef] = payment
	}
	if payment.ExecutionPlan == nil {
		return
	}
	for _, step := range payment.ExecutionPlan.Steps {
		if step == nil || strings.TrimSpace(step.TransferRef) == "" {
			continue
		}
		s.byChain[strings.TrimSpace(step.TransferRef)] = payment
	}
}

var _ storage.PaymentsStore = (*stubPaymentsStore)(nil)

// testClock satisfies clock.Clock

type testClock struct {
	now time.Time
}

func (c testClock) Now() time.Time { return c.now }
