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

const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");

const TASK = "task429-trash-restore-location-reveal-smoke";
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const CONVEX_URL = (process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_SELF_HOSTED_URL || "http://127.0.0.1:3210").replace(/\/+$/, "");
const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join("");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");

async function writeResult(payload) {
  await fs.mkdir(OUTPUT_DIR, { recursive: true });
  await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...payload, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8");
}

async function requestJson(request, requestPath, init = {}) {
  const response = await request.fetch(`${BASE_URL}${requestPath}`, {
    ...init,
    headers: {
      ...(init.data !== undefined ? { "content-type": "application/json" } : {}),
      ...(init.headers || {}),
    },
    timeout: 20_000,
  });
  const text = await response.text();
  let payload = null;
  try {
    payload = text ? JSON.parse(text) : null;
  } catch {
    payload = text;
  }
  if (!response.ok()) {
    throw new Error(`${requestPath} 请求失败: ${response.status()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`);
  }
  return payload;
}

async function convexCall(context, kind, convexPath, args) {
  const cookies = await context.cookies(BASE_URL);
  const jwt = cookies.find((cookie) => cookie.name === "__convexAuthJWT")?.value;
  assert(jwt, "缺少 __convexAuthJWT cookie");
  const response = await fetch(`${CONVEX_URL}/api/${kind}`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${jwt}`,
      "content-type": "application/json",
      "Convex-Client": TASK,
    },
    body: JSON.stringify({ path: convexPath, format: "convex_encoded_json", args: [args] }),
  });
  const body = await response.json();
  if (!response.ok || body.status !== "success") {
    throw new Error(`Convex ${kind} ${convexPath} 失败: ${response.status} ${JSON.stringify(body)}`);
  }
  return body.value;
}

async function createPage(request, workspaceId, title, parentId = null) {
  const payload = await requestJson(request, "/api/tree/commands", {
    method: "POST",
    data: {
      action: "create",
      workspaceId,
      parentId,
      title,
    },
  });
  const result = payload.result || payload;
  assert(result.documentId, `创建页面失败: ${JSON.stringify(payload)}`);
  return result.documentId;
}

async function postTreeCommand(request, data) {
  return await requestJson(request, "/api/tree/commands", {
    method: "POST",
    data,
  });
}

function pageTreeRowSelector(documentId) {
  const escaped = String(documentId).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
  return `[data-testid="page-tree-row"][data-node-id="${escaped}"], [data-testid="wolai-sidebar-row"][data-node-id="${escaped}"], #sidebar-file-tree-root .tree-row[data-document-id="${escaped}"], #sidebar-file-tree-root .tree-row[data-doc-id="${escaped}"]`;
}

async function waitForPageTreeRow(page, documentId, label) {
  await page.locator(pageTreeRowSelector(documentId)).first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }).catch(async (error) => {
    const text = await page.locator('[data-testid="sidebar-page-tree-shell"], #sidebar-file-tree-root, [data-testid="wolai-sidebar"]').innerText({ timeout: 3_000 }).catch(() => "");
    throw new Error(`${label} 未出现在侧边栏树: ${error.message}; tree=${text.slice(0, 1600)}`);
  });
}

