"use strict";

const { chromium } = require("playwright");
const {
  BASE_URL,
  REQUEST_TIMEOUT_MS,
  UI_TIMEOUT_MS,
  assert,
  createTempDocument,
  ensureAuthenticated,
  openDocument,
  purgeDocument,
  renameDocument,
  requestJson,
} = require("./tree-shell-smoke-helpers");

const AI_TIMEOUT_MS = Number(process.env.MNOTE_AI_SMOKE_TIMEOUT_MS || 180_000);

function parseJsonSafely(text) {
  try {
    return text ? JSON.parse(text) : null;
  } catch {
    return text;
  }
}

function parseSseText(rawText) {
  return String(rawText || "")
    .split(/\n\n+/)
    .map((chunk) => chunk.trim())
    .filter(Boolean)
    .filter((chunk) => !chunk.startsWith(":"))
    .map((chunk) => {
      const lines = chunk.split(/\r?\n/);
      let type = "message";
      const dataLines = [];
      for (const line of lines) {
        if (line.startsWith("event:")) {
          type = line.slice("event:".length).trim() || "message";
          continue;
        }
        if (line.startsWith("data:")) {
          dataLines.push(line.slice("data:".length).trimStart());
        }
      }
      const dataText = dataLines.join("\n");
      return {
        type,
        dataText,
        data: parseJsonSafely(dataText),
      };
    });
}

function serializeForError(value) {
  try {
    return JSON.stringify(value, null, 2);
  } catch {
    return String(value);
  }
}

async function requestSseEvents(requestContext, path, body) {
  const response = await requestContext.fetch(`${BASE_URL}${path}`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
    },
    data: body,
    timeout: AI_TIMEOUT_MS,
  });

  const text = await response.text();
  if (!response.ok()) {
    throw new Error(`${path} 请求失败: ${response.status()} ${response.statusText()} ${text.slice(0, 600)}`);
  }

  const contentType = response.headers()["content-type"] || "";
  assert(contentType.includes("text/event-stream"), `${path} 未返回 SSE：${contentType}`);

  return parseSseText(text);
}

async function fetchPageAggregate(requestContext, documentId, workspaceId) {
  return await requestJson(
    requestContext,
    `/api/documents/page?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,
    { method: "GET" },
  );
}

async function fetchDocumentMeta(requestContext, documentId, workspaceId) {
  return await requestJson(
    requestContext,
    `/api/documents/meta?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,
    { method: "GET" },
  );
}

