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

const assert = require("node:assert");
const { spawn } = require("node:child_process");
const net = require("node:net");
const { chromium } = require("playwright");

const UI_TIMEOUT_MS = Number(process.env.MNOTE_UI_TIMEOUT_MS || 45000);

function findFreePort() {
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.listen(0, "127.0.0.1", () => {
      const port = server.address().port;
      server.close(() => resolve(port));
    });
    server.on("error", reject);
  });
}

async function waitForGateway(baseUrl, timeoutMs = 120000) {
  const deadline = Date.now() + timeoutMs;
  let lastError = "";
  while (Date.now() < deadline) {
    try {
      const response = await fetch(`${baseUrl}/`);
      if (response.status === 200 || response.status === 303) return;
      lastError = `${response.status} ${await response.text().catch(() => "")}`;
    } catch (error) {
      lastError = error && error.message ? error.message : String(error);
    }
    await new Promise((resolve) => setTimeout(resolve, 500));
  }
  throw new Error(`mnote-web 未就绪: ${lastError}`);
}

function startGateway(port) {
  const env = {
    ...process.env,
    MNOTE_WEB_BIND: `127.0.0.1:${port}`,
    MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
    MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
    MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
    MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
  };
  const child = spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
    cwd: "/mnt/Data1T/mnote/rust",
    env,
    stdio: ["ignore", "pipe", "pipe"],
  });
  let stderr = "";
  child.stderr.on("data", (chunk) => {
    stderr += chunk.toString("utf8");
  });
  child.stdout.on("data", () => {});
  return {
    child,
    stderr: () => stderr,
  };
}

async function stopGateway(gateway) {
  if (!gateway || gateway.child.killed) return;
  gateway.child.kill("SIGTERM");
  await new Promise((resolve) => setTimeout(resolve, 500));
  if (!gateway.child.killed) gateway.child.kill("SIGKILL");
}

async function requestJson(baseUrl, path, init = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    ...init,
    headers: {
      "content-type": "application/json",
      ...(init.headers || {}),
    },
  });
  const body = response.status === 204 ? null : await response.json().catch(() => null);
  return { status: response.status, body };
}

async function enqueueCommand(baseUrl, sessionId, token, action, payload) {
  const response = await requestJson(baseUrl, "/api/onlyoffice/bridge/commands", {
    method: "POST",
    body: JSON.stringify({ sessionId, token, action, payload }),
  });
  assert.equal(response.status, 200, JSON.stringify(response));
  assert(response.body && response.body.command && response.body.command.id, JSON.stringify(response));
  return response.body.command.id;
}

async function readResult(baseUrl, sessionId, token, commandId) {
  const query = new URLSearchParams({
    sessionId,
    token,
    commandId,
    timeoutMs: "5000",
  });
  const response = await requestJson(baseUrl, `/api/onlyoffice/bridge/results?${query.toString()}`);
  assert.equal(response.status, 200, JSON.stringify(response));
  assert.equal(response.body && response.body.ok, true, JSON.stringify(response));
  return response.body.result;
}

