package gateway

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/tech/sendico/gateway/aurora/internal/service/provider"
	"github.com/tech/sendico/gateway/aurora/storage/model"
	clockpkg "github.com/tech/sendico/pkg/clock"
	"github.com/tech/sendico/pkg/merrors"
	gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	"go.uber.org/zap"
)

type roundTripperFunc func(*http.Request) (*http.Response, error)

func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
	return f(r)
}

type staticClock struct {
	now time.Time
}

func (s staticClock) Now() time.Time {
	return s.now
}

func mustMarshalJSON(t *testing.T, value any) []byte {
	t.Helper()
	body, err := json.Marshal(value)
	if err != nil {
		t.Fatalf("json marshal failed: %v", err)
	}
	return body
}

type apiResponse struct {
	RequestID string `json:"request_id"`
	Status    string `json:"status"`
	Message   string `json:"message"`
	Code      string `json:"code"`
	Operation struct {
		RequestID string `json:"request_id"`
		Status    string `json:"status"`
		Code      string `json:"code"`
		Message   string `json:"message"`
	} `json:"operation"`
}

func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		ProjectID:         99,
		AllowedCurrencies: []string{"RUB"},
	}

	existingCreated := time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC)

	repo := newMockRepository()
	repo.payouts.Save(&model.CardPayout{
		PaymentRef:   "payment-parent-1",
		OperationRef: "payout-1",
		CreatedAt:    existingCreated,
	})

	httpClient := &http.Client{
		Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
			resp := apiResponse{}
			resp.Operation.RequestID = "req-123"
			body := mustMarshalJSON(t, resp)
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader(body)),
				Header:     http.Header{"Content-Type": []string{"application/json"}},
			}, nil
		}),
	}

	now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	processor := newCardPayoutProcessor(zap.NewNop(), cfg, staticClock{now: now}, repo, httpClient, nil)

	req := validCardPayoutRequest()
	req.ProjectId = 0
	req.Comment = "invoice-2002"

	resp, err := processor.Submit(context.Background(), req)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted payout response")
	}
	if resp.GetPayout().GetProjectId() != cfg.ProjectID {
		t.Fatalf("expected project id %d, got %d", cfg.ProjectID, resp.GetPayout().GetProjectId())
	}
	if resp.GetPayout().GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING {
		t.Fatalf("expected waiting status, got %v", resp.GetPayout().GetStatus())
	}
	if !resp.GetPayout().GetCreatedAt().AsTime().Equal(existingCreated) {
		t.Fatalf("expected created_at preserved, got %v", resp.GetPayout().GetCreatedAt().AsTime())
	}

	stored, ok := repo.payouts.Get(req.GetPayoutId())
	if !ok || stored == nil {
		t.Fatalf("expected payout state stored")
	}
	if stored.ProviderPaymentID == "" {
		t.Fatalf("expected provider payment id")
	}
	if got, want := stored.Comment, "invoice-2002"; got != want {
		t.Fatalf("expected stored comment %q, got %q", want, got)
	}
	if got, want := resp.GetPayout().GetComment(), "invoice-2002"; got != want {
		t.Fatalf("expected response comment %q, got %q", want, got)
	}
	if !stored.CreatedAt.Equal(existingCreated) {
		t.Fatalf("expected created_at preserved in model, got %v", stored.CreatedAt)
	}
}

func TestCardPayoutProcessor_Submit_MissingConfig(t *testing.T) {
	cfg := provider.Config{
		AllowedCurrencies: []string{"RUB"},
	}

	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		clockpkg.NewSystem(),
		newMockRepository(),
		&http.Client{},
		nil,
	)

	_, err := processor.Submit(context.Background(), validCardPayoutRequest())
	if err == nil {
		t.Fatalf("expected error")
	}
	if !errors.Is(err, merrors.ErrInternal) {
		t.Fatalf("expected internal error, got %v", err)
	}
}

func TestCardPayoutProcessor_Submit_RejectsAmountBelowConfiguredMinimum(t *testing.T) {
	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		AllowedCurrencies: []string{"RUB"},
	}

	repo := newMockRepository()
	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)},
		repo,
		&http.Client{},
		nil,
	)
	processor.applyGatewayDescriptor(&gatewayv1.GatewayInstanceDescriptor{
		Limits: &gatewayv1.Limits{
			PerTxMinAmount: "20.00",
		},
	})

	req := validCardPayoutRequest() // 15.00 RUB
	_, err := processor.Submit(context.Background(), req)
	requireReason(t, err, "amount_below_minimum")
}

