package gateway

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

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

func TestAuroraCardPayoutScenarios(t *testing.T) {
	cfg := provider.Config{
		ProjectID:         1001,
		AllowedCurrencies: []string{"RUB"},
	}
	now := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)
	repo := newMockRepository()
	processor := newCardPayoutProcessor(zap.NewNop(), cfg, staticClock{now: now}, repo, nil, nil)
	processor.dispatchThrottleInterval = 0

	tests := []struct {
		name              string
		pan               string
		wantAccepted      bool
		wantStatus        mntxv1.PayoutStatus
		wantErrorCode     string
		wantProviderCode  string
		wantProviderMatch string
	}{
		{
			name:              "approved_instant",
			pan:               scenarioPAN("approved_instant", 0),
			wantAccepted:      true,
			wantStatus:        mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS,
			wantErrorCode:     "00",
			wantProviderCode:  "00",
			wantProviderMatch: "Approved",
		},
		{
			name:              "pending_issuer_review",
			pan:               scenarioPAN("pending_issuer_review", 0),
			wantAccepted:      true,
			wantStatus:        mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
			wantErrorCode:     "P01",
			wantProviderCode:  "P01",
			wantProviderMatch: "Pending issuer review",
		},
		{
			name:              "insufficient_funds",
			pan:               scenarioPAN("insufficient_funds", 0),
			wantAccepted:      false,
			wantStatus:        mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
			wantErrorCode:     "51",
			wantProviderCode:  "51",
			wantProviderMatch: "Insufficient funds",
		},
		{
			name:              "unknown_card_default_queue",
			pan:               defaultScenarioPAN(),
			wantAccepted:      true,
			wantStatus:        mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
			wantErrorCode:     "P00",
			wantProviderCode:  "P00",
			wantProviderMatch: "Queued for provider processing",
		},
		{
			name:              "provider_maintenance",
			pan:               scenarioPAN("provider_maintenance", 0),
			wantAccepted:      false,
			wantStatus:        mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
			wantErrorCode:     "91",
			wantProviderCode:  "91",
			wantProviderMatch: "inoperative",
		},
		{
			name:              "provider_system_malfunction",
			pan:               scenarioPAN("provider_system_malfunction", 0),
			wantAccepted:      false,
			wantStatus:        mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
			wantErrorCode:     "96",
			wantProviderCode:  "96",
			wantProviderMatch: "System malfunction",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			req := validCardPayoutRequest()
			req.PayoutId = ""
			req.OperationRef = "op-" + tc.name
			req.ParentPaymentRef = "parent-" + tc.name
			req.IdempotencyKey = "idem-" + tc.name
			req.CardPan = tc.pan

			resp, err := processor.Submit(context.Background(), req)
			if err != nil {
				t.Fatalf("submit failed: %v", err)
			}
			if got, want := resp.GetAccepted(), tc.wantAccepted; got != want {
				t.Fatalf("accepted mismatch: got=%v want=%v", got, want)
			}
			if got, want := resp.GetPayout().GetStatus(), tc.wantStatus; got != want {
				t.Fatalf("status mismatch: got=%v want=%v", got, want)
			}
			if got, want := strings.TrimSpace(resp.GetErrorCode()), tc.wantErrorCode; got != want {
				t.Fatalf("error_code mismatch: got=%q want=%q", got, want)
			}

			state, ok := repo.payouts.Get(req.GetOperationRef())
			if !ok || state == nil {
				t.Fatalf("expected persisted payout state")
			}
			if got, want := strings.TrimSpace(state.ProviderCode), tc.wantProviderCode; got != want {
				t.Fatalf("provider_code mismatch: got=%q want=%q", got, want)
			}
			if tc.wantProviderMatch != "" && !strings.Contains(state.ProviderMessage, tc.wantProviderMatch) {
				t.Fatalf("provider_message mismatch: got=%q expected to contain %q", state.ProviderMessage, tc.wantProviderMatch)
			}
		})
	}
}

func TestAuroraTransportFailureScenarioEventuallyFails(t *testing.T) {
	cfg := provider.Config{
		ProjectID:         1001,
		AllowedCurrencies: []string{"RUB"},
	}
	repo := newMockRepository()
	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)},
		repo,
		nil,
		nil,
	)
	defer processor.stopRetries()
	processor.dispatchThrottleInterval = 0
	processor.dispatchMaxAttempts = 2
	processor.retryDelayFn = func(uint32) time.Duration { return time.Millisecond }

	req := validCardPayoutRequest()
	req.PayoutId = ""
	req.OperationRef = "op-transport-timeout"
	req.ParentPaymentRef = "parent-transport-timeout"
	req.IdempotencyKey = "idem-transport-timeout"
	req.CardPan = scenarioPAN("provider_timeout_transport", 0)

	resp, err := processor.Submit(context.Background(), req)
	if err != nil {
		t.Fatalf("submit failed: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted response while transport retry is scheduled")
	}
	if got, want := resp.GetPayout().GetStatus(), mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING; got != want {
		t.Fatalf("initial status mismatch: got=%v want=%v", got, want)
	}

	deadline := time.Now().Add(2 * time.Second)
	for {
		state, ok := repo.payouts.Get(req.GetOperationRef())
		if ok && state != nil && state.Status == model.PayoutStatusFailed {
			if !strings.Contains(strings.ToLower(state.FailureReason), "transport error") {
				t.Fatalf("expected transport failure reason, got=%q", state.FailureReason)
			}
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("timeout waiting for transport failure terminal state")
		}
		time.Sleep(10 * time.Millisecond)
	}
}

