import assert from "node:assert/strict";
import { createServer as createHttpServer } from "node:http";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";

import { EventType } from "@ag-ui/core";
import { createAuthenticatedTestClient } from "./lib/authenticated-test-client.mjs";

const root = mkdtempSync(join(tmpdir(), "open-data-foundry-copilotkit-run-"));
const metadataPath = join(root, "metadata.sqlite");
const mastraStoragePath = join(root, "mastra.sqlite");
const workspaceRoot = join(root, "workspaces");

process.env.AUTH_SESSION_SECRET = "copilotkit-run-session-secret-32b!!!";
process.env.AUTH_PUBLIC_BASE_URL = "http://127.0.0.1:3000";
process.env.AUTH_EMAIL_DELIVERY = "test";
process.env.AUTH_REGISTRATION_MODE = "open";
process.env.EMBEDDING_API_KEY = "";
process.env.LLM_PROVIDER = "openai-compatible";
process.env.LLM_MODEL = "copilotkit-smoke-model";
process.env.LLM_API_KEY = "copilotkit-smoke-key";
process.env.MASTRA_STORAGE_PATH = mastraStoragePath;
process.env.MEMORY_EXTRACTION_TIMEOUT_MS = "25";
process.env.METADATA_DB_PATH = metadataPath;
process.env.SECRET_MASTER_KEY = "copilotkit-smoke-secret-master-key";
process.env.STORAGE_ROOT_DIR = root;
process.env.WORKSPACE_ROOT = workspaceRoot;

let llmRequestCount = 0;
const llmRequests = [];
const modelProviderServer = createHttpServer(async (request, response) => {
  const chunks = [];
  for await (const chunk of request) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
  }
  const body = chunks.length > 0 ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
  llmRequestCount += 1;
  llmRequests.push({
    authorization: request.headers.authorization,
    body,
    method: request.method,
    path: request.url
  });

  if (body.stream) {
    response.writeHead(200, {
      "Connection": "close",
      "Content-Type": "text/event-stream; charset=utf-8"
    });
    const latestUserText = [...(body.messages ?? [])].reverse().find((message) => message.role === "user")?.content;
    const hasToolResult = (body.messages ?? []).some((message) => message.role === "tool");
    if (String(latestUserText).includes("需要询问用户") && !hasToolResult) {
      writeStreamChunk(response, body.model, { role: "assistant" });
      writeStreamChunk(response, body.model, {
        tool_calls: [{
          index: 0,
          id: "call_ask_user",
          type: "function",
          function: {
            name: "ask_user",
            arguments: JSON.stringify({
              question: "请选择要分析的数据源",
              options: [{ label: "api-duckdb-demo" }]
            })
          }
        }]
      });
      writeStreamDone(response, body.model, "tool_calls");
      return;
    }
    if (String(latestUserText).includes("使用 copilotkit smoke skill") && !hasToolResult) {
      writeStreamChunk(response, body.model, { role: "assistant" });
      writeStreamChunk(response, body.model, {
        tool_calls: [{
          index: 0,
          id: "call_skill",
          type: "function",
          function: {
            name: "skill",
            arguments: JSON.stringify({ name: "copilotkit-smoke-skill" })
          }
        }]
      });
      writeStreamDone(response, body.model, "tool_calls");
      return;
    }
    if (String(latestUserText).includes("触发运行超时")) {
      await sleep(2000);
      writeStreamChunk(response, body.model, { role: "assistant" });
      writeStreamChunk(response, body.model, { content: "too late" });
      writeStreamDone(response, body.model, "stop");
      return;
    }
    if (!hasToolResult) {
      writeStreamChunk(response, body.model, { role: "assistant" });
      writeStreamChunk(response, body.model, {
        tool_calls: [{
          index: 0,
          id: "call_inspect_schema",
          type: "function",
          function: {
            name: "inspect_schema",
            arguments: JSON.stringify({ datasource_id: "api-duckdb-demo" })
          }
        }]
      });
      writeStreamDone(response, body.model, "tool_calls");
      return;
    }
    writeStreamChunk(response, body.model, { role: "assistant" });
    writeStreamChunk(response, body.model, {
      content: "已检查 api-duckdb-demo 的 schema,可以继续进行只读分析。"
    });
    writeStreamDone(response, body.model, "stop");
    return;
  }

  await sleep(250);
  response.writeHead(200, { "Connection": "close", "Content-Type": "application/json" });
  response.end(JSON.stringify({
    id: `chatcmpl-copilotkit-smoke-${llmRequestCount}`,
    object: "chat.completion",
    created: Math.floor(Date.now() / 1000),
    model: body.model,
    choices: [{
      index: 0,
      message: { role: "assistant", content: "OK" },
      finish_reason: "stop"
    }],
    usage: { prompt_tokens: 8, completion_tokens: 1, total_tokens: 9 }
  }));
});

