"use strict";

// 说明：
// - 这是 Sidebar / PageTree Rust Web tree shell 的真实网页 smoke 脚本。
// - 目标覆盖：主页面已挂载 iframe tree shell、展开/折叠、在 shell 内创建子页面、重命名、移动、导航。
// - 脚本会创建临时页面并在结束后清理，避免污染现有数据。

const { chromium } = require("playwright");

const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
const MNOTE_WEB_BASE_URL = process.env.MNOTE_WEB_SMOKE_BASE_URL || "";
const REQUEST_TIMEOUT_MS = 20_000;
const UI_TIMEOUT_MS = 30_000;
const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录";

function assert(condition, message) {
  if (!condition) {
    throw new Error(message);
  }
}

function requireMnoteWebSmokeBaseUrl() {
  assert(
    Boolean(MNOTE_WEB_BASE_URL),
    "当前 smoke 仅用于 legacy mnote-web tree shell，对应端口已默认退役；如需执行，请显式设置 MNOTE_WEB_SMOKE_BASE_URL。",
  );
  return MNOTE_WEB_BASE_URL;
}

async function requestJson(requestContext, path, init = {}) {
  const response = await requestContext.fetch(`${BASE_URL}${path}`, {
    ...init,
    headers:
      init.data !== undefined
        ? {
            "content-type": "application/json",
            ...(init.headers || {}),
          }
        : {
            ...(init.headers || {}),
          },
    timeout: REQUEST_TIMEOUT_MS,
  });

  const text = await response.text();
  let payload = null;
  try {
    payload = text ? JSON.parse(text) : null;
  } catch {
    payload = text;
  }

  if (!response.ok()) {
    throw new Error(
      `${path} 请求失败: ${response.status()} ${response.statusText()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
    );
  }

  const contentType = response.headers()["content-type"] || "";
  if (!contentType.includes("application/json")) {
    const snippet = typeof payload === "string" ? payload.slice(0, 200) : JSON.stringify(payload).slice(0, 200);
    throw new Error(`${path} 返回了非 JSON 内容：${snippet}`);
  }

  return payload;
}

async function createTempDocument(requestContext, parentId = null) {
  const payload = await requestJson(requestContext, "/api/documents/create", {
    method: "POST",
    data: { parentId },
  });
  assert(payload && typeof payload.id === "string", "创建临时页面失败：缺少 id");
  assert(payload && typeof payload.workspace_id === "string", "创建临时页面失败：缺少 workspace_id");
  return {
    documentId: payload.id,
    workspaceId: payload.workspace_id,
  };
}

async function renameDocument(requestContext, workspaceId, documentId, title) {
  await requestJson(requestContext, "/api/documents/title", {
    method: "POST",
    data: {
      workspaceId,
      documentId,
      title,
    },
  });
}

async function purgeDocument(requestContext, documentId) {
  await requestJson(requestContext, "/api/documents/purge", {
    method: "POST",
    data: { documentId },
  });
}

async function getViewerIdentity(requestContext) {
  const payload = await requestJson(requestContext, "/api/auth/whoami", { method: "GET" });
  assert(payload && typeof payload.userId === "string" && payload.userId, "获取当前用户失败：缺少 userId");
  return payload;
}

async function ensureAuthenticated(page, requestContext) {
  await page.goto(`${BASE_URL}/auth`, { waitUntil: "networkidle", timeout: UI_TIMEOUT_MS });

  if (page.url().includes("/auth")) {
    const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME });
    await quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
    await page.waitForURL((url) => !url.toString().includes("/auth"), {
      timeout: UI_TIMEOUT_MS,
    });
  }

  return await getViewerIdentity(requestContext);
}

async function runTreeShellRegression(page, requestContext, viewer, target) {
  const runtimeBaseUrl = requireMnoteWebSmokeBaseUrl();
  const uniqueSuffix = Date.now().toString();
  const rootTitle = `task091-root-${uniqueSuffix}`;
  const childTitle = `task091-child-${uniqueSuffix}`;
  const createdTitle = `task091-created-${uniqueSuffix}`;
  const renamedTitle = `task091-renamed-${uniqueSuffix}`;

  await renameDocument(requestContext, target.workspaceId, target.parentId, rootTitle);
  await renameDocument(requestContext, target.workspaceId, target.childId, childTitle);

  const documentUrl = `${BASE_URL}/documents/${target.parentId}?workspaceId=${encodeURIComponent(target.workspaceId)}`;
  await page.goto(documentUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });

  const groupButton = page.getByRole("button", { name: "分组" });
  await groupButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await groupButton.click({ timeout: UI_TIMEOUT_MS });

  const sidebarPanel = page.getByText("页面树");
  await sidebarPanel.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

  const iframe = page.locator('iframe[title="mnote-web tree shell"]');
  await iframe.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  const iframeSrc = await iframe.getAttribute("src");
  assert(iframeSrc && iframeSrc.includes(`${runtimeBaseUrl}/tree`), `Sidebar 未挂载 mnote-web tree shell：${iframeSrc}`);
  assert(iframeSrc.includes(`actorId=${encodeURIComponent(viewer.userId)}`), `tree shell 未透传当前用户 actorId：${iframeSrc}`);

  const getTreeFrame = () => page.frameLocator('iframe[title="mnote-web tree shell"]');
  const waitForTreeReady = async () => {
    const frame = getTreeFrame();
    await frame.locator('[data-testid="tree-create-root"]').waitFor({
      state: "visible",
      timeout: UI_TIMEOUT_MS,
    });
    return frame;
  };
  const getFirstChildId = async (parentId) => {
    const frame = await waitForTreeReady();
    return await frame
      .locator(`.tree-node[data-node-id="${parentId}"] > .tree-children > .tree-node`)
      .first()
      .getAttribute("data-node-id");
  };
  const waitForFirstChildId = async (parentId, expectedId) => {
    const deadline = Date.now() + UI_TIMEOUT_MS;
    while (Date.now() < deadline) {
      const actualId = await getFirstChildId(parentId);
      if (actualId === expectedId) {
        return actualId;
      }
      await page.waitForTimeout(250);
    }
    return await getFirstChildId(parentId);
  };
  let treeFrame = await waitForTreeReady();

  const parentRow = treeFrame.locator(`.tree-row[data-node-id="${target.parentId}"]`);
  const childRow = treeFrame.locator(`.tree-row[data-node-id="${target.childId}"]`);
  await parentRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await childRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

  const toggleButton = parentRow.locator('[data-testid="tree-node-toggle"]');
  await toggleButton.click({ timeout: UI_TIMEOUT_MS });
  await childRow.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
  await toggleButton.click({ timeout: UI_TIMEOUT_MS });
  await childRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

  const createResponsePromise = page.waitForResponse(
    (response) =>
      response.url().includes(`${runtimeBaseUrl}/api/tree/commands`) &&
      response.request().method() === "POST" &&
      response.status() === 200 &&
      (response.request().postData() || "").includes('"action":"create"'),
    { 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();
  const createdDocumentId = createPayload?.result?.documentId;
  assert(typeof createdDocumentId === "string" && createdDocumentId, "tree shell 创建子页面失败：缺少 documentId");
  await page.waitForURL((url) => url.toString().includes(`/documents/${createdDocumentId}`), {
    timeout: UI_TIMEOUT_MS,
  });
  target.createdIds.push(createdDocumentId);

  treeFrame = await waitForTreeReady();
  const createdRow = treeFrame.locator(`.tree-row[data-node-id="${createdDocumentId}"]`);
  await createdRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

  page.once("dialog", (dialog) => dialog.accept(renamedTitle));
  const renameResponsePromise = page.waitForResponse(
    (response) =>
      response.url().includes(`${runtimeBaseUrl}/api/tree/commands`) &&
      response.request().method() === "POST" &&
      response.status() === 200 &&
      (response.request().postData() || "").includes('"action":"rename"'),
    { timeout: UI_TIMEOUT_MS },
  );
  await createdRow.locator('[data-testid="tree-action-rename"]').click({ timeout: UI_TIMEOUT_MS });
  await renameResponsePromise;
  treeFrame = await waitForTreeReady();
  const renamedRow = treeFrame.locator(`.tree-row[data-node-id="${createdDocumentId}"]`);
  await renamedRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

  const childOrderBeforeMove = await getFirstChildId(target.parentId);
  assert(
    childOrderBeforeMove === target.childId,
    `移动前的首个子节点异常：期望 ${target.childId}，实际 ${childOrderBeforeMove}`,
  );

  const moveResponsePromise = page.waitForResponse(
    (response) =>
      response.url().includes(`${runtimeBaseUrl}/api/tree/commands`) &&
      response.request().method() === "POST" &&
      response.status() === 200 &&
      (response.request().postData() || "").includes('"action":"move"'),
    { timeout: UI_TIMEOUT_MS },
  );
  await renamedRow.locator('[data-testid="tree-action-move-up"]').click({ timeout: UI_TIMEOUT_MS });
  await moveResponsePromise;
  const firstChildAfterMove = await waitForFirstChildId(target.parentId, createdDocumentId);
  assert(
    firstChildAfterMove === createdDocumentId,
    `移动后排序未生效：期望首个子节点为 ${createdDocumentId}，实际 ${firstChildAfterMove}`,
  );

  treeFrame = await waitForTreeReady();
  const latestChildRow = treeFrame.locator(`.tree-row[data-node-id="${target.childId}"]`);
  await latestChildRow.locator('[data-testid="tree-node-open"]').click({ timeout: UI_TIMEOUT_MS });
  await page.waitForURL((url) => url.toString().includes(`/documents/${target.childId}`), {
    timeout: UI_TIMEOUT_MS,
  });

  return {
    documentUrl,
    rootTitle,
    childTitle,
    createdTitle,
    renamedTitle,
    createdDocumentId,
  };
}

async function runPickerRegression(page, target) {
  const documentUrl = `${BASE_URL}/documents/${target.parentId}?workspaceId=${encodeURIComponent(target.workspaceId)}`;
  await page.goto(documentUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });

  const inspectorToggle = page.getByRole("button", { name: /显示页面选项|隐藏页面选项/ });
  await inspectorToggle.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  const currentLabel = (await inspectorToggle.getAttribute("aria-label")) || "";
  if (currentLabel.includes("显示")) {
    await inspectorToggle.click({ timeout: UI_TIMEOUT_MS });
  }

  const moveEmbedButton = page.getByRole("button", { name: "移动/嵌入到..." });
  await moveEmbedButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await moveEmbedButton.click({ timeout: UI_TIMEOUT_MS });

  const dialog = page.getByRole("dialog");
  await dialog.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });

  const pickerIframe = dialog.locator('iframe[title="mnote-web tree shell"]');
  await pickerIframe.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  const iframeSrc = await pickerIframe.getAttribute("src");
  assert(iframeSrc && iframeSrc.includes("mode=picker"), `picker 未以 tree shell 轻量模式挂载：${iframeSrc}`);
  assert(iframeSrc.includes("allowRootPick=1"), `picker 未透传 allowRootPick：${iframeSrc}`);
  assert(
    iframeSrc.includes(`excludeIds=${encodeURIComponent(target.parentId)}`),
    `picker 未透传 excludeIds：${iframeSrc}`,
  );

  const frame = dialog.frameLocator('iframe[title="mnote-web tree shell"]');
  const rootPick = frame.locator('[data-testid="tree-picker-root"]');
  await rootPick.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await rootPick.click({ timeout: UI_TIMEOUT_MS });

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

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 caughtError = null;
  let result = null;
  const createdIds = [];

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

    result = await runTreeShellRegression(page, context.request, viewer, {
      workspaceId: parent.workspaceId,
      parentId: parent.documentId,
      childId: child.documentId,
      createdIds,
    });
    await runPickerRegression(page, {
      workspaceId: parent.workspaceId,
      parentId: parent.documentId,
    });

    console.log(
      JSON.stringify(
        {
          ok: true,
          workspaceId: parent.workspaceId,
          viewerUserId: viewer.userId,
          parentId: parent.documentId,
          childId: child.documentId,
          ...result,
        },
        null,
        2,
      ),
    );
  } catch (error) {
    caughtError = error;
  } finally {
    for (const documentId of [...createdIds].reverse()) {
      try {
        await purgeDocument(context.request, 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);
});
