package psvc

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"sort"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oobs"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ssched"
	"github.com/tech/sendico/payments/storage/model"
	quotestorage "github.com/tech/sendico/payments/storage/quote"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	menv "github.com/tech/sendico/pkg/messaging/envelope"
	pm "github.com/tech/sendico/pkg/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
	tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestExecutePayment_EndToEndSyncSettled(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
	})
	env.quotes.Put(newExecutableQuote(env.orgID, "quote-sync", "intent-sync", buildLedgerRoute()))

	resp, err := env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
		Meta:             testMeta(env.orgID, "idem-sync"),
		QuotationRef:     "quote-sync",
		ClientPaymentRef: "client-1",
	})
	if err != nil {
		t.Fatalf("ExecutePayment returned error: %v", err)
	}
	if resp.GetPayment() == nil {
		t.Fatal("expected payment in response")
	}
	if got, want := resp.GetPayment().GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED; got != want {
		t.Fatalf("state mismatch: got=%s want=%s", got, want)
	}

	getResp, err := env.svc.GetPayment(context.Background(), &orchestrationv2.GetPaymentRequest{
		Meta:       testMeta(env.orgID, ""),
		PaymentRef: resp.GetPayment().GetPaymentRef(),
	})
	if err != nil {
		t.Fatalf("GetPayment returned error: %v", err)
	}
	if getResp.GetPayment() == nil {
		t.Fatal("expected payment from GetPayment")
	}
	if got, want := getResp.GetPayment().GetPaymentRef(), resp.GetPayment().GetPaymentRef(); got != want {
		t.Fatalf("payment_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := getResp.GetOrganizationRef(), env.orgID.Hex(); got != want {
		t.Fatalf("organization_ref mismatch: got=%q want=%q", got, want)
	}

	getRespGlobal, err := env.svc.GetPayment(context.Background(), &orchestrationv2.GetPaymentRequest{
		PaymentRef: resp.GetPayment().GetPaymentRef(),
	})
	if err != nil {
		t.Fatalf("GetPayment(global) returned error: %v", err)
	}
	if getRespGlobal.GetPayment() == nil {
		t.Fatal("expected payment from global GetPayment")
	}
	if got, want := getRespGlobal.GetPayment().GetPaymentRef(), resp.GetPayment().GetPaymentRef(); got != want {
		t.Fatalf("global payment_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := getRespGlobal.GetOrganizationRef(), env.orgID.Hex(); got != want {
		t.Fatalf("global organization_ref mismatch: got=%q want=%q", got, want)
	}

	timeline, err := env.observer.PaymentTimeline(context.Background(), oobs.PaymentTimelineInput{
		PaymentRef: resp.GetPayment().GetPaymentRef(),
	})
	if err != nil {
		t.Fatalf("PaymentTimeline returned error: %v", err)
	}
	assertTimelineHasEvent(t, timeline.Items, "created")
	assertTimelineHasEvent(t, timeline.Items, "settled")
}

func TestExecutePayment_PublishesStatusUpdates(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
	})
	env.quotes.Put(newExecutableQuote(env.orgID, "quote-status", "intent-status", buildLedgerRoute()))

	resp, err := env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
		Meta:             testMeta(env.orgID, "idem-status"),
		QuotationRef:     "quote-status",
		ClientPaymentRef: "client-status",
	})
	if err != nil {
		t.Fatalf("ExecutePayment returned error: %v", err)
	}
	if resp.GetPayment() == nil {
		t.Fatal("expected payment in response")
	}

	type publishedEnvelope struct {
		EventID string          `json:"event_id"`
		Type    string          `json:"type"`
		Data    json.RawMessage `json:"data"`
	}
	type publishedData struct {
		PaymentRef string `json:"payment_ref"`
		State      string `json:"state"`
		Event      string `json:"event"`
	}

	msgs := env.producer.Messages()
	if len(msgs) == 0 {
		t.Fatal("expected published status updates")
	}

	seenCreated := false
	seenSettled := false
	for i := range msgs {
		if got, want := msgs[i].Subject, "payment_orchestrator_updated"; got != want {
			t.Fatalf("subject mismatch at %d: got=%q want=%q", i, got, want)
		}

		var outer publishedEnvelope
		if err := json.Unmarshal(msgs[i].Data, &outer); err != nil {
			t.Fatalf("failed to unmarshal published envelope[%d]: %v", i, err)
		}
		if strings.TrimSpace(outer.EventID) == "" {
			t.Fatalf("expected non-empty event_id at %d", i)
		}
		if got, want := outer.Type, pm.PaymentStatusUpdatedType; got != want {
			t.Fatalf("event type mismatch at %d: got=%q want=%q", i, got, want)
		}

		var inner publishedData
		if err := json.Unmarshal(outer.Data, &inner); err != nil {
			t.Fatalf("failed to unmarshal published data[%d]: %v", i, err)
		}
		if got, want := inner.PaymentRef, resp.GetPayment().GetPaymentRef(); got != want {
			t.Fatalf("payment_ref mismatch at %d: got=%q want=%q", i, got, want)
		}
		if inner.Event == "created" && inner.State == string(agg.StateCreated) {
			seenCreated = true
		}
		if inner.State == string(agg.StateSettled) {
			seenSettled = true
		}
	}

	if !seenCreated {
		t.Fatal("expected created payment status update")
	}
	if !seenSettled {
		t.Fatal("expected settled payment status update")
	}
}

