import { CircuitBreaker, CircuitOpenError, CircuitState } from '../circuit-breaker';

describe('CircuitBreaker', () => {
  let breaker: CircuitBreaker;
  let stateChanges: Array<{ from: CircuitState; to: CircuitState }>;

  beforeEach(() => {
    stateChanges = [];
    breaker = new CircuitBreaker({
      name: 'test-service',
      failureThreshold: 3,
      resetTimeMs: 100, // fast for tests
      onStateChange: (from, to) => stateChanges.push({ from, to }),
    });
  });

  it('starts in CLOSED state', () => {
    expect(breaker.getState()).toBe(CircuitState.CLOSED);
    expect(breaker.isAvailable()).toBe(true);
  });

  it('stays CLOSED when failures are below threshold', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));

    await breaker.exec(failingFn).catch(() => {});
    await breaker.exec(failingFn).catch(() => {});

    expect(breaker.getState()).toBe(CircuitState.CLOSED);
    expect(breaker.isAvailable()).toBe(true);
  });

  it('opens after reaching failure threshold', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));

    for (let i = 0; i < 3; i++) {
      await breaker.exec(failingFn).catch(() => {});
    }

    expect(breaker.getState()).toBe(CircuitState.OPEN);
    expect(breaker.isAvailable()).toBe(false);
    expect(stateChanges).toEqual([{ from: CircuitState.CLOSED, to: CircuitState.OPEN }]);
  });

  it('rejects calls immediately when OPEN', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));
    for (let i = 0; i < 3; i++) {
      await breaker.exec(failingFn).catch(() => {});
    }

    await expect(breaker.exec(() => Promise.resolve('ok'))).rejects.toThrow(CircuitOpenError);
  });

  it('transitions to HALF_OPEN after reset timeout', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));
    for (let i = 0; i < 3; i++) {
      await breaker.exec(failingFn).catch(() => {});
    }
    expect(breaker.getState()).toBe(CircuitState.OPEN);

    // Wait for the reset timeout
    await new Promise((r) => setTimeout(r, 150));

    expect(breaker.getState()).toBe(CircuitState.HALF_OPEN);
    expect(breaker.isAvailable()).toBe(true);
  });

  it('closes on successful probe in HALF_OPEN', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));
    for (let i = 0; i < 3; i++) {
      await breaker.exec(failingFn).catch(() => {});
    }

    await new Promise((r) => setTimeout(r, 150));
    expect(breaker.getState()).toBe(CircuitState.HALF_OPEN);

    const result = await breaker.exec(() => Promise.resolve('recovered'));
    expect(result).toBe('recovered');
    expect(breaker.getState()).toBe(CircuitState.CLOSED);
  });

  it('re-opens on failed probe in HALF_OPEN', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));
    for (let i = 0; i < 3; i++) {
      await breaker.exec(failingFn).catch(() => {});
    }

    await new Promise((r) => setTimeout(r, 150));
    expect(breaker.getState()).toBe(CircuitState.HALF_OPEN);

    await breaker.exec(failingFn).catch(() => {});
    expect(breaker.getState()).toBe(CircuitState.OPEN);
  });

  it('resets failure count on success', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));
    const succeedingFn = vi.fn().mockResolvedValue('ok');

    // 2 failures, then success, then 2 more failures → should still be CLOSED
    await breaker.exec(failingFn).catch(() => {});
    await breaker.exec(failingFn).catch(() => {});
    await breaker.exec(succeedingFn);
    await breaker.exec(failingFn).catch(() => {});
    await breaker.exec(failingFn).catch(() => {});

    expect(breaker.getState()).toBe(CircuitState.CLOSED);
  });

  it('manual reset() brings breaker back to CLOSED', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));
    for (let i = 0; i < 3; i++) {
      await breaker.exec(failingFn).catch(() => {});
    }
    expect(breaker.getState()).toBe(CircuitState.OPEN);

    breaker.reset();
    expect(breaker.getState()).toBe(CircuitState.CLOSED);
    expect(breaker.isAvailable()).toBe(true);
  });

  it('recordSuccess() transitions from OPEN to CLOSED after timeout', async () => {
    const failingFn = vi.fn().mockRejectedValue(new Error('fail'));
    for (let i = 0; i < 3; i++) {
      await breaker.exec(failingFn).catch(() => {});
    }

    await new Promise((r) => setTimeout(r, 150));
    breaker.recordSuccess();
    expect(breaker.getState()).toBe(CircuitState.CLOSED);
  });

  it('recordFailure() increments failures', () => {
    breaker.recordFailure();
    breaker.recordFailure();
    expect(breaker.getState()).toBe(CircuitState.CLOSED);

    breaker.recordFailure();
    expect(breaker.getState()).toBe(CircuitState.OPEN);
  });
});