await new Promise((resolve) => modelProviderServer.listen(0, "127.0.0.1", resolve));
const modelProviderAddress = modelProviderServer.address();
assert(modelProviderAddress && typeof modelProviderAddress === "object");
process.env.LLM_BASE_URL = `http://127.0.0.1:${modelProviderAddress.port}`;

const { createServer } = await import("../apps/api/dist/server.js");
const { createMetadataStore } = await import("../packages/metadata/dist/index.js");

const server = await createServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert(address && typeof address === "object");
const baseUrl = `http://127.0.0.1:${address.port}`;

const client = createAuthenticatedTestClient({ baseUrl });
const identity = await client.registerAndLogin({ displayName: "CopilotKit Run Smoke" });
const { userId } = identity;

try {
  const demoDbPath = join(root, "api-duckdb-demo.sqlite");
  const demoDb = new DatabaseSync(demoDbPath);
  demoDb.exec(`
    CREATE TABLE orders (id INTEGER, amount REAL);
    INSERT INTO orders VALUES (1, 10.5), (2, 20);
  `);
  demoDb.close();
  const demoDatasource = await client.fetch("/api/v1/datasources", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      id: "api-duckdb-demo",
      name: "CopilotKit Smoke Demo",
      type: "sqlite",
      settings: { filePath: demoDbPath }
    })
  });
  assert.equal(demoDatasource.status, 201, await demoDatasource.text());

  const skillForm = new FormData();
  skillForm.set("file", new Blob([
    "---\n",
    "name: copilotkit-smoke-skill\n",
    "description: Use for CopilotKit smoke skill runs.\n",
    "version: 1.0.0\n",
    "tags: [copilotkit, smoke]\n",
    "allowed-tools: [inspect_schema]\n",
    "---\n",
    "Use this skill to prove Mastra skill tool loading works in the AG-UI runtime.\n"
  ], { type: "text/markdown" }), "SKILL.md");
  const skillUploadResponse = await client.fetch("/api/v1/skills", { method: "POST", body: skillForm });
  assert.equal(skillUploadResponse.status, 201);
  const skillUpload = await skillUploadResponse.json();
  assert.equal(skillUpload.data.validationStatus, "valid");

  const timeoutProfileResponse = await client.fetch("/api/v1/model-profiles", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      id: "copilotkit-timeout-profile",
      name: "CopilotKit Timeout Profile",
      provider: "openai-compatible",
      modelName: "copilotkit-smoke-model",
      baseUrl: `http://127.0.0.1:${modelProviderAddress.port}`,
      credentials: { apiKey: "copilotkit-smoke-key" },
      timeoutMs: 1000
    })
  });
  assert.equal(timeoutProfileResponse.status, 201);

  const badModelRunId = `copilotkit-bad-model-run-${Date.now()}`;
  const badModelThreadId = `copilotkit-bad-model-thread-${Date.now()}`;
  const badModelEvents = await runCopilotKitAgent(baseUrl, {
    threadId: badModelThreadId,
    runId: badModelRunId,
    parentRunId: undefined,
    state: {},
    messages: [{
      id: "user-message-bad-model",
      role: "user",
      content: "这条错误模型请求应该保留并显示失败。"
    }],
    tools: [],
    context: [],
    forwardedProps: {
      run_config: {
        activeDatasourceId: "api-duckdb-demo",
        activeLlmProfileId: "copilotkit-missing-profile",
        enabledDatasourceIds: ["api-duckdb-demo"],
        enabledKnowledgeIds: [],
        enabledMcpServerIds: [],
        enabledSkillIds: []
      }
    }
  });
  assert(
    badModelEvents.some((event) => event.type === EventType.RUN_ERROR),
    "Missing model profile should emit RUN_ERROR"
  );
  assertRunStatusDelta(badModelEvents, "failed");
  const badModelConversationResponse = await client.fetch(
    `/api/v1/sessions/${badModelThreadId}/conversation?limit=10`
  );
  assert.equal(badModelConversationResponse.status, 200);
  const badModelConversation = await badModelConversationResponse.json();
  assert(
    badModelConversation.data.messages.some((message) =>
      message.runId === badModelRunId
      && message.role === "user"
      && message.contentText === "这条错误模型请求应该保留并显示失败。"
    ),
    `Early model config failure should persist the failed user turn: ${JSON.stringify(badModelConversation.data.messages)}`
  );

  const runId = `copilotkit-run-smoke-${Date.now()}`;
  const threadId = `copilotkit-thread-smoke-${Date.now()}`;
  const events = await runCopilotKitAgent(baseUrl, {
    threadId,
    runId,
    parentRunId: undefined,
    state: {},
    messages: [{
      id: "user-message-1",
      role: "user",
      content: "请先检查 api-duckdb-demo 的 schema,然后简要说明结果。"
    }],
    tools: [],
    context: [],
    forwardedProps: {
      run_config: {
        activeDatasourceId: "api-duckdb-demo",
        activeLlmProfileId: "server-default",
        enabledDatasourceIds: ["api-duckdb-demo"],
        enabledKnowledgeIds: [],
        enabledMcpServerIds: [],
        enabledSkillIds: []
      }
    }
  });

  assert(events.length > 0, "CopilotKit run should emit events");
  if (events[events.length - 1]?.type !== EventType.RUN_FINISHED) {
    console.error("Unexpected normal run events:", JSON.stringify(events, null, 2));
  }
  assert.equal(events[events.length - 1]?.type, EventType.RUN_FINISHED, "RUN_FINISHED should be the last event");
  assertEventOrder(events, EventType.RUN_STARTED, EventType.RUN_FINISHED);
  assertRunStatusCompletedBeforeFinish(events);
  assertNoDuplicatePlanSnapshots(events);
  assertToolResultsBeforeTerminal(events);
  assertMemoryExtractionTimeoutBeforeTerminal(events);
  assert(events.some((event) => event.type === EventType.TOOL_CALL_END), "Expected at least one tool call");
  assert(events.some((event) => event.type === EventType.TOOL_CALL_RESULT), "Expected at least one tool result");
  assert.equal(
    events.some((event) => event.type === EventType.CUSTOM && event.name === "memory.long-term.extracted"),
    false,
    "Timed-out completed-run memory extraction should not emit late extracted event"
  );
  assert.equal(llmRequests[0]?.path, "/chat/completions");
  assert.equal(llmRequests[0]?.authorization, "Bearer copilotkit-smoke-key");
  assert(llmRequests.length >= 2, "The fake model should be called once for tool call and once for final answer");

  const branchParentSessionId = `copilotkit-branch-parent-${Date.now()}`;
  const branchParentRunId = `copilotkit-branch-parent-run-${Date.now()}`;
  const branchSeedStore = createMetadataStore({ database_path: metadataPath });
  try {
    branchSeedStore.sessions.create({
      user_id: userId,
      id: branchParentSessionId,
      title: "CopilotKit branch smoke"
    });
    branchSeedStore.runs.create({
      user_id: userId,
      id: branchParentRunId,
      session_id: branchParentSessionId,
      user_input: "请分析订单。",
      status: "completed"
    });
    branchSeedStore.conversationMessages.append({
      user_id: userId,
      session_id: branchParentSessionId,
      run_id: branchParentRunId,
      id: `${branchParentRunId}:user`,
      role: "user",
      source: "client",
      message_id: `${branchParentRunId}:frontend-user`,
      content_text: "请分析订单。",
      content: { text: "请分析订单。" }
    });
    branchSeedStore.conversationMessages.append({
      user_id: userId,
      session_id: branchParentSessionId,
      run_id: branchParentRunId,
      id: `${branchParentRunId}:assistant`,
      role: "assistant",
      source: "agent",
      message_id: `${branchParentRunId}:assistant`,
      content_text: "订单分析已完成。",
      content: { text: "订单分析已完成。" }
    });
    branchSeedStore.sessions.touchLastMessage({
      user_id: userId,
      session_id: branchParentSessionId
    });
  } finally {
    branchSeedStore.close();
  }
  const branchResponse = await client.fetch(`/api/v1/sessions/${branchParentSessionId}/branches`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ runId: branchParentRunId })
  });
  const branchPayload = await branchResponse.json();
  assert.equal(branchResponse.status, 201, JSON.stringify(branchPayload));
  const branchThreadId = branchPayload.data.session.id;
  const branchRunId = `copilotkit-branch-run-${Date.now()}`;
  const branchEvents = await runCopilotKitAgent(baseUrl, {
    threadId: branchThreadId,
    runId: branchRunId,
    parentRunId: branchParentRunId,
    state: {},
    messages: [{
      id: "user-message-branch",
      role: "user",
      content: "请把订单分析改成表格。"
    }],
    tools: [],
    context: [],
    forwardedProps: {
      run_config: {
        activeDatasourceId: "api-duckdb-demo",
        activeLlmProfileId: "server-default",
        enabledDatasourceIds: ["api-duckdb-demo"],
        enabledKnowledgeIds: [],
        enabledMcpServerIds: [],
        enabledSkillIds: []
      }
    }
  });
  assert.equal(
    branchEvents.some((event) => event.type === EventType.RUN_ERROR && String(event.message).includes("Thread")),
    false,
    `Branch session run should create its working-memory thread before Mastra run: ${JSON.stringify(branchEvents)}`
  );
  assert.equal(branchEvents[branchEvents.length - 1]?.type, EventType.RUN_FINISHED);

  const skillRunId = `copilotkit-skill-run-smoke-${Date.now()}`;
  const skillThreadId = `copilotkit-skill-thread-smoke-${Date.now()}`;
  const skillEvents = await runCopilotKitAgent(baseUrl, {
    threadId: skillThreadId,
    runId: skillRunId,
    parentRunId: undefined,
    state: {},
    messages: [{
      id: "user-message-skill",
      role: "user",
      content: "请使用 copilotkit smoke skill,然后说明它已经加载。"
    }],
    tools: [],
    context: [],
    forwardedProps: {
      run_config: {
        activeDatasourceId: "api-duckdb-demo",
        activeLlmProfileId: "server-default",
        enabledDatasourceIds: ["api-duckdb-demo"],
        enabledKnowledgeIds: [],
        enabledMcpServerIds: [],
        skill_mode: "auto",
        skill_tags: ["copilotkit"]
      }
    }
  });
  assert.equal(skillEvents[skillEvents.length - 1]?.type, EventType.RUN_FINISHED);
  assert(
    skillEvents.some((event) => event.type === EventType.CUSTOM && event.name === "skill.selection"),
    "Skill run should emit skill.selection"
  );
  assert(
    skillEvents.some((event) => event.type === EventType.TOOL_CALL_END && event.toolCallId === "call_skill"),
    "Skill run should call the Mastra skill tool"
  );
  assert(
    skillEvents.some((event) => event.type === EventType.TOOL_CALL_RESULT && event.toolCallId === "call_skill"),
    "Skill run should emit a skill tool result"
  );

  const timeoutRunId = `copilotkit-timeout-run-${Date.now()}`;
  const timeoutThreadId = `copilotkit-timeout-thread-${Date.now()}`;
  const timeoutEvents = await runCopilotKitAgent(baseUrl, {
    threadId: timeoutThreadId,
    runId: timeoutRunId,
    parentRunId: undefined,
    state: {},
    messages: [{
      id: "user-message-timeout",
      role: "user",
      content: "触发运行超时。"
    }],
    tools: [],
    context: [],
    forwardedProps: {
      run_config: {
        activeDatasourceId: "api-duckdb-demo",
        activeLlmProfileId: "copilotkit-timeout-profile",
        enabledDatasourceIds: ["api-duckdb-demo"],
        enabledKnowledgeIds: [],
        enabledMcpServerIds: [],
        enabledSkillIds: []
      }
    }
  });
  assert.equal(timeoutEvents.some((event) => event.type === EventType.RUN_FINISHED), false);
  assert.equal(timeoutEvents[timeoutEvents.length - 1]?.type, EventType.RUN_ERROR);
  assert.equal(timeoutEvents[timeoutEvents.length - 1]?.message, "RUN_TIMEOUT:1000");
  assertRunStatusDelta(timeoutEvents, "failed");

  const store = createMetadataStore({ database_path: metadataPath });
  try {
    const persisted = store.runEvents.listByRun({ user_id: userId, run_id: runId });
    assert(persisted.length > 0, "DataFoundryAgUiAgent.run should persist AG-UI events");
    const persistedEvents = persisted.map((item) => JSON.parse(item.payload_json));
    assert.equal(persistedEvents[persistedEvents.length - 1]?.type, EventType.RUN_FINISHED);
    assertRunStatusCompletedBeforeFinish(persistedEvents);
  } finally {
    store.close();
  }

  const replayEvents = await connectCopilotKitAgent(baseUrl, threadId);
  assert(replayEvents.length > 0, "CopilotKit connect should replay persisted runtime events");
  assert.equal(
    replayEvents.filter((event) => event.type === EventType.RUN_FINISHED).length,
    1,
    "Replay should include one terminal event"
  );
  assert.equal(replayEvents[replayEvents.length - 1]?.type, EventType.RUN_FINISHED);

  const suspendedRunId = `copilotkit-suspended-smoke-${Date.now()}`;
  const suspendedThreadId = `copilotkit-suspended-thread-${Date.now()}`;
  const suspendedEvents = await runCopilotKitAgent(baseUrl, {
    threadId: suspendedThreadId,
    runId: suspendedRunId,
    parentRunId: undefined,
    state: {},
    messages: [{
      id: "user-message-suspended",
      role: "user",
      content: "需要询问用户之后再继续。"
    }],
    tools: [],
    context: [],
    forwardedProps: {
      run_config: {
        activeDatasourceId: "api-duckdb-demo",
        activeLlmProfileId: "server-default",
        enabledDatasourceIds: ["api-duckdb-demo"],
        enabledKnowledgeIds: [],
        enabledMcpServerIds: [],
        enabledSkillIds: []
      }
    }
  });
  assert(
    suspendedEvents.some((event) => event.type === EventType.CUSTOM && event.name === "interaction.requested"),
    "Suspended run should emit interaction.requested"
  );
  assertRunStatusDelta(suspendedEvents, "suspended");
  assert.equal(
    suspendedEvents.some((event) => event.type === EventType.CUSTOM && event.name === "memory.long-term.extracted"),
    false,
    "Suspended run should not emit completed-run memory extraction"
  );

  const suspendedStore = createMetadataStore({ database_path: metadataPath });
  try {
    const suspendedRun = suspendedStore.runs.get({ user_id: userId, run_id: suspendedRunId });
    assert.equal(suspendedRun.status, "suspended");
    const suspendedPersistedEvents = suspendedStore.runEvents
      .listByRun({ user_id: userId, run_id: suspendedRunId })
      .map((item) => JSON.parse(item.payload_json));
    assert.equal(
      suspendedPersistedEvents.some((event) => event.type === EventType.RUN_FINISHED),
      false,
      `Suspended run should not persist RUN_FINISHED; persisted=${suspendedPersistedEvents
        .map((event) => event.type)
        .join(",")}`
    );
    assertRunStatusDelta(suspendedPersistedEvents, "suspended");
    const suspendedMessages = suspendedStore.conversationMessages.listRecent({
      user_id: userId,
      session_id: suspendedThreadId,
      limit: 20
    });
    assert.equal(
      suspendedMessages.some((message) => message.run_id === suspendedRunId && message.role === "assistant"),
      false,
      "Suspended run should not flush partial assistant memory"
    );
  } finally {
    suspendedStore.close();
  }

  console.log(
    "CopilotKit run smoke OK: endpoint run, branch thread creation, bad model failure persistence, terminal order, tool results, persistence, replay, suspended state, run timeout"
  );
} finally {
  await closeHttpServer(server);
  await closeHttpServer(modelProviderServer);
}

