/**
 * Per-engine circuit breaker (§2) — pure, deterministic unit tests.
 * Time is injected so no real clock is involved.
 */

import { describe, expect, it } from 'vitest';
import { EngineCircuitBreaker, isEngineBreakerEnabled } from './engine-breaker.js';

const T0 = Date.parse('2026-06-01T00:00:00.000Z');

describe('engine-breaker §2 — state machine', () => {
  it('a fresh (factory, engine) pair is CLOSED ⇒ routable', () => {
    const b = new EngineCircuitBreaker();
    expect(b.allow('f1', 'codex', T0)).toBe(true);
    expect(b.isOpen('f1', 'codex', T0)).toBe(false);
  });

  it('opens after the failure threshold of consecutive failures', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 3, resetTimeoutMs: 60_000 });
    b.recordFailure('f1', 'codex', T0);
    b.recordFailure('f1', 'codex', T0);
    expect(b.allow('f1', 'codex', T0)).toBe(true); // 2 < 3 — still closed
    b.recordFailure('f1', 'codex', T0);
    expect(b.allow('f1', 'codex', T0)).toBe(false); // 3rd failure trips it OPEN
  });

  it('a success before the threshold resets the failure count', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 3 });
    b.recordFailure('f1', 'codex', T0);
    b.recordFailure('f1', 'codex', T0);
    b.recordSuccess('f1', 'codex');
    b.recordFailure('f1', 'codex', T0);
    b.recordFailure('f1', 'codex', T0);
    expect(b.allow('f1', 'codex', T0)).toBe(true); // count restarted ⇒ 2 < 3
  });

  it('isolates per factory AND per engine', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 1 });
    b.recordFailure('f1', 'codex', T0);
    expect(b.allow('f1', 'codex', T0)).toBe(false); // tripped
    expect(b.allow('f2', 'codex', T0)).toBe(true); // other factory unaffected
    expect(b.allow('f1', 'devin', T0)).toBe(true); // other engine unaffected
  });
});

describe('engine-breaker §2 — half-open recovery', () => {
  it('after the reset window it allows exactly one probe (HALF_OPEN)', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 60_000 });
    b.recordFailure('f1', 'codex', T0);
    expect(b.allow('f1', 'codex', T0 + 30_000)).toBe(false); // still inside the window
    expect(b.allow('f1', 'codex', T0 + 60_000)).toBe(true); // window elapsed ⇒ probe
  });

  it('a successful probe closes the breaker', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 60_000 });
    b.recordFailure('f1', 'codex', T0);
    b.allow('f1', 'codex', T0 + 60_000); // → HALF_OPEN
    b.recordSuccess('f1', 'codex');
    expect(b.allow('f1', 'codex', T0 + 61_000)).toBe(true);
    expect(b.isOpen('f1', 'codex', T0 + 61_000)).toBe(false);
  });

  it('a failed probe immediately re-opens the breaker', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 3, resetTimeoutMs: 60_000 });
    b.recordFailure('f1', 'codex', T0);
    b.recordFailure('f1', 'codex', T0);
    b.recordFailure('f1', 'codex', T0); // OPEN
    b.allow('f1', 'codex', T0 + 60_000); // → HALF_OPEN (probe allowed)
    b.recordFailure('f1', 'codex', T0 + 61_000); // probe fails
    expect(b.allow('f1', 'codex', T0 + 61_000)).toBe(false); // re-opened, fresh window
  });
});

describe('engine-breaker §2 — snapshot + clear', () => {
  it('snapshot reports tracked pairs and reflects the reset window', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 60_000 });
    b.recordFailure('f1', 'codex', T0);
    const open = b.snapshot(T0 + 1_000);
    expect(open).toHaveLength(1);
    expect(open[0]).toMatchObject({ factoryId: 'f1', engine: 'codex', state: 'OPEN' });
    expect(open[0].lastFailureAt).toBe(new Date(T0).toISOString());
    // once the window elapses the snapshot shows HALF_OPEN (a probe is available)
    const half = b.snapshot(T0 + 60_000);
    expect(half[0].state).toBe('HALF_OPEN');
  });

  it('clear drops all state', () => {
    const b = new EngineCircuitBreaker({ failureThreshold: 1 });
    b.recordFailure('f1', 'codex', T0);
    b.clear();
    expect(b.snapshot(T0)).toEqual([]);
    expect(b.allow('f1', 'codex', T0)).toBe(true);
  });
});

describe('engine-breaker §2 — feature flag', () => {
  it('isEngineBreakerEnabled defaults OFF and honors truthy env values', () => {
    const prev = process.env.FLEET_ENGINE_BREAKER;
    try {
      delete process.env.FLEET_ENGINE_BREAKER;
      expect(isEngineBreakerEnabled()).toBe(false);
      process.env.FLEET_ENGINE_BREAKER = '1';
      expect(isEngineBreakerEnabled()).toBe(true);
      process.env.FLEET_ENGINE_BREAKER = 'true';
      expect(isEngineBreakerEnabled()).toBe(true);
      process.env.FLEET_ENGINE_BREAKER = 'off';
      expect(isEngineBreakerEnabled()).toBe(false);
    } finally {
      if (prev === undefined) delete process.env.FLEET_ENGINE_BREAKER;
      else process.env.FLEET_ENGINE_BREAKER = prev;
    }
  });
});