func TestCardPayoutProcessor_SubmitToken_RejectsAmountBelowCurrencyMinimum(t *testing.T) {
	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		AllowedCurrencies: []string{"USD"},
	}

	repo := newMockRepository()
	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)},
		repo,
		&http.Client{},
		nil,
	)
	processor.applyGatewayDescriptor(&gatewayv1.GatewayInstanceDescriptor{
		Limits: &gatewayv1.Limits{
			PerTxMinAmount: "20.00",
			CurrencyLimits: map[string]*gatewayv1.LimitsOverride{
				"USD": {MinAmount: "30.00"},
			},
		},
	})

	req := validCardTokenPayoutRequest() // 25.00 USD
	_, err := processor.SubmitToken(context.Background(), req)
	requireReason(t, err, "amount_below_minimum")
}

func TestCardPayoutProcessor_ProcessCallback(t *testing.T) {
	cfg := provider.Config{
		SecretKey:         "secret",
		StatusSuccess:     "success",
		StatusProcessing:  "processing",
		AllowedCurrencies: []string{"RUB"},
	}

	repo := newMockRepository()

	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)},
		repo,
		&http.Client{},
		nil,
	)

	cb := baseCallback()
	cb.Payment.Sum.Currency = "RUB"

	sig, err := provider.SignPayload(cb, cfg.SecretKey)
	if err != nil {
		t.Fatalf("failed to sign callback: %v", err)
	}
	cb.Signature = sig

	payload, err := json.Marshal(cb)
	if err != nil {
		t.Fatalf("failed to marshal callback: %v", err)
	}

	status, err := processor.ProcessCallback(context.Background(), payload)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if status != http.StatusOK {
		t.Fatalf("expected status ok, got %d", status)
	}

	state, ok := repo.payouts.Get(cb.Payment.ID)
	if !ok || state == nil {
		t.Fatalf("expected payout state stored")
	}

	if state.Status != model.PayoutStatusSuccess {
		t.Fatalf("expected success status in model, got %v", state.Status)
	}
}