process.exit(0);

function writeStreamChunk(response, model, delta) {
  response.write(`data: ${JSON.stringify({
    id: `chatcmpl-copilotkit-smoke-${llmRequestCount}`,
    object: "chat.completion.chunk",
    created: Math.floor(Date.now() / 1000),
    model,
    choices: [{ index: 0, delta, finish_reason: null }]
  })}\n\n`);
}

function writeStreamDone(response, model, finishReason) {
  response.write(`data: ${JSON.stringify({
    id: `chatcmpl-copilotkit-smoke-${llmRequestCount}`,
    object: "chat.completion.chunk",
    created: Math.floor(Date.now() / 1000),
    model,
    choices: [{ index: 0, delta: {}, finish_reason: finishReason }]
  })}\n\n`);
  response.end("data: [DONE]\n\n");
}

async function runCopilotKitAgent(_baseUrl, input) {
  return readAgUiEventStream(await client.fetch("/api/copilotkit", {
    method: "POST",
    headers: {
      "Accept": "text/event-stream",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      method: "agent/run",
      params: { agentId: "dataFoundry" },
      body: input
    })
  }));
}

async function connectCopilotKitAgent(_baseUrl, threadId) {
  return readAgUiEventStream(await client.fetch("/api/copilotkit", {
    method: "POST",
    headers: {
      "Accept": "text/event-stream",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      method: "agent/connect",
      params: { agentId: "dataFoundry" },
      body: {
        threadId,
        runId: `connect-${threadId}`,
        state: {},
        messages: [],
        tools: [],
        context: [],
        forwardedProps: {}
      }
    })
  }));
}

