"use strict";

const { chromium } = require("playwright");
const {
  BASE_URL,
  UI_TIMEOUT_MS,
  assert,
  createTempDocument,
  ensureAuthenticated,
  purgeDocument,
} = require("./tree-shell-smoke-helpers");

async function waitForRuntimeIsland(page) {
  await page.waitForFunction(
    () => {
      const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
      const editor = host?.querySelector(".editor-surface .ProseMirror[contenteditable='true']");
      return (
        host?.getAttribute("data-editor-host-kind") === "leptos_tiptap_island" &&
        host?.getAttribute("data-runtime-editor-status") !== "error" &&
        editor instanceof HTMLElement &&
        editor.isContentEditable
      );
    },
    null,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForSaved(page) {
  await page.waitForFunction(
    () =>
      document
        .querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]')
        ?.getAttribute("data-runtime-editor-status") === "saved",
    null,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForTextPresent(page, text) {
  await page.waitForFunction(
    (expected) => {
      const editor = document.querySelector(
        '[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror',
      );
      return (editor?.textContent ?? "").includes(expected);
    },
    text,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function waitForPersisted(page, saveRequests, documentId, expectedText) {
  const deadline = Date.now() + UI_TIMEOUT_MS;
  while (Date.now() < deadline) {
    const editorText = await readEditorText(page);
    const runtimeStatus = await page.evaluate(() => {
      return (
        document
          .querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]')
          ?.getAttribute("data-runtime-editor-status") ?? null
      );
    });
    const hasSaveRequest = saveRequests.some((item) => item.documentId === documentId);
    if (editorText.includes(expectedText) && (runtimeStatus === "saved" || hasSaveRequest)) {
      return;
    }
    await page.waitForTimeout(250);
  }
  throw new Error(`等待持久化超时：${documentId}`);
}

async function typeIntoEditor(page, text) {
  const editor = page
    .locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
    .first();
  await editor.click({ timeout: UI_TIMEOUT_MS });
  await page.keyboard.press("Control+a");
  await page.keyboard.press("Backspace");
  await page.keyboard.type(text, { delay: 30 });
}

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

async function openDocument(page, documentId, workspaceId) {
  await page.goto(
    `${BASE_URL}/documents/${documentId}?workspaceId=${encodeURIComponent(workspaceId)}`,
    { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS },
  );
  await waitForRuntimeIsland(page);
}

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 docA = null;
  let docB = null;
  let caughtError = null;
  const saveRequests = [];

  page.on("request", (request) => {
    if (!request.url().includes("/api/documents/save") || request.method() !== "POST") {
      return;
    }
    const payload = request.postDataJSON();
    saveRequests.push({
      documentId: payload?.documentId ?? null,
      workspaceId: payload?.workspaceId ?? null,
      revision: payload?.revision ?? null,
    });
  });

  try {
    await ensureAuthenticated(page, context.request);
    docA = await createTempDocument(context.request, null);
    docB = await createTempDocument(context.request, null);

    const textA = `doc-a-${Date.now().toString().slice(-6)}`;
    const textB = `doc-b-${(Date.now() + 1).toString().slice(-6)}`;

    await openDocument(page, docA.documentId, docA.workspaceId);
    await typeIntoEditor(page, textA);
    await waitForTextPresent(page, textA);
    await waitForPersisted(page, saveRequests, docA.documentId, textA);
    assert((await readEditorText(page)).includes(textA), "A 页文本未进入编辑区");

    await openDocument(page, docB.documentId, docB.workspaceId);
    await typeIntoEditor(page, textB);
    await waitForTextPresent(page, textB);
    await waitForPersisted(page, saveRequests, docB.documentId, textB);
    assert((await readEditorText(page)).includes(textB), "B 页文本未进入编辑区");

    await openDocument(page, docA.documentId, docA.workspaceId);
    assert((await readEditorText(page)).includes(textA), "切回 A 页后内容串页或丢失");

    await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
    await waitForRuntimeIsland(page);
    assert((await readEditorText(page)).includes(textA), "A 页刷新后未回填");

    await openDocument(page, docB.documentId, docB.workspaceId);
    assert((await readEditorText(page)).includes(textB), "切回 B 页后内容串页或丢失");

    await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
    await waitForRuntimeIsland(page);
    assert((await readEditorText(page)).includes(textB), "B 页刷新后未回填");

    const savedDocumentIds = new Set(saveRequests.map((item) => item.documentId).filter(Boolean));
    assert(savedDocumentIds.has(docA.documentId), "未观察到 A 页保存请求");
    assert(savedDocumentIds.has(docB.documentId), "未观察到 B 页保存请求");
    assert(
      saveRequests.some(
        (item) => item.documentId === docA.documentId && item.workspaceId === docA.workspaceId,
      ),
      "A 页保存请求缺少正确 workspaceId/documentId 绑定",
    );
    assert(
      saveRequests.some(
        (item) => item.documentId === docB.documentId && item.workspaceId === docB.workspaceId,
      ),
      "B 页保存请求缺少正确 workspaceId/documentId 绑定",
    );

    console.log(
      JSON.stringify(
        {
          ok: true,
          docA,
          docB,
          saveRequests,
          textA,
          textB,
        },
        null,
        2,
      ),
    );
  } catch (error) {
    caughtError = error;
  } finally {
    for (const doc of [docA, docB]) {
      if (!doc?.documentId) {
        continue;
      }
      try {
        await purgeDocument(context.request, doc.documentId);
      } catch (cleanupError) {
        if (!caughtError) {
          caughtError = 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);
});
