#!/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 = "task449-tree-sse-reconnect-snapshot-recovery-smoke";
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const AUTH_BASE_URL = (process.env.MNOTE_AUTH_BASE_URL || BASE_URL).replace(/\/+$/, "");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 45_000);
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "tree-live-cache-smoke", "20260516-task449-reconnect");
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const SCREENSHOT_B_DOCUMENT = path.join(OUTPUT_DIR, "b-document-after-reconnect.png");
const SCREENSHOT_B_FILETREE = path.join(OUTPUT_DIR, "b-filetree-after-reconnect.png");

const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join("");

function cssString(value) {
  return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

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, baseUrl, requestPath, init = {}) {
  const response = await request.fetch(`${baseUrl}${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 authenticate(request, email, name) {
  await requestJson(request, AUTH_BASE_URL, "/api/auth", {
    method: "POST",
    data: {
      action: "auth:signIn",
      args: {
        provider: "password",
        params: { email, password: e2ePassword(), flow: "signUp", name },
      },
    },
  });
}

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

async function treeCommand(request, workspaceId, action, documentId, extra = {}) {
  return await requestJson(request, BASE_URL, "/api/tree/commands", {
    method: "POST",
    data: { action, workspaceId, documentId, ...extra },
  });
}

async function emptyDocumentTrash(request, workspaceId) {
  return await requestJson(request, BASE_URL, "/api/documents/empty-trash", {
    method: "POST",
    data: { workspaceId },
  });
}

async function cleanup(request, workspaceId, documentIds) {
  if (!workspaceId) return;
  for (const documentId of documentIds.filter(Boolean)) {
    await treeCommand(request, workspaceId, "archive", documentId).catch(() => null);
  }
  await emptyDocumentTrash(request, workspaceId).catch(() => null);
}

async function installTreeEventRecorder(page, label, requests) {
  await page.addInitScript(() => {
    window.__MNOTE_TASK449_TREE_EVENTS__ = [];
    const record = (name, event) => {
      const detail = event && event.detail ? event.detail : {};
      const payload = detail.payload || detail || {};
      let raw = "";
      try {
        raw = JSON.stringify(payload);
      } catch {
        raw = "";
      }
      window.__MNOTE_TASK449_TREE_EVENTS__.push({
        name,
        at: Date.now(),
        revision: detail.revision || payload.revision || payload.cursor || "",
        kind: payload.kind || "",
        op: payload && payload.data && payload.data.op ? payload.data.op : payload.op || "",
        raw: raw.slice(0, 2400),
      });
    };
    window.addEventListener("tree:snapshot", (event) => record("tree:snapshot", event));
    window.addEventListener("tree:delta", (event) => record("tree:delta", event));
    window.addEventListener("tree:resync", (event) => record("tree:resync", event));
  });
  page.on("request", (request) => {
    const url = request.url();
    if (url.includes("/api/tree/events")) {
      requests.push({ label, type: "request", method: request.method(), url, at: Date.now() });
    }
  });
  page.on("requestfailed", (request) => {
    const url = request.url();
    if (url.includes("/api/tree/events")) {
      requests.push({
        label,
        type: "requestfailed",
        method: request.method(),
        url,
        failure: request.failure()?.errorText || "",
        at: Date.now(),
      });
    }
  });
}

function recordNavigation(page, label, records) {
  page.on("framenavigated", (frame) => {
    if (frame === page.mainFrame()) {
      records.push({ label, url: frame.url(), at: Date.now() });
    }
  });
}

async function openDocument(page, workspaceId, documentId) {
  await page.goto(`${BASE_URL}/documents/${encodeURIComponent(documentId)}?workspaceId=${encodeURIComponent(workspaceId)}`, {
    waitUntil: "commit",
    timeout: UI_TIMEOUT_MS,
  });
  await page.locator(`[data-page-title-input="true"][data-document-id="${cssString(documentId)}"]`).first().waitFor({
    state: "visible",
    timeout: UI_TIMEOUT_MS,
  });
}

async function openFileTree(page, workspaceId, documentId) {
  await openDocument(page, workspaceId, documentId);
  await page.evaluate(() => {
    const visible = (node) =>
      node instanceof HTMLElement &&
      !node.hidden &&
      getComputedStyle(node).display !== "none" &&
      getComputedStyle(node).visibility !== "hidden" &&
      node.getClientRects().length > 0;
    const fileRoot = document.getElementById("sidebar-file-tree-root");
    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.locator("#sidebar-file-tree-root").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
}

async function waitForRows(page, documentIds) {
  await page.waitForFunction(
    (ids) =>
      ids.every(
        (id) =>
          document.querySelector(`#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${CSS.escape(id)}"]`) &&
          document.querySelector(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="${CSS.escape(`doc:${id}`)}"]`),
      ),
    documentIds,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForReconnectRecoveryWithRows(page, documentIds, minSnapshotCount) {
  await page.waitForFunction(
    ({ ids, count }) => {
      const events = Array.isArray(window.__MNOTE_TASK449_TREE_EVENTS__) ? window.__MNOTE_TASK449_TREE_EVENTS__ : [];
      const snapshotCount = events.filter((event) => event && event.name === "tree:snapshot").length;
      const hasRecoveryEvent = snapshotCount >= count || events.some((event) => event && event.name === "tree:resync");
      if (!hasRecoveryEvent) return false;
      const applied = document.documentElement.getAttribute("data-mnote-tree-live-applied") || "";
      const status = document.documentElement.getAttribute("data-mnote-tree-live-status") || "";
      const error = document.documentElement.getAttribute("data-mnote-tree-live-apply-error") || "";
      if (!["snapshot", "resync"].includes(applied) || status !== "connected" || error) return false;
      return ids.every(
        (id) =>
          document.querySelector(`#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${CSS.escape(id)}"]`) &&
          document.querySelector(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="${CSS.escape(`doc:${id}`)}"]`),
      );
    },
    { ids: documentIds, count: minSnapshotCount },
    { timeout: UI_TIMEOUT_MS },
  );
}

async function readState(page, rootDocumentId) {
  return await page.evaluate((rootId) => {
    const readDirectChildren = (mode) => {
      const rootSelector =
        mode === "filetree"
          ? `#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="doc:${CSS.escape(rootId)}"]`
          : `#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${CSS.escape(rootId)}"]`;
      const rootRow = document.querySelector(rootSelector);
      const list = rootRow?.closest(".tree-node")?.querySelector(":scope > .tree-children");
      if (!list) return [];
      return Array.from(list.querySelectorAll(":scope > .tree-node > .tree-row")).map((row) => ({
        documentId: row instanceof HTMLElement ? row.dataset.documentId || row.dataset.docId || row.dataset.nodeId || "" : "",
        rowId: row instanceof HTMLElement ? row.dataset.rowId || "" : "",
        title: row instanceof HTMLElement ? row.querySelector(".tree-link-title")?.textContent?.trim() || "" : "",
      }));
    };
    return {
      url: window.location.href,
      liveStatus: document.documentElement.getAttribute("data-mnote-tree-live-status") || "",
      liveApplied: document.documentElement.getAttribute("data-mnote-tree-live-applied") || "",
      liveRevision: document.documentElement.getAttribute("data-mnote-tree-live-revision") || "",
      liveError: document.documentElement.getAttribute("data-mnote-tree-live-apply-error") || "",
      pageOrder: readDirectChildren("page"),
      fileTreeOrder: readDirectChildren("filetree"),
      treeEvents: window.__MNOTE_TASK449_TREE_EVENTS__ || [],
    };
  }, rootDocumentId);
}

