import { describe, expect, it } from "vitest";
import { waitForCardReady, type CardReadyDeps } from "./cardReadyGate";

// Deterministic clock: each wait advances virtual time, each sample pops the
// next scripted fingerprint.
function harness(samples: string[], step = 90, checkAbort?: () => string) {
  let clock = 0;
  let index = 0;
  const deps: CardReadyDeps = {
    sampleFingerprint: async () => samples[Math.min(index++, samples.length - 1)],
    wait: async (ms) => {
      clock += ms;
    },
    now: () => clock,
    checkAbort,
  };
  return { deps, sampleCount: () => index, step };
}

describe("waitForCardReady", () => {
  it("returns ready once the card changed and stabilized", async () => {
    const { deps } = harness(["old", "new", "new"]);
    const result = await waitForCardReady(deps, "old", { minStableSamples: 2, maxWaitMs: 900, pollIntervalMs: 90 });
    expect(result.ready).toBe(true);
    expect(result.changed).toBe(true);
    expect(result.stable).toBe(true);
    expect(result.fingerprint).toBe("new");
    expect(result.polls).toBe(3);
  });

  it("keeps polling while the detail still shows the previous artifact", async () => {
    const { deps } = harness(["old", "old", "new", "new"]);
    const result = await waitForCardReady(deps, "old", { minStableSamples: 2 });
    expect(result.ready).toBe(true);
    expect(result.fingerprint).toBe("new");
    expect(result.polls).toBe(4);
  });

  it("proceeds after the budget when content changed but never stabilizes (animation)", async () => {
    // Always different (animated glow): changed but never two-in-a-row equal.
    const animated = ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"];
    const { deps } = harness(animated, 90);
    const result = await waitForCardReady(deps, "old", { minStableSamples: 2, maxWaitMs: 300, pollIntervalMs: 90 });
    expect(result.changed).toBe(true);
    expect(result.stable).toBe(false);
    expect(result.ready).toBe(true); // budget spent, but content did change
  });

  it("proceeds with changed animated content after the configured minimum elapsed time", async () => {
    const animated = ["a1", "a2", "a3", "a4", "a5"];
    const { deps } = harness(animated, 90);
    const result = await waitForCardReady(deps, "old", {
      minStableSamples: 2,
      maxWaitMs: 900,
      pollIntervalMs: 90,
      acceptChangedAfterMs: 180,
    });
    expect(result.ready).toBe(true);
    expect(result.changed).toBe(true);
    expect(result.stable).toBe(false);
    expect(result.polls).toBe(3);
  });

  it("reports not-ready when the detail never changes within budget", async () => {
    const { deps } = harness(["old", "old", "old", "old", "old", "old"], 90);
    const result = await waitForCardReady(deps, "old", { minStableSamples: 2, maxWaitMs: 200, pollIntervalMs: 90 });
    expect(result.changed).toBe(false);
    expect(result.ready).toBe(false);
  });

  it("aborts immediately when checkAbort returns a reason", async () => {
    const { deps } = harness(["new", "new"], 90, () => "ESC gehalten");
    const result = await waitForCardReady(deps, "old", {});
    expect(result.abortReason).toBe("ESC gehalten");
    expect(result.ready).toBe(false);
    expect(result.polls).toBe(0);
  });

  it("ignores empty fingerprints for stability", async () => {
    const { deps } = harness(["", "", "new", "new"], 90);
    const result = await waitForCardReady(deps, "old", { minStableSamples: 2, maxWaitMs: 900 });
    expect(result.ready).toBe(true);
    expect(result.fingerprint).toBe("new");
  });
});
