"use strict";

// 说明：
// - 这份 smoke 用来覆盖 tree rust family checklist 第 9 节的通用回归要求。
// - legacy React island 负责验证真实导航、上下文菜单、picker 对话框；3000 Rust gateway 负责 API 与 stream。
// - `/api/tree/shell` 直连页已退到显式 debug/internal 边界，默认不再进入这条链路。

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

const RUN_DIRECT_TREE_SHELL_CHECKS =
  String(process.env.MNOTE_TREE_SHELL_DIRECT_SMOKE || "").trim() === "1";
const ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST =
  String(process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST || "").trim() === "1";
const CHROMIUM_STABLE_ARGS = ["--disable-dev-shm-usage", "--disable-gpu"];

function buildTreeShellUrl(workspaceId, params = {}) {
  const search = new URLSearchParams({
    workspaceId,
    ...Object.fromEntries(
      Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== ""),
    ),
  });
  return `${BASE_URL}/api/tree/shell?${search.toString()}`;
}

function treeHostImplementationUsesIframe(implementation) {
  return (
    implementation === "mnote_web_iframe_proxy" ||
    implementation === "rust_inline_compat_host" ||
    implementation === "rust_runtime_artifact_host"
  );
}

function assertDefaultHostIsDomWasm(implementation, surfaceTestId) {
  if (treeHostImplementationUsesIframe(implementation) && !ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST) {
    throw new Error(
      `${surfaceTestId} 默认主路径不能再使用 legacy iframe host: ${implementation || "unknown"}`,
    );
  }
}

