import { expect, test } from "bun:test";
import { createThrottle, getOrgThrottle } from "./throttle";

/** Horloge + sleep déterministes : `sleep(ms)` fait avancer l'horloge de `ms`. */
function fakeClock() {
  let t = 0;
  const sleeps: number[] = [];
  return {
    now: () => t,
    sleep: (ms: number) => {
      sleeps.push(ms);
      t += ms;
      return Promise.resolve();
    },
    sleeps,
    advance: (ms: number) => {
      t += ms;
    },
  };
}

test("espace 3 runs d'au moins minIntervalMs (500 ms) via l'horloge fake", async () => {
  const clock = fakeClock();
  const t = createThrottle({
    minIntervalMs: 500,
    now: clock.now,
    sleep: clock.sleep,
  });
  const starts: number[] = [];
  const record = () => {
    starts.push(clock.now());
    return Promise.resolve("ok");
  };

  await t.run(record);
  await t.run(record);
  await t.run(record);

  expect(starts).toEqual([0, 500, 1000]);
  for (let i = 1; i < starts.length; i++) {
    expect(starts[i] - starts[i - 1]).toBeGreaterThanOrEqual(500);
  }
});

test("pas d'attente si l'intervalle est déjà écoulé", async () => {
  const clock = fakeClock();
  const t = createThrottle({
    minIntervalMs: 500,
    now: clock.now,
    sleep: clock.sleep,
  });

  await t.run(() => Promise.resolve());
  clock.advance(700); // 700 ms se sont écoulés hors throttle
  await t.run(() => Promise.resolve());

  expect(clock.sleeps).toEqual([]); // aucun sleep déclenché
});

test("sérialise les exécutions même sous appels concurrents", async () => {
  const clock = fakeClock();
  const t = createThrottle({
    minIntervalMs: 500,
    now: clock.now,
    sleep: clock.sleep,
  });
  const order: string[] = [];
  let active = 0;
  let maxActive = 0;
  const task = (label: string) => async () => {
    active += 1;
    maxActive = Math.max(maxActive, active);
    order.push(`start-${label}`);
    await Promise.resolve();
    await Promise.resolve();
    order.push(`end-${label}`);
    active -= 1;
  };

  // Lancés sans await → concurrents.
  const p1 = t.run(task("A"));
  const p2 = t.run(task("B"));
  const p3 = t.run(task("C"));
  await Promise.all([p1, p2, p3]);

  expect(maxActive).toBe(1); // jamais deux fn en vol simultanément
  expect(order).toEqual([
    "start-A",
    "end-A",
    "start-B",
    "end-B",
    "start-C",
    "end-C",
  ]);
});

test("une rejection ne casse pas la file : le run suivant s'exécute", async () => {
  const clock = fakeClock();
  const t = createThrottle({
    minIntervalMs: 500,
    now: clock.now,
    sleep: clock.sleep,
  });

  await expect(t.run(() => Promise.reject(new Error("boom")))).rejects.toThrow(
    "boom",
  );
  await expect(t.run(() => Promise.resolve("suivant"))).resolves.toBe(
    "suivant",
  );
});

test("getOrgThrottle renvoie la même instance par org, distincte entre orgs", () => {
  const a1 = getOrgThrottle("org-a");
  const a2 = getOrgThrottle("org-a");
  const b1 = getOrgThrottle("org-b");
  expect(a1).toBe(a2);
  expect(a1).not.toBe(b1);
});
