import { rmSync } from "node:fs";
import { describe, expect, test } from "bun:test";

import { runAgentRunCommand } from "./agentrun";

const RUN_ID = "run_long_poll_fixture";
const agentRunClientYaml = [
  "version: 1",
  "kind: AgentRunConfig",
  "metadata:",
  "  name: agentrun",
  "manager:",
  "  baseUrl: __BASE_URL__",
  "  timeoutMs: 15000",
  "auth:",
  "  env: HWLAB_API_KEY",
  "  file: /tmp/hwlab-api-key.env",
  "  header: Authorization",
  "  scheme: Bearer",
  "client:",
  "  role: render-only",
  "  transport: direct-http",
  "  sessionPolicy:",
  "    tenantId: unidesk",
  "    projectId: test",
  "    providerId: NC01",
  "    backendProfile: codex",
  "    workspaceRef:",
  "      kind: opaque",
  "    executionPolicy:",
  "      sandbox: workspace-write",
  "      approval: never",
  "      timeoutMs: 600000",
  "      network: enabled",
  "      secretScope:",
  "        allowCredentialEcho: false",
].join("\n");

function renderedText(value: Awaited<ReturnType<typeof runAgentRunCommand>>): string {
  if (!("renderedText" in value) || typeof value.renderedText !== "string") throw new Error("expected rendered AgentRun CLI output");
  return value.renderedText;
}

function event(seq: number): Record<string, unknown> {
  return {
    id: `evt_${seq}`,
    seq,
    type: "backend_status",
    payload: { commandId: "cmd_fixture", phase: `phase-${seq}`, message: `event-${seq}` },
  };
}

async function withManager(
  handler: (request: Request) => Response | Promise<Response>,
  action: () => Promise<void>,
): Promise<void> {
  const server = Bun.serve({ port: 0, fetch: handler });
  const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
  const previousKey = process.env.HWLAB_API_KEY;
  const configPath = `/tmp/unidesk-agentrun-events-long-poll-${process.pid}-${Date.now()}.yaml`;
  process.env.AGENTRUN_CLIENT_CONFIG = configPath;
  process.env.HWLAB_API_KEY = "fixture-secret";
  await Bun.write(configPath, agentRunClientYaml.replace("__BASE_URL__", server.url.href.replace(/\/$/u, "")));
  try {
    await action();
  } finally {
    server.stop(true);
    rmSync(configPath, { force: true });
    if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
    else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
    if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
    else process.env.HWLAB_API_KEY = previousKey;
  }
}

