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

const assert = require("node:assert");
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const {
  BASE_URL,
  UI_TIMEOUT_MS,
  cleanupDocuments,
  createTempDocument,
  ensureAuthenticated,
  openDocument,
  requestJson,
} = require("./tree-shell-smoke-helpers");

const OUT_DIR = path.join(process.cwd(), "tmp", "page-aggregate-body-sync-smoke");

async function fetchPageAggregate(requestContext, workspaceId, documentId) {
  const payload = await requestJson(
    requestContext,
    `/api/page-aggregate/${encodeURIComponent(documentId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
    { method: "GET" },
  );
  assert.equal(payload.schema, "mnote.page_aggregate.v1", "Page Aggregate schema 应保持稳定");
  return payload.result;
}

function findBlockWithText(aggregate, text) {
  const blocks = aggregate?.body?.blockDocument?.blocks;
  if (!Array.isArray(blocks)) return null;
  return blocks.find((block) => typeof block?.text === "string" && block.text.includes(text)) ?? null;
}

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 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: 20 });
}

async function waitForBodySync(requestContext, workspaceId, documentId, beforeRevision, beforeConflictKey, expectedText) {
  const deadline = Date.now() + UI_TIMEOUT_MS;
  let lastAggregate = null;
  while (Date.now() < deadline) {
    lastAggregate = await fetchPageAggregate(requestContext, workspaceId, documentId);
    const body = lastAggregate.body ?? {};
    const matchedBlock = findBlockWithText(lastAggregate, expectedText);
    if (
      matchedBlock &&
      typeof body.revision === "number" &&
      body.revision > beforeRevision &&
      typeof body.conflictDetectionKey === "string" &&
      body.conflictDetectionKey &&
      body.conflictDetectionKey !== beforeConflictKey &&
      body.blockDocument?.documentId === documentId
    ) {
      return { aggregate: lastAggregate, matchedBlock };
    }
    await new Promise((resolve) => setTimeout(resolve, 350));
  }
  throw new Error(
    `等待 Page Aggregate body 同步超时：${JSON.stringify({
      documentId,
      beforeRevision,
      beforeConflictKey,
      lastRevision: lastAggregate?.body?.revision ?? null,
      lastConflictDetectionKey: lastAggregate?.body?.conflictDetectionKey ?? null,
      hasExpectedBlock: Boolean(lastAggregate && findBlockWithText(lastAggregate, expectedText)),
    })}`,
  );
}

async function main() {
  await fs.mkdir(OUT_DIR, { recursive: true });
  const suffix = Date.now().toString(36);
  const expectedText = `Page Aggregate body sync ${suffix}`;
  const evidencePath = path.join(OUT_DIR, `${suffix}.json`);
  const screenshotPath = path.join(OUT_DIR, `${suffix}.png`);
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
  const page = await context.newPage();
  const saveRequests = [];
  const createdIds = [];

  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,
      conflictDetectionKey: payload?.conflictDetectionKey ?? null,
      commandName: payload?.commandName ?? null,
    });
  });

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

    const before = await fetchPageAggregate(context.request, target.workspaceId, target.documentId);
    const beforeRevision = typeof before.body?.revision === "number" ? before.body.revision : -1;
    const beforeConflictKey = typeof before.body?.conflictDetectionKey === "string" ? before.body.conflictDetectionKey : "";

    await openDocument(page, target.workspaceId, target.documentId);
    await waitForRuntimeIsland(page);
    await typeIntoEditor(page, expectedText);

    const saveResponse = await page.waitForResponse(
      async (response) => {
        if (!response.url().includes("/api/documents/save") || response.request().method() !== "POST") return false;
        const payload = response.request().postDataJSON();
        return payload?.documentId === target.documentId && response.ok();
      },
      { timeout: UI_TIMEOUT_MS },
    );

    const { aggregate: after, matchedBlock } = await waitForBodySync(
      context.request,
      target.workspaceId,
      target.documentId,
      beforeRevision,
      beforeConflictKey,
      expectedText,
    );
    await page.screenshot({ path: screenshotPath, fullPage: false });

    const evidence = {
      ok: true,
      baseUrl: BASE_URL,
      viewerUserId: viewer.userId,
      workspaceId: target.workspaceId,
      documentId: target.documentId,
      expectedText,
      before: {
        revision: before.body?.revision ?? null,
        conflictDetectionKey: before.body?.conflictDetectionKey ?? null,
        blockCount: before.body?.blockDocument?.blocks?.length ?? null,
      },
      after: {
        revision: after.body?.revision ?? null,
        conflictDetectionKey: after.body?.conflictDetectionKey ?? null,
        blockProjectionVersion: after.body?.blockProjectionVersion ?? null,
        projectionSource: after.body?.projectionSource ?? null,
        blockCount: after.body?.blockDocument?.blocks?.length ?? null,
      },
      matchedBlock: {
        blockId: matchedBlock.blockId,
        type: matchedBlock.type,
        text: matchedBlock.text,
        revisionRef: matchedBlock.revisionRef,
        editable: matchedBlock.editable,
      },
      saveResponseStatus: saveResponse.status(),
      saveRequests,
      screenshotPath,
      evidencePath,
    };
    await fs.writeFile(evidencePath, JSON.stringify(evidence, null, 2), "utf8");
    console.log(JSON.stringify(evidence, null, 2));
  } finally {
    await cleanupDocuments(context.request, createdIds).catch(() => undefined);
    await page.close().catch(() => undefined);
    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack || error.message : String(error));
  process.exit(1);
});