async function waitForInlineTreeShellReady(page, surfaceTestId) {
  await page.waitForFunction(
    (testId) => {
      const host = document.querySelector(`[data-testid="${testId}"]`);
      if (!(host instanceof HTMLElement) || host.getClientRects().length === 0) {
        return false;
      }
      const implementation = host.getAttribute("data-tree-host-implementation") || "";
      if (
        implementation === "mnote_web_iframe_proxy" ||
        implementation === "rust_inline_compat_host" ||
        implementation === "rust_runtime_artifact_host"
      ) {
        const iframe = host.querySelector(`[data-testid="${testId}-rust-iframe"]`);
        if (!(iframe instanceof HTMLIFrameElement) || iframe.getClientRects().length === 0) {
          return false;
        }
        const doc = iframe.contentDocument;
        const status = doc?.querySelector("#tree-shell-status")?.textContent ?? "";
        return Boolean(doc?.querySelector("#tree-shell-state")) && status.includes("就绪");
      }
      const domHost = host.querySelector(`[data-testid="${testId}-dom-host"]`);
      return (
        domHost instanceof HTMLElement &&
        domHost.getClientRects().length > 0 &&
        domHost.getAttribute("data-tree-browser-bridge") === "dom_wasm" &&
        domHost.getAttribute("data-tree-dom-shell-ready") === "true"
      );
    },
    surfaceTestId,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function renameFixtureDocuments(requestContext, fixture) {
  await renameDocument(requestContext, fixture.workspaceId, fixture.parentId, fixture.parentTitle);
  await renameDocument(requestContext, fixture.workspaceId, fixture.childAId, fixture.childATitle);
  await renameDocument(requestContext, fixture.workspaceId, fixture.childBId, fixture.childBTitle);
  await renameDocument(requestContext, fixture.workspaceId, fixture.targetId, fixture.targetTitle);
  for (let index = 0; index < fixture.extraChildIds.length; index += 1) {
    await renameDocument(
      requestContext,
      fixture.workspaceId,
      fixture.extraChildIds[index],
      fixture.extraChildTitles[index],
    );
  }
}

async function prepareFixture(requestContext) {
  const uniqueSuffix = Date.now().toString();
  const parent = await createTempDocument(requestContext, null);
  const childA = await createTempDocument(requestContext, parent.documentId);
  const childB = await createTempDocument(requestContext, parent.documentId);
  const target = await createTempDocument(requestContext, null);
  const extraChildren = [];
  const extraChildTitles = [];
  for (let index = 0; index < 18; index += 1) {
    const nextChild = await createTempDocument(requestContext, parent.documentId);
    extraChildren.push(nextChild.documentId);
    extraChildTitles.push(`task112-scroll-${index.toString().padStart(2, "0")}-${uniqueSuffix}`);
  }
  const fixture = {
    workspaceId: parent.workspaceId,
    parentId: parent.documentId,
    childAId: childA.documentId,
    childBId: childB.documentId,
    targetId: target.documentId,
    extraChildIds: extraChildren,
    extraChildTitles,
    parentTitle: `task112-parent-${uniqueSuffix}`,
    childATitle: `task112-child-a-${uniqueSuffix}`,
    childBTitle: `task112-child-b-${uniqueSuffix}`,
    targetTitle: `task112-target-${uniqueSuffix}`,
    fallbackTitle: `task112-fallback-${uniqueSuffix}`,
    createdIds: [
      parent.documentId,
      childA.documentId,
      childB.documentId,
      target.documentId,
      ...extraChildren,
    ],
  };
  await renameFixtureDocuments(requestContext, fixture);
  return fixture;
}

async function cleanupFixture(requestContext, fixture) {
  for (const documentId of [...fixture.createdIds].reverse()) {
    try {
      await purgeDocument(requestContext, documentId);
    } catch {
      // 说明：部分 smoke 会真实移动节点或由父级 purge 级联删除，这里忽略重复清理错误。
    }
  }
}

async function waitForUrlContains(page, fragment) {
  await page.waitForURL((url) => url.toString().includes(fragment), {
    timeout: UI_TIMEOUT_MS,
  });
}

async function waitForTextAnywhere(page, expectedText) {
  await page.waitForFunction(
    (text) => {
      const bodyText = document.body?.textContent ?? "";
      if (bodyText.includes(text)) {
        return true;
      }
      return Array.from(document.querySelectorAll("iframe")).some((frame) => {
        try {
          return (frame.contentDocument?.body?.textContent ?? "").includes(text);
        } catch {
          return false;
        }
      });
    },
    expectedText,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function getPageTreeHostDriver(page) {
  const host = page.getByTestId("sidebar-page-tree-shell");
  if ((await host.count()) === 0) {
    const currentHost = page.getByTestId("wolai-sidebar-page-tree-shell");
    await currentHost.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await page
      .locator('#sidebar-tree-root .tree-row[data-shell-mode="page"]')
      .first()
      .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    return { kind: "dom", host: currentHost, scope: page.locator("#sidebar-tree-root") };
  }
  await host.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await page.waitForFunction(
    () => {
      const element = document.querySelector('[data-testid="sidebar-page-tree-shell"]');
      if (!(element instanceof HTMLElement) || element.getClientRects().length === 0) {
        return false;
      }
      const implementation = element.getAttribute("data-tree-host-implementation") || "";
      if (
        implementation === "mnote_web_iframe_proxy" ||
        implementation === "rust_inline_compat_host" ||
        implementation === "rust_runtime_artifact_host"
      ) {
        const iframe = element.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
        return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
      }
      const domHost = element.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]');
      return (
        domHost instanceof HTMLElement &&
        domHost.getClientRects().length > 0 &&
        domHost.getAttribute("data-tree-browser-bridge") === "dom_wasm" &&
        domHost.getAttribute("data-tree-dom-shell-ready") === "true"
      );
    },
    undefined,
    { timeout: UI_TIMEOUT_MS },
  );
  const implementation = await host.getAttribute("data-tree-host-implementation");
  assertDefaultHostIsDomWasm(implementation, "sidebar-page-tree-shell");
  if (treeHostImplementationUsesIframe(implementation)) {
    await waitForInlineTreeShellReady(page, "sidebar-page-tree-shell");
    return {
      kind: "iframe",
      host,
      scope: page.frameLocator('[data-testid="sidebar-page-tree-shell-rust-iframe"]'),
    };
  }
  await waitForInlineTreeShellReady(page, "sidebar-page-tree-shell");
  return { kind: "dom", host, scope: host };
}

async function getFileTreeHostDriver(page) {
  const host = page.getByTestId("sidebar-file-tree-shell");
  if ((await host.count()) === 0) {
    const currentHost = page.locator("#sidebar-file-tree-root");
    await currentHost.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await page
      .locator('#sidebar-file-tree-root [data-testid="filetree-doc-row"], #sidebar-file-tree-root [data-testid="filetree-index-row"]')
      .first()
      .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    return { kind: "dom", host: currentHost, scope: currentHost };
  }
  await host.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await page.waitForFunction(
    () => {
      const element = document.querySelector('[data-testid="sidebar-file-tree-shell"]');
      if (!(element instanceof HTMLElement) || element.getClientRects().length === 0) {
        return false;
      }
      const implementation = element.getAttribute("data-tree-host-implementation") || "";
      if (
        implementation === "mnote_web_iframe_proxy" ||
        implementation === "rust_inline_compat_host" ||
        implementation === "rust_runtime_artifact_host"
      ) {
        const iframe = element.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]');
        return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
      }
      const domHost = element.querySelector('[data-testid="sidebar-file-tree-shell-dom-host"]');
      return (
        domHost instanceof HTMLElement &&
        domHost.getClientRects().length > 0 &&
        domHost.getAttribute("data-tree-browser-bridge") === "dom_wasm" &&
        domHost.getAttribute("data-tree-dom-shell-ready") === "true"
      );
    },
    undefined,
    { timeout: UI_TIMEOUT_MS },
  );
  const implementation = await host.getAttribute("data-tree-host-implementation");
  assertDefaultHostIsDomWasm(implementation, "sidebar-file-tree-shell");
  if (treeHostImplementationUsesIframe(implementation)) {
    await waitForInlineTreeShellReady(page, "sidebar-file-tree-shell");
    return {
      kind: "iframe",
      host,
      scope: page.frameLocator('[data-testid="sidebar-file-tree-shell-rust-iframe"]'),
    };
  }
  await waitForInlineTreeShellReady(page, "sidebar-file-tree-shell");
  return { kind: "dom", host, scope: host };
}

async function openPageTreeDocumentFromHost(page, driver, documentId) {
  const popupPromise = page.context().waitForEvent("page", {
    timeout: UI_TIMEOUT_MS,
  }).catch(() => null);
  if (driver.kind === "iframe") {
    const openButton = driver.scope.locator(`.tree-row[data-node-id="${documentId}"] [data-testid="tree-node-open"]`);
    await openButton.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
    await openButton.evaluate((button) => {
      if (!(button instanceof HTMLButtonElement)) {
        throw new Error("页面树打开按钮不存在");
      }
      button.click();
    });
  } else {
    await driver.scope
      .locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"] [data-testid="tree-node-open"]`)
      .click({ timeout: UI_TIMEOUT_MS });
  }
  const popupPage = await popupPromise;
  if (popupPage) {
    await popupPage.close().catch(() => undefined);
    throw new Error("页面树普通打开不应创建新窗口");
  }
  await waitForUrlContains(page, `/documents/${documentId}`);
}

async function openPageTreeContextMenuFromHost(page, driver, documentId) {
  if (driver.kind === "iframe") {
    const row = driver.scope.locator(`.tree-row[data-node-id="${documentId}"]`);
    await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const menuButton = row.locator('[data-testid="tree-action-menu"]');
    await menuButton.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
    await menuButton.evaluate((button) => {
      if (!(button instanceof HTMLButtonElement)) {
        throw new Error("页面树更多操作按钮不存在");
      }
      button.click();
    });
  } else {
    const row = driver.scope.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`);
    await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const menuButton = row.locator('[data-testid="tree-action-menu"]');
    await menuButton.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
    await menuButton.evaluate((button) => {
      if (!(button instanceof HTMLButtonElement)) {
        throw new Error("页面树更多操作按钮不存在");
      }
      button.click();
    });
  }
  await page.getByText("重命名", { exact: true }).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await page.evaluate(() => {
    document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
  });
}

async function doubleClickFileTreeDocumentFromHost(page, driver, documentId) {
  const popupPromise = page.context().waitForEvent("page", {
    timeout: UI_TIMEOUT_MS,
  }).catch(() => null);
  if (driver.kind === "iframe") {
    await driver.scope
      .locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"]`)
      .dblclick({ timeout: UI_TIMEOUT_MS });
  } else {
    await driver.scope
      .locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"], [data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
      .dblclick({ timeout: UI_TIMEOUT_MS });
  }
  const popupPage = await popupPromise;
  if (popupPage) {
    await popupPage.close().catch(() => undefined);
    throw new Error("文件树普通打开不应创建新窗口");
  }
  await waitForUrlContains(page, `/documents/${documentId}`);
}

async function waitForPageTitleInput(page) {
  const primaryInput = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
  if ((await primaryInput.count()) > 0) {
    await primaryInput.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    return primaryInput;
  }
  const input = page.getByLabel("页面标题").first();
  await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  return input;
}

async function renameThroughPageHead(page, documentId, title) {
  const saveResponse = 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 },
  );
  const titleInput = await waitForPageTitleInput(page);
  await titleInput.click({ timeout: UI_TIMEOUT_MS });
  await page.keyboard.press("Control+a");
  await page.keyboard.type(title, { delay: 18 });
  await titleInput.blur();
  await saveResponse;
}