async function waitForSession(baseUrl, sessionId, timeoutMs = 5000) {
  const deadline = Date.now() + timeoutMs;
  let lastResponse = null;
  while (Date.now() < deadline) {
    lastResponse = await requestJson(baseUrl, "/api/onlyoffice/bridge/sessions");
    if (lastResponse.status === 200 && lastResponse.body && Array.isArray(lastResponse.body.sessions)) {
      const found = lastResponse.body.sessions.find((session) => session.sessionId === sessionId);
      if (found) return { response: lastResponse, session: found };
    }
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  throw new Error(`bridge session 未注册: ${JSON.stringify(lastResponse)}`);
}

async function main() {
  const port = await findFreePort();
  const baseUrl = `http://127.0.0.1:${port}`;
  const gateway = startGateway(port);
  const suffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
  const sessionId = `mnote-oo-plugin-direct-${suffix}`;
  const token = `token-${suffix}`;
  const consoleErrors = [];
  const networkFailures = [];

  try {
    await waitForGateway(baseUrl);
    const browser = await chromium.launch({ headless: true });
    const context = await browser.newContext();
    const page = await context.newPage();
    page.on("console", (message) => {
      if (message.type() === "error") consoleErrors.push(message.text());
    });
    page.on("requestfailed", (request) => {
      networkFailures.push({ url: request.url(), failure: request.failure()?.errorText || "" });
    });
    await page.route("**/onlyoffice-server/sdkjs-plugins/v1/plugins.js", async (route) => {
      await route.fulfill({
        contentType: "application/javascript; charset=utf-8",
        body: `
          window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__ = { executeCalls: [], commandCalls: [] };
          window.Asc = {
            scope: {},
            plugin: {
              info: { editorType: "word" },
              executeMethod(name, args, callback) {
                window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__.executeCalls.push({ name, args });
                const value = name === "GetSelectedText"
                  ? "mock selected text"
                  : name === "PasteText"
                    ? true
                    : name === "ConvertDocument"
                      ? "# mock document"
                      : null;
                callback(value);
              },
              callCommand(func, _close, _calc, callback) {
                window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__.commandCalls.push({ scope: window.Asc.scope.mnoteBridge || {} });
                callback(func());
              },
              init: null,
              button: null
            }
          };
        `,
      });
    });

    const pluginUrl = new URL(`${baseUrl}/api/onlyoffice/bridge/plugin/index`);
    pluginUrl.searchParams.set("sessionId", sessionId);
    pluginUrl.searchParams.set("token", token);
    pluginUrl.searchParams.set("apiBase", baseUrl);
    pluginUrl.searchParams.set("documentId", "doc_plugin_direct");
    pluginUrl.searchParams.set("assetId", "asset_plugin_direct");
    pluginUrl.searchParams.set("fileType", "docx");
    pluginUrl.searchParams.set("docKey", "doc_key_plugin_direct");
    pluginUrl.searchParams.set("pageOrigin", baseUrl);
    await page.goto(pluginUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
    await page.evaluate(() => window.Asc.plugin.init());

    const { session: registeredSession } = await waitForSession(baseUrl, sessionId);
    assert.equal(registeredSession.docKey, "doc_key_plugin_direct", JSON.stringify(registeredSession));
    assert.equal(registeredSession.pageOrigin, baseUrl, JSON.stringify(registeredSession));

    const wrongToken = await requestJson(
      baseUrl,
      `/api/onlyoffice/bridge/commands/next?sessionId=${encodeURIComponent(sessionId)}&token=wrong-${encodeURIComponent(token)}&timeoutMs=1`,
    );
    assert.equal(wrongToken.status, 401, JSON.stringify(wrongToken));

    const selectionCommandId = await enqueueCommand(baseUrl, sessionId, token, "selection.get", {});
    const selection = await readResult(baseUrl, sessionId, token, selectionCommandId);
    assert.equal(selection.editorType, "word", JSON.stringify(selection));
    assert.equal(selection.text, "mock selected text", JSON.stringify(selection));

    const insertCommandId = await enqueueCommand(baseUrl, sessionId, token, "document.insert_text", { text: "hello" });
    const inserted = await readResult(baseUrl, sessionId, token, insertCommandId);
    assert.equal(inserted.value, true, JSON.stringify(inserted));

    const mockState = await page.evaluate(() => window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__);
    assert(mockState.executeCalls.some((call) => call.name === "GetSelectedText"), JSON.stringify(mockState));
    assert(mockState.executeCalls.some((call) => call.name === "PasteText"), JSON.stringify(mockState));

    await browser.close();
    console.log(JSON.stringify({
      ok: true,
      task: "task517-onlyoffice-bridge-plugin-direct-smoke",
      baseUrl,
      sessionId,
      wrongToken: { status: wrongToken.status, code: wrongToken.body && wrongToken.body.code },
      registeredSession,
      selection,
      inserted,
      executeCalls: mockState.executeCalls.map((call) => call.name),
      consoleErrors,
      networkFailures,
    }, null, 2));
  } finally {
    await stopGateway(gateway);
  }
}

main().catch((error) => {
  console.error(error && error.stack ? error.stack : error);
  process.exit(1);
});