describe.serial("AgentRun events manager-held long polling", () => {
  test("forwards default 120s timeout and returns as soon as expect is reached", async () => {
    let requests = 0;
    await withManager(async (request) => {
      requests += 1;
      const url = new URL(request.url);
      expect(url.pathname).toBe(`/api/v1/runs/${RUN_ID}/events`);
      expect(url.searchParams.get("afterSeq")).toBe("100");
      expect(url.searchParams.get("expect")).toBe("2");
      expect(url.searchParams.get("timeoutMs")).toBe("120000");
      expect(url.searchParams.get("limit")).toBe("21");
      await Promise.all([Bun.sleep(10), Bun.sleep(20)]);
      return Response.json({
        ok: true,
        data: {
          items: [event(101), event(102)],
          timedOut: false,
          observed: 2,
          nextAfterSeq: 102,
          completionReason: "expect-reached",
        },
      });
    }, async () => {
      const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "100", "--expect", "2", "-o", "json"]);
      expect(result.ok).toBe(true);
      const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
      expect(payload).toMatchObject({ count: 2, expect: 2, timeoutMs: 120000, timedOut: false, observed: 2, nextAfterSeq: 102, completionReason: "expect-reached" });
      expect(requests).toBe(1);
    });
  });

  test("renders partial and empty timeout responses with an explicit cursor", async () => {
    for (const fixture of [
      { items: [event(201)], observed: 1, nextAfterSeq: 201 },
      { items: [], observed: 0, nextAfterSeq: 200 },
    ]) {
      await withManager((request) => {
        const url = new URL(request.url);
        expect(url.searchParams.get("timeoutMs")).toBe("3000");
        return Response.json({ ok: true, data: { ...fixture, timedOut: true, completionReason: "timeout" } });
      }, async () => {
        const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "200", "--expect", "10", "--timeout", "3s", "-o", "json"]);
        const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
        expect(payload).toMatchObject({ timedOut: true, observed: fixture.observed, nextAfterSeq: fixture.nextAfterSeq, completionReason: "timeout" });
      });
    }
  });

  test("returns concurrent event arrivals in one manager request", async () => {
    let requests = 0;
    await withManager(async () => {
      requests += 1;
      const arrivals: Record<string, unknown>[] = [];
      await Promise.all([
        Bun.sleep(10).then(() => arrivals.push(event(301))),
        Bun.sleep(20).then(() => arrivals.push(event(302))),
      ]);
      return Response.json({ ok: true, data: { items: arrivals, timedOut: false, observed: 2, nextAfterSeq: 302, completionReason: "expect-reached" } });
    }, async () => {
      const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "300", "--expect", "2", "--timeout", "1m", "-o", "json"]);
      const payload = JSON.parse(renderedText(result)) as { items?: Array<{ seq?: number }> };
      expect(payload.items?.map((item) => item.seq)).toEqual([301, 302]);
      expect(requests).toBe(1);
    });
  });

  test("returns immediately when the target is terminal before expect is reached", async () => {
    await withManager(() => Response.json({
      ok: true,
      data: {
        items: [],
        timedOut: false,
        observed: 0,
        nextAfterSeq: 400,
        completionReason: "target-completed",
      },
    }), async () => {
      const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "400", "--expect", "10", "--timeout", "120s", "-o", "json"]);
      const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
      expect(payload).toMatchObject({ timedOut: false, observed: 0, nextAfterSeq: 400, completionReason: "target-completed" });
    });
  });

  test("rejects invalid duration before request and preserves manager errors", async () => {
    let requests = 0;
    await withManager(() => {
      requests += 1;
      return Response.json({ ok: false, failureKind: "manager-unavailable", message: "event notifier unavailable" }, { status: 503 });
    }, async () => {
      const invalid = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "2", "--timeout", "3minutes", "-o", "json"]);
      expect(invalid.ok).toBe(false);
      expect(renderedText(invalid)).toContain("--timeout must use a positive integer duration with ms, s, or m units");
      expect(requests).toBe(0);

      const failed = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "2", "--timeout", "1s", "-o", "json"]);
      expect(failed.ok).toBe(false);
      expect(renderedText(failed)).toContain("event notifier unavailable");
      expect(requests).toBe(1);
    });
  });

  test("fails visibly when a manager ignores the long-poll response contract", async () => {
    await withManager(() => Response.json({ ok: true, data: { items: [], nextAfterSeq: 500 } }), async () => {
      const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "500", "--expect", "2", "--timeout", "1s", "-o", "json"]);
      expect(result.ok).toBe(false);
      expect(renderedText(result)).toContain("schema-mismatch");
      expect(renderedText(result)).toContain("must include boolean timedOut and non-negative observed fields");
    });
  });

  test("accepts 60m and rejects zero, values above 60m, and hour units before request", async () => {
    let requests = 0;
    await withManager((request) => {
      requests += 1;
      const url = new URL(request.url);
      expect(url.searchParams.get("timeoutMs")).toBe("3600000");
      return Response.json({ ok: true, data: { items: [], timedOut: true, observed: 0, nextAfterSeq: 0, completionReason: "timeout" } });
    }, async () => {
      const accepted = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "1", "--timeout", "60m", "-o", "json"]);
      expect(accepted.ok).toBe(true);
      expect(requests).toBe(1);

      for (const invalidDuration of ["0ms", "61m", "1h"]) {
        const invalid = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "1", "--timeout", invalidDuration, "-o", "json"]);
        expect(invalid.ok).toBe(false);
      }
      expect(requests).toBe(1);
    });
  });
});
