/**
 * Unit tests for backoff policy
 */

import { 
  calculateBackoffDelay,
  createDefaultBackoffPolicy,
  createRateLimitBackoffPolicy,
  createNetworkErrorBackoffPolicy,
  createServerErrorBackoffPolicy
} from '../backoff';
import { BackoffPolicy } from '../types';

describe('Backoff Policy', () => {
  describe('calculateBackoffDelay', () => {
    const defaultPolicy: BackoffPolicy = {
      maxAttempts: 3,
      baseDelayMs: 1000,
      maxDelayMs: 30000,
      strategy: 'exponential',
      jitterEnabled: false, // Disable for predictable tests
      jitterFactor: 0.25,
      respectRetryAfter: true,
      retryAfterMaxMs: 300000
    };

    it('should calculate exponential backoff correctly', () => {
      const policy = { ...defaultPolicy, strategy: 'exponential' as const };
      
      expect(calculateBackoffDelay(1, policy)).toBe(1000); // 1s
      expect(calculateBackoffDelay(2, policy)).toBe(2000); // 2s
      expect(calculateBackoffDelay(3, policy)).toBe(4000); // 4s
      expect(calculateBackoffDelay(4, policy)).toBe(8000); // 8s
    });

    it('should calculate linear backoff correctly', () => {
      const policy = { ...defaultPolicy, strategy: 'linear' as const };
      
      expect(calculateBackoffDelay(1, policy)).toBe(1000); // 1s
      expect(calculateBackoffDelay(2, policy)).toBe(2000); // 2s
      expect(calculateBackoffDelay(3, policy)).toBe(3000); // 3s
      expect(calculateBackoffDelay(4, policy)).toBe(4000); // 4s
    });

    it('should calculate fixed backoff correctly', () => {
      const policy = { ...defaultPolicy, strategy: 'fixed' as const };
      
      expect(calculateBackoffDelay(1, policy)).toBe(1000); // 1s
      expect(calculateBackoffDelay(2, policy)).toBe(1000); // 1s
      expect(calculateBackoffDelay(3, policy)).toBe(1000); // 1s
      expect(calculateBackoffDelay(4, policy)).toBe(1000); // 1s
    });

    it('should respect Retry-After header', () => {
      const policy = { ...defaultPolicy, respectRetryAfter: true };
      const retryAfterMs = 5000; // 5 seconds
      
      expect(calculateBackoffDelay(1, policy, retryAfterMs)).toBe(5000);
      expect(calculateBackoffDelay(2, policy, retryAfterMs)).toBe(5000);
      expect(calculateBackoffDelay(3, policy, retryAfterMs)).toBe(5000);
    });

    it('should cap Retry-After at maxDelayMs', () => {
      const policy = { ...defaultPolicy, maxDelayMs: 10000 };
      const retryAfterMs = 60000; // 60 seconds (exceeds max)
      
      expect(calculateBackoffDelay(1, policy, retryAfterMs)).toBe(10000);
    });

    it('should cap Retry-After at retryAfterMaxMs', () => {
      const policy = { ...defaultPolicy, retryAfterMaxMs: 15000 };
      const retryAfterMs = 30000; // 30 seconds (exceeds retryAfterMaxMs)
      
      expect(calculateBackoffDelay(1, policy, retryAfterMs)).toBe(15000);
    });

    it('should apply jitter when enabled', () => {
      const policy = { ...defaultPolicy, jitterEnabled: true, jitterFactor: 0.5 };
      
      // Run multiple times to test jitter range
      const delays = Array.from({ length: 100 }, () => 
        calculateBackoffDelay(1, policy)
      );
      
      // All delays should be within jitter range (500ms to 1500ms)
      delays.forEach(delay => {
        expect(delay).toBeGreaterThanOrEqual(500);
        expect(delay).toBeLessThanOrEqual(1500);
      });
      
      // Should have some variation due to jitter
      const uniqueDelays = new Set(delays);
      expect(uniqueDelays.size).toBeGreaterThan(1);
    });

    it('should cap delay at maxDelayMs', () => {
      const policy = { ...defaultPolicy, maxDelayMs: 5000 };
      
      expect(calculateBackoffDelay(10, policy)).toBe(5000); // Capped at 5s
    });

    it('should handle zero jitter factor', () => {
      const policy = { ...defaultPolicy, jitterEnabled: true, jitterFactor: 0 };
      
      expect(calculateBackoffDelay(1, policy)).toBe(1000); // No jitter applied
    });
  });

  describe('Policy Creators', () => {
    it('should create default backoff policy', () => {
      const policy = createDefaultBackoffPolicy();
      
      expect(policy.maxAttempts).toBe(3);
      expect(policy.baseDelayMs).toBe(1000);
      expect(policy.maxDelayMs).toBe(30000);
      expect(policy.strategy).toBe('exponential');
      expect(policy.jitterEnabled).toBe(true);
      expect(policy.jitterFactor).toBe(0.25);
      expect(policy.respectRetryAfter).toBe(true);
    });

    it('should create rate limit backoff policy', () => {
      const retryAfterMs = 60000; // 1 minute
      const policy = createRateLimitBackoffPolicy(retryAfterMs);
      
      expect(policy.maxAttempts).toBe(5);
      expect(policy.baseDelayMs).toBe(60000);
      expect(policy.strategy).toBe('fixed');
      expect(policy.jitterEnabled).toBe(true);
      expect(policy.jitterFactor).toBe(0.1);
      expect(policy.respectRetryAfter).toBe(true);
    });

    it('should create network error backoff policy', () => {
      const policy = createNetworkErrorBackoffPolicy();
      
      expect(policy.maxAttempts).toBe(3);
      expect(policy.baseDelayMs).toBe(1000);
      expect(policy.strategy).toBe('exponential');
      expect(policy.jitterEnabled).toBe(true);
      expect(policy.respectRetryAfter).toBe(false);
    });

    it('should create server error backoff policy', () => {
      const policy = createServerErrorBackoffPolicy();
      
      expect(policy.maxAttempts).toBe(3);
      expect(policy.baseDelayMs).toBe(2000);
      expect(policy.strategy).toBe('exponential');
      expect(policy.jitterEnabled).toBe(true);
      expect(policy.jitterFactor).toBe(0.5);
      expect(policy.respectRetryAfter).toBe(false);
    });
  });

  describe('429 + Retry-After Branch', () => {
    it('should handle 429 with Retry-After header', () => {
      const policy = createRateLimitBackoffPolicy(30000); // 30 seconds
      policy.jitterEnabled = false; // Disable jitter for predictable tests
      
      // First attempt should use Retry-After
      expect(calculateBackoffDelay(1, policy, 30000)).toBe(30000);
      
      // Subsequent attempts should also use Retry-After
      expect(calculateBackoffDelay(2, policy, 30000)).toBe(30000);
      expect(calculateBackoffDelay(3, policy, 30000)).toBe(30000);
    });

    it('should handle 429 without Retry-After header', () => {
      const policy = createRateLimitBackoffPolicy(30000);
      policy.jitterEnabled = false; // Disable jitter for predictable tests
      
      // Should fall back to fixed strategy
      expect(calculateBackoffDelay(1, policy)).toBe(30000);
      expect(calculateBackoffDelay(2, policy)).toBe(30000);
    });

    it('should cap 429 Retry-After at reasonable limits', () => {
      const policy = createRateLimitBackoffPolicy(300000); // 5 minutes
      policy.jitterEnabled = false; // Disable jitter for predictable tests
      
      // Should be capped at retryAfterMaxMs (600000ms = 10 minutes)
      expect(calculateBackoffDelay(1, policy, 300000)).toBe(300000);
      expect(calculateBackoffDelay(1, policy, 900000)).toBe(600000); // Capped
    });
  });

  describe('Jitter Bounds', () => {
    it('should respect jitter bounds for exponential backoff', () => {
      const policy: BackoffPolicy = {
        maxAttempts: 3,
        baseDelayMs: 1000,
        maxDelayMs: 30000,
        strategy: 'exponential',
        jitterEnabled: true,
        jitterFactor: 0.25,
        respectRetryAfter: false
      };
      
      // Test multiple attempts to ensure jitter is within bounds
      for (let attempt = 1; attempt <= 5; attempt++) {
        const baseDelay = 1000 * Math.pow(2, attempt - 1);
        const minDelay = baseDelay * (1 - 0.25);
        const maxDelay = baseDelay * (1 + 0.25);
        
        const delays = Array.from({ length: 50 }, () => 
          calculateBackoffDelay(attempt, policy)
        );
        
        delays.forEach(delay => {
          expect(delay).toBeGreaterThanOrEqual(minDelay);
          expect(delay).toBeLessThanOrEqual(maxDelay);
        });
      }
    });

    it('should not apply negative jitter', () => {
      const policy: BackoffPolicy = {
        maxAttempts: 3,
        baseDelayMs: 1000,
        maxDelayMs: 30000,
        strategy: 'fixed',
        jitterEnabled: true,
        jitterFactor: 1.0, // 100% jitter
        respectRetryAfter: false
      };
      
      const delays = Array.from({ length: 100 }, () => 
        calculateBackoffDelay(1, policy)
      );
      
      // All delays should be non-negative
      delays.forEach(delay => {
        expect(delay).toBeGreaterThanOrEqual(0);
      });
    });
  });
});