func TestExecutePayment_IdempotencyMismatch(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
	})
	env.quotes.Put(newExecutableQuote(env.orgID, "quote-idem", "intent-idem", buildLedgerRoute()))

	_, err := env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
		Meta:             testMeta(env.orgID, "idem-shared"),
		QuotationRef:     "quote-idem",
		ClientPaymentRef: "client-a",
	})
	if err != nil {
		t.Fatalf("first ExecutePayment returned error: %v", err)
	}

	_, err = env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
		Meta:             testMeta(env.orgID, "idem-shared"),
		QuotationRef:     "quote-idem",
		ClientPaymentRef: "client-b",
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for mismatch, got %v", err)
	}
}

func TestExecutePayment_BatchQuoteRequiresBatchExecution(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
	})
	env.quotes.Put(newExecutableBatchQuote(env.orgID, "quote-batch", []string{"intent-a", "intent-b"}, buildLedgerRoute()))

	_, err := env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
		Meta:             testMeta(env.orgID, "idem-batch"),
		QuotationRef:     "quote-batch",
		ClientPaymentRef: "client-batch",
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for unsupported batch quote, got %v", err)
	}
	if got := err.Error(); !strings.Contains(got, "batch quotation is not supported for ExecutePayment; use ExecuteBatchPayment") {
		t.Fatalf("unexpected error message: %q", got)
	}
}

func TestExecutePayment_RetryThenSuccess(t *testing.T) {
	env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		if req.StepExecution.Attempt == 1 {
			return nil, errors.New("temporary ledger failure")
		}
		step := req.StepExecution
		step.State = agg.StepStateCompleted
		return &sexec.ExecuteOutput{StepExecution: step}, nil
	})
	env.quotes.Put(newExecutableQuote(env.orgID, "quote-retry", "intent-retry", buildLedgerRoute()))

	resp, err := env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
		Meta:             testMeta(env.orgID, "idem-retry"),
		QuotationRef:     "quote-retry",
		ClientPaymentRef: "client-retry",
	})
	if err != nil {
		t.Fatalf("ExecutePayment returned error: %v", err)
	}
	if got, want := resp.GetPayment().GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED; got != want {
		t.Fatalf("state mismatch: got=%s want=%s", got, want)
	}
	if len(resp.GetPayment().GetStepExecutions()) != 1 {
		t.Fatalf("expected one step execution, got=%d", len(resp.GetPayment().GetStepExecutions()))
	}
	if got, want := resp.GetPayment().GetStepExecutions()[0].GetAttempt(), uint32(2); got != want {
		t.Fatalf("attempt mismatch: got=%d want=%d", got, want)
	}
}

