"use strict";

// 说明：
// - 这是 task-022 的最小真实浏览器回归脚本。
// - 目标覆盖 OnlyOffice 页面打开、插件桥接插入文本、forcesave 按钮、callback 写回闭环。
// - 脚本会创建临时页面并上传临时 docx，回归结束后 purge 页面，避免污染现有数据。

const fs = require("node:fs");
const { chromium } = require("playwright");

const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
const REQUEST_TIMEOUT_MS = 20_000;
const UI_TIMEOUT_MS = 120_000;
const CALLBACK_TIMEOUT_MS = 90_000;
const PROBE_DOCX_PATH = process.env.MNOTE_ONLYOFFICE_PROBE_DOCX || "/tmp/mnote-onlyoffice-probe/probe.docx";
const ONLYOFFICE_PLUGIN_CHANNEL = "mnote_onlyoffice_agent_tools_v1";
const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录";

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

async function requestPayload(requestContext, path, init = {}) {
  const headers =
    init.multipart || init.form
      ? { ...(init.headers || {}) }
      : init.data !== undefined
        ? {
            "content-type": "application/json",
            ...(init.headers || {}),
          }
        : { ...(init.headers || {}) };

  const response = await requestContext.fetch(`${BASE_URL}${path}`, {
    ...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)}`,
    );
  }

  return payload;
}

async function createTempDocument(requestContext) {
  const payload = await requestPayload(requestContext, "/api/documents/create", {
    method: "POST",
    data: { parentId: null },
  });

  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 purgeTempDocument(requestContext, documentId) {
  await requestPayload(requestContext, "/api/documents/purge", {
    method: "POST",
    data: { documentId },
  });
}

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

async function uploadProbeDocx(requestContext, target) {
  assert(fs.existsSync(PROBE_DOCX_PATH), `缺少探测文件：${PROBE_DOCX_PATH}`);
  const buffer = fs.readFileSync(PROBE_DOCX_PATH);
  const payload = await requestPayload(requestContext, "/api/media/upload", {
    method: "POST",
    multipart: {
      file: {
        name: "task022-probe.docx",
        mimeType: DOCX_MIME,
        buffer,
      },
      workspaceId: target.workspaceId,
      documentId: target.documentId,
    },
  });

  assert(payload && payload.asset && typeof payload.asset.id === "string", "上传探测 docx 失败：缺少 asset.id");
  return payload.asset;
}

async function getSignedAsset(requestContext, assetId) {
  const payload = await requestPayload(requestContext, `/api/media/sign?assetId=${encodeURIComponent(assetId)}`, {
    method: "GET",
  });
  assert(payload && typeof payload.signedUrl === "string" && payload.signedUrl, "缺少 signedUrl");
  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 installPluginBridge(page) {
  await page.addInitScript(
    ({ channel }) => {
      const state = {
        channel,
        ready: false,
        origin: "*",
        target: null,
        pending: new Map(),
      };

      window.__TASK022_ONLYOFFICE_PLUGIN__ = state;
      window.addEventListener("message", (event) => {
        const data = event?.data;
        if (!data || typeof data !== "object") return;
        if (data.channel !== channel) return;

        if (data.type === "ready") {
          state.ready = true;
          state.origin = String(event.origin || "*");
          state.target =
            event.source && typeof event.source.postMessage === "function" ? event.source : null;
          return;
        }

        if (data.type === "result") {
          const callId = String(data.callId || "").trim();
          if (!callId) return;
          const pending = state.pending.get(callId);
          if (!pending) return;
          state.pending.delete(callId);
          window.clearTimeout(pending.timeoutId);
          if (data.ok) {
            pending.resolve(data.result ?? null);
          } else {
            pending.reject(new Error(String(data.error || "插件执行失败")));
          }
        }
      });
    },
    { channel: ONLYOFFICE_PLUGIN_CHANNEL },
  );
}

async function waitForOnlyOfficeReady(page) {
  await page.waitForFunction(() => window.__MNOTE_ONLYOFFICE_READY__ === true, {
    timeout: UI_TIMEOUT_MS,
  });
  await page.waitForFunction(
    () => {
      const root = document.getElementById("onlyoffice-frame");
      if (root && root.querySelector("iframe,canvas")) return true;
      return Boolean(document.querySelector("iframe,canvas"));
    },
    { timeout: UI_TIMEOUT_MS },
  );
}

function getEditorIframe(page) {
  return page.locator('iframe[src*="/documenteditor/main/index.html"]').first();
}

async function waitForPluginReady(page) {
  await page.waitForFunction(
    () => Boolean(window.__TASK022_ONLYOFFICE_PLUGIN__?.ready && window.__TASK022_ONLYOFFICE_PLUGIN__?.target),
    { timeout: UI_TIMEOUT_MS },
  );
}

async function callOnlyOfficePlugin(page, tool, args) {
  return page.evaluate(
    async ({ channel, toolName, toolArgs }) => {
      const state = window.__TASK022_ONLYOFFICE_PLUGIN__;
      if (!state || !state.ready || !state.target) {
        throw new Error("OnlyOffice 插件桥未就绪");
      }

      const callId = `task022-${Date.now()}-${Math.random().toString(16).slice(2)}`;
      return await new Promise((resolve, reject) => {
        const timeoutId = window.setTimeout(() => {
          state.pending.delete(callId);
          reject(new Error(`插件调用超时: ${toolName}`));
        }, 60_000);

        state.pending.set(callId, { resolve, reject, timeoutId });
        state.target.postMessage(
          {
            channel,
            type: "call",
            callId,
            tool: toolName,
            args: toolArgs,
          },
          state.origin || "*",
        );
      });
    },
    {
      channel: ONLYOFFICE_PLUGIN_CHANNEL,
      toolName: tool,
      toolArgs: args,
    },
  );
}

async function getOnlyOfficeDebug(page) {
  return page.evaluate(() => ({
    ready: Boolean(window.__MNOTE_ONLYOFFICE_READY__),
    debug: window.__MNOTE_ONLYOFFICE_DEBUG__ ?? null,
    errlog: window.__MNOTE_ONLYOFFICE_ERRLOG__ ?? [],
  }));
}

async function waitForStorageIdChange(requestContext, assetId, previousStorageId) {
  const startedAt = Date.now();
  while (Date.now() - startedAt < CALLBACK_TIMEOUT_MS) {
    const payload = await getSignedAsset(requestContext, assetId);
    const nextStorageId = String(payload.asset?.storage_id || "").trim();
    if (nextStorageId && nextStorageId !== previousStorageId) {
      return payload;
    }
    await new Promise((resolve) => setTimeout(resolve, 2_000));
  }
  throw new Error(`等待 callback 写回超时：storage_id 仍为 ${previousStorageId || "<empty>"}`);
}

async function runBrowserRegression(page, requestContext, viewer, target) {
  const asset = await uploadProbeDocx(requestContext, target);
  const initialSigned = await getSignedAsset(requestContext, asset.id);
  const initialStorageId = String(initialSigned.asset?.storage_id || "").trim();
  assert(initialStorageId, "初始 storage_id 为空");

  const uniqueSuffix = Date.now().toString();
  const insertedText = ` task022-onlyoffice-${uniqueSuffix} `;

  await installPluginBridge(page);

  try {
    const pageUrl = new URL("/onlyoffice", BASE_URL);
    pageUrl.searchParams.set("fileUrl", String(initialSigned.signedUrl));
    pageUrl.searchParams.set("fileName", "task022-probe.docx");
    pageUrl.searchParams.set("fileType", "docx");
    pageUrl.searchParams.set("mode", "edit");
    pageUrl.searchParams.set("assetId", asset.id);
    pageUrl.searchParams.set("documentId", target.documentId);
    pageUrl.searchParams.set("userId", viewer.userId);
    pageUrl.searchParams.set("channel", "web");

    await page.goto(pageUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
    await page.getByText("ONLYOFFICE 加载失败").waitFor({ state: "hidden", timeout: 5_000 }).catch(() => null);

    await waitForOnlyOfficeReady(page);
    await waitForPluginReady(page);

    const editorIframe = getEditorIframe(page);
    await editorIframe.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await editorIframe.click({ position: { x: 160, y: 120 }, timeout: UI_TIMEOUT_MS });

    const initialDebug = await getOnlyOfficeDebug(page);
    assert(initialDebug.ready === true, "OnlyOffice ready 标记未就绪");
    assert(initialDebug.debug && initialDebug.debug.assetId === asset.id, "OnlyOffice debug.assetId 不正确");
    assert(initialDebug.debug && initialDebug.debug.documentId === target.documentId, "OnlyOffice debug.documentId 不正确");
    assert(initialDebug.debug && initialDebug.debug.baseUrl === "/onlyoffice-server", `OnlyOffice baseUrl 异常：${JSON.stringify(initialDebug.debug)}`);
    assert(
      initialDebug.debug && typeof initialDebug.debug.resolvedFileUrl === "string" && initialDebug.debug.resolvedFileUrl.includes("/api/onlyoffice/proxy"),
      `OnlyOffice resolvedFileUrl 未走 proxy：${JSON.stringify(initialDebug.debug)}`,
    );
    assert(initialDebug.debug && typeof initialDebug.debug.docKey === "string" && initialDebug.debug.docKey, "OnlyOffice debug.docKey 为空");

    const pluginResult = await callOnlyOfficePlugin(page, "oo_insert_text", { text: insertedText });
    assert(pluginResult && pluginResult.ok === true, `插件插入文本失败：${JSON.stringify(pluginResult)}`);

    await page.waitForTimeout(2_000);

    const forceSaveResponsePromise = page.waitForResponse(
      (response) =>
        response.url().includes(`/api/onlyoffice/forcesave?assetId=${encodeURIComponent(asset.id)}`) &&
        response.request().method() === "POST" &&
        response.status() === 200,
      { timeout: UI_TIMEOUT_MS },
    );

    const forceSaveButton = page.getByRole("button", { name: "同步保存" });
    await forceSaveButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await forceSaveButton.click({ timeout: UI_TIMEOUT_MS });
    const forceSaveResponse = await forceSaveResponsePromise;
    const forceSavePayload = await forceSaveResponse.json();
    assert(forceSavePayload && forceSavePayload.ok === true, `forcesave 返回异常：${JSON.stringify(forceSavePayload)}`);

    await page.getByText("已触发同步保存").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const updatedSigned = await waitForStorageIdChange(requestContext, asset.id, initialStorageId);

    await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
    await waitForOnlyOfficeReady(page);
    await waitForPluginReady(page);

    const reloadDebug = await getOnlyOfficeDebug(page);
    assert(reloadDebug.ready === true, "刷新后 OnlyOffice ready 标记未就绪");
    assert(
      String(updatedSigned.asset?.storage_id || "").trim() !== initialStorageId,
      "callback 写回后 storage_id 未发生变化",
    );

    return {
      pageUrl: pageUrl.toString(),
      assetId: asset.id,
      initialStorageId,
      updatedStorageId: String(updatedSigned.asset?.storage_id || "").trim(),
      insertedText,
      debug: reloadDebug.debug,
      errlog: reloadDebug.errlog,
    };
  } catch (error) {
    const debug = await getOnlyOfficeDebug(page).catch(() => null);
    if (debug) {
      console.error(JSON.stringify({ onlyofficeDebug: debug }, null, 2));
    }
    throw error;
  }
}

async function main() {
  const health = await fetch(`${BASE_URL}/`, {
    method: "HEAD",
    redirect: "manual",
    signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
  });
  assert([200, 307, 308].includes(health.status), `首页探活失败：收到状态码 ${health.status}`);

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width: 1440, height: 960 },
  });
  const page = await context.newPage();
  let tempDocument = null;
  let caughtError = null;

  try {
    const viewer = await ensureAuthenticated(page, context.request);
    tempDocument = await createTempDocument(context.request);
    const result = await runBrowserRegression(page, context.request, viewer, tempDocument);
    console.log(
      JSON.stringify(
        {
          ok: true,
          workspaceId: tempDocument.workspaceId,
          documentId: tempDocument.documentId,
          ...result,
        },
        null,
        2,
      ),
    );
  } catch (error) {
    caughtError = error;
  } finally {
    if (tempDocument?.documentId) {
      try {
        await purgeTempDocument(context.request, tempDocument.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);
});
