package retry

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"
	"time"

	"pgregory.net/rapid"
)

// TestExponentialBackoffRetryMechanism tests Property 9: 指数退避重试机制
// **Validates: Requirements 5.4, 8.1**
//
// Property 9: 指数退避重试机制
// *对于任意* 失败的 API 调用，重试间隔应按指数增长，且不超过最大重试次数。
func TestExponentialBackoffRetryMechanism(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate valid retry configuration
		maxRetries := rapid.IntRange(1, 5).Draw(t, "maxRetries")
		initialBackoffMs := rapid.IntRange(1, 50).Draw(t, "initialBackoffMs")
		maxBackoffMs := rapid.IntRange(initialBackoffMs*2, initialBackoffMs*100).Draw(t, "maxBackoffMs")
		multiplier := rapid.Float64Range(1.5, 3.0).Draw(t, "multiplier")

		cfg := Config{
			MaxRetries:     maxRetries,
			InitialBackoff: time.Duration(initialBackoffMs) * time.Millisecond,
			MaxBackoff:     time.Duration(maxBackoffMs) * time.Millisecond,
			Multiplier:     multiplier,
			Jitter:         false, // Disable jitter for deterministic testing
		}

		// Track actual backoff intervals
		var backoffIntervals []time.Duration
		var lastCallTime time.Time
		var callCount int32

		ctx := context.Background()
		_ = Do(ctx, cfg, func() error {
			now := time.Now()
			if !lastCallTime.IsZero() {
				backoffIntervals = append(backoffIntervals, now.Sub(lastCallTime))
			}
			lastCallTime = now
			atomic.AddInt32(&callCount, 1)
			return NewRetryableError(errors.New("always fails"))
		})

		// Verify: Total attempts should be MaxRetries + 1 (initial attempt + retries)
		expectedAttempts := maxRetries + 1
		if int(callCount) != expectedAttempts {
			t.Fatalf("Expected %d attempts, got %d", expectedAttempts, callCount)
		}

		// Verify: Number of backoff intervals should equal MaxRetries
		if len(backoffIntervals) != maxRetries {
			t.Fatalf("Expected %d backoff intervals, got %d", maxRetries, len(backoffIntervals))
		}

		// Verify: Backoff intervals should grow exponentially (with tolerance)
		expectedBackoff := cfg.InitialBackoff
		tolerance := 10 * time.Millisecond // Allow some timing tolerance

		for i, actualBackoff := range backoffIntervals {
			// Verify backoff doesn't exceed MaxBackoff
			if actualBackoff > cfg.MaxBackoff+tolerance {
				t.Fatalf("Backoff interval %d (%v) exceeded MaxBackoff (%v)", i, actualBackoff, cfg.MaxBackoff)
			}

			// Verify backoff is approximately as expected (within tolerance)
			expectedCapped := expectedBackoff
			if expectedCapped > cfg.MaxBackoff {
				expectedCapped = cfg.MaxBackoff
			}

			// Allow for timing variations
			minExpected := expectedCapped - tolerance
			maxExpected := expectedCapped + tolerance
			if minExpected < 0 {
				minExpected = 0
			}

			if actualBackoff < minExpected || actualBackoff > maxExpected {
				t.Logf("Warning: Backoff interval %d was %v, expected around %v (tolerance: %v)", i, actualBackoff, expectedCapped, tolerance)
			}

			// Calculate next expected backoff
			expectedBackoff = time.Duration(float64(expectedBackoff) * cfg.Multiplier)
		}
	})
}

// TestRetryIntervalsDoNotExceedMaxBackoff tests that retry intervals are capped at MaxBackoff
// **Validates: Requirements 5.4, 8.1**
func TestRetryIntervalsDoNotExceedMaxBackoff(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate configuration where exponential growth would exceed MaxBackoff
		maxRetries := rapid.IntRange(3, 8).Draw(t, "maxRetries")
		initialBackoffMs := rapid.IntRange(10, 50).Draw(t, "initialBackoffMs")
		// Set MaxBackoff to be less than what exponential growth would produce
		maxBackoffMs := rapid.IntRange(initialBackoffMs, initialBackoffMs*4).Draw(t, "maxBackoffMs")
		multiplier := rapid.Float64Range(2.0, 3.0).Draw(t, "multiplier")

		cfg := Config{
			MaxRetries:     maxRetries,
			InitialBackoff: time.Duration(initialBackoffMs) * time.Millisecond,
			MaxBackoff:     time.Duration(maxBackoffMs) * time.Millisecond,
			Multiplier:     multiplier,
			Jitter:         false,
		}

		var backoffIntervals []time.Duration
		var lastCallTime time.Time

		ctx := context.Background()
		_ = Do(ctx, cfg, func() error {
			now := time.Now()
			if !lastCallTime.IsZero() {
				backoffIntervals = append(backoffIntervals, now.Sub(lastCallTime))
			}
			lastCallTime = now
			return NewRetryableError(errors.New("always fails"))
		})

		// Verify: All backoff intervals should not exceed MaxBackoff (with tolerance)
		tolerance := 15 * time.Millisecond
		for i, interval := range backoffIntervals {
			if interval > cfg.MaxBackoff+tolerance {
				t.Fatalf("Backoff interval %d (%v) exceeded MaxBackoff (%v) + tolerance (%v)",
					i, interval, cfg.MaxBackoff, tolerance)
			}
		}
	})
}