func TestAuroraRetryableScenarioEventuallyFails(t *testing.T) {
	cfg := provider.Config{
		ProjectID:         1001,
		AllowedCurrencies: []string{"RUB"},
	}
	repo := newMockRepository()
	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)},
		repo,
		nil,
		nil,
	)
	defer processor.stopRetries()
	processor.dispatchThrottleInterval = 0
	processor.dispatchMaxAttempts = 2
	processor.retryDelayFn = func(uint32) time.Duration { return time.Millisecond }

	req := validCardPayoutRequest()
	req.PayoutId = ""
	req.OperationRef = "op-retryable-issuer-unavailable"
	req.ParentPaymentRef = "parent-retryable-issuer-unavailable"
	req.IdempotencyKey = "idem-retryable-issuer-unavailable"
	req.CardPan = scenarioPAN("issuer_unavailable_retryable", 0)

	resp, err := processor.Submit(context.Background(), req)
	if err != nil {
		t.Fatalf("submit failed: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted response while retry is scheduled")
	}
	if got, want := resp.GetPayout().GetStatus(), mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING; got != want {
		t.Fatalf("initial status mismatch: got=%v want=%v", got, want)
	}

	deadline := time.Now().Add(2 * time.Second)
	for {
		state, ok := repo.payouts.Get(req.GetOperationRef())
		if ok && state != nil && state.Status == model.PayoutStatusFailed {
			if !strings.Contains(state.FailureReason, "10101") {
				t.Fatalf("expected retryable provider code in failure_reason, got=%q", state.FailureReason)
			}
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("timeout waiting for retryable scenario terminal failure")
		}
		time.Sleep(10 * time.Millisecond)
	}
}

func TestAuroraTokenPayoutUsesTokenizedPANScenario(t *testing.T) {
	cfg := provider.Config{
		ProjectID:         1001,
		AllowedCurrencies: []string{"RUB", "USD"},
	}
	repo := newMockRepository()
	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)},
		repo,
		nil,
		nil,
	)
	processor.dispatchThrottleInterval = 0

	tokenizeReq := validCardTokenizeRequest()
	tokenizeReq.RequestId = "tok-req-insufficient"
	tokenizeReq.CardPan = scenarioPAN("insufficient_funds", 0)

	tokenizeResp, err := processor.Tokenize(context.Background(), tokenizeReq)
	if err != nil {
		t.Fatalf("tokenize failed: %v", err)
	}
	if tokenizeResp.GetToken() == "" {
		t.Fatalf("expected non-empty token")
	}

	payoutReq := validCardTokenPayoutRequest()
	payoutReq.PayoutId = ""
	payoutReq.OperationRef = "op-token-insufficient"
	payoutReq.ParentPaymentRef = "parent-token-insufficient"
	payoutReq.IdempotencyKey = "idem-token-insufficient"
	payoutReq.CardToken = tokenizeResp.GetToken()
	payoutReq.MaskedPan = tokenizeResp.GetMaskedPan()

	resp, err := processor.SubmitToken(context.Background(), payoutReq)
	if err != nil {
		t.Fatalf("submit token payout failed: %v", err)
	}
	if resp.GetAccepted() {
		t.Fatalf("expected declined payout for insufficient funds token scenario")
	}
	if got, want := resp.GetErrorCode(), "51"; got != want {
		t.Fatalf("error_code mismatch: got=%q want=%q", got, want)
	}
	if got, want := resp.GetPayout().GetStatus(), mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED; got != want {
		t.Fatalf("status mismatch: got=%v want=%v", got, want)
	}
}

func TestAuroraTokenPayoutFallsBackToMaskedPANScenario(t *testing.T) {
	cfg := provider.Config{
		ProjectID:         1001,
		AllowedCurrencies: []string{"RUB", "USD"},
	}
	repo := newMockRepository()
	processor := newCardPayoutProcessor(
		zap.NewNop(),
		cfg,
		staticClock{now: time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)},
		repo,
		nil,
		nil,
	)
	processor.dispatchThrottleInterval = 0

	req := validCardTokenPayoutRequest()
	req.PayoutId = ""
	req.OperationRef = "op-token-masked-fallback"
	req.ParentPaymentRef = "parent-token-masked-fallback"
	req.IdempotencyKey = "idem-token-masked-fallback"
	req.CardToken = "unknown-token"
	req.MaskedPan = "220000******6666"

	resp, err := processor.SubmitToken(context.Background(), req)
	if err != nil {
		t.Fatalf("submit token payout failed: %v", err)
	}
	if resp.GetAccepted() {
		t.Fatalf("expected declined payout for masked-pan fallback scenario")
	}
	if got, want := resp.GetErrorCode(), "05"; got != want {
		t.Fatalf("error_code mismatch: got=%q want=%q", got, want)
	}
	if got, want := resp.GetPayout().GetStatus(), mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED; got != want {
		t.Fatalf("status mismatch: got=%v want=%v", got, want)
	}
}
