import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  __testing,
  readTerminalSnapshotFromGatewayDedupe,
  setGatewayDedupeEntry,
  waitForTerminalGatewayDedupe,
} from "./agent-wait-dedupe.js";

describe("agent wait dedupe helper", () => {
  beforeEach(() => {
    __testing.resetWaiters();
    vi.useFakeTimers();
  });

  afterEach(() => {
    __testing.resetWaiters();
    vi.useRealTimers();
  });

  it("unblocks waiters when a terminal chat dedupe entry is written", async () => {
    const dedupe = new Map();
    const runId = "run-chat-terminal";
    const waiter = waitForTerminalGatewayDedupe({
      dedupe,
      runId,
      timeoutMs: 1_000,
    });

    await Promise.resolve();
    expect(__testing.getWaiterCount(runId)).toBe(1);

    setGatewayDedupeEntry({
      dedupe,
      key: `chat:${runId}`,
      entry: {
        ts: Date.now(),
        ok: true,
        payload: {
          runId,
          status: "ok",
          startedAt: 100,
          endedAt: 200,
        },
      },
    });

    await expect(waiter).resolves.toEqual({
      status: "ok",
      startedAt: 100,
      endedAt: 200,
      error: undefined,
    });
    expect(__testing.getWaiterCount(runId)).toBe(0);
  });

  it("keeps stale chat dedupe blocked while agent dedupe is in-flight", async () => {
    const dedupe = new Map();
    const runId = "run-stale-chat";
    setGatewayDedupeEntry({
      dedupe,
      key: `chat:${runId}`,
      entry: {
        ts: Date.now(),
        ok: true,
        payload: {
          runId,
          status: "ok",
        },
      },
    });
    setGatewayDedupeEntry({
      dedupe,
      key: `agent:${runId}`,
      entry: {
        ts: Date.now(),
        ok: true,
        payload: {
          runId,
          status: "accepted",
        },
      },
    });

    const snapshot = readTerminalSnapshotFromGatewayDedupe({
      dedupe,
      runId,
    });
    expect(snapshot).toBeNull();

    const blockedWait = waitForTerminalGatewayDedupe({
      dedupe,
      runId,
      timeoutMs: 25,
    });
    await vi.advanceTimersByTimeAsync(30);
    await expect(blockedWait).resolves.toBeNull();
    expect(__testing.getWaiterCount(runId)).toBe(0);
  });

  it("uses newer terminal chat snapshot when agent entry is non-terminal", () => {
    const dedupe = new Map();
    const runId = "run-nonterminal-agent-with-newer-chat";
    setGatewayDedupeEntry({
      dedupe,
      key: `agent:${runId}`,
      entry: {
        ts: 100,
        ok: true,
        payload: {
          runId,
          status: "accepted",
        },
      },
    });
    setGatewayDedupeEntry({
      dedupe,
      key: `chat:${runId}`,
      entry: {
        ts: 200,
        ok: true,
        payload: {
          runId,
          status: "ok",
          startedAt: 1,
          endedAt: 2,
        },
      },
    });

    expect(
      readTerminalSnapshotFromGatewayDedupe({
        dedupe,
        runId,
      }),
    ).toEqual({
      status: "ok",
      startedAt: 1,
      endedAt: 2,
      error: undefined,
    });
  });

  it("ignores stale agent snapshots when waiting for an active chat run", async () => {
    const dedupe = new Map();
    const runId = "run-chat-active-ignore-agent";
    setGatewayDedupeEntry({
      dedupe,
      key: `agent:${runId}`,
      entry: {
        ts: Date.now(),
        ok: true,
        payload: {
          runId,
          status: "ok",
        },
      },
    });

    expect(
      readTerminalSnapshotFromGatewayDedupe({
        dedupe,
        runId,
        ignoreAgentTerminalSnapshot: true,
      }),
    ).toBeNull();

    const wait = waitForTerminalGatewayDedupe({
      dedupe,
      runId,
      timeoutMs: 1_000,
      ignoreAgentTerminalSnapshot: true,
    });
    await Promise.resolve();
    expect(__testing.getWaiterCount(runId)).toBe(1);

    setGatewayDedupeEntry({
      dedupe,
      key: `chat:${runId}`,
      entry: {
        ts: Date.now(),
        ok: true,
        payload: {
          runId,
          status: "ok",
          startedAt: 123,
          endedAt: 456,
        },
      },
    });

    await expect(wait).resolves.toEqual({
      status: "ok",
      startedAt: 123,
      endedAt: 456,
      error: undefined,
    });
  });

  it("prefers the freshest terminal snapshot when agent/chat dedupe keys collide", () => {
    const runId = "run-collision";
    const dedupe = new Map();

    setGatewayDedupeEntry({
      dedupe,
      key: `agent:${runId}`,
      entry: {
        ts: 100,
        ok: true,
        payload: { runId, status: "ok", startedAt: 10, endedAt: 20 },
      },
    });
    setGatewayDedupeEntry({
      dedupe,
      key: `chat:${runId}`,
      entry: {
        ts: 200,
        ok: false,
        payload: { runId, status: "error", startedAt: 30, endedAt: 40, error: "chat failed" },
      },
    });

    expect(
      readTerminalSnapshotFromGatewayDedupe({
        dedupe,
        runId,
      }),
    ).toEqual({
      status: "error",
      startedAt: 30,
      endedAt: 40,
      error: "chat failed",
    });

    const dedupeReverse = new Map();
    setGatewayDedupeEntry({
      dedupe: dedupeReverse,
      key: `chat:${runId}`,
      entry: {
        ts: 100,
        ok: true,
        payload: { runId, status: "ok", startedAt: 1, endedAt: 2 },
      },
    });
    setGatewayDedupeEntry({
      dedupe: dedupeReverse,
      key: `agent:${runId}`,
      entry: {
        ts: 200,
        ok: true,
        payload: { runId, status: "timeout", startedAt: 3, endedAt: 4, error: "still running" },
      },
    });

    expect(
      readTerminalSnapshotFromGatewayDedupe({
        dedupe: dedupeReverse,
        runId,
      }),
    ).toEqual({
      status: "timeout",
      startedAt: 3,
      endedAt: 4,
      error: "still running",
    });
  });

  it("resolves multiple waiters for the same run id", async () => {
    const dedupe = new Map();
    const runId = "run-multi";
    const first = waitForTerminalGatewayDedupe({
      dedupe,
      runId,
      timeoutMs: 1_000,
    });
    const second = waitForTerminalGatewayDedupe({
      dedupe,
      runId,
      timeoutMs: 1_000,
    });

    await Promise.resolve();
    expect(__testing.getWaiterCount(runId)).toBe(2);

    setGatewayDedupeEntry({
      dedupe,
      key: `chat:${runId}`,
      entry: {
        ts: Date.now(),
        ok: true,
        payload: { runId, status: "ok" },
      },
    });

    await expect(first).resolves.toEqual(
      expect.objectContaining({
        status: "ok",
      }),
    );
    await expect(second).resolves.toEqual(
      expect.objectContaining({
        status: "ok",
      }),
    );
    expect(__testing.getWaiterCount(runId)).toBe(0);
  });

  it("cleans up waiter registration on timeout", async () => {
    const dedupe = new Map();
    const runId = "run-timeout";
    const wait = waitForTerminalGatewayDedupe({
      dedupe,
      runId,
      timeoutMs: 20,
    });

    await Promise.resolve();
    expect(__testing.getWaiterCount(runId)).toBe(1);

    await vi.advanceTimersByTimeAsync(25);
    await expect(wait).resolves.toBeNull();
    expect(__testing.getWaiterCount(runId)).toBe(0);
  });
});
