#!/usr/bin/env node
"use strict";

const assert = require("node:assert");
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const {
  BASE_URL,
  REQUEST_TIMEOUT_MS,
  UI_TIMEOUT_MS,
  cleanupDocuments,
  createTempDocument,
  ensureAuthenticated,
  renameDocument,
  requestJson,
} = require("./tree-shell-smoke-helpers");

const OUT_DIR = path.join(process.cwd(), "tmp", "acp-multi-session-stability-smoke");
const PROFILE = process.env.MNOTE_ACP_STABILITY_PROFILE || "reasonix";
const RUNTIME = process.env.MNOTE_ACP_STABILITY_RUNTIME || "";
const RUN_COUNT = Math.max(1, Number(process.env.MNOTE_ACP_STABILITY_RUN_COUNT || 3));
const STREAM_TIMEOUT_MS = Number(process.env.MNOTE_ACP_STABILITY_STREAM_TIMEOUT_MS || 60_000);
const CANCEL_INDEX = Math.min(Math.max(0, Number(process.env.MNOTE_ACP_STABILITY_CANCEL_INDEX || 1)), RUN_COUNT - 1);

function nowSuffix() {
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}

async function createHermesSession(request, target, suffix, index) {
  return requestJson(request, "/api/hermes/client/sessions", {
    method: "POST",
    headers: { "x-mnote-actor-id": "acp-smoke-user" },
    data: {
      workspaceId: target.workspaceId,
      documentId: target.documentId,
      title: `ACP 稳定性 ${suffix} #${index + 1}`,
      profile: PROFILE,
      traceId: `trace_acp_session_${suffix}_${index}`,
    },
  });
}

async function createHermesRun(request, target, sessionId, suffix, index) {
  const payload = {
    workspaceId: target.workspaceId,
    documentId: target.documentId,
    sessionId,
    profile: PROFILE,
    message:
      index === CANCEL_INDEX
        ? `请用中文逐条列出 1 到 200 的检查项，每条都包含 ACP 多会话 smoke ${suffix}，不要修改文件。`
        : `请用一句中文回复：ACP 多会话 smoke ${suffix} 第 ${index + 1} 条。不要修改文件。`,
    contextScope: "summary",
    traceId: `trace_acp_run_${suffix}_${index}`,
    actorId: "acp-smoke-user",
  };
  if (RUNTIME) {
    payload.acpRuntime = RUNTIME;
  }
  return requestJson(request, "/api/hermes/client/runs", {
    method: "POST",
    headers: { "x-mnote-actor-id": "acp-smoke-user" },
    data: payload,
  });
}

async function abortRun(request, runId) {
  return requestJson(request, `/api/hermes/client/runs/${encodeURIComponent(runId)}/abort`, {
    method: "POST",
    headers: { "x-mnote-actor-id": "acp-smoke-user" },
    data: { reason: "task488_acp_stability_smoke_cancel" },
  });
}

function parseSseChunk(text, events) {
  for (const frame of text.split(/\n\n+/)) {
    const lines = frame.split(/\n/).filter(Boolean);
    if (lines.length === 0) continue;
    let event = "message";
    const dataLines = [];
    for (const line of lines) {
      if (line.startsWith("event:")) {
        event = line.slice("event:".length).trim() || "message";
      } else if (line.startsWith("data:")) {
        dataLines.push(line.slice("data:".length).trimStart());
      }
    }
    if (dataLines.length === 0) continue;
    const dataText = dataLines.join("\n");
    let data = dataText;
    try {
      data = JSON.parse(dataText);
    } catch {
      // 保留原始文本，便于排障。
    }
    events.push({ event, data });
  }
}

async function cookieHeaderForContext(context) {
  const cookies = await context.cookies(BASE_URL);
  return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
}

async function streamRunEvents(runId, label, cookieHeader) {
  const events = [];
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS);
  try {
    const response = await fetch(`${BASE_URL}/api/hermes/client/events/${encodeURIComponent(runId)}`, {
      headers: {
        "x-mnote-actor-id": "acp-smoke-user",
        ...(cookieHeader ? { cookie: cookieHeader } : {}),
      },
      signal: controller.signal,
    });
    if (!response.ok) {
      const text = await response.text();
      throw new Error(`${label} events 请求失败: ${response.status} ${response.statusText} ${text.slice(0, 1200)}`);
    }
    const decoder = new TextDecoder();
    const reader = response.body.getReader();
    let raw = "";
    let pending = "";
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value, { stream: true });
      raw += chunk;
      pending += chunk;
      const frames = pending.split(/\n\n+/);
      pending = frames.pop() || "";
      if (frames.length > 0) {
        parseSseChunk(frames.join("\n\n"), events);
      }
      if (terminalEvent(events)) {
        await reader.cancel().catch(() => undefined);
        break;
      }
    }
    const rest = decoder.decode();
    if (rest) {
      raw += rest;
      pending += rest;
    }
    if (pending.trim()) {
      parseSseChunk(pending, events);
    }
    if (!terminalEvent(events)) {
      throw new Error(`${label} events 未收到 terminal event`);
    }
    return { runId, status: response.status, events, raw };
  } finally {
    clearTimeout(timeout);
    controller.abort();
  }
}

function terminalEvent(events) {
  return events.find((entry) => entry.event === "run.completed" || entry.event === "run.failed" || entry.event === "run.aborted");
}

