/**
 * Tests for retry utilities
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { retry, sleep } from './retry';

describe('Retry', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('retry', () => {
    it('should succeed on first attempt', async () => {
      const fn = vi.fn().mockResolvedValue('success');
      const result = await retry(fn);
      
      expect(result).toBe('success');
      expect(fn).toHaveBeenCalledTimes(1);
    });

    it('should retry on failure and eventually succeed', async () => {
      let attempts = 0;
      const fn = vi.fn().mockImplementation(async () => {
        attempts++;
        if (attempts < 3) {
          throw new Error('Temporary failure');
        }
        return 'success';
      });

      const result = await retry(fn, { maxRetries: 3, initialDelay: 10 });
      
      expect(result).toBe('success');
      expect(fn).toHaveBeenCalledTimes(3);
    });

    it('should fail after max retries', async () => {
      const fn = vi.fn().mockRejectedValue(new Error('Permanent failure'));
      
      await expect(retry(fn, { maxRetries: 2, initialDelay: 10 })).rejects.toThrow('Permanent failure');
      expect(fn).toHaveBeenCalledTimes(3); // Initial + 2 retries
    });

    it('should call onRetry callback', async () => {
      const onRetry = vi.fn();
      let attempts = 0;
      const fn = vi.fn().mockImplementation(async () => {
        attempts++;
        if (attempts < 2) {
          throw new Error('Temporary failure');
        }
        return 'success';
      });

      await retry(fn, { maxRetries: 2, initialDelay: 10, onRetry });
      
      expect(onRetry).toHaveBeenCalledTimes(1);
      expect(onRetry).toHaveBeenCalledWith(expect.any(Error), 1);
    });

    it('should respect maxDelay', async () => {
      const start = Date.now();
      const fn = vi.fn().mockRejectedValue(new Error('Failure'));
      
      await expect(retry(fn, { maxRetries: 2, initialDelay: 1000, maxDelay: 100, factor: 2 })).rejects.toThrow();
      
      const duration = Date.now() - start;
      // Should be less than if maxDelay wasn't respected (would be 1000 + 2000 = 3000ms)
      expect(duration).toBeLessThan(2000);
    });

    it('should apply jitter', async () => {
      const delays: number[] = [];
      let attempts = 0;
      const fn = vi.fn().mockImplementation(async () => {
        attempts++;
        if (attempts < 2) {
          throw new Error('Failure');
        }
        return 'success';
      });

      const originalSetTimeout = global.setTimeout;
      global.setTimeout = vi.fn((callback: () => void, delay: number) => {
        delays.push(delay);
        return originalSetTimeout(callback, delay);
      }) as unknown as typeof setTimeout;

      await retry(fn, { maxRetries: 1, initialDelay: 100, jitter: true });
      
      global.setTimeout = originalSetTimeout;
      
      // With jitter, delay should be between 100 and 130 (100 + 30% jitter)
      expect(delays[0]).toBeGreaterThanOrEqual(100);
      expect(delays[0]).toBeLessThan(130);
    });
  });

  describe('sleep', () => {
    it('should sleep for specified duration', async () => {
      const start = Date.now();
      await sleep(100);
      const duration = Date.now() - start;
      
      expect(duration).toBeGreaterThanOrEqual(90); // Allow some tolerance
      expect(duration).toBeLessThan(150);
    });
  });
});

