import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { promiseWithRetry, RetryError } from './promiseWithRetry';
describe('promiseWithRetry', () => {
    beforeEach(() => {
        vi.useFakeTimers();
    });
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    it('should return the result if the function succeeds on the first attempt', async () => {
        const fn = vi.fn().mockResolvedValue('success');
        const resultPromise = promiseWithRetry(fn, 3, 100);
        const result = await resultPromise;
        expect(result).toBe('success');
        expect(fn).toHaveBeenCalledTimes(1);
    });
    it('should retry if the function throws a RetryError', async () => {
        const fn = vi
            .fn()
            .mockRejectedValueOnce(new RetryError('retry', 'cause1'))
            .mockRejectedValueOnce(new RetryError('retry', 'cause2'))
            .mockResolvedValue('success');
        const resultPromise = promiseWithRetry(fn, 3, 100);
        await vi.runAllTimersAsync();
        const result = await resultPromise;
        expect(result).toBe('success');
        expect(fn).toHaveBeenCalledTimes(3);
    });
    it('should throw the RetryError if all retries are exhausted', async () => {
        const lastError = new RetryError('retry', 'final cause');
        const fn = vi
            .fn()
            .mockRejectedValueOnce(new RetryError('retry', 'cause1'))
            .mockRejectedValueOnce(new RetryError('retry', 'cause2'))
            .mockRejectedValue(lastError);
        const resultPromise = promiseWithRetry(fn, 2, 100);
        // oxlint-disable-next-line vitest/valid-expect -- We need to capture the promise rejection before advancing timers
        const assertionPromise = expect(resultPromise).rejects.toThrow(lastError);
        await vi.runAllTimersAsync();
        await assertionPromise;
        expect(fn).toHaveBeenCalledTimes(3);
    });
    it('should not retry if the function throws a non-RetryError', async () => {
        const error = new Error('non-retry error');
        const fn = vi.fn().mockRejectedValue(error);
        await expect(promiseWithRetry(fn, 3, 100)).rejects.toThrow(error);
        expect(fn).toHaveBeenCalledTimes(1);
    });
    it('should use the specified delay between retries', async () => {
        const fn = vi
            .fn()
            .mockRejectedValueOnce(new RetryError('retry', 'cause'))
            .mockResolvedValue('success');
        const resultPromise = promiseWithRetry(fn, 3, 100);
        expect(fn).toHaveBeenCalledTimes(1);
        // Timer should not fire before 100ms
        await vi.advanceTimersByTimeAsync(99);
        expect(fn).toHaveBeenCalledTimes(1);
        // After 100ms, retry should happen
        await vi.advanceTimersByTimeAsync(1);
        await resultPromise;
        expect(fn).toHaveBeenCalledTimes(2);
    });
    it('should use exponential backoff when enabled', async () => {
        const fn = vi
            .fn()
            .mockRejectedValueOnce(new RetryError('retry', 'cause1'))
            .mockRejectedValueOnce(new RetryError('retry', 'cause2'))
            .mockResolvedValue('success');
        const resultPromise = promiseWithRetry(fn, 3, 50, true);
        expect(fn).toHaveBeenCalledTimes(1);
        // First retry after 50ms
        await vi.advanceTimersByTimeAsync(50);
        expect(fn).toHaveBeenCalledTimes(2);
        // Second retry after 100ms (doubled)
        await vi.advanceTimersByTimeAsync(100);
        await resultPromise;
        expect(fn).toHaveBeenCalledTimes(3);
    });
    it('should cap exponential backoff with maxDelayMs', async () => {
        const fn = vi
            .fn()
            .mockRejectedValueOnce(new RetryError('retry', 'cause1'))
            .mockRejectedValueOnce(new RetryError('retry', 'cause2'))
            .mockRejectedValueOnce(new RetryError('retry', 'cause3'))
            .mockRejectedValueOnce(new RetryError('retry', 'cause4'))
            .mockResolvedValue('success');
        // delay 1000, backoff, maxDelay 3000 → delays are 1000, 2000, 3000, 3000
        const resultPromise = promiseWithRetry(fn, 5, 1000, true, 3000);
        expect(fn).toHaveBeenCalledTimes(1);
        // First retry after 1000ms
        await vi.advanceTimersByTimeAsync(1000);
        expect(fn).toHaveBeenCalledTimes(2);
        // Second retry after 2000ms (doubled)
        await vi.advanceTimersByTimeAsync(2000);
        expect(fn).toHaveBeenCalledTimes(3);
        // Third retry after 3000ms (capped at maxDelayMs)
        await vi.advanceTimersByTimeAsync(3000);
        expect(fn).toHaveBeenCalledTimes(4);
        // Fourth retry after 3000ms (still capped)
        await vi.advanceTimersByTimeAsync(3000);
        await resultPromise;
        expect(fn).toHaveBeenCalledTimes(5);
    });
    it('should not affect delay when maxDelayMs is above the current delay', async () => {
        const fn = vi
            .fn()
            .mockRejectedValueOnce(new RetryError('retry', 'cause1'))
            .mockRejectedValueOnce(new RetryError('retry', 'cause2'))
            .mockResolvedValue('success');
        // maxDelayMs of 10000 should have no effect when delays are 50 and 100
        const resultPromise = promiseWithRetry(fn, 3, 50, true, 10000);
        expect(fn).toHaveBeenCalledTimes(1);
        // First retry after 50ms
        await vi.advanceTimersByTimeAsync(50);
        expect(fn).toHaveBeenCalledTimes(2);
        // Second retry after 100ms (doubled, still under cap)
        await vi.advanceTimersByTimeAsync(100);
        await resultPromise;
        expect(fn).toHaveBeenCalledTimes(3);
    });
    it('should not retry if retries is 0', async () => {
        const error = new RetryError('retry', 'cause');
        const fn = vi.fn().mockRejectedValue(error);
        await expect(promiseWithRetry(fn, 0, 100)).rejects.toThrow(error);
        expect(fn).toHaveBeenCalledTimes(1);
    });
    it('should handle functions that return different types', async () => {
        const objectFn = vi.fn().mockResolvedValue({ data: 'test' });
        const numberFn = vi.fn().mockResolvedValue(42);
        const arrayFn = vi.fn().mockResolvedValue([1, 2, 3]);
        const objectResult = await promiseWithRetry(objectFn, 3, 100);
        const numberResult = await promiseWithRetry(numberFn, 3, 100);
        const arrayResult = await promiseWithRetry(arrayFn, 3, 100);
        expect(objectResult).toEqual({ data: 'test' });
        expect(numberResult).toBe(42);
        expect(arrayResult).toEqual([1, 2, 3]);
    });
    it('should preserve the cause property of RetryError', async () => {
        const cause = { detail: 'some error detail' };
        const error = new RetryError('retry failed', cause);
        const fn = vi.fn().mockRejectedValue(error);
        try {
            await promiseWithRetry(fn, 0, 100);
        }
        catch (e) {
            expect(e).toBeInstanceOf(RetryError);
            if (e instanceof RetryError) {
                expect(e.cause).toBe(cause);
                expect(e.message).toBe('retry failed');
            }
        }
    });
});
describe('RetryError', () => {
    it('should create an error with a message and cause', () => {
        const cause = new Error('original error');
        const error = new RetryError('operation failed', cause);
        expect(error).toBeInstanceOf(Error);
        expect(error.message).toBe('operation failed');
        expect(error.cause).toBe(cause);
    });
    it('should handle non-Error causes', () => {
        const cause = 'string error';
        const error = new RetryError('operation failed', cause);
        expect(error.cause).toBe(cause);
    });
});
//# sourceMappingURL=promiseWithRetry.test.js.map