async function readAgUiEventStream(response) {
  if (response.status !== 200) {
    throw new Error(`Unexpected CopilotKit status: ${response.status} ${await response.text()}`);
  }
  assert(response.headers.get("content-type")?.includes("text/event-stream"), "Expected AG-UI event stream");
  const text = await response.text();
  return text
    .split("\n\n")
    .map((chunk) => chunk.trim())
    .filter((chunk) => chunk.startsWith("data: "))
    .map((chunk) => JSON.parse(chunk.slice("data: ".length)));
}

function assertEventOrder(events, beforeType, afterType) {
  const beforeIndex = events.findIndex((event) => event.type === beforeType);
  const afterIndex = events.findIndex((event) => event.type === afterType);
  assert(beforeIndex >= 0, `${beforeType} should be present`);
  assert(afterIndex >= 0, `${afterType} should be present`);
  assert(beforeIndex < afterIndex, `${beforeType} should be before ${afterType}`);
}

function assertRunStatusCompletedBeforeFinish(events) {
  const finishIndex = events.findIndex((event) => event.type === EventType.RUN_FINISHED);
  const completedIndex = events.findIndex((event) =>
    event.type === EventType.STATE_DELTA
    && Array.isArray(event.delta)
    && event.delta.some((op) => op.path === "/runStatus" && op.value === "completed")
  );
  assert(completedIndex >= 0, "runStatus=completed delta should be emitted");
  assert(finishIndex >= 0, "RUN_FINISHED should be emitted");
  assert(completedIndex < finishIndex, "runStatus=completed delta should be before RUN_FINISHED");
}

