import assert from "node:assert/strict";
import test from "node:test";

import type { ActivityRef } from "../src/api/client.ts";

function buildMutableActivityRef(initial: number): { ref: ActivityRef; update: (at: number) => void } {
  const ref: ActivityRef = {
    lastActivityAt: initial,
    lastActivityIso: new Date(initial).toISOString(),
    waitingFor: "test",
    lastEventLabel: null
  };
  return {
    ref,
    update: (at: number) => {
      ref.lastActivityAt = at;
      ref.lastActivityIso = new Date(at).toISOString();
    }
  };
}

function bindSignalFetch(): typeof fetch {
  return (async (_input, init) => {
    return await new Promise<Response>((_, reject) => {
      const signal = init?.signal;
      if (signal) {
        if (signal.aborted) reject(new DOMException("aborted", "AbortError"));
        else signal.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true });
      }
    });
  }) as typeof fetch;
}

test("fetchJson 总超时：未传 activityRef 时 timeoutMs 走总窗口", { timeout: 5000 }, async () => {
  const originalFetch = globalThis.fetch;
  globalThis.fetch = bindSignalFetch();
  try {
    const { fetchJson } = await import("../src/api/client.ts");
    const startedAt = Date.now();
    const result = await fetchJson("/v1/agent/chat", { timeoutMs: 200, timeoutName: "test-total" });
    const elapsed = Date.now() - startedAt;
    assert.equal(result.ok, false, "fetch should fail when no activity is supplied");
    assert.match(result.error ?? "", /超时/u, `expected timeout error, got: ${result.error}`);
    assert.ok(elapsed < 1500, `fetchJson should abort near 200ms but took ${elapsed}ms`);
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test("fetchJson inactivity：activityRef 每 100ms 推一次活动，stop 后 ~2s abort", { timeout: 15000 }, async () => {
  const { ref, update } = buildMutableActivityRef(Date.now());
  const originalFetch = globalThis.fetch;
  globalThis.fetch = bindSignalFetch();

  const interval = setInterval(() => {
    update(Date.now());
  }, 100);

  try {
    const { fetchJson } = await import("../src/api/client.ts");
    const startedAt = Date.now();
    const promise = fetchJson("/v1/agent/chat", { timeoutMs: 2000, timeoutName: "test-inactivity", activityRef: () => ref });
    setTimeout(() => clearInterval(interval), 1800);
    const result = await promise;
    const elapsed = Date.now() - startedAt;
    assert.equal(result.ok, false, "fetch should fail once activity stops");
    assert.match(result.error ?? "", /无新活动/u, `expected inactivity-timeout error, got: ${result.error}`);
    assert.ok(elapsed >= 1800, `should not abort before 1800ms with continuous activity, got ${elapsed}ms`);
    assert.ok(elapsed < 6000, `should abort ~1-2s after activity stops, got ${elapsed}ms`);
  } finally {
    clearInterval(interval);
    globalThis.fetch = originalFetch;
  }
});

test("fetchJson 无 total timeout：10s 持续活动窗口内不会被总上限打掉", { timeout: 30000 }, async () => {
  // HWLAB #795 PR feedback + #802: the per-request inactivity window is the
  // SOLE abort signal. There is no client-side total-timeout ceiling on
  // waitForAgentResult / pollRunnerTrace. A continuous 200ms-cadence
  // activity stream must keep a fetchJson request alive well past the
  // 3s inactivity window set on the test, and the test only ends when
  // the activity stream stops.
  const { ref, update } = buildMutableActivityRef(Date.now());
  const originalFetch = globalThis.fetch;
  globalThis.fetch = bindSignalFetch();

  const interval = setInterval(() => {
    update(Date.now());
  }, 200);

  try {
    const { fetchJson } = await import("../src/api/client.ts");
    const startedAt = Date.now();
    const promise = fetchJson("/v1/agent/chat/result/abc", { timeoutMs: 3000, timeoutName: "trace-poll", activityRef: () => ref });
    setTimeout(() => clearInterval(interval), 10_000);
    const result = await promise;
    const elapsed = Date.now() - startedAt;
    assert.equal(result.ok, false, "fetch should fail once activity stops");
    assert.ok(
      elapsed >= 10_000,
      `continuous 200ms activity must keep the request alive for the full 10s; a total cap or wall-clock shrink would abort earlier; got ${elapsed}ms`
    );
    assert.match(result.error ?? "", /无新活动/u, `expected inactivity-timeout error, got: ${result.error}`);
  } finally {
    clearInterval(interval);
    globalThis.fetch = originalFetch;
  }
});