func TestReconcileExternal_AdvancesAsyncPaymentToSettled(t *testing.T) {
	var observeStepRef string
	env := newTestEnv(t, func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
		step := req.StepExecution
		switch kind {
		case "card_payout":
			step.State = agg.StepStateCompleted
			return &sexec.ExecuteOutput{StepExecution: step}, nil
		case "observe_confirm":
			step.State = agg.StepStateRunning
			step.ExternalRefs = append(step.ExternalRefs, agg.ExternalRef{
				GatewayInstanceID: "gw-card",
				Kind:              erecon.ExternalRefKindOperation,
				Ref:               "op-1",
			})
			observeStepRef = step.StepRef
			return &sexec.ExecuteOutput{StepExecution: step, Async: true}, nil
		default:
			step.State = agg.StepStateCompleted
			return &sexec.ExecuteOutput{StepExecution: step}, nil
		}
	})
	env.quotes.Put(newExecutableQuote(env.orgID, "quote-async", "intent-async", buildCardRoute()))

	resp, err := env.svc.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{
		Meta:             testMeta(env.orgID, "idem-async"),
		QuotationRef:     "quote-async",
		ClientPaymentRef: "client-async",
	})
	if err != nil {
		t.Fatalf("ExecutePayment returned error: %v", err)
	}
	if got, want := resp.GetPayment().GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_EXECUTING; got != want {
		t.Fatalf("expected executing before external reconcile, got=%s", got)
	}
	if observeStepRef == "" {
		t.Fatal("expected observe step ref to be captured")
	}

	reconciled, err := env.svc.ReconcileExternal(context.Background(), ReconcileExternalInput{
		OrganizationRef: env.orgID.Hex(),
		PaymentRef:      resp.GetPayment().GetPaymentRef(),
		Event: erecon.Event{
			Gateway: &erecon.GatewayEvent{
				StepRef:      observeStepRef,
				OperationRef: "op-1",
				Status:       erecon.GatewayStatusSuccess,
			},
		},
	})
	if err != nil {
		t.Fatalf("ReconcileExternal returned error: %v", err)
	}
	if reconciled == nil || reconciled.Payment == nil {
		t.Fatal("expected reconciled payment")
	}
	if got, want := reconciled.Payment.GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED; got != want {
		t.Fatalf("state mismatch after reconcile: got=%s want=%s", got, want)
	}
}

func TestListPayments_FiltersAndCursor(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
	})
	base := time.Date(2026, time.February, 20, 10, 0, 0, 0, time.UTC)

	mustCreatePayment(t, env.repo, testPayment(env.orgID, "p-1", "idem-1", "quote-list", agg.StateExecuting, base.Add(3*time.Minute)))
	mustCreatePayment(t, env.repo, testPayment(env.orgID, "p-2", "idem-2", "quote-list", agg.StateSettled, base.Add(2*time.Minute)))
	mustCreatePayment(t, env.repo, testPayment(env.orgID, "p-3", "idem-3", "quote-list", agg.StateFailed, base.Add(1*time.Minute)))
	mustCreatePayment(t, env.repo, testPayment(env.orgID, "p-4", "idem-4", "quote-other", agg.StateExecuting, base.Add(4*time.Minute)))

	first, err := env.svc.ListPayments(context.Background(), &orchestrationv2.ListPaymentsRequest{
		Meta:         testMeta(env.orgID, ""),
		States:       []orchestrationv2.OrchestrationState{orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_EXECUTING, orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED},
		QuotationRef: "quote-list",
		Page:         &paginationv1.CursorPageRequest{Limit: 1},
	})
	if err != nil {
		t.Fatalf("ListPayments(first) returned error: %v", err)
	}
	if len(first.GetPayments()) != 1 {
		t.Fatalf("expected one payment in first page, got=%d", len(first.GetPayments()))
	}
	if got, want := first.GetPayments()[0].GetPaymentRef(), "p-1"; got != want {
		t.Fatalf("first page payment mismatch: got=%q want=%q", got, want)
	}
	if strings.TrimSpace(first.GetPage().GetNextCursor()) == "" {
		t.Fatal("expected next cursor")
	}

	second, err := env.svc.ListPayments(context.Background(), &orchestrationv2.ListPaymentsRequest{
		Meta:         testMeta(env.orgID, ""),
		States:       []orchestrationv2.OrchestrationState{orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_EXECUTING, orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED},
		QuotationRef: "quote-list",
		Page: &paginationv1.CursorPageRequest{
			Limit:  2,
			Cursor: first.GetPage().GetNextCursor(),
		},
	})
	if err != nil {
		t.Fatalf("ListPayments(second) returned error: %v", err)
	}
	if len(second.GetPayments()) != 1 {
		t.Fatalf("expected one payment in second page, got=%d", len(second.GetPayments()))
	}
	if got, want := second.GetPayments()[0].GetPaymentRef(), "p-2"; got != want {
		t.Fatalf("second page payment mismatch: got=%q want=%q", got, want)
	}
}

