package orchestrator

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

	chainclient "github.com/tech/sendico/chain/gateway/client"
	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"
	mo "github.com/tech/sendico/pkg/model"
	gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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/bson/primitive"
	"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,
		ledger: ledgerDependency{client: &ledgerclient.Fake{
			ApplyFXWithChargesFn: func(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
				return &ledgerv1.PostResponse{JournalEntryRef: "fx-entry"}, nil
			},
		}},
	}

	payment := &model.Payment{
		PaymentRef:            "fx-1",
		IdempotencyKey:        "fx-1",
		OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: primitive.NewObjectID()},
		Intent: model.PaymentIntent{
			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: &moneyv1.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()
	repo := &stubRepository{store: store}
	svc := &Service{
		logger:  zap.NewNop(),
		clock:   testClock{now: time.Now()},
		storage: repo,
		gateway: gatewayDependency{client: &chainclient.Fake{
			SubmitTransferFn: func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
				return nil, errors.New("chain failure")
			},
		}},
	}

	payment := &model.Payment{
		PaymentRef:            "chain-1",
		IdempotencyKey:        "chain-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.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-dst",
				},
			},
			Amount: &moneyv1.Money{Currency: "USD", 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},
	}

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

	reSP, err := gsresponse.Execute(ctx, svc.processTransferUpdateHandler(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 TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) {
	ctx := context.Background()
	payment := &model.Payment{
		PaymentRef: "pay-2",
		State:      model.PaymentStateSubmitted,
		Intent: model.PaymentIntent{
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "wallet-dst",
				},
			},
			Amount: &moneyv1.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},
	}

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

	reSP, err := gsresponse.Execute(ctx, svc.processDepositObservedHandler(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
}

func (r *stubRepository) Ping(context.Context) error      { return nil }
func (r *stubRepository) Payments() storage.PaymentsStore { return r.store }

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
	if payment.Execution != nil && payment.Execution.ChainTransferRef != "" {
		s.byChain[payment.Execution.ChainTransferRef] = 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
	if payment.Execution != nil && payment.Execution.ChainTransferRef != "" {
		s.byChain[payment.Execution.ChainTransferRef] = 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 primitive.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
}

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

// testClock satisfies clock.Clock

type testClock struct {
	now time.Time
}

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