/**
 * Tests for CircuitBreaker — state machine (CLOSED → OPEN → HALF_OPEN).
 */

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

describe('CircuitBreaker', () => {
  let cb: CircuitBreaker;

  beforeEach(() => {
    cb = new CircuitBreaker({ failureThreshold: 3, resetTimeoutMs: 1000 });
  });

  describe('initial state', () => {
    it('starts in CLOSED state', () => {
      expect(cb.currentState).toBe('CLOSED');
    });

    it('allows requests when CLOSED', () => {
      expect(cb.allowRequest()).toBe(true);
    });

    it('stats reflect initial state', () => {
      const stats = cb.stats;
      expect(stats.state).toBe('CLOSED');
      expect(stats.failureCount).toBe(0);
      expect(stats.threshold).toBe(3);
      expect(stats.resetMs).toBe(1000);
    });
  });

  describe('default options', () => {
    it('uses failureThreshold=5 and resetTimeoutMs=30000 by default', () => {
      const defaultCb = new CircuitBreaker();
      const stats = defaultCb.stats;
      expect(stats.threshold).toBe(5);
      expect(stats.resetMs).toBe(30_000);
    });
  });

  describe('failure tracking', () => {
    it('stays CLOSED below threshold', () => {
      cb.recordFailure();
      expect(cb.currentState).toBe('CLOSED');
      expect(cb.stats.failureCount).toBe(1);

      cb.recordFailure();
      expect(cb.currentState).toBe('CLOSED');
      expect(cb.stats.failureCount).toBe(2);
    });

    it('transitions to OPEN at threshold', () => {
      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();

      expect(cb.currentState).toBe('OPEN');
      expect(cb.stats.failureCount).toBe(3);
    });

    it('blocks requests when OPEN', () => {
      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();

      expect(cb.allowRequest()).toBe(false);
    });
  });

  describe('success resets', () => {
    it('resets failure count on success', () => {
      cb.recordFailure();
      cb.recordFailure();
      expect(cb.stats.failureCount).toBe(2);

      cb.recordSuccess();
      expect(cb.stats.failureCount).toBe(0);
      expect(cb.currentState).toBe('CLOSED');
    });

    it('returns to CLOSED from any state on success', () => {
      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();
      expect(cb.currentState).toBe('OPEN');

      cb.recordSuccess();
      expect(cb.currentState).toBe('CLOSED');
      expect(cb.allowRequest()).toBe(true);
    });
  });

  describe('HALF_OPEN transition', () => {
    it('transitions to HALF_OPEN after reset timeout', () => {
      vi.useFakeTimers();

      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();
      expect(cb.currentState).toBe('OPEN');
      expect(cb.allowRequest()).toBe(false);

      // Advance past reset timeout
      vi.advanceTimersByTime(1001);

      // allowRequest should transition to HALF_OPEN
      expect(cb.allowRequest()).toBe(true);
      expect(cb.currentState).toBe('HALF_OPEN');

      vi.useRealTimers();
    });

    it('returns to CLOSED on success during HALF_OPEN', () => {
      vi.useFakeTimers();

      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();

      vi.advanceTimersByTime(1001);
      cb.allowRequest(); // transitions to HALF_OPEN

      cb.recordSuccess();
      expect(cb.currentState).toBe('CLOSED');
      expect(cb.stats.failureCount).toBe(0);

      vi.useRealTimers();
    });

    it('returns to OPEN on failure during HALF_OPEN', () => {
      vi.useFakeTimers();

      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();

      vi.advanceTimersByTime(1001);
      cb.allowRequest(); // transitions to HALF_OPEN

      // Failure during HALF_OPEN should re-open
      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();
      expect(cb.currentState).toBe('OPEN');

      vi.useRealTimers();
    });

    it('allows probe request in HALF_OPEN state', () => {
      vi.useFakeTimers();

      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();

      vi.advanceTimersByTime(1001);
      expect(cb.allowRequest()).toBe(true); // probe allowed
      expect(cb.currentState).toBe('HALF_OPEN');

      vi.useRealTimers();
    });
  });

  describe('OPEN state timing', () => {
    it('stays OPEN before reset timeout', () => {
      vi.useFakeTimers();

      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();

      vi.advanceTimersByTime(500); // Half of reset timeout
      expect(cb.allowRequest()).toBe(false);
      expect(cb.currentState).toBe('OPEN');

      vi.useRealTimers();
    });

    it('transitions exactly at reset timeout boundary', () => {
      vi.useFakeTimers();

      cb.recordFailure();
      cb.recordFailure();
      cb.recordFailure();

      vi.advanceTimersByTime(1000); // Exactly at boundary
      expect(cb.allowRequest()).toBe(true);
      expect(cb.currentState).toBe('HALF_OPEN');

      vi.useRealTimers();
    });
  });

  describe('edge cases', () => {
    it('handles rapid success/failure alternation', () => {
      cb.recordFailure();
      cb.recordSuccess();
      cb.recordFailure();
      cb.recordSuccess();
      cb.recordFailure();

      expect(cb.currentState).toBe('CLOSED');
      expect(cb.stats.failureCount).toBe(1);
    });

    it('threshold of 1 opens immediately on first failure', () => {
      const strictCb = new CircuitBreaker({ failureThreshold: 1 });
      strictCb.recordFailure();
      expect(strictCb.currentState).toBe('OPEN');
    });

    it('multiple successes keep state CLOSED', () => {
      cb.recordSuccess();
      cb.recordSuccess();
      cb.recordSuccess();
      expect(cb.currentState).toBe('CLOSED');
      expect(cb.stats.failureCount).toBe(0);
    });
  });
});