func assertTimelineHasEvent(t *testing.T, items []oobs.TimelineEntry, event string) {
	t.Helper()
	for i := range items {
		if items[i].Event == event {
			return
		}
	}
	t.Fatalf("timeline missing event %q", event)
}

type testEnv struct {
	svc      Service
	repo     *memoryRepo
	quotes   *memoryQuoteStore
	observer oobs.Observer
	producer *capturingProducer
	orgID    bson.ObjectID
}

func newTestEnv(t *testing.T, handler func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error)) *testEnv {
	return newTestEnvWithPolicy(t, BatchOptimizationPolicy{}, handler)
}

func newTestEnvWithPolicy(
	t *testing.T,
	policy BatchOptimizationPolicy,
	handler func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error),
) *testEnv {
	t.Helper()
	repo := newMemoryRepo(func() time.Time {
		return time.Now().UTC()
	})
	quotes := newMemoryQuoteStore()
	observer, err := oobs.New(oobs.Dependencies{})
	if err != nil {
		t.Fatalf("oobs.New returned error: %v", err)
	}

	script := &scriptedExecutors{handler: handler}
	registry := sexec.New(sexec.Dependencies{
		Ledger:             script,
		Crypto:             script,
		ProviderSettlement: script,
		CardPayout:         script,
		ObserveConfirm:     script,
		Guard:              script,
	})

	producer := &capturingProducer{}
	svc, err := New(Dependencies{
		QuoteStore:              quotes,
		Repository:              repo,
		Executors:               registry,
		Observer:                observer,
		Producer:                producer,
		RetryPolicy:             ssched.RetryPolicy{MaxAttempts: 2},
		BatchOptimizationPolicy: policy,
		MaxTicks:                20,
	})
	if err != nil {
		t.Fatalf("New returned error: %v", err)
	}
	return &testEnv{
		svc:      svc,
		repo:     repo,
		quotes:   quotes,
		observer: observer,
		producer: producer,
		orgID:    bson.NewObjectID(),
	}
}

type capturedMessage struct {
	Subject string
	Data    []byte
}

type capturingProducer struct {
	mu    sync.Mutex
	items []capturedMessage
}

func (p *capturingProducer) SendMessage(envelope menv.Envelope) error {
	if envelope == nil {
		return nil
	}
	data := envelope.GetData()
	if len(data) == 0 {
		serialized, err := envelope.Serialize()
		if err != nil {
			return err
		}
		data = envelope.GetData()
		if len(data) == 0 {
			data = serialized
		}
	}
	p.mu.Lock()
	p.items = append(p.items, capturedMessage{
		Subject: envelope.GetSignature().ToString(),
		Data:    append([]byte(nil), data...),
	})
	p.mu.Unlock()
	return nil
}

func (p *capturingProducer) Messages() []capturedMessage {
	p.mu.Lock()
	defer p.mu.Unlock()
	out := make([]capturedMessage, len(p.items))
	copy(out, p.items)
	return out
}

type scriptedExecutors struct {
	handler func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error)
}

func (s *scriptedExecutors) ExecuteLedger(_ context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	return s.handler("ledger", req)
}
func (s *scriptedExecutors) ExecuteCrypto(_ context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	return s.handler("crypto", req)
}
func (s *scriptedExecutors) ExecuteProviderSettlement(_ context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	return s.handler("provider_settlement", req)
}
func (s *scriptedExecutors) ExecuteCardPayout(_ context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	return s.handler("card_payout", req)
}
func (s *scriptedExecutors) ExecuteObserveConfirm(_ context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	return s.handler("observe_confirm", req)
}
func (s *scriptedExecutors) ExecuteGuard(_ context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	return s.handler("guard", req)
}

type memoryQuoteStore struct {
	mu   sync.Mutex
	data map[string]*model.PaymentQuoteRecord
}

func newMemoryQuoteStore() *memoryQuoteStore {
	return &memoryQuoteStore{data: map[string]*model.PaymentQuoteRecord{}}
}

func (s *memoryQuoteStore) Put(record *model.PaymentQuoteRecord) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.data[quoteKey(record.OrganizationRef, record.QuoteRef)] = cloneQuoteRecord(record)
}