async function waitForBreadcrumbTitle(page, title) {
  await page.waitForFunction(
    (expectedTitle) => {
      const nav = document.querySelector("header nav");
      return (nav?.textContent ?? "").includes(expectedTitle);
    },
    title,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForSidebarRowTitle(page, documentId, title) {
  const legacyRow = page.locator(`aside a[href="/documents/${documentId}"]`).first();
  if ((await legacyRow.count()) > 0) {
    await page.waitForFunction(
      ({ docId, expectedTitle }) => {
        const row = document.querySelector(`aside a[href="/documents/${docId}"]`);
        return (row?.textContent ?? "").includes(expectedTitle);
      },
      { docId: documentId, expectedTitle: title },
      { timeout: UI_TIMEOUT_MS },
    );
    return;
  }

  await waitForPageTreeTitle(page, documentId, title);
}

async function waitForPageTreeTitle(page, documentId, title) {
  await openSectionView(page);
  const driver = await getPageTreeHostDriver(page);
  if (driver.kind === "iframe") {
    await driver.scope
      .locator(`.tree-row[data-node-id="${documentId}"]`)
      .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await driver.scope
      .locator(`.tree-row[data-node-id="${documentId}"]`)
      .getByText(title)
      .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    return;
  }

  await driver.scope
    .locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`)
    .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await page.waitForFunction(
    ({ nodeId, expectedTitle }) => {
      const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
      return (row?.textContent ?? "").includes(expectedTitle);
    },
    { nodeId: documentId, expectedTitle: title },
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForPageShellTitle(page, documentId, title) {
  await page.waitForFunction(
    ({ nodeId, expectedTitle }) => {
      const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
      return row instanceof HTMLElement && (row.textContent ?? "").includes(expectedTitle);
    },
    { nodeId: documentId, expectedTitle: title },
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForFileTreeTitle(page, documentId, title) {
  await openFilesystemView(page);
  const driver = await getFileTreeHostDriver(page);
  if (driver.kind === "iframe") {
    await driver.scope
      .locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"]`)
      .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await driver.scope
      .locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"]`)
      .getByText(title)
      .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    return;
  }

  await driver.scope
    .locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"], [data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
    .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await page.waitForFunction(
    ({ docId, expectedTitle }) => {
      const row = document.querySelector(
        `[data-testid="filetree-doc-row"][data-document-id="${docId}"], [data-testid="filetree-doc-row"][data-doc-id="${docId}"]`,
      );
      return (row?.textContent ?? "").includes(expectedTitle);
    },
    { docId: documentId, expectedTitle: title },
    { timeout: UI_TIMEOUT_MS },
  );
}

async function readPageShellOrder(page, nodeIds) {
  return await page.evaluate((ids) => {
    const rows = ids
      .map((nodeId) => {
        const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
        if (!(row instanceof HTMLElement)) {
          return null;
        }
        const rect = row.getBoundingClientRect();
        return { nodeId, top: rect.top };
      })
      .filter(Boolean);
    return rows.sort((left, right) => left.top - right.top).map((row) => row.nodeId);
  }, nodeIds);
}