async function openDocumentFileTree(page, workspaceId, documentId) {
  await page.goto(`${BASE_URL}/documents/${encodeURIComponent(documentId)}?workspaceId=${encodeURIComponent(workspaceId)}`, {
    waitUntil: "commit",
    timeout: UI_TIMEOUT_MS,
  });
  await page.waitForFunction(
    () => {
      const fileRoot = document.getElementById("sidebar-file-tree-root");
      const fileTab = document.querySelector('[data-mnote-sidebar-tree-tab="filetree"], button[aria-label="文件"]');
      return Boolean(fileRoot || fileTab);
    },
    undefined,
    { timeout: UI_TIMEOUT_MS },
  );
  await page.evaluate(() => {
    const fileRoot = document.getElementById("sidebar-file-tree-root");
    const visible = (node) =>
      node instanceof HTMLElement &&
      !node.hidden &&
      getComputedStyle(node).display !== "none" &&
      getComputedStyle(node).visibility !== "hidden" &&
      node.getClientRects().length > 0;
    if (visible(fileRoot)) return;
    const tab = document.querySelector('[data-mnote-sidebar-tree-tab="filetree"], button[aria-label="文件"]');
    if (tab instanceof HTMLElement) tab.click();
  });
  await page.waitForFunction(
    () => {
      const fileRoot = document.getElementById("sidebar-file-tree-root");
      return fileRoot instanceof HTMLElement && fileRoot.getClientRects().length > 0;
    },
    undefined,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function clickTrashButton(page) {
  if (await page.getByRole("heading", { name: "垃圾桶" }).isVisible().catch(() => false)) {
    return;
  }
  await page.getByRole("button", { name: /^垃圾桶\b/ }).first().click({ timeout: UI_TIMEOUT_MS });
  await page.getByRole("heading", { name: "垃圾桶" }).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
}

async function restoreTrashItemWithDialogs(page, title, expectFallbackAlert = false) {
  const confirmPromise = page.waitForEvent("dialog", { timeout: UI_TIMEOUT_MS }).then(async (dialog) => {
    const message = dialog.message();
    await dialog.accept();
    return message;
  });
  const clicked = await page.evaluate((targetTitle) => {
      const rows = Array.from(document.querySelectorAll("div"));
      for (const row of rows) {
        if (!row.textContent?.includes(targetTitle)) continue;
        const button = Array.from(row.querySelectorAll("button")).find((candidate) => candidate.textContent?.trim() === "恢复");
        if (button instanceof HTMLButtonElement) {
          button.click();
          return true;
        }
      }
      return false;
  }, title);
  assert(clicked, `未找到垃圾桶恢复按钮: ${title}`);
  const dialogs = [await confirmPromise];
  if (expectFallbackAlert) {
    const alertMessage = await page.waitForEvent("dialog", { timeout: UI_TIMEOUT_MS }).then(async (dialog) => {
      const message = dialog.message();
      await dialog.accept();
      return message;
    });
    dialogs.push(alertMessage);
    assert(dialogs.some((message) => message.includes("原父页面已不存在")), `缺少 fallback 提示: ${JSON.stringify(dialogs)}`);
  }
  return dialogs;
}

async function listDocuments(context, workspaceId) {
  return await convexCall(context, "query", "documents:listByWorkspace", { workspaceId });
}

async function cleanup(request, workspaceId, rootId, orphanId) {
  if (!workspaceId) return;
  if (orphanId) {
    await postTreeCommand(request, { action: "archive", workspaceId, documentId: orphanId }).catch(() => null);
  }
  if (rootId) {
    await postTreeCommand(request, { action: "archive", workspaceId, documentId: rootId }).catch(() => null);
  }
  await requestJson(request, "/api/documents/empty-trash", { method: "POST", data: { workspaceId } }).catch(() => null);
}

async function main() {
  const stamp = Date.now();
  const email = `mnote.restore.${stamp}@example.com`;
  const username = `restore-${stamp}`;
  const parentTitle = `TEST-10REVIEW-07-P6-parent-${stamp}`;
  const childTitle = `TEST-10REVIEW-07-P6-child-${stamp}`;
  const siblingTitle = `TEST-10REVIEW-07-P6-sibling-${stamp}`;
  const orphanTitle = `TEST-10REVIEW-07-P6-orphan-${stamp}`;
  const orphanId = `tree_restore_orphan_${stamp}`;
  const missingParentId = `tree_missing_parent_${stamp}`;
  const result = {
    ok: false,
    task: TASK,
    baseUrl: BASE_URL,
    convexUrl: CONVEX_URL,
    email,
    dialogs: [],
  };

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext();
  const page = await context.newPage();
  const request = context.request;
  let workspaceId = null;
  let parentId = null;

  try {
    await requestJson(request, "/api/auth", {
      method: "POST",
      data: {
        action: "auth:signIn",
        args: {
          provider: "password",
          params: { email, password: e2ePassword(), flow: "signUp", name: username },
        },
      },
    });

    await page.goto(`${BASE_URL}/`, { waitUntil: "commit", timeout: UI_TIMEOUT_MS });
    const currentUser = await convexCall(context, "query", "users:currentUser", {});
    const userId = currentUser?._id;
    assert(userId, "缺少 Convex currentUser._id");
    const workspaces = await convexCall(context, "query", "workspaces:fetchWorkspaceSummaries", {});
    workspaceId = workspaces.activeWorkspaceId;
    assert(workspaceId, "缺少隔离 workspaceId");

    parentId = await createPage(request, workspaceId, parentTitle);
    const childId = await createPage(request, workspaceId, childTitle, parentId);
    const siblingId = await createPage(request, workspaceId, siblingTitle, parentId);
    await convexCall(context, "mutation", "documents:create", {
      id: orphanId,
      workspaceId,
      parentId: missingParentId,
      title: orphanTitle,
      accessScope: "private",
      content: [],
    });

    await page.goto(`${BASE_URL}/documents/${encodeURIComponent(parentId)}?workspaceId=${encodeURIComponent(workspaceId)}`, {
      waitUntil: "commit",
      timeout: UI_TIMEOUT_MS,
    });
    await waitForPageTreeRow(page, parentId, "父页面");
    await waitForPageTreeRow(page, childId, "子页面");
    await waitForPageTreeRow(page, siblingId, "兄弟页面");

    await postTreeCommand(request, { action: "archive", workspaceId, documentId: childId });
    await page.locator(pageTreeRowSelector(childId)).first().waitFor({ state: "detached", timeout: UI_TIMEOUT_MS });
    await postTreeCommand(request, { action: "archive", workspaceId, documentId: orphanId });

    const restorePayload = await postTreeCommand(request, { action: "restore", workspaceId, documentId: childId });
    result.restorePayload = restorePayload.result || restorePayload;
    await waitForPageTreeRow(page, childId, "恢复后的子页面");
    await page.waitForFunction(
      (documentId) => {
        const row = document.querySelector(`[data-testid="page-tree-row"][data-node-id="${documentId}"], [data-testid="wolai-sidebar-row"][data-node-id="${documentId}"], #sidebar-file-tree-root .tree-row[data-document-id="${documentId}"], #sidebar-file-tree-root .tree-row[data-doc-id="${documentId}"]`);
        return row instanceof HTMLElement && row.getClientRects().length > 0;
      },
      childId,
      { timeout: UI_TIMEOUT_MS },
    );

    let docs = await listDocuments(context, workspaceId);
    const childDoc = docs.find((doc) => doc.id === childId);
    const siblingDoc = docs.find((doc) => doc.id === siblingId);
    assert.equal(childDoc?.parent_id, parentId, "恢复后的子页面应回到原父页面");
    assert.equal(childDoc?.sort_order, 0, "恢复后的子页面应回到原排序位");
    assert.equal(siblingDoc?.sort_order, 1, "原排序位被恢复页面占用时，兄弟页面应后移");

    const fallbackPayload = await postTreeCommand(request, { action: "restore", workspaceId, documentId: orphanId });
    result.fallbackPayload = fallbackPayload.result || fallbackPayload;
    assert.equal(
      result.fallbackPayload?.execution?.restore_location?.fallback_reason,
      "parent_missing_or_deleted",
      `fallback restore 应返回 parent_missing_or_deleted: ${JSON.stringify(result.fallbackPayload)}`,
    );
    await waitForPageTreeRow(page, orphanId, "fallback 恢复后的孤儿页面");
    docs = await listDocuments(context, workspaceId);
    const orphanDoc = docs.find((doc) => doc.id === orphanId);
    assert.equal(orphanDoc?.parent_id ?? null, null, "原父页面不存在时应恢复到根目录");

    const order = await page.evaluate(
      ({ child, sibling }) =>
        Array.from(document.querySelectorAll('[data-testid="page-tree-row"], [data-testid="wolai-sidebar-row"], #sidebar-file-tree-root .tree-row'))
          .map((row) => row instanceof HTMLElement ? row.dataset.nodeId : "")
          .filter((id) => id === child || id === sibling)
          .filter((id, index, list) => list.indexOf(id) === index),
      { child: childId, sibling: siblingId },
    );
    assert.deepEqual(order, [childId, siblingId], `页面树顺序应为 child -> sibling: ${JSON.stringify(order)}`);

    result.ok = true;
    result.workspaceId = workspaceId;
    result.parentId = parentId;
    result.childId = childId;
    result.siblingId = siblingId;
    result.orphanId = orphanId;
    await writeResult(result);
  } catch (error) {
    result.error = error instanceof Error ? error.stack || error.message : String(error);
    result.pageTreeText = await page.locator('[data-testid="sidebar-page-tree-shell"], #sidebar-file-tree-root, [data-testid="wolai-sidebar"]').innerText({ timeout: 3_000 }).catch(() => "");
    await writeResult(result);
    throw error;
  } finally {
    await cleanup(request, workspaceId, parentId, orphanId).catch((error) => {
      console.warn(`清理 task429 临时数据失败: ${error instanceof Error ? error.message : String(error)}`);
    });
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