async function main() {
  const suffix = nowSuffix();
  const title = `TEST-ACP-STABILITY-${suffix}`;
  const createdIds = [];
  const evidence = {
    ok: false,
    baseUrl: BASE_URL,
    profile: PROFILE,
    runtime: RUNTIME || null,
    runCount: RUN_COUNT,
    cancelIndex: CANCEL_INDEX,
    title,
    sessions: [],
    runs: [],
    streams: [],
  };
  await fs.mkdir(OUT_DIR, { recursive: true });

  const browser = await chromium.launch({
    headless: true,
    args: ["--no-sandbox", "--disable-dev-shm-usage"],
  });
  const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
  const page = await context.newPage();

  try {
    await ensureAuthenticated(page, context.request);
    const target = await createTempDocument(context.request);
    createdIds.push(target.documentId);
    evidence.documentId = target.documentId;
    evidence.workspaceId = target.workspaceId;
    await renameDocument(context.request, target.workspaceId, target.documentId, title);

    const sessions = [];
    for (let index = 0; index < RUN_COUNT; index += 1) {
      const session = await createHermesSession(context.request, target, suffix, index);
      assert(session.sessionId, `session ${index + 1} 缺少 sessionId`);
      sessions.push(session);
      evidence.sessions.push({
        index,
        sessionId: session.sessionId,
        persistence: session.persistence || null,
        profile: session.profile || PROFILE,
      });
    }

    const runs = await Promise.all(
      sessions.map((session, index) => createHermesRun(context.request, target, session.sessionId, suffix, index)),
    );
    for (let index = 0; index < runs.length; index += 1) {
      assert(runs[index].runId, `run ${index + 1} 缺少 runId`);
      evidence.runs.push({
        index,
        runId: runs[index].runId,
        sessionId: runs[index].sessionId,
        persistence: runs[index].persistence || null,
        runtime: runs[index].runtime || null,
      });
    }

    const cookieHeader = await cookieHeaderForContext(context);
    const streamPromises = runs.map((run, index) => streamRunEvents(run.runId, `run ${index + 1}`, cookieHeader));
    await new Promise((resolve) => setTimeout(resolve, 500));
    const abortPayload = await abortRun(context.request, runs[CANCEL_INDEX].runId);
    evidence.abort = {
      runId: runs[CANCEL_INDEX].runId,
      status: abortPayload.status,
      ok: abortPayload.ok,
      events: abortPayload.events || [],
    };

    const streamResults = await Promise.allSettled(streamPromises);
    const rejectedStreams = streamResults
      .map((result, index) => ({ result, index }))
      .filter(({ result }) => result.status === "rejected");
    if (rejectedStreams.length > 0) {
      evidence.streamErrors = rejectedStreams.map(({ result, index }) => ({
        index,
        runId: runs[index].runId,
        message: result.reason instanceof Error ? result.reason.message : String(result.reason),
        stack: result.reason instanceof Error ? result.reason.stack : null,
      }));
      throw new Error(`ACP SSE 读取失败: ${evidence.streamErrors.map((item) => `run ${item.index + 1}: ${item.message}`).join("; ")}`);
    }
    const streams = streamResults.map((result) => result.value);
    for (let index = 0; index < streams.length; index += 1) {
      const stream = streams[index];
      const terminal = terminalEvent(stream.events);
      evidence.streams.push({
        index,
        runId: stream.runId,
        eventCount: stream.events.length,
        eventNames: stream.events.map((entry) => entry.event),
        terminalEvent: terminal ? terminal.event : null,
      });
      await fs.writeFile(
        path.join(OUT_DIR, `events-${index + 1}-${stream.runId}.json`),
        JSON.stringify(stream.events, null, 2),
        "utf8",
      );
      assert(terminal, `run ${index + 1} 必须收到 run.completed、run.failed 或 run.aborted`);
      if (index === CANCEL_INDEX) {
        assert.equal(terminal.event, "run.aborted", "被 abort 的 run 必须以 run.aborted 结束");
      }
    }

    assert.equal(new Set(evidence.sessions.map((item) => item.sessionId)).size, RUN_COUNT, "sessionId 必须互相隔离");
    assert.equal(new Set(evidence.runs.map((item) => item.runId)).size, RUN_COUNT, "runId 必须互相隔离");
    assert(evidence.abort.ok === true, "abort API 必须返回 ok=true");
    evidence.ok = true;
    await fs.writeFile(path.join(OUT_DIR, "result.json"), JSON.stringify(evidence, null, 2), "utf8");
    console.log(JSON.stringify(evidence, null, 2));
  } finally {
    await cleanupDocuments(context.request, createdIds).catch(() => undefined);
    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
  }
}

main().catch(async (error) => {
  await fs.mkdir(OUT_DIR, { recursive: true }).catch(() => undefined);
  await fs
    .writeFile(
      path.join(OUT_DIR, "error.json"),
      JSON.stringify(
        {
          ok: false,
          message: error instanceof Error ? error.message : String(error),
          stack: error instanceof Error ? error.stack : null,
        },
        null,
        2,
      ),
      "utf8",
    )
    .catch(() => undefined);
  console.error(error instanceof Error ? error.stack || error.message : String(error));
  process.exit(1);
});