func (s *memoryQuoteStore) GetByRef(_ context.Context, orgRef bson.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	record := s.data[quoteKey(orgRef, quoteRef)]
	if record == nil {
		return nil, quotestorage.ErrQuoteNotFound
	}
	return cloneQuoteRecord(record), nil
}

func quoteKey(orgRef bson.ObjectID, quoteRef string) string {
	return orgRef.Hex() + "|" + strings.TrimSpace(quoteRef)
}

func cloneQuoteRecord(in *model.PaymentQuoteRecord) *model.PaymentQuoteRecord {
	if in == nil {
		return nil
	}
	data, _ := bson.Marshal(in)
	out := &model.PaymentQuoteRecord{}
	_ = bson.Unmarshal(data, out)
	return out
}

type memoryRepo struct {
	mu  sync.Mutex
	now func() time.Time

	byID          map[bson.ObjectID]*agg.Payment
	byPaymentRef  map[string]bson.ObjectID
	byIdempotency map[string]bson.ObjectID
}

func newMemoryRepo(now func() time.Time) *memoryRepo {
	return &memoryRepo{
		now:           now,
		byID:          map[bson.ObjectID]*agg.Payment{},
		byPaymentRef:  map[string]bson.ObjectID{},
		byIdempotency: map[string]bson.ObjectID{},
	}
}

func (r *memoryRepo) Create(_ context.Context, payment *agg.Payment) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if payment == nil {
		return merrors.InvalidArgument("payment is required")
	}
	if payment.ID.IsZero() {
		payment.ID = bson.NewObjectID()
	}
	if strings.TrimSpace(payment.PaymentRef) == "" {
		payment.PaymentRef = payment.ID.Hex()
	}
	if payment.CreatedAt.IsZero() {
		payment.CreatedAt = r.now().UTC()
	}
	payment.UpdatedAt = payment.CreatedAt
	if payment.Version == 0 {
		payment.Version = 1
	}

	refKey := repoPaymentRefKey(payment.OrganizationRef, payment.PaymentRef)
	if _, exists := r.byPaymentRef[refKey]; exists {
		return prepo.ErrDuplicatePayment
	}
	idemKey := repoIdemKey(payment.OrganizationRef, payment.IdempotencyKey)
	if _, exists := r.byIdempotency[idemKey]; exists {
		return prepo.ErrDuplicatePayment
	}
	cloned := clonePayment(payment)
	r.byID[cloned.ID] = cloned
	r.byPaymentRef[refKey] = cloned.ID
	r.byIdempotency[idemKey] = cloned.ID
	*payment = *clonePayment(cloned)
	return nil
}

func (r *memoryRepo) UpdateCAS(_ context.Context, payment *agg.Payment, expectedVersion uint64) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if payment == nil {
		return merrors.InvalidArgument("payment is required")
	}
	stored := r.byID[payment.ID]
	if stored == nil {
		return prepo.ErrPaymentNotFound
	}
	if stored.OrganizationRef != payment.OrganizationRef {
		return prepo.ErrPaymentNotFound
	}
	if stored.Version != expectedVersion {
		return prepo.ErrVersionConflict
	}
	next := clonePayment(payment)
	next.Version = expectedVersion + 1
	next.UpdatedAt = r.now().UTC()
	r.byID[next.ID] = next
	*payment = *clonePayment(next)
	return nil
}

func (r *memoryRepo) GetByPaymentRef(_ context.Context, orgRef bson.ObjectID, paymentRef string) (*agg.Payment, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	id, ok := r.byPaymentRef[repoPaymentRefKey(orgRef, paymentRef)]
	if !ok {
		return nil, prepo.ErrPaymentNotFound
	}
	return clonePayment(r.byID[id]), nil
}

func (r *memoryRepo) GetByPaymentRefGlobal(_ context.Context, paymentRef string) (*agg.Payment, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	ref := strings.TrimSpace(paymentRef)
	for _, payment := range r.byID {
		if strings.TrimSpace(payment.PaymentRef) != ref {
			continue
		}
		return clonePayment(payment), nil
	}
	return nil, prepo.ErrPaymentNotFound
}

