import { describe, expect, test } from "bun:test";
import {
  Bulkhead,
  CircuitBreaker,
  ResilienceError,
  durationMs,
  resilientCall,
} from "../src/index.ts";

describe("resilience primitives", () => {
  test("parses durations and validates bad configuration", () => {
    expect(durationMs("1.5s")).toBe(1_500);
    expect(durationMs("2m")).toBe(120_000);
    expect(() => durationMs("soon" as never)).toThrow("Invalid duration");
  });

  test("retries with exponential backoff and reports attempts", async () => {
    const waits: number[] = [];
    let calls = 0;
    const value = await resilientCall({
      retries: 2,
      retryDelay: 1,
      backoff: "exponential",
      onRetry: (_error, _attempt, wait) => waits.push(wait),
      run: async (_signal, attempt) => {
        calls += 1;
        if (attempt < 3) throw new Error("temporary");
        return "ready";
      },
    });
    expect(value).toBe("ready");
    expect(calls).toBe(3);
    expect(waits).toEqual([1, 2]);
  });

  test("times out cooperative operations and supports fallback", async () => {
    const value = await resilientCall({
      timeout: "5ms",
      fallback: (error) => (error as ResilienceError).code,
      run: (signal) =>
        new Promise((_resolve, reject) =>
          signal.addEventListener("abort", () => reject(signal.reason)),
        ),
    });
    expect(value).toBe("WRN-RESILIENCE-TIMEOUT");
  });

  test("times out integrations that ignore cancellation", async () => {
    await expect(
      resilientCall({ timeout: "2ms", run: () => new Promise(() => {}) }),
    ).rejects.toMatchObject({ code: "WRN-RESILIENCE-TIMEOUT" });
  });

  test("opens a circuit and exposes health", async () => {
    const breaker = new CircuitBreaker({ failures: 2, resetAfter: "1h" });
    for (let index = 0; index < 2; index += 1) {
      await expect(
        breaker.execute(async () => {
          throw new Error("down");
        }),
      ).rejects.toThrow("down");
    }
    expect(breaker.snapshot().state).toBe("open");
    await expect(breaker.execute(async () => "nope")).rejects.toMatchObject({
      code: "WRN-RESILIENCE-CIRCUIT-OPEN",
    });
  });

  test("retains circuit state for a reused declarative configuration", async () => {
    const circuitBreaker = { failures: 1, resetAfter: "1h" } as const;
    await expect(
      resilientCall({
        circuitBreaker,
        run: async () => {
          throw new Error("down");
        },
      }),
    ).rejects.toThrow("down");
    await expect(
      resilientCall({ circuitBreaker, run: async () => "unreachable" }),
    ).rejects.toMatchObject({ code: "WRN-RESILIENCE-CIRCUIT-OPEN" });
  });

  test("bulkhead bounds concurrency and queue depth", async () => {
    const bulkhead = new Bulkhead({ concurrency: 1, queue: 1 });
    let release!: () => void;
    const first = bulkhead.execute(
      () =>
        new Promise<void>((resolve) => {
          release = resolve;
        }),
    );
    const second = bulkhead.execute(async () => "second");
    await expect(bulkhead.execute(async () => "third")).rejects.toMatchObject({
      code: "WRN-RESILIENCE-BULKHEAD-FULL",
    });
    expect(bulkhead.snapshot).toEqual({ active: 1, queued: 1, capacity: 1 });
    release();
    await first;
    expect(await second).toBe("second");
  });
});