func TestCardPayoutProcessor_Submit_SameParentDifferentOperationsStoredSeparately(t *testing.T) {
	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		ProjectID:         99,
		AllowedCurrencies: []string{"RUB"},
	}

	repo := newMockRepository()
	var callN int
	httpClient := &http.Client{
		Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
			callN++
			resp := apiResponse{}
			resp.Operation.RequestID = fmt.Sprintf("req-%d", callN)
			body := mustMarshalJSON(t, resp)
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader(body)),
				Header:     http.Header{"Content-Type": []string{"application/json"}},
			}, nil
		}),
	}
	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 4, 1, 2, 3, 0, time.UTC)},
		repo,
		httpClient,
		nil,
	)

	parentPaymentRef := "payment-parent-1"
	op1 := parentPaymentRef + ":hop_4_card_payout_send"
	op2 := parentPaymentRef + ":hop_4_card_payout_send_2"

	req1 := validCardPayoutRequest()
	req1.PayoutId = ""
	req1.OperationRef = op1
	req1.IdempotencyKey = "idem-1"
	req1.ParentPaymentRef = parentPaymentRef
	req1.CardPan = scenarioPAN("approved_instant", 1)

	req2 := validCardPayoutRequest()
	req2.PayoutId = ""
	req2.OperationRef = op2
	req2.IdempotencyKey = "idem-2"
	req2.ParentPaymentRef = parentPaymentRef
	req2.CardPan = scenarioPAN("pending_issuer_review", 1)

	if _, err := processor.Submit(context.Background(), req1); err != nil {
		t.Fatalf("first submit failed: %v", err)
	}
	if _, err := processor.Submit(context.Background(), req2); err != nil {
		t.Fatalf("second submit failed: %v", err)
	}

	first, err := repo.payouts.FindByOperationRef(context.Background(), op1)
	if err != nil || first == nil {
		t.Fatalf("expected first operation stored, err=%v", err)
	}
	second, err := repo.payouts.FindByOperationRef(context.Background(), op2)
	if err != nil || second == nil {
		t.Fatalf("expected second operation stored, err=%v", err)
	}

	if got, want := first.PaymentRef, parentPaymentRef; got != want {
		t.Fatalf("first parent payment ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := second.PaymentRef, parentPaymentRef; got != want {
		t.Fatalf("second parent payment ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := first.OperationRef, op1; got != want {
		t.Fatalf("first operation ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := second.OperationRef, op2; got != want {
		t.Fatalf("second operation ref mismatch: got=%q want=%q", got, want)
	}
	if first.ProviderPaymentID == "" || second.ProviderPaymentID == "" {
		t.Fatalf("expected provider payment ids for both operations")
	}
	if first.ProviderPaymentID == second.ProviderPaymentID {
		t.Fatalf("expected different provider payment ids, got=%q", first.ProviderPaymentID)
	}
}

func TestCardPayoutProcessor_StrictMode_BlocksSecondOperationUntilFirstFinalCallback(t *testing.T) {
	t.Skip("aurora simulator has no external provider transport call counting")

	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		ProjectID:         99,
		StatusSuccess:     "success",
		StatusProcessing:  "processing",
		AllowedCurrencies: []string{"RUB"},
	}

	repo := newMockRepository()
	var callN atomic.Int32
	httpClient := &http.Client{
		Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
			n := callN.Add(1)
			resp := apiResponse{}
			resp.Operation.RequestID = fmt.Sprintf("req-%d", n)
			body := mustMarshalJSON(t, resp)
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader(body)),
				Header:     http.Header{"Content-Type": []string{"application/json"}},
			}, nil
		}),
	}

	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 4, 2, 3, 4, 0, time.UTC)},
		repo,
		httpClient,
		nil,
	)
	defer processor.stopRetries()
	processor.dispatchThrottleInterval = 0
	processor.setExecutionMode(newStrictIsolatedPayoutExecutionMode())

	req1 := validCardPayoutRequest()
	req1.PayoutId = ""
	req1.OperationRef = "op-strict-1"
	req1.ParentPaymentRef = "payment-strict-1"
	req1.IdempotencyKey = "idem-strict-1"
	req1.CardPan = scenarioPAN("approved_instant", 2)

	req2 := validCardPayoutRequest()
	req2.PayoutId = ""
	req2.OperationRef = "op-strict-2"
	req2.ParentPaymentRef = "payment-strict-2"
	req2.IdempotencyKey = "idem-strict-2"
	req2.CardPan = scenarioPAN("pending_issuer_review", 2)

	if _, err := processor.Submit(context.Background(), req1); err != nil {
		t.Fatalf("first submit failed: %v", err)
	}

	secondDone := make(chan error, 1)
	go func() {
		_, err := processor.Submit(context.Background(), req2)
		secondDone <- err
	}()

	select {
	case err := <-secondDone:
		t.Fatalf("second submit should block before first operation is final, err=%v", err)
	case <-time.After(120 * time.Millisecond):
	}

	cb := baseCallback()
	cb.Payment.ID = req1.GetOperationRef()
	cb.Payment.Status = "success"
	cb.Operation.Status = "success"
	cb.Operation.Code = "0"
	cb.Operation.Message = "Success"
	cb.Payment.Sum.Currency = "RUB"

	sig, err := provider.SignPayload(cb, cfg.SecretKey)
	if err != nil {
		t.Fatalf("failed to sign callback: %v", err)
	}
	cb.Signature = sig
	payload, err := json.Marshal(cb)
	if err != nil {
		t.Fatalf("failed to marshal callback: %v", err)
	}

	status, err := processor.ProcessCallback(context.Background(), payload)
	if err != nil {
		t.Fatalf("callback failed: %v", err)
	}
	if status != http.StatusOK {
		t.Fatalf("unexpected callback status: %d", status)
	}

	select {
	case err := <-secondDone:
		if err != nil {
			t.Fatalf("second submit returned error: %v", err)
		}
	case <-time.After(2 * time.Second):
		t.Fatalf("timeout waiting for second submit to unblock")
	}

	if got, want := callN.Load(), int32(2); got != want {
		t.Fatalf("unexpected provider call count: got=%d want=%d", got, want)
	}
}