// TestTotalRetriesDoNotExceedMaxRetries tests that total retries don't exceed MaxRetries
// **Validates: Requirements 5.4, 8.1**
func TestTotalRetriesDoNotExceedMaxRetries(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		maxRetries := rapid.IntRange(0, 10).Draw(t, "maxRetries")

		cfg := Config{
			MaxRetries:     maxRetries,
			InitialBackoff: 1 * time.Millisecond,
			MaxBackoff:     10 * time.Millisecond,
			Multiplier:     2.0,
			Jitter:         false,
		}

		var callCount int32

		ctx := context.Background()
		err := Do(ctx, cfg, func() error {
			atomic.AddInt32(&callCount, 1)
			return NewRetryableError(errors.New("always fails"))
		})

		// Verify: Should return MaxRetriesExceededError
		if !IsMaxRetriesExceeded(err) {
			t.Fatalf("Expected MaxRetriesExceededError, got %v", err)
		}

		// Verify: Total calls should be exactly MaxRetries + 1
		expectedCalls := maxRetries + 1
		if int(callCount) != expectedCalls {
			t.Fatalf("Expected %d calls (1 initial + %d retries), got %d",
				expectedCalls, maxRetries, callCount)
		}
	})
}

// TestNonRetryableErrorsAreNotRetried tests that non-retryable errors stop retry immediately
// **Validates: Requirements 5.4, 8.1**
func TestNonRetryableErrorsAreNotRetried(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		maxRetries := rapid.IntRange(1, 10).Draw(t, "maxRetries")
		// Generate a random number of successful calls before the non-retryable error
		failAtCall := rapid.IntRange(1, maxRetries+1).Draw(t, "failAtCall")

		cfg := Config{
			MaxRetries:     maxRetries,
			InitialBackoff: 1 * time.Millisecond,
			MaxBackoff:     10 * time.Millisecond,
			Multiplier:     2.0,
			Jitter:         false,
		}

		var callCount int32
		nonRetryableErr := errors.New("non-retryable error")

		ctx := context.Background()
		err := Do(ctx, cfg, func() error {
			count := atomic.AddInt32(&callCount, 1)
			if int(count) < failAtCall {
				return NewRetryableError(errors.New("retryable error"))
			}
			return nonRetryableErr // Non-retryable error
		})

		// Verify: Should return the non-retryable error
		if !errors.Is(err, nonRetryableErr) {
			t.Fatalf("Expected non-retryable error, got %v", err)
		}

		// Verify: Should have stopped at the call that returned non-retryable error
		if int(callCount) != failAtCall {
			t.Fatalf("Expected %d calls before stopping, got %d", failAtCall, callCount)
		}
	})
}

// TestDoWithResultExponentialBackoff tests DoWithResult follows exponential backoff
// **Validates: Requirements 5.4, 8.1**
func TestDoWithResultExponentialBackoff(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		maxRetries := rapid.IntRange(1, 5).Draw(t, "maxRetries")
		initialBackoffMs := rapid.IntRange(1, 30).Draw(t, "initialBackoffMs")
		maxBackoffMs := rapid.IntRange(initialBackoffMs*2, initialBackoffMs*50).Draw(t, "maxBackoffMs")
		multiplier := rapid.Float64Range(1.5, 3.0).Draw(t, "multiplier")

		cfg := Config{
			MaxRetries:     maxRetries,
			InitialBackoff: time.Duration(initialBackoffMs) * time.Millisecond,
			MaxBackoff:     time.Duration(maxBackoffMs) * time.Millisecond,
			Multiplier:     multiplier,
			Jitter:         false,
		}

		var callCount int32

		ctx := context.Background()
		result, err := DoWithResult(ctx, cfg, func() (int, error) {
			atomic.AddInt32(&callCount, 1)
			return 0, NewRetryableError(errors.New("always fails"))
		})

		// Verify: Should return MaxRetriesExceededError
		if !IsMaxRetriesExceeded(err) {
			t.Fatalf("Expected MaxRetriesExceededError, got %v", err)
		}

		// Verify: Result should be zero value
		if result != 0 {
			t.Fatalf("Expected zero result, got %d", result)
		}

		// Verify: Total calls should be exactly MaxRetries + 1
		expectedCalls := maxRetries + 1
		if int(callCount) != expectedCalls {
			t.Fatalf("Expected %d calls, got %d", expectedCalls, callCount)
		}
	})
}

