"use strict";
/**
 * Tests for Retry Manager
 */
Object.defineProperty(exports, "__esModule", { value: true });
const retry_manager_1 = require("./retry-manager");
describe('RetryManager', () => {
    describe('Failure Classification', () => {
        it('should classify transient network errors', () => {
            const errors = [
                new Error('ECONNREFUSED'),
                new Error('ETIMEDOUT'),
                new Error('ENETUNREACH'),
                new Error('503 Service Unavailable'),
                new Error('429 Too Many Requests'),
            ];
            errors.forEach(error => {
                expect(retry_manager_1.RetryManager.classifyFailure(error)).toBe(retry_manager_1.FailureType.TRANSIENT);
            });
        });
        it('should classify transient file system errors', () => {
            const errors = [
                new Error('EBUSY resource busy'),
                new Error('EAGAIN try again'),
                new Error('EMFILE too many open files'),
            ];
            errors.forEach(error => {
                expect(retry_manager_1.RetryManager.classifyFailure(error)).toBe(retry_manager_1.FailureType.TRANSIENT);
            });
        });
        it('should classify transient database errors', () => {
            const errors = [
                new Error('connection refused'),
                new Error('deadlock detected'),
                new Error('lock timeout exceeded'),
            ];
            errors.forEach(error => {
                expect(retry_manager_1.RetryManager.classifyFailure(error)).toBe(retry_manager_1.FailureType.TRANSIENT);
            });
        });
        it('should classify permanent authentication errors', () => {
            const errors = [
                new Error('401 Unauthorized'),
                new Error('403 Forbidden'),
                new Error('unauthorized access'),
            ];
            errors.forEach(error => {
                expect(retry_manager_1.RetryManager.classifyFailure(error)).toBe(retry_manager_1.FailureType.PERMANENT);
            });
        });
        it('should classify permanent validation errors', () => {
            const errors = [
                new Error('400 Bad Request'),
                new Error('invalid input'),
                new Error('malformed request'),
                new Error('syntax error'),
            ];
            errors.forEach(error => {
                expect(retry_manager_1.RetryManager.classifyFailure(error)).toBe(retry_manager_1.FailureType.PERMANENT);
            });
        });
        it('should classify permanent not found errors', () => {
            const errors = [
                new Error('404 Not Found'),
                new Error('resource does not exist'),
            ];
            errors.forEach(error => {
                expect(retry_manager_1.RetryManager.classifyFailure(error)).toBe(retry_manager_1.FailureType.PERMANENT);
            });
        });
        it('should classify unknown errors as unknown', () => {
            const error = new Error('Some random error');
            expect(retry_manager_1.RetryManager.classifyFailure(error)).toBe(retry_manager_1.FailureType.UNKNOWN);
        });
    });
    describe('Retry Execution', () => {
        it('should succeed on first attempt', async () => {
            const fn = jest.fn().mockResolvedValue('success');
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.success).toBe(true);
            expect(result.result).toBe('success');
            expect(result.attempts).toBe(1);
            expect(fn).toHaveBeenCalledTimes(1);
        });
        it('should retry transient failures', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockRejectedValueOnce(new Error('ECONNREFUSED'))
                .mockResolvedValue('success');
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.success).toBe(true);
            expect(result.result).toBe('success');
            expect(result.attempts).toBe(3);
            expect(fn).toHaveBeenCalledTimes(3);
        });
        it('should not retry permanent failures', async () => {
            const fn = jest.fn().mockRejectedValue(new Error('401 Unauthorized'));
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.success).toBe(false);
            expect(result.attempts).toBe(1);
            expect(result.failureType).toBe(retry_manager_1.FailureType.PERMANENT);
            expect(fn).toHaveBeenCalledTimes(1);
        });
        it('should respect max attempts', async () => {
            const fn = jest.fn().mockRejectedValue(new Error('ETIMEDOUT'));
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.success).toBe(false);
            expect(result.attempts).toBe(3);
            expect(fn).toHaveBeenCalledTimes(3);
        });
        it('should call onRetry callback', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockResolvedValue('success');
            const onRetry = jest.fn();
            await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
                onRetry,
            });
            expect(onRetry).toHaveBeenCalledTimes(1);
            expect(onRetry).toHaveBeenCalledWith(1, expect.any(Error));
        });
        it('should respect custom shouldRetry', async () => {
            const fn = jest.fn().mockRejectedValue(new Error('Custom error'));
            const shouldRetry = jest.fn().mockReturnValue(false);
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
                shouldRetry,
            });
            expect(result.success).toBe(false);
            expect(result.attempts).toBe(1);
            expect(shouldRetry).toHaveBeenCalledWith(expect.any(Error));
            expect(fn).toHaveBeenCalledTimes(1);
        });
        it('should track total time', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockResolvedValue('success');
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.totalTime).toBeGreaterThanOrEqual(0);
            expect(typeof result.totalTime).toBe('number');
        });
    });
    describe('Retry Strategies', () => {
        jest.setTimeout(10000); // Increase timeout for delay tests
        it('should use exponential backoff', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockResolvedValue('success');
            const startTime = Date.now();
            await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.EXPONENTIAL_BACKOFF,
                initialDelay: 100,
                backoffMultiplier: 2,
            });
            const duration = Date.now() - startTime;
            // Should take at least 100ms (first retry) + 200ms (second retry) = 300ms
            // Adding jitter and execution time, should be around 300-500ms
            expect(duration).toBeGreaterThanOrEqual(250);
        });
        it('should use linear backoff', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockResolvedValue('success');
            const startTime = Date.now();
            await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.LINEAR_BACKOFF,
                initialDelay: 100,
            });
            const duration = Date.now() - startTime;
            // Should take at least 100ms + 200ms = 300ms
            expect(duration).toBeGreaterThanOrEqual(250);
        });
        it('should use immediate retry', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockResolvedValue('success');
            const startTime = Date.now();
            await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            const duration = Date.now() - startTime;
            // Should be very fast (< 50ms)
            expect(duration).toBeLessThan(50);
        });
        it('should cap at max delay', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockResolvedValue('success');
            const startTime = Date.now();
            await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 3,
                strategy: retry_manager_1.RetryStrategy.EXPONENTIAL_BACKOFF,
                initialDelay: 1000,
                maxDelay: 500, // Cap at 500ms
                backoffMultiplier: 10,
            });
            const duration = Date.now() - startTime;
            // Should be capped at 500ms per retry, so max ~1000ms total
            expect(duration).toBeLessThan(1500);
        });
    });
    describe('Timeout', () => {
        it('should timeout slow operations', async () => {
            const fn = jest.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 2000)));
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 1,
                timeout: 100, // 100ms timeout
            });
            expect(result.success).toBe(false);
            expect(result.error?.message).toContain('timed out');
        });
        it('should not timeout fast operations', async () => {
            const fn = jest.fn().mockResolvedValue('success');
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 1,
                timeout: 1000,
            });
            expect(result.success).toBe(true);
        });
    });
    describe('Batch Retry', () => {
        it('should execute all operations', async () => {
            const ops = [
                jest.fn().mockResolvedValue('result1'),
                jest.fn().mockResolvedValue('result2'),
                jest.fn().mockResolvedValue('result3'),
            ];
            const result = await retry_manager_1.RetryManager.executeBatch(ops, {
                maxAttempts: 2,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.allSucceeded).toBe(true);
            expect(result.succeeded).toBe(3);
            expect(result.failed).toBe(0);
            expect(result.totalOperations).toBe(3);
        });
        it('should retry failed operations', async () => {
            const ops = [
                jest.fn().mockResolvedValue('result1'),
                jest.fn()
                    .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                    .mockResolvedValue('result2'),
                jest.fn().mockResolvedValue('result3'),
            ];
            const result = await retry_manager_1.RetryManager.executeBatch(ops, {
                maxAttempts: 2,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.allSucceeded).toBe(true);
            expect(result.succeeded).toBe(3);
            expect(ops[1]).toHaveBeenCalledTimes(2);
        });
        it('should stop on permanent failure', async () => {
            const ops = [
                jest.fn().mockResolvedValue('result1'),
                jest.fn().mockRejectedValue(new Error('401 Unauthorized')),
                jest.fn().mockResolvedValue('result3'),
            ];
            const result = await retry_manager_1.RetryManager.executeBatch(ops, {
                maxAttempts: 2,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.allSucceeded).toBe(false);
            expect(result.executedOperations).toBe(2); // Stopped after permanent failure
            expect(ops[2]).not.toHaveBeenCalled();
        });
        it('should track results for each operation', async () => {
            const ops = [
                jest.fn().mockResolvedValue('result1'),
                jest.fn().mockRejectedValue(new Error('ETIMEDOUT')),
            ];
            const result = await retry_manager_1.RetryManager.executeBatch(ops, {
                maxAttempts: 2,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.results.length).toBe(2);
            expect(result.results[0].success).toBe(true);
            expect(result.results[1].success).toBe(false);
        });
    });
    describe('CircuitBreaker', () => {
        it('should start in closed state', () => {
            const breaker = new retry_manager_1.CircuitBreaker('test-service', 3, 1000);
            expect(breaker.getState()).toBe('closed');
        });
        it('should execute successfully when closed', async () => {
            const breaker = new retry_manager_1.CircuitBreaker('test-service', 3, 1000);
            const fn = jest.fn().mockResolvedValue('success');
            const result = await breaker.execute(fn);
            expect(result).toBe('success');
            expect(fn).toHaveBeenCalled();
        });
        it('should open after threshold failures', async () => {
            const breaker = new retry_manager_1.CircuitBreaker('test-service', 3, 1000);
            const fn = jest.fn().mockRejectedValue(new Error('failure'));
            // Fail 3 times
            for (let i = 0; i < 3; i++) {
                try {
                    await breaker.execute(fn);
                }
                catch (_e) {
                    // Expected
                }
            }
            expect(breaker.getState()).toBe('open');
        });
        it('should reject requests when open', async () => {
            const breaker = new retry_manager_1.CircuitBreaker('test-service', 2, 1000);
            const fn = jest.fn().mockRejectedValue(new Error('failure'));
            // Fail twice to open
            for (let i = 0; i < 2; i++) {
                try {
                    await breaker.execute(fn);
                }
                catch (_e) {
                    // Expected
                }
            }
            // Should reject without calling function
            await expect(breaker.execute(fn)).rejects.toThrow('Circuit breaker');
            expect(fn).toHaveBeenCalledTimes(2); // Not called again
        });
        it('should transition to half-open after timeout', async () => {
            const breaker = new retry_manager_1.CircuitBreaker('test-service', 2, 100);
            const fn = jest.fn().mockRejectedValue(new Error('failure'));
            // Open the circuit
            for (let i = 0; i < 2; i++) {
                try {
                    await breaker.execute(fn);
                }
                catch (_e) {
                    // Expected
                }
            }
            expect(breaker.getState()).toBe('open');
            // Wait for timeout
            await new Promise(resolve => setTimeout(resolve, 150));
            // Try again - should move to half-open
            const successFn = jest.fn().mockResolvedValue('success');
            await breaker.execute(successFn);
            expect(breaker.getState()).toBe('closed');
            expect(successFn).toHaveBeenCalled();
        });
        it('should reset on success', async () => {
            const breaker = new retry_manager_1.CircuitBreaker('test-service', 3, 1000);
            // Fail once
            try {
                await breaker.execute(jest.fn().mockRejectedValue(new Error('fail')));
            }
            catch (_e) {
                // Expected
            }
            // Succeed
            await breaker.execute(jest.fn().mockResolvedValue('success'));
            // Circuit should be closed with reset count
            expect(breaker.getState()).toBe('closed');
        });
        it('should reset manually', async () => {
            const breaker = new retry_manager_1.CircuitBreaker('test-service', 2, 1000);
            const fn = jest.fn().mockRejectedValue(new Error('failure'));
            // Open the circuit
            for (let i = 0; i < 2; i++) {
                try {
                    await breaker.execute(fn);
                }
                catch (_e) {
                    // Expected
                }
            }
            expect(breaker.getState()).toBe('open');
            breaker.reset();
            expect(breaker.getState()).toBe('closed');
        });
    });
    describe('RetryHelpers', () => {
        it('should retry file operations', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('EBUSY'))
                .mockResolvedValue('success');
            const result = await retry_manager_1.RetryHelpers.retryFileOperation(fn, 'test operation');
            expect(result).toBe('success');
            expect(fn).toHaveBeenCalledTimes(2);
        });
        it('should retry network operations with longer delays', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                .mockResolvedValue('success');
            const startTime = Date.now();
            const result = await retry_manager_1.RetryHelpers.retryNetworkOperation(fn, 'test operation');
            const duration = Date.now() - startTime;
            expect(result).toBe('success');
            // Network operations use longer initial delay (2000ms)
            expect(duration).toBeGreaterThanOrEqual(1800);
        });
        it('should retry database operations', async () => {
            const fn = jest.fn()
                .mockRejectedValueOnce(new Error('deadlock'))
                .mockResolvedValue('success');
            const result = await retry_manager_1.RetryHelpers.retryDatabaseOperation(fn, 'test operation');
            expect(result).toBe('success');
            expect(fn).toHaveBeenCalledTimes(2);
        });
        it('should not retry database syntax errors', async () => {
            const fn = jest.fn().mockRejectedValue(new Error('syntax error in query'));
            await expect(retry_manager_1.RetryHelpers.retryDatabaseOperation(fn, 'test operation')).rejects.toThrow(retry_manager_1.RetryError);
            expect(fn).toHaveBeenCalledTimes(1);
        });
        it('should throw RetryError on failure', async () => {
            const fn = jest.fn().mockRejectedValue(new Error('ETIMEDOUT'));
            await expect(retry_manager_1.RetryHelpers.retryFileOperation(fn, 'test operation')).rejects.toThrow(retry_manager_1.RetryError);
        });
    });
    describe('RetryError', () => {
        it('should format error message', () => {
            const originalError = new Error('ETIMEDOUT');
            const retryError = new retry_manager_1.RetryError('Operation failed', 3, retry_manager_1.FailureType.TRANSIENT, originalError);
            const formatted = retryError.format();
            expect(formatted).toContain('⚠️');
            expect(formatted).toContain('failed after 3 attempts');
            expect(formatted).toContain('transient');
            expect(formatted).toContain('ETIMEDOUT');
        });
        it('should include suggestion for transient failures', () => {
            const retryError = new retry_manager_1.RetryError('Operation failed', 3, retry_manager_1.FailureType.TRANSIENT, new Error('temp failure'));
            const formatted = retryError.format();
            expect(formatted).toContain('temporary issue');
            expect(formatted).toContain('Try again');
        });
        it('should include suggestion for permanent failures', () => {
            const retryError = new retry_manager_1.RetryError('Operation failed', 3, retry_manager_1.FailureType.PERMANENT, new Error('401 Unauthorized'));
            const formatted = retryError.format();
            expect(formatted).toContain('persistent issue');
            expect(formatted).toContain('Check your configuration');
        });
    });
    describe('Edge Cases', () => {
        it('should handle non-Error rejections', async () => {
            const fn = jest.fn().mockRejectedValue('string error');
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 2,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.success).toBe(false);
            expect(result.error).toBeDefined();
        });
        it('should handle zero max attempts gracefully', async () => {
            const fn = jest.fn().mockResolvedValue('success');
            const result = await retry_manager_1.RetryManager.execute(fn, {
                maxAttempts: 0,
                strategy: retry_manager_1.RetryStrategy.IMMEDIATE,
            });
            expect(result.success).toBe(false);
            expect(fn).not.toHaveBeenCalled();
        });
        it('should add jitter to prevent thundering herd', async () => {
            const delays = [];
            // Run multiple iterations to observe jitter variation
            for (let i = 0; i < 20; i++) {
                const fn = jest.fn()
                    .mockRejectedValueOnce(new Error('ETIMEDOUT'))
                    .mockResolvedValue('success');
                const startTime = Date.now();
                await retry_manager_1.RetryManager.execute(fn, {
                    maxAttempts: 2,
                    strategy: retry_manager_1.RetryStrategy.EXPONENTIAL_BACKOFF,
                    initialDelay: 100,
                });
                delays.push(Date.now() - startTime);
            }
            // Delays should vary due to jitter
            // Use smaller bucket size (5ms) to detect jitter variation
            const uniqueDelays = new Set(delays.map(d => Math.floor(d / 5)));
            expect(uniqueDelays.size).toBeGreaterThanOrEqual(2);
        }, 20000); // Increase timeout
    });
});
//# sourceMappingURL=retry-manager.test.js.map