func TestCardPayoutProcessor_ProcessCallback_UpdatesMatchingOperationWithinSameParent(t *testing.T) {
	cfg := provider.Config{
		SecretKey:         "secret",
		StatusSuccess:     "success",
		StatusProcessing:  "processing",
		AllowedCurrencies: []string{"RUB"},
	}

	parentPaymentRef := "payment-parent-1"
	op1 := parentPaymentRef + ":hop_4_card_payout_send"
	op2 := parentPaymentRef + ":hop_4_card_payout_send_2"
	now := time.Date(2026, 3, 4, 2, 3, 4, 0, time.UTC)

	repo := newMockRepository()
	repo.payouts.Save(&model.CardPayout{
		PaymentRef:   parentPaymentRef,
		OperationRef: op1,
		Status:       model.PayoutStatusWaiting,
		CreatedAt:    now.Add(-time.Minute),
		UpdatedAt:    now.Add(-time.Minute),
	})
	repo.payouts.Save(&model.CardPayout{
		PaymentRef:   parentPaymentRef,
		OperationRef: op2,
		Status:       model.PayoutStatusWaiting,
		CreatedAt:    now.Add(-time.Minute),
		UpdatedAt:    now.Add(-time.Minute),
	})

	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: now},
		repo,
		&http.Client{},
		nil,
	)

	cb := baseCallback()
	cb.Payment.ID = op2
	cb.Payment.Status = "success"
	cb.Operation.Status = "success"
	cb.Operation.Code = "0"
	cb.Operation.Provider.PaymentID = "provider-op-2"
	cb.Payment.Sum.Currency = "RUB"

	sig, err := provider.SignPayload(cb, cfg.SecretKey)
	if err != nil {
		t.Fatalf("failed to sign callback: %v", err)
	}
	cb.Signature = sig
	payload, err := json.Marshal(cb)
	if err != nil {
		t.Fatalf("failed to marshal callback: %v", err)
	}

	status, err := processor.ProcessCallback(context.Background(), payload)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if status != http.StatusOK {
		t.Fatalf("expected status ok, got %d", status)
	}

	first, err := repo.payouts.FindByOperationRef(context.Background(), op1)
	if err != nil || first == nil {
		t.Fatalf("expected first operation present, err=%v", err)
	}
	second, err := repo.payouts.FindByOperationRef(context.Background(), op2)
	if err != nil || second == nil {
		t.Fatalf("expected second operation present, err=%v", err)
	}

	if got, want := first.Status, model.PayoutStatusWaiting; got != want {
		t.Fatalf("first operation status mismatch: got=%v want=%v", got, want)
	}
	if got, want := second.Status, model.PayoutStatusSuccess; got != want {
		t.Fatalf("second operation status mismatch: got=%v want=%v", got, want)
	}
	if got, want := second.PaymentRef, parentPaymentRef; got != want {
		t.Fatalf("second parent payment ref mismatch: got=%q want=%q", got, want)
	}
}

func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineUntilSuccess(t *testing.T) {
	t.Skip("aurora simulator uses deterministic scenario dispatch instead of mocked provider HTTP")

	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		ProjectID:         99,
		AllowedCurrencies: []string{"RUB"},
	}

	repo := newMockRepository()
	var calls atomic.Int32
	httpClient := &http.Client{
		Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
			n := calls.Add(1)
			resp := apiResponse{}
			if n == 1 {
				resp.Code = providerCodeDeclineAmountOrFrequencyLimit
				resp.Message = "Decline due to amount or frequency limit"
				body := mustMarshalJSON(t, resp)
				return &http.Response{
					StatusCode: http.StatusTooManyRequests,
					Body:       io.NopCloser(bytes.NewReader(body)),
					Header:     http.Header{"Content-Type": []string{"application/json"}},
				}, nil
			}
			resp.Operation.RequestID = "req-retry-success"
			body := mustMarshalJSON(t, resp)
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader(body)),
				Header:     http.Header{"Content-Type": []string{"application/json"}},
			}, nil
		}),
	}

	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 4, 1, 2, 3, 0, time.UTC)},
		repo,
		httpClient,
		nil,
	)
	defer processor.stopRetries()
	processor.dispatchThrottleInterval = 0
	processor.retryDelayFn = func(uint32) time.Duration { return 10 * time.Millisecond }

	req := validCardPayoutRequest()
	resp, err := processor.Submit(context.Background(), req)
	if err != nil {
		t.Fatalf("submit returned error: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted response when retry is scheduled")
	}

	deadline := time.Now().Add(2 * time.Second)
	for {
		state, ok := repo.payouts.Get(req.GetPayoutId())
		if ok && state != nil && state.Status == model.PayoutStatusWaiting && state.ProviderPaymentID == "req-retry-success" {
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("timeout waiting for successful retry result")
		}
		time.Sleep(20 * time.Millisecond)
	}
	if got, want := calls.Load(), int32(2); got != want {
		t.Fatalf("unexpected provider call count: got=%d want=%d", got, want)
	}
}