function assertRunStatusDelta(events, status) {
  assert(
    events.some((event) =>
      event.type === EventType.STATE_DELTA
      && Array.isArray(event.delta)
      && event.delta.some((op) => op.path === "/runStatus" && op.value === status)
    ),
    `runStatus=${status} delta should be emitted`
  );
}

function assertNoDuplicatePlanSnapshots(events) {
  const planSnapshots = events.filter((event) => event.type === EventType.ACTIVITY_SNAPSHOT
    && event.activityType === "PLAN");
  const keys = planSnapshots.map((event) => JSON.stringify(event.content));
  assert.equal(new Set(keys).size, keys.length, "PLAN snapshots should not be duplicated");
}

function assertToolResultsBeforeTerminal(events) {
  const terminalIndex = events.findIndex((event) =>
    event.type === EventType.RUN_FINISHED || event.type === EventType.RUN_ERROR
  );
  assert(terminalIndex >= 0, "Terminal event should be emitted");
  const resultByToolCallId = new Set(
    events.slice(0, terminalIndex)
      .filter((event) => event.type === EventType.TOOL_CALL_RESULT && event.toolCallId)
      .map((event) => event.toolCallId)
  );
  const missing = events.slice(0, terminalIndex)
    .filter((event) => event.type === EventType.TOOL_CALL_END && event.toolCallId)
    .map((event) => event.toolCallId)
    .filter((toolCallId) => !resultByToolCallId.has(toolCallId));
  assert.deepEqual(missing, [], "Every TOOL_CALL_END should have TOOL_CALL_RESULT before terminal event");
}

function assertMemoryExtractionTimeoutBeforeTerminal(events) {
  const timeoutIndex = events.findIndex((event) =>
    event.type === EventType.CUSTOM && event.name === "memory.completed-flush.timeout"
  );
  const finishIndex = events.findIndex((event) => event.type === EventType.RUN_FINISHED);
  assert(timeoutIndex >= 0, "Slow completed-run memory extraction should emit timeout event");
  assert(finishIndex >= 0, "RUN_FINISHED should be emitted");
  assert(timeoutIndex < finishIndex, "Memory extraction timeout event should be before RUN_FINISHED");
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function closeHttpServer(httpServer) {
  await new Promise((resolve, reject) => {
    httpServer.close((error) => error ? reject(error) : resolve());
    setImmediate(() => httpServer.closeAllConnections?.());
  });
  httpServer.unref?.();
}