async function waitForPageShellOrder(page, expectedOrder) {
  await page.waitForFunction(
    (ids) => {
      const rows = ids
        .map((nodeId) => {
          const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
          if (!(row instanceof HTMLElement)) {
            return null;
          }
          const rect = row.getBoundingClientRect();
          return { nodeId, top: rect.top };
        })
        .filter(Boolean);

      if (rows.length !== ids.length) {
        return false;
      }

      const order = rows.sort((left, right) => left.top - right.top).map((row) => row.nodeId);
      return JSON.stringify(order) === JSON.stringify(ids);
    },
    expectedOrder,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function createFileDropDataTransfer(page, fileName, content) {
  return await page.evaluateHandle(
    ({ nextFileName, nextContent }) => {
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(
        new File([nextContent], nextFileName, {
          type: "text/plain",
        }),
      );
      return dataTransfer;
    },
    { nextFileName: fileName, nextContent: content },
  );
}

async function dispatchTextDragSequence(page, input) {
  await page.evaluate(({ sourceSelector, targetSelector, entries }) => {
    const source = document.querySelector(sourceSelector);
    const target = document.querySelector(targetSelector);
    if (!(source instanceof HTMLElement) || !(target instanceof HTMLElement)) {
      throw new Error(`拖拽节点不存在: ${sourceSelector} -> ${targetSelector}`);
    }
    const dataTransfer = new DataTransfer();
    entries.forEach(({ type, value }) => {
      dataTransfer.setData(type, value);
    });

    const createDragEvent = (type) =>
      new DragEvent(type, {
        bubbles: true,
        cancelable: true,
        dataTransfer,
      });

    source.dispatchEvent(createDragEvent("dragstart"));
    target.dispatchEvent(createDragEvent("dragover"));
    target.dispatchEvent(createDragEvent("drop"));
    source.dispatchEvent(createDragEvent("dragend"));
  }, input);
}

function buildSnapshotSseBody(snapshot, workspaceId) {
  return `event: snapshot\ndata: ${JSON.stringify({
    kind: "snapshot",
    stream: "workspace",
    workspaceId,
    rootNodeId: null,
    cursor: "evt_task112_snapshot",
    projection: "sidebar_tree",
    data: snapshot,
  })}\n\n`;
}

async function runPageTreeHostChecks(page, fixture) {
  await openDocument(page, fixture.workspaceId, fixture.parentId);
  await openSectionView(page);
  const driver = await getPageTreeHostDriver(page);
  await openPageTreeDocumentFromHost(page, driver, fixture.childAId);

  await openSectionView(page);
  const nextDriver = await getPageTreeHostDriver(page);
  await openPageTreeContextMenuFromHost(page, nextDriver, fixture.childAId);

  return {
    pageTreeNavigate: true,
    pageTreeContextMenu: true,
    hostKind: nextDriver.kind,
  };
}

async function runPageTreeShellChecks(context, fixture) {
  const page = await context.newPage();
  try {
    await page.setViewportSize({ width: 1280, height: 640 });
    await page.goto(
      buildTreeShellUrl(fixture.workspaceId, {
        mode: "page",
        activeDocumentId: fixture.childAId,
      }),
      { waitUntil: "commit", timeout: UI_TIMEOUT_MS },
    );
    await page.locator("#tree-shell-title").getByText("页面树").waitFor({
      state: "visible",
      timeout: UI_TIMEOUT_MS,
    });

    const parentRow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${fixture.parentId}"]`);
    const childARow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${fixture.childAId}"]`);
    const childBRow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${fixture.childBId}"]`);
    await parentRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await childBRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

    await childARow.evaluate((element) => {
      if (!(element instanceof HTMLElement) || element.dataset.active !== "true") {
        throw new Error("当前页高亮未落到 activeDocumentId 对应行");
      }
    });
    await parentRow.evaluate((element) => {
      if (!(element instanceof HTMLElement) || element.getAttribute("aria-expanded") !== "true") {
        throw new Error("当前页祖先未自动展开");
      }
    });

    await childARow.focus();
    await childARow.press("ArrowDown");
    await childBRow.evaluate((element) => {
      if (!(element instanceof HTMLElement) || element.dataset.focused !== "true") {
        throw new Error("ArrowDown 后焦点未移动到下一行");
      }
    });
    await childBRow.press("ArrowUp");
    await childARow.evaluate((element) => {
      if (!(element instanceof HTMLElement) || element.dataset.focused !== "true") {
        throw new Error("ArrowUp 后焦点未回到上一行");
      }
    });

    await parentRow.focus();
    await parentRow.press("ArrowLeft");
    await childARow.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
    await childBRow.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
    await parentRow.press("ArrowRight");
    await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await childBRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

    const orderBefore = await readPageShellOrder(page, [fixture.childAId, fixture.childBId]);
    assert(
      JSON.stringify(orderBefore) === JSON.stringify([fixture.childAId, fixture.childBId]),
      `页面树拖拽前顺序异常：${JSON.stringify(orderBefore)}`,
    );

    await dispatchTextDragSequence(page, {
      sourceSelector: `.tree-row[data-shell-mode="page"][data-node-id="${fixture.childBId}"]`,
      targetSelector: `.tree-row[data-shell-mode="page"][data-node-id="${fixture.childAId}"]`,
      entries: [
        { type: "application/x-mnote-page-tree-node", value: fixture.childBId },
        { type: "text/plain", value: fixture.childBId },
      ],
    });

    await waitForPageShellOrder(page, [fixture.childBId, fixture.childAId]);
    const orderAfter = await readPageShellOrder(page, [fixture.childAId, fixture.childBId]);
    assert(
      JSON.stringify(orderAfter) === JSON.stringify([fixture.childBId, fixture.childAId]),
      `页面树拖拽后顺序未更新：${JSON.stringify(orderAfter)}`,
    );

    await parentRow.hover({ timeout: UI_TIMEOUT_MS });
    const createResponsePromise = page.waitForResponse(
      async (response) => {
        if (
          !response.url().includes("/api/tree/commands") ||
          response.request().method() !== "POST" ||
          !response.ok()
        ) {
          return false;
        }
        const payload = response.request().postDataJSON();
        return payload?.action === "create" && payload?.parentId === fixture.parentId;
      },
      { timeout: UI_TIMEOUT_MS },
    );
    await parentRow.locator('[data-testid="tree-action-create"]').click({ timeout: UI_TIMEOUT_MS });
    const createResponse = await createResponsePromise;
    const createPayload = await createResponse.json().catch(() => null);
    const createdDocumentId = String(createPayload?.result?.documentId ?? createPayload?.id ?? "").trim();
    assert(createdDocumentId, "页面树新建子页面未返回 documentId");
    fixture.createdIds.push(createdDocumentId);
    await waitForPageShellTitle(page, createdDocumentId, "无标题");

    const renamedTitle = `task112-created-renamed-${Date.now().toString().slice(-6)}`;
    const createdRow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${createdDocumentId}"]`);
    await createdRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await createdRow.hover({ timeout: UI_TIMEOUT_MS });
    const renameDialogPromise = page
      .waitForEvent("dialog", { timeout: UI_TIMEOUT_MS })
      .then((dialog) => dialog.accept(renamedTitle));
    const renameResponsePromise = page.waitForResponse(
      async (response) => {
        if (
          !response.url().includes("/api/tree/commands") ||
          response.request().method() !== "POST" ||
          !response.ok()
        ) {
          return false;
        }
        const payload = response.request().postDataJSON();
        return (
          payload?.action === "rename" &&
          payload?.documentId === createdDocumentId &&
          payload?.title === renamedTitle
        );
      },
      { timeout: UI_TIMEOUT_MS },
    );
    await createdRow.locator('[data-testid="tree-action-rename"]').click({ timeout: UI_TIMEOUT_MS });
    await renameDialogPromise;
    await renameResponsePromise;
    await waitForPageShellTitle(page, createdDocumentId, renamedTitle);

    const visibleNodeIds = await page.evaluate(() =>
      Array.from(document.querySelectorAll('.tree-row[data-shell-mode="page"]'))
        .map((element) => element.getAttribute("data-node-id") || "")
        .filter(Boolean),
    );
    assert(visibleNodeIds.length >= 12, `页面树大树夹具数量不足：${visibleNodeIds.length}`);
    const lastVisibleNodeId = visibleNodeIds[visibleNodeIds.length - 1];
    await parentRow.focus();
    for (let index = 1; index < visibleNodeIds.length; index += 1) {
      await page.keyboard.press("ArrowDown");
    }
    await page.waitForFunction(
      (nodeId) => {
        const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
        return row instanceof HTMLElement && row.dataset.focused === "true";
      },
      lastVisibleNodeId,
      { timeout: UI_TIMEOUT_MS },
    );
    const scrollState = await page.evaluate((nodeId) => {
      const scroller = document.querySelector("#tree-shell-app");
      const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
      if (!(row instanceof HTMLElement)) {
        return null;
      }
      const rowRect = row.getBoundingClientRect();
      const scrollerRect =
        scroller instanceof HTMLElement ? scroller.getBoundingClientRect() : null;
      return {
        scrollTop: scroller instanceof HTMLElement ? scroller.scrollTop : 0,
        scrollHeight: scroller instanceof HTMLElement ? scroller.scrollHeight : 0,
        clientHeight: scroller instanceof HTMLElement ? scroller.clientHeight : 0,
        windowScrollY: window.scrollY,
        viewportHeight: window.innerHeight,
        pageScrollHeight: document.documentElement.scrollHeight,
        rowTop: rowRect.top,
        rowBottom: rowRect.bottom,
        scrollerTop: scrollerRect?.top ?? 0,
        scrollerBottom: scrollerRect?.bottom ?? window.innerHeight,
      };
    }, lastVisibleNodeId);
    assert(scrollState, "页面树滚动状态读取失败");
    const hasOverflow =
      scrollState.scrollHeight > scrollState.clientHeight ||
      scrollState.pageScrollHeight > scrollState.viewportHeight;
    assert(
      hasOverflow,
      `页面树大树场景未形成可滚动内容：${JSON.stringify(scrollState)}`,
    );
    assert(
      scrollState.scrollTop > 0 || scrollState.windowScrollY > 0,
      `页面树滚动未推进：${JSON.stringify(scrollState)}`,
    );
    assert(
      scrollState.rowTop >= -2 &&
        scrollState.rowBottom <= scrollState.viewportHeight + 2,
      `页面树滚动后目标行仍未进入可视区：${JSON.stringify(scrollState)}`,
    );

    return {
      pageTreeExpandCollapse: true,
      pageTreeCurrentHighlight: true,
      pageTreeAncestorAutoExpand: true,
      pageTreeKeyboardNavigation: true,
      pageTreeDrag: true,
      pageTreeCreateChild: true,
      pageTreeRename: true,
      pageTreeStableScroll: true,
      orderBefore,
      orderAfter,
      createdDocumentId,
      renamedTitle,
      scrollTop: scrollState.scrollTop,
      windowScrollY: scrollState.windowScrollY,
      visiblePageRowCount: visibleNodeIds.length,
    };
  } finally {
    await page.close().catch(() => undefined);
  }
}

async function runFileTreeShellChecks(context, fixture) {
  const page = await context.newPage();
  try {
    await page.goto(
      buildTreeShellUrl(fixture.workspaceId, {
        mode: "filetree",
        activeDocumentId: fixture.parentId,
      }),
      { waitUntil: "commit", timeout: UI_TIMEOUT_MS },
    );
    await page.locator("#tree-shell-title").getByText("资源管理器").waitFor({
      state: "visible",
      timeout: UI_TIMEOUT_MS,
    });

    const parentRow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.parentId}"]`);
    const childARow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.childAId}"]`);
    const childBRow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.childBId}"]`);
    const targetRow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.targetId}"]`);
    await parentRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await childBRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await targetRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

    await childARow.click({ timeout: UI_TIMEOUT_MS });
    await childBRow.click({ modifiers: ["Control"], timeout: UI_TIMEOUT_MS });
    await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await childARow.evaluate((element) => {
      if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
        throw new Error("多选后 childA 未进入选中态");
      }
    });
    await childBRow.evaluate((element) => {
      if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
        throw new Error("多选后 childB 未进入选中态");
      }
    });

    await parentRow.click({ timeout: UI_TIMEOUT_MS });
    await childBRow.click({ modifiers: ["Shift"], timeout: UI_TIMEOUT_MS });
    const selectedCount = await page.locator('.tree-row[data-shell-mode="filetree"][data-selected="true"]').count();
    assert(selectedCount >= 2, `范围选择后选中数量异常：${selectedCount}`);

    const fileTreePayload = JSON.stringify({
      type: "mnote-file-tree-dnd",
      version: 1,
      rowIds: [`doc:${fixture.childBId}`],
    });
    await dispatchTextDragSequence(page, {
      sourceSelector: `[data-testid="filetree-doc-row"][data-document-id="${fixture.childBId}"]`,
      targetSelector: `[data-testid="filetree-doc-row"][data-document-id="${fixture.targetId}"]`,
      entries: [
        { type: "application/x-mnote-filetree-row-ids", value: fileTreePayload },
        { type: "application/x-mnote-file-tree", value: fileTreePayload },
        { type: "text/plain", value: fileTreePayload },
      ],
    });
    await page.waitForFunction(
      () => (document.querySelector("#tree-shell-last-action")?.textContent ?? "").includes("已发送移动拖放到"),
      undefined,
      { timeout: UI_TIMEOUT_MS },
    );

    const dataTransfer = await createFileDropDataTransfer(page, "task112-upload.txt", "task112 external drop");
    await targetRow.dispatchEvent("dragover", { dataTransfer });
    await targetRow.dispatchEvent("drop", { dataTransfer });
    await page.waitForFunction(
      () => (document.querySelector("#tree-shell-last-action")?.textContent ?? "").includes("已发送 1 个外部文件到宿主"),
      undefined,
      { timeout: UI_TIMEOUT_MS },
    );
    await dataTransfer.dispose();

    return {
      fileTreeMultiSelect: true,
      fileTreeRangeSelect: true,
      fileTreeInternalDrop: true,
      fileTreeExternalDrop: true,
      selectedCount,
    };
  } finally {
    await page.close().catch(() => undefined);
  }
}