func (r *memoryRepo) GetByIdempotencyKey(_ context.Context, orgRef bson.ObjectID, idempotencyKey string) (*agg.Payment, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	id, ok := r.byIdempotency[repoIdemKey(orgRef, idempotencyKey)]
	if !ok {
		return nil, prepo.ErrPaymentNotFound
	}
	return clonePayment(r.byID[id]), nil
}

func (r *memoryRepo) ListByQuotationRef(_ context.Context, in prepo.ListByQuotationRefInput) (*prepo.ListOutput, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	items := make([]*agg.Payment, 0)
	for _, payment := range r.byID {
		if payment.OrganizationRef != in.OrganizationRef {
			continue
		}
		if payment.QuotationRef != in.QuotationRef {
			continue
		}
		if !isBeforeCursor(payment, in.Cursor) {
			continue
		}
		items = append(items, clonePayment(payment))
	}
	return paginatePayments(items, in.Limit), nil
}

func (r *memoryRepo) ListByState(_ context.Context, in prepo.ListByStateInput) (*prepo.ListOutput, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	items := make([]*agg.Payment, 0)
	for _, payment := range r.byID {
		if payment.OrganizationRef != in.OrganizationRef {
			continue
		}
		if payment.State != in.State {
			continue
		}
		if !isBeforeCursor(payment, in.Cursor) {
			continue
		}
		items = append(items, clonePayment(payment))
	}
	return paginatePayments(items, in.Limit), nil
}

func (r *memoryRepo) ListByStateGlobal(_ context.Context, in prepo.ListByStateGlobalInput) (*prepo.ListOutput, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	items := make([]*agg.Payment, 0)
	for _, payment := range r.byID {
		if payment.State != in.State {
			continue
		}
		if !isBeforeCursor(payment, in.Cursor) {
			continue
		}
		items = append(items, clonePayment(payment))
	}
	return paginatePayments(items, in.Limit), nil
}

func repoPaymentRefKey(orgRef bson.ObjectID, paymentRef string) string {
	return orgRef.Hex() + "|" + strings.TrimSpace(paymentRef)
}

func repoIdemKey(orgRef bson.ObjectID, key string) string {
	return orgRef.Hex() + "|" + strings.TrimSpace(key)
}

func clonePayment(in *agg.Payment) *agg.Payment {
	if in == nil {
		return nil
	}
	data, _ := bson.Marshal(in)
	out := &agg.Payment{}
	_ = bson.Unmarshal(data, out)
	return out
}

func isBeforeCursor(payment *agg.Payment, cursor *prepo.ListCursor) bool {
	if cursor == nil {
		return true
	}
	if payment.CreatedAt.Before(cursor.CreatedAt) {
		return true
	}
	if payment.CreatedAt.After(cursor.CreatedAt) {
		return false
	}
	return bytes.Compare(payment.ID[:], cursor.ID[:]) < 0
}

func paginatePayments(items []*agg.Payment, limit int32) *prepo.ListOutput {
	sort.Slice(items, func(i, j int) bool {
		left := items[i]
		right := items[j]
		if !left.CreatedAt.Equal(right.CreatedAt) {
			return left.CreatedAt.After(right.CreatedAt)
		}
		return bytes.Compare(left.ID[:], right.ID[:]) > 0
	})
	if limit <= 0 {
		limit = 50
	}
	max := int(limit)
	if max > len(items) {
		max = len(items)
	}
	page := items[:max]
	out := &prepo.ListOutput{Items: page}
	if len(items) > max && max > 0 {
		last := page[len(page)-1]
		out.NextCursor = &prepo.ListCursor{
			CreatedAt: last.CreatedAt.UTC(),
			ID:        last.ID,
		}
	}
	return out
}

func newExecutableQuote(orgRef bson.ObjectID, quoteRef, intentRef string, route *paymenttypes.QuoteRouteSpecification) *model.PaymentQuoteRecord {
	now := time.Now().UTC()
	return &model.PaymentQuoteRecord{
		Base: modelBase(now),
		OrganizationBoundBase: pm.OrganizationBoundBase{
			OrganizationRef: orgRef,
		},
		QuoteRef:     quoteRef,
		RequestShape: model.QuoteRequestShapeSingle,
		Items: []*model.PaymentQuoteItemV2{
			{
				Intent: &model.PaymentIntent{
					Ref:                intentRef,
					Kind:               model.PaymentKindPayout,
					Source:             testLedgerEndpoint("ledger-src"),
					Destination:        testLedgerEndpoint("ledger-dst"),
					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},
			},
		},
		ExpiresAt: now.Add(1 * time.Hour),
	}
}