(async () => {
  const stamp = Date.now();
  const email = `mnote.stage8.reconnect.${stamp}@example.com`;
  const prefix = `TEST-10REVIEW-08-RECONNECT-${stamp}`;
  const result = {
    ok: false,
    task: TASK,
    baseUrl: BASE_URL,
    authBaseUrl: AUTH_BASE_URL,
    email,
    prefix,
    fixture: {},
    navigationEvents: [],
    treeEventRequests: [],
    states: {},
    screenshots: {
      bDocument: SCREENSHOT_B_DOCUMENT,
      bFileTree: SCREENSHOT_B_FILETREE,
    },
  };

  const browser = await chromium.launch({ headless: process.env.HEADFUL !== "1" });
  const contextA = await browser.newContext();
  const contextB = await browser.newContext();
  const documentB = await contextB.newPage();
  const fileTreeB = await contextB.newPage();
  const requestA = contextA.request;
  let workspaceId = "";
  const cleanupIds = [];

  for (const page of [documentB, fileTreeB]) {
    await installTreeEventRecorder(page, page === documentB ? "B-document" : "B-filetree", result.treeEventRequests);
    recordNavigation(page, page === documentB ? "B-document" : "B-filetree", result.navigationEvents);
  }

  try {
    for (const requestContext of [requestA, contextB.request]) {
      await authenticate(requestContext, email, `stage8-reconnect-${stamp}`);
    }

    const root = await createPage(requestA, null, `${prefix}-root`);
    workspaceId = root.workspaceId;
    cleanupIds.push(root.documentId);
    const childA = await createPage(requestA, workspaceId, `${prefix}-a`, root.documentId);
    cleanupIds.push(childA.documentId);
    result.fixture = {
      workspaceId,
      rootId: root.documentId,
      initialChildId: childA.documentId,
    };

    await openDocument(documentB, workspaceId, root.documentId);
    await openFileTree(fileTreeB, workspaceId, root.documentId);
    await waitForRows(documentB, [childA.documentId]);
    await waitForRows(fileTreeB, [childA.documentId]);
    await Promise.all([
      documentB.waitForFunction(() => document.documentElement.getAttribute("data-mnote-tree-live-status") === "connected", null, { timeout: UI_TIMEOUT_MS }),
      fileTreeB.waitForFunction(() => document.documentElement.getAttribute("data-mnote-tree-live-status") === "connected", null, { timeout: UI_TIMEOUT_MS }),
    ]);

    result.states.beforeDisconnect = {
      document: await readState(documentB, root.documentId),
      fileTree: await readState(fileTreeB, root.documentId),
    };
    const snapshotCounts = {
      document:
        result.states.beforeDisconnect.document.treeEvents.filter((event) => event.name === "tree:snapshot").length,
      fileTree:
        result.states.beforeDisconnect.fileTree.treeEvents.filter((event) => event.name === "tree:snapshot").length,
    };

    await contextB.setOffline(true);
    await Promise.all([documentB.waitForTimeout(500), fileTreeB.waitForTimeout(500)]);
    result.states.offline = {
      document: await readState(documentB, root.documentId),
      fileTree: await readState(fileTreeB, root.documentId),
    };

    const navigationStart = result.navigationEvents.length;
    const childB = await createPage(requestA, workspaceId, `${prefix}-b`, root.documentId);
    const childC = await createPage(requestA, workspaceId, `${prefix}-c`, root.documentId);
    cleanupIds.push(childB.documentId, childC.documentId);
    result.fixture.recoveredChildIds = [childB.documentId, childC.documentId];

    assert(
      !(await documentB.locator(`#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${cssString(childB.documentId)}"]`).count()),
      "B 文档页离线期间不应提前看到 childB",
    );
    assert(
      !(await fileTreeB.locator(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="${cssString(`doc:${childB.documentId}`)}"]`).count()),
      "B File Tree 离线期间不应提前看到 childB",
    );

    await contextB.setOffline(false);
    await waitForReconnectRecoveryWithRows(documentB, [childB.documentId, childC.documentId], snapshotCounts.document + 1);
    await waitForReconnectRecoveryWithRows(fileTreeB, [childB.documentId, childC.documentId], snapshotCounts.fileTree + 1);
    await documentB.waitForTimeout(500);
    await fileTreeB.waitForTimeout(500);

    result.states.afterReconnect = {
      document: await readState(documentB, root.documentId),
      fileTree: await readState(fileTreeB, root.documentId),
      navigationEvents: result.navigationEvents.slice(navigationStart),
    };

    assert(
      ["snapshot", "resync"].includes(result.states.afterReconnect.document.liveApplied),
      `B 文档页应通过重连 snapshot/resync 应用最新树快照: ${result.states.afterReconnect.document.liveApplied}`,
    );
    assert(
      ["snapshot", "resync"].includes(result.states.afterReconnect.fileTree.liveApplied),
      `B File Tree 应通过重连 snapshot/resync 应用最新树快照: ${result.states.afterReconnect.fileTree.liveApplied}`,
    );
    assert(!result.states.afterReconnect.document.liveError, `B 文档页存在 live apply error: ${result.states.afterReconnect.document.liveError}`);
    assert(!result.states.afterReconnect.fileTree.liveError, `B File Tree 存在 live apply error: ${result.states.afterReconnect.fileTree.liveError}`);
    assert.equal(result.states.afterReconnect.navigationEvents.length, 0, `B 浏览器发生了刷新/导航: ${JSON.stringify(result.states.afterReconnect.navigationEvents)}`);

    await documentB.screenshot({ path: SCREENSHOT_B_DOCUMENT, fullPage: true });
    await fileTreeB.screenshot({ path: SCREENSHOT_B_FILETREE, fullPage: true });
    result.ok = true;
  } catch (error) {
    result.error = error instanceof Error ? error.stack || error.message : String(error);
    result.failure = {
      document: await readState(documentB, result.fixture.rootId || "").catch(() => null),
      fileTree: await readState(fileTreeB, result.fixture.rootId || "").catch(() => null),
    };
    process.exitCode = 1;
  } finally {
    await contextB.setOffline(false).catch(() => undefined);
    await cleanup(requestA, workspaceId, cleanupIds).catch((error) => {
      result.cleanupError = error instanceof Error ? error.message : String(error);
    });
    await browser.close().catch(() => undefined);
    await writeResult(result);
  }
})();