async function fetchDocumentContent(requestContext, documentId, workspaceId) {
  return await requestJson(
    requestContext,
    `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,
    { method: "GET" },
  );
}

function buildAiContextFromPageAggregate(pagePayload) {
  const page = pagePayload?.page ?? null;
  const pageSubtree = page?.tree?.pageSubtree ?? null;
  const subtreeNodes = Array.isArray(pageSubtree?.subtree?.nodes)
    ? pageSubtree.subtree.nodes.slice(0, 160)
    : [];

  return {
    documentId: page?.identity?.documentId ?? null,
    documentBlocks: page?.body?.content ?? null,
    node: pageSubtree?.rootNode ?? null,
    subtree: pageSubtree
      ? {
          projectionId: pageSubtree.projectionId ?? null,
          rootNodeId: pageSubtree?.subtree?.rootNodeId ?? null,
          stats: pageSubtree?.stats ?? null,
          nodes: subtreeNodes,
        }
      : null,
    outline: Array.isArray(pageSubtree?.outline) ? pageSubtree.outline.slice(0, 40) : null,
    evidence: Array.isArray(pageSubtree?.evidence) ? pageSubtree.evidence.slice(0, 32) : null,
    pageOptions: page?.layout?.pageOptions ?? {},
  };
}

function findSuccessfulToolResult(events, toolName) {
  return [...events]
    .reverse()
    .find(
      (event) =>
        event?.type === "tool_result" &&
        event?.data &&
        event.data.tool === toolName &&
        event.data.ok === true,
    ) ?? null;
}

function findErrorEvent(events) {
  return events.find((event) => event?.type === "error") ?? null;
}

function extractRenamedTitleFromSlashToolResult(toolEvent, documentId) {
  if (!toolEvent?.data?.result || toolEvent?.data?.tool !== "slash_run") {
    return null;
  }
  const resultRecord =
    toolEvent.data.result && typeof toolEvent.data.result === "object"
      ? toolEvent.data.result
      : null;
  const parsed =
    resultRecord?.parsed && typeof resultRecord.parsed === "object"
      ? resultRecord.parsed
      : null;
  const params =
    parsed?.params && typeof parsed.params === "object"
      ? parsed.params
      : null;
  if (!parsed || parsed.command !== "rename_doc" || !params) {
    return null;
  }
  if (String(params.documentId ?? "") !== documentId) {
    return null;
  }
  const title = String(params.title ?? "").trim();
  return title || null;
}

function extractBodySnapshotFromToolResult(toolEvent) {
  if (!toolEvent?.data?.result || !["doc_insert_blocks", "doc_replace_range"].includes(toolEvent?.data?.tool)) {
    return null;
  }
  const resultRecord =
    toolEvent.data.result && typeof toolEvent.data.result === "object"
      ? toolEvent.data.result
      : null;
  return resultRecord && Array.isArray(resultRecord.data) ? resultRecord.data : null;
}

function extractInsertedBlockIdFromToolResult(toolEvent) {
  if (!toolEvent?.data?.result || toolEvent?.data?.tool !== "doc_insert_blocks") {
    return null;
  }
  const resultRecord =
    toolEvent.data.result && typeof toolEvent.data.result === "object"
      ? toolEvent.data.result
      : null;
  const inserted = resultRecord && Array.isArray(resultRecord.inserted) ? resultRecord.inserted : null;
  const blockId = typeof inserted?.[0] === "string" ? inserted[0].trim() : "";
  return blockId || null;
}

async function persistAiTitleResult(requestContext, fixture, toolEvent) {
  const renamedTitle = extractRenamedTitleFromSlashToolResult(toolEvent, fixture.documentId);
  assert(renamedTitle, `slash_run 结果未产出当前页标题：${serializeForError(toolEvent)}`);
  await renameDocument(requestContext, fixture.workspaceId, fixture.documentId, renamedTitle);
  return renamedTitle;
}

async function persistAiBodyResult(requestContext, fixture, toolEvent) {
  const nextBlocks = extractBodySnapshotFromToolResult(toolEvent);
  assert(Array.isArray(nextBlocks), `文档写工具未返回 legacy blocks 快照：${serializeForError(toolEvent)}`);

  const aggregate = await fetchPageAggregate(requestContext, fixture.documentId, fixture.workspaceId);
  await requestJson(requestContext, "/api/documents/save", {
    method: "POST",
    data: {
      documentId: fixture.documentId,
      workspaceId: fixture.workspaceId,
      // task111 对齐正式 page body save：正文写工具结果只接受 legacy blocks 快照。
      content: nextBlocks,
      revision: aggregate?.page?.body?.revision ?? null,
      conflictDetectionKey: aggregate?.page?.body?.conflictDetectionKey ?? null,
      snapshotCapturedAt: new Date().toISOString(),
      blockCount: nextBlocks.length,
    },
  });
  return nextBlocks;
}

async function waitFor(check, description, timeoutMs = UI_TIMEOUT_MS, intervalMs = 500) {
  const deadline = Date.now() + timeoutMs;
  let lastValue = null;
  while (Date.now() < deadline) {
    lastValue = await check();
    if (lastValue) {
      return lastValue;
    }
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
  throw new Error(`${description} 超时，最后结果：${serializeForError(lastValue)}`);
}

async function waitForPageAggregateBodyText(requestContext, fixture, expectedText) {
  await waitFor(
    async () => {
      const pagePayload = await fetchPageAggregate(requestContext, fixture.documentId, fixture.workspaceId);
      const raw = JSON.stringify(pagePayload?.page?.body?.content ?? null);
      return raw.includes(expectedText) ? pagePayload : null;
    },
    `等待 page aggregate 正文同步到 ${expectedText}`,
    AI_TIMEOUT_MS,
  );
}

async function waitForRuntimeIsland(page) {
  await page.waitForFunction(
    () => {
      const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
      const editor = host?.querySelector(".editor-surface .ProseMirror[contenteditable='true']");
      return (
        host?.getAttribute("data-editor-host-kind") === "leptos_tiptap_island" &&
        host?.getAttribute("data-runtime-editor-status") !== "error" &&
        editor instanceof HTMLElement &&
        editor.isContentEditable
      );
    },
    null,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForTitleInput(page) {
  const input = page.getByLabel("页面标题");
  await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  return input;
}

async function waitForEditorText(page, expectedText) {
  await page.waitForFunction(
    (text) => {
      const editor = document.querySelector(
        '[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror',
      );
      return (editor?.textContent ?? "").includes(text);
    },
    expectedText,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function readVisibleTitle(page) {
  const input = page.getByLabel("页面标题");
  if (await input.isVisible().catch(() => false)) {
    return ((await input.inputValue().catch(() => "")) || "").trim();
  }
  const heading = page.locator("h1").first();
  return ((await heading.textContent().catch(() => "")) || "").trim();
}

async function readEditorText(page) {
  return await page.evaluate(() => {
    const editor = document.querySelector(
      '[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror',
    );
    return (editor?.textContent ?? "").trim();
  });
}

async function typeIntoEditor(page, text) {
  const editor = page
    .locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
    .first();
  await editor.click({ timeout: UI_TIMEOUT_MS });
  await page.keyboard.press("Control+a");
  await page.keyboard.press("Backspace");
  await page.keyboard.type(text, { delay: 20 });
}

async function waitForPersistedSave(page, saveRequests, documentId, expectedText) {
  const deadline = Date.now() + UI_TIMEOUT_MS;
  while (Date.now() < deadline) {
    const editorText = await readEditorText(page);
    const hasSaveRequest = saveRequests.some((item) => item.documentId === documentId);
    const runtimeStatus = await page.evaluate(() => {
      return (
        document
          .querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]')
          ?.getAttribute("data-runtime-editor-status") ?? null
      );
    });
    if (editorText.includes(expectedText) && (runtimeStatus === "saved" || hasSaveRequest)) {
      return;
    }
    await page.waitForTimeout(250);
  }
  throw new Error(`等待编辑器保存超时：${documentId}`);
}

async function runTitleRename(requestContext, fixture, renamedTitle) {
  const aggregate = await fetchPageAggregate(requestContext, fixture.documentId, fixture.workspaceId);
  const events = await requestSseEvents(requestContext, "/api/ai-agent/run", {
    stream: true,
    maxSteps: 8,
    scope: "document",
    messages: [
      {
        role: "user",
        content: `请把当前页面标题改成“${renamedTitle}”。只改标题，不要改正文；改完后用一句话确认。`,
      },
    ],
    context: buildAiContextFromPageAggregate(aggregate),
    options: {
      ai: {
        provider: "online",
      },
    },
  });

  const errorEvent = findErrorEvent(events);
  assert(!errorEvent, `标题改名流返回 error：${serializeForError(errorEvent)}`);

  const slashResult = findSuccessfulToolResult(events, "slash_run");
  assert(slashResult, `标题改名未得到成功的 slash_run：${serializeForError(events)}`);
  const persistedTitle = await persistAiTitleResult(requestContext, fixture, slashResult);
  assert(
    persistedTitle === renamedTitle,
    `AI 标题结果与目标不一致，期望 ${renamedTitle}，实际 ${persistedTitle}`,
  );

  await waitFor(
    async () => {
      const meta = await fetchDocumentMeta(requestContext, fixture.documentId, fixture.workspaceId);
      return String(meta?.doc?.title ?? "").trim() === renamedTitle ? meta : null;
    },
    "等待标题改名落盘",
    AI_TIMEOUT_MS,
  );

  return events;
}

async function runBodyInsert(requestContext, fixture, initialBody) {
  const aggregate = await fetchPageAggregate(requestContext, fixture.documentId, fixture.workspaceId);
  const events = await requestSseEvents(requestContext, "/api/ai-agent/run", {
    stream: true,
    maxSteps: 8,
    scope: "document",
    messages: [
      {
        role: "user",
        content: `请在当前页面插入一段正文：“${initialBody}”。只插入这一段，不要改标题；完成后简短确认。`,
      },
    ],
    context: buildAiContextFromPageAggregate(aggregate),
    options: {
      ai: {
        provider: "online",
      },
    },
  });

  const errorEvent = findErrorEvent(events);
  assert(!errorEvent, `正文插入流返回 error：${serializeForError(errorEvent)}`);

  const insertResult = findSuccessfulToolResult(events, "doc_insert_blocks");
  assert(insertResult, `正文插入未得到成功的 doc_insert_blocks：${serializeForError(events)}`);
  await persistAiBodyResult(requestContext, fixture, insertResult);
  const insertedBlockId = extractInsertedBlockIdFromToolResult(insertResult);
  assert(insertedBlockId, `正文插入结果缺少 inserted blockId：${serializeForError(insertResult)}`);

  await waitFor(
    async () => {
      const content = await fetchDocumentContent(requestContext, fixture.documentId, fixture.workspaceId);
      const raw = JSON.stringify(content?.content ?? null);
      return raw.includes(initialBody) ? content : null;
    },
    "等待正文插入落盘",
    AI_TIMEOUT_MS,
  );
  await waitForPageAggregateBodyText(requestContext, fixture, initialBody);

  return {
    events,
    insertedBlockId,
  };
}

async function runBodyRewrite(requestContext, fixture, targetBlockId, rewrittenBody) {
  const aggregate = await fetchPageAggregate(requestContext, fixture.documentId, fixture.workspaceId);
  const events = await requestSseEvents(requestContext, "/api/ai-agent/run", {
    stream: true,
    maxSteps: 8,
    scope: "document",
    messages: [
      {
        role: "user",
        content: `请把当前页面中 blockId 为“${targetBlockId}”的那一段改写为“${rewrittenBody}”。只改这个 blockId 对应的段落，不要改标题；直接用 doc_replace_range 完成，不需要先搜索。改完后简短确认。`,
      },
    ],
    context: buildAiContextFromPageAggregate(aggregate),
    options: {
      ai: {
        provider: "online",
      },
    },
  });

  const errorEvent = findErrorEvent(events);
  assert(!errorEvent, `正文改写流返回 error：${serializeForError(errorEvent)}`);

  const replaceResult = findSuccessfulToolResult(events, "doc_replace_range");
  assert(replaceResult, `正文改写未得到成功的 doc_replace_range：${serializeForError(events)}`);
  await persistAiBodyResult(requestContext, fixture, replaceResult);

  await waitFor(
    async () => {
      const content = await fetchDocumentContent(requestContext, fixture.documentId, fixture.workspaceId);
      const raw = JSON.stringify(content?.content ?? null);
      return raw.includes(rewrittenBody) ? content : null;
    },
    "等待正文改写落盘",
    AI_TIMEOUT_MS,
  );

  return events;
}

async function verifyDocumentUi(page, fixture, expectedTitle, expectedBody) {
  await openDocument(page, fixture.workspaceId, fixture.documentId);
  await waitForRuntimeIsland(page);
  await waitForTitleInput(page);
  await waitForEditorText(page, expectedBody);

  const visibleTitle = await readVisibleTitle(page);
  const editorText = await readEditorText(page);
  assert(
    visibleTitle.includes(expectedTitle),
    `页面标题未同步到 UI，期望包含 ${expectedTitle}，实际为 ${visibleTitle}`,
  );
  assert(
    editorText.includes(expectedBody),
    `编辑区正文未同步到 UI，期望包含 ${expectedBody}，实际为 ${editorText}`,
  );
}

async function main() {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width: 1440, height: 960 },
  });
  const page = await context.newPage();

  let fixture = null;
  let caughtError = null;

  try {
    const viewer = await ensureAuthenticated(page, context.request);
    fixture = await createTempDocument(context.request, null);

    const uniqueSuffix = Date.now().toString().slice(-6);
    const initialTitle = `phase7-ai-initial-${uniqueSuffix}`;
    const renamedTitle = `phase7-ai-renamed-${uniqueSuffix}`;
    const initialBody = `phase7 AI 原始正文 ${uniqueSuffix}`;
    const rewrittenBody = `phase7 AI 改写正文 ${uniqueSuffix}`;

    await renameDocument(context.request, fixture.workspaceId, fixture.documentId, initialTitle);

    const insertRun = await runBodyInsert(context.request, fixture, initialBody);
    const renameEvents = await runTitleRename(context.request, fixture, renamedTitle);
    const rewriteEvents = await runBodyRewrite(
      context.request,
      fixture,
      insertRun.insertedBlockId,
      rewrittenBody,
    );

    const meta = await fetchDocumentMeta(context.request, fixture.documentId, fixture.workspaceId);
    const content = await fetchDocumentContent(context.request, fixture.documentId, fixture.workspaceId);

    assert(
      String(meta?.doc?.title ?? "").trim() === renamedTitle,
      `标题未完成持久化，期望 ${renamedTitle}，实际 ${String(meta?.doc?.title ?? "").trim()}`,
    );
    const serializedContent = JSON.stringify(content?.content ?? null);
    assert(
      serializedContent.includes(rewrittenBody),
      `正文未完成持久化，未命中 ${rewrittenBody}`,
    );
    assert(
      !serializedContent.includes(initialBody),
      `正文仍保留旧文本 ${initialBody}`,
    );
    const pageAggregate = await fetchPageAggregate(context.request, fixture.documentId, fixture.workspaceId);
    assert(
      String(pageAggregate?.page?.head?.title ?? "").trim() === renamedTitle,
      `page aggregate 标题未更新为 ${renamedTitle}`,
    );
    assert(
      JSON.stringify(pageAggregate?.page?.body?.content ?? null).includes(rewrittenBody),
      `page aggregate 正文未更新为 ${rewrittenBody}`,
    );

    console.log(
      JSON.stringify(
        {
          ok: true,
          baseUrl: BASE_URL,
          viewerUserId: viewer.userId,
          workspaceId: fixture.workspaceId,
          documentId: fixture.documentId,
          renamedTitle,
          rewrittenBody,
          insertToolSequence: insertRun.events
            .filter((event) => event.type === "tool_result")
            .map((event) => event.data?.tool ?? null)
            .filter(Boolean),
          renameToolSequence: renameEvents
            .filter((event) => event.type === "tool_result")
            .map((event) => event.data?.tool ?? null)
            .filter(Boolean),
          rewriteToolSequence: rewriteEvents
            .filter((event) => event.type === "tool_result")
            .map((event) => event.data?.tool ?? null)
            .filter(Boolean),
        },
        null,
        2,
      ),
    );
  } catch (error) {
    caughtError = error;
  } finally {
    if (fixture?.documentId) {
      try {
        await purgeDocument(context.request, fixture.documentId);
      } catch (cleanupError) {
        if (!caughtError) {
          caughtError = cleanupError;
        } else {
          console.error(
            `清理临时页面失败：${
              cleanupError instanceof Error
                ? cleanupError.stack || cleanupError.message
                : String(cleanupError)
            }`,
          );
        }
      }
    }

    await page.close().catch(() => undefined);
    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
  }

  if (caughtError) {
    throw caughtError;
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack || error.message : String(error));
  process.exit(1);
});