func newExecutableBatchQuote(orgRef bson.ObjectID, quoteRef string, intentRefs []string, route *paymenttypes.QuoteRouteSpecification) *model.PaymentQuoteRecord {
	now := time.Now().UTC()
	items := make([]*model.PaymentQuoteItemV2, 0, len(intentRefs))
	for _, intentRef := range intentRefs {
		items = append(items, &model.PaymentQuoteItemV2{
			Intent: &model.PaymentIntent{
				Ref:                intentRef,
				Kind:               model.PaymentKindPayout,
				Source:             testLedgerEndpoint("ledger-src"),
				Destination:        testLedgerEndpoint("ledger-dst"),
				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},
		})
	}
	return &model.PaymentQuoteRecord{
		Base: modelBase(now),
		OrganizationBoundBase: pm.OrganizationBoundBase{
			OrganizationRef: orgRef,
		},
		QuoteRef:     quoteRef,
		RequestShape: model.QuoteRequestShapeBatch,
		Items:        items,
		ExpiresAt:    now.Add(1 * time.Hour),
	}
}

func buildLedgerRoute() *paymenttypes.QuoteRouteSpecification {
	return &paymenttypes.QuoteRouteSpecification{
		Hops: []*paymenttypes.QuoteRouteHop{
			{Index: 10, Rail: "LEDGER"},
			{Index: 20, Rail: "LEDGER"},
		},
	}
}

func buildCardRoute() *paymenttypes.QuoteRouteSpecification {
	return &paymenttypes.QuoteRouteSpecification{
		Rail:     "CARD",
		Provider: "gw-card",
		Network:  "visa",
	}
}

func testMeta(orgRef bson.ObjectID, idempotencyKey string) *sharedv1.RequestMeta {
	meta := &sharedv1.RequestMeta{OrganizationRef: orgRef.Hex()}
	if strings.TrimSpace(idempotencyKey) != "" {
		meta.Trace = &tracev1.TraceContext{IdempotencyKey: idempotencyKey}
	}
	return meta
}

func modelBase(at time.Time) storable.Base {
	return storable.Base{
		ID:        bson.NewObjectID(),
		CreatedAt: at.UTC(),
		UpdatedAt: at.UTC(),
	}
}

func testPayment(orgRef bson.ObjectID, paymentRef, idem, quoteRef string, state agg.State, createdAt time.Time) *agg.Payment {
	return &agg.Payment{
		Base: modelBase(createdAt),
		OrganizationBoundBase: pm.OrganizationBoundBase{
			OrganizationRef: orgRef,
		},
		PaymentRef:       paymentRef,
		IdempotencyKey:   idem,
		QuotationRef:     quoteRef,
		ClientPaymentRef: "client-" + paymentRef,
		IntentSnapshot: model.PaymentIntent{
			Ref:                "intent-" + paymentRef,
			Kind:               model.PaymentKindPayout,
			Source:             testLedgerEndpoint("ledger-src"),
			Destination:        testLedgerEndpoint("ledger-dst"),
			Amount:             &paymenttypes.Money{Amount: "10", Currency: "USD"},
			SettlementCurrency: "USD",
		},
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			QuoteRef:    quoteRef,
			DebitAmount: &paymenttypes.Money{Amount: "10", Currency: "USD"},
			Route:       buildLedgerRoute(),
		},
		State:   state,
		Version: 1,
		StepExecutions: []agg.StepExecution{
			{StepRef: "step-1", StepCode: "edge.10_20.ledger.move", State: agg.StepStateCompleted, Attempt: 1},
		},
	}
}

func mustCreatePayment(t *testing.T, repo prepo.Repository, payment *agg.Payment) {
	t.Helper()
	if err := repo.Create(context.Background(), payment); err != nil {
		t.Fatalf("Create returned error: %v", err)
	}
}

func testLedgerEndpoint(account string) model.PaymentEndpoint {
	return model.PaymentEndpoint{
		Type: model.EndpointTypeLedger,
		Ledger: &model.LedgerEndpoint{
			LedgerAccountRef: strings.TrimSpace(account),
		},
	}
}
