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

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

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

  it("starts in closed state", () => {
    expect(cb.state).toBe("closed");
  });

  it("passes through successful calls in closed state", async () => {
    const result = await cb.exec(() => Promise.resolve("ok"));
    expect(result).toBe("ok");
    expect(cb.state).toBe("closed");
  });

  it("transitions to open after failureThreshold consecutive failures", async () => {
    const fail = () => Promise.reject(new Error("fail"));
    for (let i = 0; i < 3; i++) {
      await expect(cb.exec(fail)).rejects.toThrow("fail");
    }
    expect(cb.state).toBe("open");
  });

  it("rejects immediately in open state without calling the function", async () => {
    const fail = () => Promise.reject(new Error("fail"));
    for (let i = 0; i < 3; i++) {
      await expect(cb.exec(fail)).rejects.toThrow();
    }

    const fn = vi.fn(() => Promise.resolve("should not run"));
    await expect(cb.exec(fn)).rejects.toThrow(/circuit.*open/i);
    expect(fn).not.toHaveBeenCalled();
  });

  it("transitions to half-open after openDurationMs elapses", async () => {
    vi.useFakeTimers();
    const fail = () => Promise.reject(new Error("fail"));
    for (let i = 0; i < 3; i++) {
      await expect(cb.exec(fail)).rejects.toThrow();
    }
    expect(cb.state).toBe("open");

    vi.advanceTimersByTime(1001);
    expect(cb.state).toBe("half-open");
    vi.useRealTimers();
  });

  it("transitions from half-open to closed on success", async () => {
    vi.useFakeTimers();
    const fail = () => Promise.reject(new Error("fail"));
    for (let i = 0; i < 3; i++) {
      await expect(cb.exec(fail)).rejects.toThrow();
    }
    vi.advanceTimersByTime(1001);
    expect(cb.state).toBe("half-open");

    const result = await cb.exec(() => Promise.resolve("recovered"));
    expect(result).toBe("recovered");
    expect(cb.state).toBe("closed");
    vi.useRealTimers();
  });

  it("transitions from half-open to open on failure", async () => {
    vi.useFakeTimers();
    const fail = () => Promise.reject(new Error("fail"));
    for (let i = 0; i < 3; i++) {
      await expect(cb.exec(fail)).rejects.toThrow();
    }
    vi.advanceTimersByTime(1001);
    expect(cb.state).toBe("half-open");

    await expect(cb.exec(fail)).rejects.toThrow("fail");
    expect(cb.state).toBe("open");
    vi.useRealTimers();
  });

  it("resets failure count on a successful call in closed state", async () => {
    const fail = () => Promise.reject(new Error("fail"));
    await expect(cb.exec(fail)).rejects.toThrow();
    await expect(cb.exec(fail)).rejects.toThrow();
    // 2 failures, threshold is 3
    await cb.exec(() => Promise.resolve("ok"));
    // count should have reset, so 2 more failures should not open
    await expect(cb.exec(fail)).rejects.toThrow();
    await expect(cb.exec(fail)).rejects.toThrow();
    expect(cb.state).toBe("closed");
  });

  it("reset() returns to closed state", async () => {
    const fail = () => Promise.reject(new Error("fail"));
    for (let i = 0; i < 3; i++) {
      await expect(cb.exec(fail)).rejects.toThrow();
    }
    expect(cb.state).toBe("open");
    cb.reset();
    expect(cb.state).toBe("closed");
  });
});