async function runFileTreeHostDoubleClickCheck(context, fixture) {
  const page = await context.newPage();
  try {
    await openDocument(page, fixture.workspaceId, fixture.parentId);
    await openFilesystemView(page);
    const driver = await getFileTreeHostDriver(page);
    await doubleClickFileTreeDocumentFromHost(page, driver, fixture.childBId);
    return {
      fileTreeDoubleClickOpen: true,
      hostKind: driver.kind,
    };
  } finally {
    await page.close().catch(() => undefined);
  }
}

function isPlaywrightTargetCrash(error) {
  const message = error instanceof Error ? error.message : String(error);
  return message.includes("Target crashed");
}

async function waitForPickerEmptyState(page, dialog, pickerUsesIframe) {
  if (!pickerUsesIframe) {
    const emptyText = dialog.getByText("没有匹配结果");
    await emptyText.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    return "没有匹配结果";
  }

  await waitForTextAnywhere(page, "没有可选择的页面。");
  return "没有可选择的页面。";
}

async function waitForPickerInputFocused(page, searchInput) {
  await searchInput.focus();
  await page.waitForFunction(
    () => {
      const active = document.activeElement;
      return active instanceof HTMLInputElement && active.placeholder === "移动到...";
    },
    undefined,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function runPickerDialogChecks(context, fixture) {
  const page = await context.newPage();
  try {
    await openDocument(page, fixture.workspaceId, fixture.childAId);
    await waitForPageTitleInput(page);
    await ensurePageOptionsVisible(page).catch(() => undefined);
    await page.getByRole("button", { name: "页面选项", exact: true }).waitFor({
      state: "visible",
      timeout: UI_TIMEOUT_MS,
    }).catch(() => undefined);
    const openButton = page.getByRole("button", { name: "移动/嵌入到..." });
    if ((await openButton.count()) === 0) {
      return {
        pickerSkipped: true,
        reason: "move_embed_entry_missing_in_current_page_options_shell",
      };
    }
    try {
      await openButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    } catch {
      return {
        pickerSkipped: true,
        reason: "move_embed_entry_hidden_in_current_page_options_shell",
      };
    }
    const openPickerDialog = async () => {
      await openButton.click({ timeout: UI_TIMEOUT_MS });
      const dialog = page.getByRole("dialog");
      await dialog.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
      const pickerSurface = dialog.getByTestId("tree-picker-surface");
      await pickerSurface.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
      const searchInput = dialog.getByPlaceholder("移动到...");
      await waitForInlineTreeShellReady(page, "tree-picker-surface");
      const implementation = await pickerSurface.getAttribute("data-tree-host-implementation");
      assertDefaultHostIsDomWasm(implementation, "tree-picker-surface");
      const pickerUsesIframe =
        treeHostImplementationUsesIframe(implementation) &&
        (await pickerSurface.locator('[data-testid="tree-picker-surface-rust-iframe"]').count()) > 0;
      return { dialog, pickerSurface, searchInput, pickerUsesIframe };
    };

    const initialDialogState = await openPickerDialog();
    const { dialog, searchInput, pickerUsesIframe } = initialDialogState;
    if (pickerUsesIframe) {
      await page.waitForFunction(
        () => {
          const frame = Array.from(
            document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
          )
            .reverse()
            .find((element) => element instanceof HTMLIFrameElement) ?? null;
          if (!(frame instanceof HTMLIFrameElement)) {
            return false;
          }
          return Boolean(frame.contentDocument?.querySelector('[data-testid="tree-picker-root"]'));
        },
        undefined,
        { timeout: UI_TIMEOUT_MS },
      );
      assert(
        !(await page.evaluate((nodeId) => {
          const frame = Array.from(
            document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
          )
            .reverse()
            .find((element) => element instanceof HTMLIFrameElement) ?? null;
          if (!(frame instanceof HTMLIFrameElement)) {
            return false;
          }
          return Boolean(frame.contentDocument?.querySelector(`.tree-row[data-node-id="${nodeId}"]`));
        }, fixture.childAId)),
        "picker 空查询态未排除当前页面",
      );
    } else {
      await dialog.locator('[data-testid="tree-picker-root"]').waitFor({
        state: "visible",
        timeout: UI_TIMEOUT_MS,
      });
      assert(
        (await dialog.locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.childAId}"]`).count()) === 0,
        "picker 空查询态未排除当前页面",
      );
    }
    const pickerRootVisible = pickerUsesIframe
      ? await page.evaluate(() => {
          const frame = Array.from(
            document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
          )
            .reverse()
            .find((element) => element instanceof HTMLIFrameElement) ?? null;
          if (!(frame instanceof HTMLIFrameElement)) {
            return false;
          }
          return Boolean(frame.contentDocument?.querySelector('[data-testid="tree-picker-root"]'));
        })
      : (await dialog.locator('[data-testid="tree-picker-root"]').count()) > 0;

    await searchInput.fill(`task112-no-result-${Date.now()}`, { timeout: UI_TIMEOUT_MS });
    const emptyStateText = await waitForPickerEmptyState(page, dialog, pickerUsesIframe);
    await page.keyboard.press("Escape");
    await dialog.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });

    const keyboardDialogState = await openPickerDialog();
    const keyboardDialog = keyboardDialogState.dialog;
    const keyboardInput = keyboardDialogState.searchInput;
    const keyboardUsesIframe = keyboardDialogState.pickerUsesIframe;
    const keyboardQuery = `task112-keyboard-${Date.now()}`;
    await page.route("**/api/search/documents", async (route) => {
      const payload = route.request().postDataJSON();
      if (payload?.query !== keyboardQuery) {
        await route.continue();
        return;
      }
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          results: [
            {
              id: fixture.parentId,
              title: fixture.parentTitle,
              matchField: "title",
            },
            {
              id: fixture.targetId,
              title: fixture.targetTitle,
              matchField: "title",
            },
          ],
        }),
      });
    });

    try {
      await keyboardInput.fill(keyboardQuery, { timeout: UI_TIMEOUT_MS });
      if (keyboardUsesIframe) {
        await page.waitForFunction(
          (nodeId) => {
            const frame = Array.from(
              document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
            )
              .reverse()
              .find((element) => {
                if (!(element instanceof HTMLIFrameElement)) {
                  return false;
                }
                const row = element.contentDocument?.querySelector(`.tree-row[data-node-id="${nodeId}"]`);
                return Boolean(row);
              }) ?? null;
            return Boolean(frame);
          },
          fixture.targetId,
          { timeout: UI_TIMEOUT_MS },
        );
      } else {
        await keyboardDialog
          .locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.targetId}"]`)
          .waitFor({
            state: "visible",
            timeout: UI_TIMEOUT_MS,
          });
      }

      const moveRequest = page.waitForResponse(
        async (response) => {
          if (
            !response.url().includes("/api/tree/commands") ||
            response.request().method() !== "POST" ||
            !response.ok()
          ) {
            return false;
          }
          const payload = response.request().postDataJSON();
          return payload?.action === "move" && payload?.documentId === fixture.childAId && payload?.parentId === fixture.targetId;
        },
        { timeout: UI_TIMEOUT_MS },
      );
      if (keyboardUsesIframe) {
        await page.evaluate((nodeId) => {
          const frame = Array.from(
            document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
          )
            .reverse()
            .find((element) => element instanceof HTMLIFrameElement) ?? null;
          if (!(frame instanceof HTMLIFrameElement)) {
            throw new Error("picker iframe 不存在");
          }
          const escapedNodeId = CSS.escape(nodeId);
          const button = frame.contentDocument?.querySelector(
            [
              `.tree-row[data-node-id="${escapedNodeId}"][data-testid="tree-picker-row"]`,
              `.tree-row[data-node-id="${escapedNodeId}"] [data-testid="tree-picker-row"]`,
              `.tree-row[data-node-id="${escapedNodeId}"] [data-testid="tree-node-open"]`,
            ].join(", "),
          );
          if (!button || typeof button.click !== "function") {
            throw new Error("picker 目标页面按钮不存在");
          }
          button.click();
        }, fixture.targetId);
      } else {
        await keyboardDialog
          .locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.targetId}"]`)
          .click({ timeout: UI_TIMEOUT_MS });
      }
      await moveRequest;
    } finally {
      await page.unroute("**/api/search/documents").catch(() => undefined);
    }

    await keyboardDialog.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });

    return {
      pickerSearch: true,
      pickerEmpty: true,
      pickerSelect: true,
      pickerHostKind: keyboardUsesIframe ? "iframe" : "dom",
      emptyStateText,
      pickerRootVisible,
      pickerExcludeCurrentDocument: true,
      pickerSearchQuery: keyboardQuery,
      pickerSearchTargetId: fixture.targetId,
    };
  } finally {
    await page.close().catch(() => undefined);
  }
}

