import { describe, expect, test } from "bun:test";
import { parseStrictDuration, readOnlyLongPollTransportTimeoutMs, runReadOnlyLongPoll } from "./read-only-long-poll";

describe("read-only long poll", () => {
  test("returns an immediate terminal observation", async () => {
    const result = await runReadOnlyLongPoll({ observe: async () => "done", completed: (value) => value === "done", waitForChange: async () => { throw new Error("unexpected wait"); }, cursor: (value) => value });
    expect(result).toMatchObject({ completed: true, timedOut: false, observations: 1, cursor: "done" });
  });

  test("returns after a backend-held change", async () => {
    let now = 0;
    let state = "running";
    const result = await runReadOnlyLongPoll({
      observe: async () => state,
      completed: (value) => value === "done",
      waitForChange: async () => { now += 25; state = "done"; },
      cursor: (value) => value,
      timeoutMs: 100,
      clock: { now: () => now },
    });
    expect(result).toMatchObject({ completed: true, timedOut: false, elapsedMs: 25, observations: 2 });
  });

  test("returns the current state after an empty timeout", async () => {
    let now = 0;
    const result = await runReadOnlyLongPoll({
      observe: async () => ({ state: "running", id: "run-1" }),
      completed: () => false,
      waitForChange: async (remainingMs) => { now += remainingMs; },
      cursor: (value) => value.id,
      timeoutMs: 100,
      clock: { now: () => now },
    });
    expect(result).toMatchObject({ completed: false, timedOut: true, cancelled: false, elapsedMs: 100, cursor: "run-1" });
  });

  test("preserves backend errors and cancellation", async () => {
    await expect(runReadOnlyLongPoll({ observe: async () => "running", completed: () => false, waitForChange: async () => { throw new Error("watch disconnected"); }, cursor: (value) => value, timeoutMs: 10 })).rejects.toThrow("watch disconnected");
    const controller = new AbortController();
    controller.abort();
    const cancelled = await runReadOnlyLongPoll({ observe: async () => "running", completed: () => false, waitForChange: async () => {}, cursor: (value) => value, signal: controller.signal });
    expect(cancelled).toMatchObject({ completed: false, timedOut: false, cancelled: true });
  });

  test("validates strict duration syntax", () => {
    expect(parseStrictDuration("120s")).toBe(120_000);
    expect(parseStrictDuration("250ms")).toBe(250);
    expect(() => parseStrictDuration("1.5s")).toThrow();
    expect(() => parseStrictDuration("120")).toThrow();
    expect(() => parseStrictDuration("0s")).toThrow();
    expect(readOnlyLongPollTransportTimeoutMs(60 * 60_000)).toBe(60 * 60_000 + 15_000);
  });
});
