/**
 * Tests for circuit breaker
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { CircuitBreaker, CircuitBreakerState } from './circuit-breaker';

describe('Circuit Breaker', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe('CircuitBreaker', () => {
    it('should start in CLOSED state', () => {
      const breaker = new CircuitBreaker();
      expect(breaker.getState()).toBe(CircuitBreakerState.CLOSED);
    });

    it('should execute function successfully in CLOSED state', async () => {
      const breaker = new CircuitBreaker();
      const fn = vi.fn().mockResolvedValue('success');
      
      const result = await breaker.execute(fn);
      
      expect(result).toBe('success');
      expect(fn).toHaveBeenCalledTimes(1);
    });

    it('should open circuit after failure threshold', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 3 });
      const fn = vi.fn().mockRejectedValue(new Error('Failure'));
      
      // Trigger failures - need to trigger enough to exceed threshold
      // The circuit opens AFTER the threshold is reached, so we need threshold + 1 failures
      for (let i = 0; i < 3; i++) {
        await expect(breaker.execute(fn)).rejects.toThrow('Failure');
      }
      // One more to trigger the opening
      await expect(breaker.execute(fn)).rejects.toThrow('Failure');
      
      // Check state after next execution attempt (which should be rejected)
      await expect(breaker.execute(() => Promise.resolve('success'))).rejects.toThrow('Circuit breaker is OPEN');
      expect(breaker.getState()).toBe(CircuitBreakerState.OPEN);
    });

    it('should reject immediately in OPEN state', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1 });
      const fn = vi.fn().mockRejectedValue(new Error('Failure'));
      
      // Open the circuit
      await expect(breaker.execute(fn)).rejects.toThrow('Failure');
      
      // Try to execute again - should fail immediately
      await expect(breaker.execute(() => Promise.resolve('success'))).rejects.toThrow('Circuit breaker is OPEN');
    });

    it('should transition to HALF_OPEN after reset timeout', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1, resetTimeout: 1000 });
      const fn = vi.fn().mockRejectedValue(new Error('Failure'));
      
      // Open the circuit by triggering failures
      await expect(breaker.execute(fn)).rejects.toThrow('Failure');
      await expect(breaker.execute(fn)).rejects.toThrow('Failure'); // Second failure opens it
      
      // Advance time past reset timeout
      vi.advanceTimersByTime(1000);
      
      // Next execution should transition to HALF_OPEN and then CLOSED on success
      const successFn = vi.fn().mockResolvedValue('success');
      const result = await breaker.execute(successFn);
      
      expect(result).toBe('success');
      expect(breaker.getState()).toBe(CircuitBreakerState.CLOSED);
    });

    it('should close circuit after successful execution in HALF_OPEN state', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 1, resetTimeout: 1000 });
      const failFn = vi.fn().mockRejectedValue(new Error('Failure'));
      
      // Open the circuit
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure');
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure'); // Opens circuit
      
      // Advance time past reset timeout
      vi.advanceTimersByTime(1000);
      
      // Execute successfully - should transition from HALF_OPEN to CLOSED
      const successFn = vi.fn().mockResolvedValue('success');
      const result = await breaker.execute(successFn);
      
      expect(result).toBe('success');
      expect(breaker.getState()).toBe(CircuitBreakerState.CLOSED);
    });

    it('should reset failure count on successful execution', async () => {
      const breaker = new CircuitBreaker({ failureThreshold: 3 });
      const failFn = vi.fn().mockRejectedValue(new Error('Failure'));
      const successFn = vi.fn().mockResolvedValue('success');
      
      // Trigger 2 failures
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure');
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure');
      
      // Success should reset the counter
      await breaker.execute(successFn);
      
      // Should need 3 failures to open (failure count resets)
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure');
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure');
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure');
      // One more to trigger opening
      await expect(breaker.execute(failFn)).rejects.toThrow('Failure');
      
      // Verify circuit is open
      await expect(breaker.execute(() => Promise.resolve('success'))).rejects.toThrow('Circuit breaker is OPEN');
    });
  });
});