// TestJitterProducesVariableBackoff tests that jitter produces variable backoff times
// **Validates: Requirements 5.4, 8.1**
func TestJitterProducesVariableBackoff(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		baseBackoffMs := rapid.IntRange(50, 200).Draw(t, "baseBackoffMs")
		baseBackoff := time.Duration(baseBackoffMs) * time.Millisecond

		// Run multiple times to collect jittered values
		var results []time.Duration
		for i := 0; i < 20; i++ {
			result := calculateBackoff(baseBackoff, true)
			results = append(results, result)

			// Verify: Jittered backoff should be within [0.5x, 1.5x) of base
			minExpected := time.Duration(float64(baseBackoff) * 0.5)
			maxExpected := time.Duration(float64(baseBackoff) * 1.5)

			if result < minExpected || result >= maxExpected {
				t.Fatalf("Jittered backoff %v is outside expected range [%v, %v)",
					result, minExpected, maxExpected)
			}
		}

		// Verify: With 20 samples, we should have some variation
		uniqueValues := make(map[time.Duration]bool)
		for _, r := range results {
			uniqueValues[r] = true
		}

		// We expect at least 2 different values with jitter enabled
		if len(uniqueValues) < 2 {
			t.Logf("Warning: Jitter produced only %d unique values out of 20 samples", len(uniqueValues))
		}
	})
}

// TestNoJitterProducesConstantBackoff tests that without jitter, backoff is constant
// **Validates: Requirements 5.4, 8.1**
func TestNoJitterProducesConstantBackoff(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		baseBackoffMs := rapid.IntRange(10, 500).Draw(t, "baseBackoffMs")
		baseBackoff := time.Duration(baseBackoffMs) * time.Millisecond

		// Run multiple times - all should be identical
		for i := 0; i < 10; i++ {
			result := calculateBackoff(baseBackoff, false)
			if result != baseBackoff {
				t.Fatalf("Without jitter, expected %v but got %v", baseBackoff, result)
			}
		}
	})
}

// TestContextCancellationStopsRetry tests that context cancellation stops retry
// **Validates: Requirements 5.4, 8.1**
func TestContextCancellationStopsRetry(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		maxRetries := rapid.IntRange(3, 10).Draw(t, "maxRetries")
		cancelAfterCalls := rapid.IntRange(1, maxRetries).Draw(t, "cancelAfterCalls")

		cfg := Config{
			MaxRetries:     maxRetries,
			InitialBackoff: 50 * time.Millisecond,
			MaxBackoff:     200 * time.Millisecond,
			Multiplier:     2.0,
			Jitter:         false,
		}

		ctx, cancel := context.WithCancel(context.Background())
		var callCount int32

		go func() {
			// Wait for some calls to happen, then cancel
			for atomic.LoadInt32(&callCount) < int32(cancelAfterCalls) {
				time.Sleep(1 * time.Millisecond)
			}
			// Small delay to ensure we're in the backoff wait
			time.Sleep(10 * time.Millisecond)
			cancel()
		}()

		err := Do(ctx, cfg, func() error {
			atomic.AddInt32(&callCount, 1)
			return NewRetryableError(errors.New("always fails"))
		})

		// Verify: Should return context error
		if !errors.Is(err, context.Canceled) {
			t.Fatalf("Expected context.Canceled error, got %v", err)
		}

		// Verify: Should have stopped before all retries completed
		if int(callCount) > maxRetries+1 {
			t.Fatalf("Expected at most %d calls, got %d", maxRetries+1, callCount)
		}
	})
}

// TestSuccessfulCallDoesNotRetry tests that successful calls don't trigger retries
// **Validates: Requirements 5.4, 8.1**
func TestSuccessfulCallDoesNotRetry(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		maxRetries := rapid.IntRange(1, 10).Draw(t, "maxRetries")
		successAfterCalls := rapid.IntRange(1, maxRetries+1).Draw(t, "successAfterCalls")

		cfg := Config{
			MaxRetries:     maxRetries,
			InitialBackoff: 1 * time.Millisecond,
			MaxBackoff:     10 * time.Millisecond,
			Multiplier:     2.0,
			Jitter:         false,
		}

		var callCount int32

		ctx := context.Background()
		err := Do(ctx, cfg, func() error {
			count := atomic.AddInt32(&callCount, 1)
			if int(count) < successAfterCalls {
				return NewRetryableError(errors.New("temporary failure"))
			}
			return nil // Success
		})

		// Verify: Should succeed without error
		if err != nil {
			t.Fatalf("Expected success, got error: %v", err)
		}

		// Verify: Should have called exactly successAfterCalls times
		if int(callCount) != successAfterCalls {
			t.Fatalf("Expected %d calls, got %d", successAfterCalls, callCount)
		}
	})
}