func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineThenFails(t *testing.T) {
	t.Skip("aurora simulator uses deterministic scenario dispatch instead of mocked provider HTTP")

	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		ProjectID:         99,
		AllowedCurrencies: []string{"RUB"},
	}

	repo := newMockRepository()
	var calls atomic.Int32
	httpClient := &http.Client{
		Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
			_ = calls.Add(1)
			resp := apiResponse{
				Code:    providerCodeDeclineAmountOrFrequencyLimit,
				Message: "Decline due to amount or frequency limit",
			}
			body := mustMarshalJSON(t, resp)
			return &http.Response{
				StatusCode: http.StatusTooManyRequests,
				Body:       io.NopCloser(bytes.NewReader(body)),
				Header:     http.Header{"Content-Type": []string{"application/json"}},
			}, nil
		}),
	}

	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 4, 1, 2, 3, 0, time.UTC)},
		repo,
		httpClient,
		nil,
	)
	defer processor.stopRetries()
	processor.dispatchThrottleInterval = 0
	processor.retryDelayFn = func(uint32) time.Duration { return time.Millisecond }

	req := validCardPayoutRequest()
	resp, err := processor.Submit(context.Background(), req)
	if err != nil {
		t.Fatalf("submit returned error: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted response when retry is scheduled")
	}

	deadline := time.Now().Add(2 * time.Second)
	for {
		state, ok := repo.payouts.Get(req.GetPayoutId())
		if ok && state != nil && state.Status == model.PayoutStatusFailed {
			if !strings.Contains(state.FailureReason, providerCodeDeclineAmountOrFrequencyLimit) {
				t.Fatalf("expected failure reason to include provider code, got=%q", state.FailureReason)
			}
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("timeout waiting for terminal failed status")
		}
		time.Sleep(10 * time.Millisecond)
	}
	if got, want := calls.Load(), int32(defaultMaxDispatchAttempts); got != want {
		t.Fatalf("unexpected provider call count: got=%d want=%d", got, want)
	}
}

func TestCardPayoutProcessor_ProcessCallback_RetryableDeclineSchedulesRetry(t *testing.T) {
	t.Skip("aurora simulator does not run provider HTTP retry flow used by legacy transport tests")

	cfg := provider.Config{
		BaseURL:           "https://provider.test",
		SecretKey:         "secret",
		ProjectID:         99,
		StatusSuccess:     "success",
		StatusProcessing:  "processing",
		AllowedCurrencies: []string{"RUB"},
	}

	repo := newMockRepository()
	var calls atomic.Int32
	httpClient := &http.Client{
		Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
			n := calls.Add(1)
			resp := apiResponse{}
			if n == 1 {
				resp.Operation.RequestID = "req-initial"
			} else {
				resp.Operation.RequestID = "req-after-callback-retry"
			}
			body := mustMarshalJSON(t, resp)
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader(body)),
				Header:     http.Header{"Content-Type": []string{"application/json"}},
			}, nil
		}),
	}

	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 4, 2, 0, 0, 0, time.UTC)},
		repo,
		httpClient,
		nil,
	)
	defer processor.stopRetries()
	processor.dispatchThrottleInterval = 0
	processor.retryDelayFn = func(uint32) time.Duration { return 5 * time.Millisecond }

	req := validCardPayoutRequest()
	resp, err := processor.Submit(context.Background(), req)
	if err != nil {
		t.Fatalf("submit returned error: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted submit response")
	}

	cb := baseCallback()
	cb.Payment.ID = req.GetPayoutId()
	cb.Payment.Status = "failed"
	cb.Operation.Status = "failed"
	cb.Operation.Code = providerCodeDeclineAmountOrFrequencyLimit
	cb.Operation.Message = "Decline due to amount or frequency limit"
	cb.Payment.Sum.Currency = "RUB"

	sig, err := provider.SignPayload(cb, cfg.SecretKey)
	if err != nil {
		t.Fatalf("failed to sign callback: %v", err)
	}
	cb.Signature = sig
	payload, err := json.Marshal(cb)
	if err != nil {
		t.Fatalf("failed to marshal callback: %v", err)
	}

	status, err := processor.ProcessCallback(context.Background(), payload)
	if err != nil {
		t.Fatalf("process callback returned error: %v", err)
	}
	if status != http.StatusOK {
		t.Fatalf("unexpected callback status: %d", status)
	}

	deadline := time.Now().Add(2 * time.Second)
	for {
		state, ok := repo.payouts.Get(req.GetPayoutId())
		if ok && state != nil && state.Status == model.PayoutStatusWaiting && state.ProviderPaymentID == "req-after-callback-retry" {
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("timeout waiting for callback-scheduled retry result")
		}
		time.Sleep(10 * time.Millisecond)
	}

	if got, want := calls.Load(), int32(2); got != want {
		t.Fatalf("unexpected provider call count: got=%d want=%d", got, want)
	}
}
