#!/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,
  UI_TIMEOUT_MS,
  cleanupDocuments,
  createTempDocument,
  ensureAuthenticated,
  openDocument,
  requestJson,
} = require("./tree-shell-smoke-helpers");

const OUT_DIR = path.join(process.cwd(), "tmp", "page-aggregate-refresh-persistence-smoke");
const CHROME_EXECUTABLE = process.env.PLAYWRIGHT_CHROME_EXECUTABLE || "";

async function callMnoteTool(requestContext, payload) {
  return requestJson(requestContext, "/api/hermes/tools/mnote/call", {
    method: "POST",
    headers: { "x-mnote-actor-id": payload.actorId || "smoke-user" },
    data: payload,
  });
}

async function fetchPageAggregate(requestContext, workspaceId, documentId) {
  const payload = await requestJson(
    requestContext,
    `/api/page-aggregate/${encodeURIComponent(documentId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
    { method: "GET" },
  );
  assert.equal(payload.schema, "mnote.page_aggregate.v1", "Page Aggregate schema 应保持稳定");
  return payload.result;
}

function pageSubtreeSummary(aggregate) {
  const subtree = aggregate?.tree?.pageSubtree ?? null;
  return {
    rootNodeId: subtree?.rootNodeId ?? null,
    outlineLength: Array.isArray(subtree?.outline) ? subtree.outline.length : null,
  };
}

async function fetchAiDoc(requestContext, target, actorId, suffix, label) {
  const response = await callMnoteTool(requestContext, {
    toolName: "mnote.doc.fetch",
    workspaceId: target.workspaceId,
    documentId: target.documentId,
    actorId,
    sessionId: `sess_page_aggregate_ai_fetch_${suffix}`,
    runId: `run_page_aggregate_ai_fetch_${suffix}_${label}`,
    toolCallId: `call_page_aggregate_ai_fetch_${suffix}_${label}`,
    traceId: `trace_page_aggregate_ai_fetch_${suffix}_${label}`,
    capabilityScope: ["page.read"],
    args: {
      scope: "full",
      detail: "with_ids",
      maxBlocks: 20,
    },
  });
  assert.equal(response.ok, true, `${label}: mnote.doc.fetch 应成功`);
  assert.equal(response.result.schema, "mnote.page_ai_context.v1", `${label}: AI fetch schema 应稳定`);
  assert(Array.isArray(response.result.blocks), `${label}: AI fetch 应返回 blocks`);
  return response.result;
}

function findBlockWithText(aggregate, text) {
  const blocks = aggregate?.body?.blockDocument?.blocks;
  if (!Array.isArray(blocks)) return null;
  return blocks.find((block) => typeof block?.text === "string" && block.text.includes(text)) ?? null;
}

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 waitForPageTitleInput(page) {
  const input = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
  await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  return input;
}

async function readVisibleTitle(page) {
  const titleInput = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
  if (await titleInput.isVisible().catch(() => false)) {
    return (await titleInput.inputValue()).trim();
  }
  const heading = page.locator("h1").first();
  return ((await heading.textContent()) ?? "").trim();
}

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

async function renameThroughPageHead(page, documentId, title) {
  const titleInput = await waitForPageTitleInput(page);
  const responsePromise = page.waitForResponse(
    async (response) => {
      if (!response.url().includes("/api/documents/title") || response.request().method() !== "POST") {
        return false;
      }
      const payload = response.request().postDataJSON();
      return (
        payload?.documentId === documentId &&
        payload?.title === title &&
        payload?.commandName === "page.head.updateTitle" &&
        response.ok()
      );
    },
    { timeout: UI_TIMEOUT_MS },
  );
  await titleInput.click({ timeout: UI_TIMEOUT_MS });
  await page.keyboard.press("Control+a");
  await page.keyboard.type(title, { delay: 10 });
  await titleInput.blur();
  const response = await responsePromise;
  return response.status();
}

async function typeIntoEditor(page, documentId, text) {
  const responsePromise = page.waitForResponse(
    async (response) => {
      if (!response.url().includes("/api/documents/save") || response.request().method() !== "POST") {
        return false;
      }
      const payload = response.request().postDataJSON();
      return payload?.documentId === documentId && response.ok();
    },
    { timeout: UI_TIMEOUT_MS },
  );
  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: 10 });
  const response = await responsePromise;
  return response.status();
}

async function openPageSettingsDialog(page) {
  const trigger = page.getByTestId("wolai-page-settings-trigger");
  await trigger.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await trigger.click({ timeout: UI_TIMEOUT_MS });
  await page.locator('[data-page-option-checkbox="wideLayout"]').waitFor({
    state: "visible",
    timeout: UI_TIMEOUT_MS,
  });
}

async function waitForOptionsResponse(page, documentId, mutate) {
  const responsePromise = page.waitForResponse(
    async (response) => {
      if (!response.url().includes("/api/documents/options") || response.request().method() !== "POST") {
        return false;
      }
      const payload = response.request().postDataJSON();
      return payload?.documentId === documentId && response.ok();
    },
    { timeout: UI_TIMEOUT_MS },
  );
  await mutate();
  const response = await responsePromise;
  const payload = await response.json();
  assert.equal(payload?.meta?.commandName, "page.layout.updateOptions", "页面设置写入必须走 page.layout.updateOptions");
  assert.equal(payload?.meta?.canonicalCommand, "page.layout.updateOptions", "页面设置 canonical command 必须稳定");
  return response.status();
}

async function setPageOptionsThroughUi(page, documentId) {
  await openPageSettingsDialog(page);
  const wideStatus = await waitForOptionsResponse(page, documentId, async () => {
    await page.locator('[data-page-option-checkbox="wideLayout"]').click({ timeout: UI_TIMEOUT_MS });
  });
  const smallStatus = await waitForOptionsResponse(page, documentId, async () => {
    await page.locator('[data-page-option-checkbox="smallText"]').click({ timeout: UI_TIMEOUT_MS });
  });
  const densityStatus = await waitForOptionsResponse(page, documentId, async () => {
    await page.locator('[data-page-settings-tab="custom"]').click({ timeout: UI_TIMEOUT_MS });
    await page.locator('[data-page-option-select="layoutDensity"]').selectOption("compact");
  });
  return { wideStatus, smallStatus, densityStatus };
}

async function readRuntimePageOptions(page) {
  return page.evaluate(() => {
    const shell = document.querySelector(".document-shell");
    const editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
    const editorSurface = editorRoot?.querySelector(".editor-surface");
    const editor = editorRoot?.querySelector(".ProseMirror");
    const firstParagraph = editorRoot?.querySelector(".ProseMirror p");
    return {
      htmlWide: document.documentElement.getAttribute("data-page-wide-layout"),
      htmlSmall: document.documentElement.getAttribute("data-page-small-text"),
      htmlDensity: document.documentElement.getAttribute("data-layout-density"),
      shellWide: shell instanceof HTMLElement ? shell.getAttribute("data-page-wide-layout") : null,
      shellSmall: shell instanceof HTMLElement ? shell.getAttribute("data-page-small-text") : null,
      shellDensity: shell instanceof HTMLElement ? shell.getAttribute("data-layout-density") : null,
      editorRootWide: editorRoot instanceof HTMLElement ? editorRoot.getAttribute("data-page-wide-layout") : null,
      editorRootSmall: editorRoot instanceof HTMLElement ? editorRoot.getAttribute("data-page-small-text") : null,
      editorSurfaceDensity:
        editorSurface instanceof HTMLElement ? editorSurface.getAttribute("data-layout-density") : null,
      editorFontSize: editor instanceof HTMLElement ? window.getComputedStyle(editor).fontSize : null,
      paragraphMarginBottom:
        firstParagraph instanceof HTMLElement ? window.getComputedStyle(firstParagraph).marginBottom : null,
    };
  });
}

async function readPageSettingsControls(page) {
  await openPageSettingsDialog(page);
  return {
    wideLayout: await page.locator('[data-page-option-checkbox="wideLayout"]').isChecked(),
    smallText: await page.locator('[data-page-option-checkbox="smallText"]').isChecked(),
    layoutDensity: await page.locator('[data-page-option-select="layoutDensity"]').inputValue(),
  };
}

async function waitForRuntimePageOptions(page) {
  try {
    await page.waitForFunction(
      () => {
        const shell = document.querySelector(".document-shell");
        const editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
        const editorSurface = editorRoot?.querySelector(".editor-surface");
        return (
          document.documentElement.getAttribute("data-page-wide-layout") === "true" &&
          document.documentElement.getAttribute("data-page-small-text") === "true" &&
          document.documentElement.getAttribute("data-layout-density") === "compact" &&
          shell?.getAttribute("data-page-wide-layout") === "true" &&
          shell?.getAttribute("data-page-small-text") === "true" &&
          shell?.getAttribute("data-layout-density") === "compact" &&
          editorRoot?.getAttribute("data-page-wide-layout") === "true" &&
          editorRoot?.getAttribute("data-page-small-text") === "true" &&
          editorSurface?.getAttribute("data-layout-density") === "compact"
        );
      },
      null,
      { timeout: UI_TIMEOUT_MS },
    );
  } catch (error) {
    const runtime = await readRuntimePageOptions(page).catch((runtimeError) => ({
      readRuntimeError: runtimeError instanceof Error ? runtimeError.message : String(runtimeError),
    }));
    const embedded = await page.evaluate(() => {
      const node = document.getElementById("__MNOTE_PAGE_AGGREGATE__");
      try {
        return node?.textContent ? JSON.parse(node.textContent) : null;
      } catch (parseError) {
        return { parseError: parseError instanceof Error ? parseError.message : String(parseError) };
      }
    }).catch((embeddedError) => ({
      readEmbeddedError: embeddedError instanceof Error ? embeddedError.message : String(embeddedError),
    }));
    throw new Error(
      `等待 runtime page options 同步超时：${JSON.stringify({ runtime, embeddedPageOptions: embedded?.layout?.pageOptions ?? null })}\n${
        error instanceof Error ? error.stack || error.message : String(error)
      }`,
    );
  }
}

async function waitForAggregateState(requestContext, workspaceId, documentId, expectedTitle, expectedText) {
  const deadline = Date.now() + UI_TIMEOUT_MS;
  let aggregate = null;
  while (Date.now() < deadline) {
    aggregate = await fetchPageAggregate(requestContext, workspaceId, documentId);
    const options = aggregate?.layout?.pageOptions ?? {};
    const matchedBlock = findBlockWithText(aggregate, expectedText);
    if (
      aggregate?.head?.title === expectedTitle &&
      matchedBlock &&
      options.wideLayout === true &&
      options.smallText === true &&
      options.layoutDensity === "compact"
    ) {
      return { aggregate, matchedBlock };
    }
    await new Promise((resolve) => setTimeout(resolve, 350));
  }
  throw new Error(
    `等待 Page Aggregate 刷新持久态同步超时：${JSON.stringify({
      documentId,
      expectedTitle,
      expectedText,
      lastTitle: aggregate?.head?.title ?? null,
      lastOptions: aggregate?.layout?.pageOptions ?? null,
      hasExpectedBlock: Boolean(aggregate && findBlockWithText(aggregate, expectedText)),
    })}`,
  );
}

function assertRuntimeOptions(runtime, label) {
  assert.equal(runtime.htmlWide, "true", `${label} html wideLayout 应保持 true`);
  assert.equal(runtime.htmlSmall, "true", `${label} html smallText 应保持 true`);
  assert.equal(runtime.htmlDensity, "compact", `${label} html layoutDensity 应保持 compact`);
  assert.equal(runtime.editorRootWide, "true", `${label} island root wideLayout 应保持 true`);
  assert.equal(runtime.editorRootSmall, "true", `${label} island root smallText 应保持 true`);
  assert.equal(runtime.editorSurfaceDensity, "compact", `${label} island surface density 应保持 compact`);
}

async function main() {
  await fs.mkdir(OUT_DIR, { recursive: true });
  const suffix = Date.now().toString(36);
  const title = `Page Aggregate refresh ${suffix}`;
  const bodyText = `Page Aggregate refresh body ${suffix}`;
  const evidencePath = path.join(OUT_DIR, `${suffix}.json`);
  const screenshotPath = path.join(OUT_DIR, `${suffix}.png`);
  const browser = await chromium.launch({
    headless: true,
    ...(CHROME_EXECUTABLE ? { executablePath: CHROME_EXECUTABLE } : {}),
    args: ["--no-sandbox", "--disable-dev-shm-usage"],
  });
  const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
  const page = await context.newPage();
  const createdIds = [];

  try {
    const viewer = await ensureAuthenticated(page, context.request);
    const target = await createTempDocument(context.request, null);
    createdIds.push(target.documentId);

    await openDocument(page, target.workspaceId, target.documentId);
    await waitForPageTitleInput(page);
    await waitForRuntimeIsland(page);

    const titleStatus = await renameThroughPageHead(page, target.documentId, title);
    assert((await readVisibleTitle(page)).includes(title), "写入后页头标题应立即显示最新值");

    const saveStatus = await typeIntoEditor(page, target.documentId, bodyText);
    await page.waitForFunction(
      (expectedText) => (document.querySelector(".editor-surface .ProseMirror")?.textContent ?? "").includes(expectedText),
      bodyText,
      { timeout: UI_TIMEOUT_MS },
    );

    const optionStatuses = await setPageOptionsThroughUi(page, target.documentId);
    await waitForRuntimePageOptions(page);
    const beforeReloadRuntime = await readRuntimePageOptions(page);
    assertRuntimeOptions(beforeReloadRuntime, "刷新前");

    const { aggregate: beforeReloadAggregate, matchedBlock: beforeReloadBlock } = await waitForAggregateState(
      context.request,
      target.workspaceId,
      target.documentId,
      title,
      bodyText,
    );
    const beforeReloadAiFetch = await fetchAiDoc(context.request, target, viewer.userId, suffix, "before_reload");
    const beforeReloadAiBlock = beforeReloadAiFetch.blocks.find((block) => block.blockId === beforeReloadBlock.blockId);
    assert.equal(beforeReloadAiFetch.revision, beforeReloadAggregate.body?.revision, "刷新前 AI fetch revision 应与 Page Aggregate 一致");
    assert.equal(
      beforeReloadAiFetch.conflictDetectionKey,
      beforeReloadAggregate.body?.conflictDetectionKey,
      "刷新前 AI fetch conflictDetectionKey 应与 Page Aggregate 一致",
    );
    assert.equal(beforeReloadAiBlock?.text, bodyText, "刷新前 AI fetch 应回读 Page Aggregate 正文块");
    assert.equal(
      beforeReloadAggregate.tree?.pageSubtree?.rootNodeId,
      target.documentId,
      "刷新前 Page Aggregate pageSubtree.rootNodeId 应等于 documentId",
    );

    await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
    await waitForPageTitleInput(page);
    await waitForRuntimeIsland(page);
    await page.waitForFunction(
      (expectedText) => (document.querySelector(".editor-surface .ProseMirror")?.textContent ?? "").includes(expectedText),
      bodyText,
      { timeout: UI_TIMEOUT_MS },
    );
    await waitForRuntimePageOptions(page);

    const afterReloadTitle = await readVisibleTitle(page);
    const afterReloadEditorText = await readEditorText(page);
    const afterReloadRuntime = await readRuntimePageOptions(page);
    assert(afterReloadTitle.includes(title), "刷新后页头标题不应回退旧快照");
    assert(afterReloadEditorText.includes(bodyText), "刷新后正文不应回退旧快照");
    assertRuntimeOptions(afterReloadRuntime, "刷新后");
    const afterReloadControls = await readPageSettingsControls(page);
    assert.equal(afterReloadControls.wideLayout, true, "刷新后页面设置 wideLayout 控件应保持 true");
    assert.equal(afterReloadControls.smallText, true, "刷新后页面设置 smallText 控件应保持 true");
    assert.equal(afterReloadControls.layoutDensity, "compact", "刷新后页面设置 layoutDensity 控件应保持 compact");

    const { aggregate: afterReloadAggregate, matchedBlock: afterReloadBlock } = await waitForAggregateState(
      context.request,
      target.workspaceId,
      target.documentId,
      title,
      bodyText,
    );
    const afterReloadAiFetch = await fetchAiDoc(context.request, target, viewer.userId, suffix, "after_reload");
    const afterReloadAiBlock = afterReloadAiFetch.blocks.find((block) => block.blockId === afterReloadBlock.blockId);
    assert.equal(afterReloadAiFetch.revision, afterReloadAggregate.body?.revision, "刷新后 AI fetch revision 应与 Page Aggregate 一致");
    assert.equal(
      afterReloadAiFetch.conflictDetectionKey,
      afterReloadAggregate.body?.conflictDetectionKey,
      "刷新后 AI fetch conflictDetectionKey 应与 Page Aggregate 一致",
    );
    assert.equal(afterReloadAiBlock?.text, bodyText, "刷新后 AI fetch 应回读 Page Aggregate 正文块");
    assert.equal(
      afterReloadAggregate.tree?.pageSubtree?.rootNodeId,
      target.documentId,
      "刷新后 Page Aggregate pageSubtree.rootNodeId 应等于 documentId",
    );

    await page.screenshot({ path: screenshotPath, fullPage: false });
    const evidence = {
      ok: true,
      baseUrl: BASE_URL,
      viewerUserId: viewer.userId,
      workspaceId: target.workspaceId,
      documentId: target.documentId,
      expected: {
        title,
        bodyText,
        pageOptions: {
          wideLayout: true,
          smallText: true,
          layoutDensity: "compact",
        },
      },
      commandStatuses: {
        titleStatus,
        saveStatus,
        ...optionStatuses,
      },
      beforeReload: {
        aggregate: {
          title: beforeReloadAggregate.head?.title ?? null,
          revision: beforeReloadAggregate.body?.revision ?? null,
          conflictDetectionKey: beforeReloadAggregate.body?.conflictDetectionKey ?? null,
          pageOptions: beforeReloadAggregate.layout?.pageOptions ?? null,
          blockProjectionVersion: beforeReloadAggregate.body?.blockProjectionVersion ?? null,
          pageSubtree: pageSubtreeSummary(beforeReloadAggregate),
        },
        matchedBlock: {
          blockId: beforeReloadBlock.blockId,
          text: beforeReloadBlock.text,
          revisionRef: beforeReloadBlock.revisionRef,
        },
        aiFetch: {
          schema: beforeReloadAiFetch.schema,
          revision: beforeReloadAiFetch.revision,
          conflictDetectionKey: beforeReloadAiFetch.conflictDetectionKey,
          matchedBlock: {
            blockId: beforeReloadAiBlock?.blockId ?? null,
            text: beforeReloadAiBlock?.text ?? null,
            revisionRef: beforeReloadAiBlock?.revisionRef ?? null,
          },
        },
        runtime: beforeReloadRuntime,
      },
      afterReload: {
        aggregate: {
          title: afterReloadAggregate.head?.title ?? null,
          revision: afterReloadAggregate.body?.revision ?? null,
          conflictDetectionKey: afterReloadAggregate.body?.conflictDetectionKey ?? null,
          pageOptions: afterReloadAggregate.layout?.pageOptions ?? null,
          blockProjectionVersion: afterReloadAggregate.body?.blockProjectionVersion ?? null,
          pageSubtree: pageSubtreeSummary(afterReloadAggregate),
        },
        matchedBlock: {
          blockId: afterReloadBlock.blockId,
          text: afterReloadBlock.text,
          revisionRef: afterReloadBlock.revisionRef,
        },
        aiFetch: {
          schema: afterReloadAiFetch.schema,
          revision: afterReloadAiFetch.revision,
          conflictDetectionKey: afterReloadAiFetch.conflictDetectionKey,
          matchedBlock: {
            blockId: afterReloadAiBlock?.blockId ?? null,
            text: afterReloadAiBlock?.text ?? null,
            revisionRef: afterReloadAiBlock?.revisionRef ?? null,
          },
        },
        title: afterReloadTitle,
        editorText: afterReloadEditorText,
        runtime: afterReloadRuntime,
        controls: afterReloadControls,
      },
      screenshotPath,
      evidencePath,
    };
    await fs.writeFile(evidencePath, JSON.stringify(evidence, null, 2), "utf8");
    console.log(JSON.stringify(evidence, null, 2));
  } finally {
    await cleanupDocuments(context.request, createdIds).catch(() => undefined);
    await page.close().catch(() => undefined);
    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
  }
}

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