async function runStreamFallbackCheck(context, fixture) {
  const response = await context.request.fetch(
    `${BASE_URL}/api/tree/events?workspaceId=${encodeURIComponent(fixture.workspaceId)}&maxPolls=0`,
    { timeout: UI_TIMEOUT_MS },
  );
  const body = await response.text();
  assert(response.ok(), `/api/tree/events 请求失败: ${response.status()} ${body.slice(0, 200)}`);
  assert(
    (response.headers()["content-type"] || "").includes("text/event-stream"),
    "/api/tree/events 未返回 SSE 内容类型",
  );
  assert(response.headers()["x-mnote-web-owner"] === "mnote-web", "/api/tree/events 缺少 mnote-web owner header");
  assert(
    response.headers()["x-mnote-tree-stream-owner"] === "rust-web",
    "/api/tree/events 缺少 rust-web stream owner header",
  );
  assert(body.includes("event: snapshot") || body.includes("event:snapshot"), "/api/tree/events 未返回 snapshot event");
  assert(body.includes('"kind":"snapshot"'), "/api/tree/events snapshot payload 缺少 kind=snapshot");

  return {
    streamFallback: false,
    rustTreeEvents: true,
    streamRequestCount: 1,
  };
}

async function main() {
  const headless =
    process.env.MNOTE_SMOKE_HEADLESS === "1"
      ? true
      : process.env.MNOTE_SMOKE_HEADLESS === "0"
        ? false
        : !process.env.DISPLAY;
  const browser = await chromium.launch({ headless, args: CHROMIUM_STABLE_ARGS });
  const browserContextOptions = {
    viewport: { width: 1440, height: 960 },
  };
  let context = await browser.newContext(browserContextOptions);
  let page = await context.newPage();

  let fixture = null;
  let caughtError = null;

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

    const pageTreeHost = await runPageTreeHostChecks(page, fixture);
    const storageState = await context.storageState();
    await page.close().catch(() => undefined);
    await context.close().catch(() => undefined);
    context = await browser.newContext({
      ...browserContextOptions,
      storageState,
    });
    page = null;
    const pageTreeShell = RUN_DIRECT_TREE_SHELL_CHECKS
      ? await runPageTreeShellChecks(context, fixture)
      : { skipped: true, reason: "direct_tree_shell_debug_disabled" };
    const fileTreeShell = RUN_DIRECT_TREE_SHELL_CHECKS
      ? await runFileTreeShellChecks(context, fixture)
      : { skipped: true, reason: "direct_tree_shell_debug_disabled" };
    let fileTreeHost;
    try {
      fileTreeHost = await runFileTreeHostDoubleClickCheck(context, fixture);
    } catch (error) {
      if (!isPlaywrightTargetCrash(error)) {
        throw error;
      }
      await context.close().catch(() => undefined);
      context = await browser.newContext({
        ...browserContextOptions,
        storageState,
      });
      fileTreeHost = await runFileTreeHostDoubleClickCheck(context, fixture);
      fileTreeHost.recoveredFromRendererCrash = true;
    }
    const picker = await runPickerDialogChecks(context, fixture);
    const streamFallback = await runStreamFallbackCheck(context, fixture);

    console.log(
      JSON.stringify(
        {
          ok: true,
          baseUrl: BASE_URL,
          documentBaseUrl: DOCUMENT_UI_BASE_URL,
          viewerUserId: viewer.userId,
          workspaceId: fixture.workspaceId,
          parentId: fixture.parentId,
          childAId: fixture.childAId,
          childBId: fixture.childBId,
          targetId: fixture.targetId,
          results: {
            pageTreeHost,
            pageTreeShell,
            fileTreeShell,
            fileTreeHost,
            picker,
            streamFallback,
          },
        },
        null,
        2,
      ),
    );
  } catch (error) {
    caughtError = error;
  } finally {
    if (fixture) {
      try {
        await cleanupFixture(context.request, fixture);
      } catch (cleanupError) {
        if (!caughtError) {
          caughtError = cleanupError;
        } else {
          console.error(
            `清理临时页面失败：${
              cleanupError instanceof Error
                ? cleanupError.stack || cleanupError.message
                : String(cleanupError)
            }`,
          );
        }
      }
    }

    if (page) {
      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);
});
