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

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

const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const TASK = "task503-mindmap-skill-capability-smoke";
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const TEST_EMAIL = "mnote.e2e@example.com";
const TEST_PASSWORD = "MnoteE2E123!";

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 callJson(url, init = {}) {
  const response = await fetch(url, {
    ...init,
    headers: {
      ...(init.data !== undefined ? { "content-type": "application/json" } : {}),
      ...(init.headers || {}),
    },
    body: init.data !== undefined ? JSON.stringify(init.data) : init.body,
  });
  const text = await response.text();
  let payload = null;
  try {
    payload = text ? JSON.parse(text) : null;
  } catch {
    payload = text;
  }
  if (!response.ok || payload?.ok === false) {
    throw new Error(`${url} failed: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`);
  }
  return payload;
}

async function callMnoteTool({ toolName, workspaceId, documentId, rootUri, args, idempotencyKey }) {
  const payload = {
    toolName,
    workspaceId,
    documentId,
    sourceKind: "local_folder",
    rootUri,
    actorId: "mnote-e2e",
    sessionId: `sess_${TASK}_${toolName.replace(/\W+/g, "_")}`,
    runId: `run_${Date.now()}`,
    toolCallId: `call_${Date.now()}`,
    traceId: `trace_${Date.now()}`,
    idempotencyKey,
    dryRun: false,
    args,
  };
  const result = await callJson(`${BASE_URL}/api/hermes/tools/mnote/call`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-mnote-actor-id": "mnote-e2e",
      "x-mnote-actor-type": "user",
    },
    body: JSON.stringify(payload),
  });
  return result.result ?? result;
}

async function signInBrowserContext(context) {
  const auth = await context.request.post(`${BASE_URL}/api/auth`, {
    data: {
      action: "auth:signIn",
      args: {
        provider: "password",
        params: {
          email: TEST_EMAIL,
          password: TEST_PASSWORD,
          flow: "signIn",
        },
      },
    },
  });
  assert(auth.ok(), `测试账号登录失败: ${auth.status()} ${await auth.text()}`);
  const whoami = await context.request.get(`${BASE_URL}/api/auth/whoami`);
  assert(whoami.ok(), `whoami 失败: ${whoami.status()} ${await whoami.text()}`);
  const viewer = await whoami.json();
  assert(viewer.userId === "mnote-e2e", `当前登录用户不是 mnote-e2e: ${JSON.stringify(viewer)}`);
  assert(viewer.actorType === "user", `当前登录不是真实 user 会话: ${JSON.stringify(viewer)}`);
  return viewer;
}

function skillCapabilityOutline() {
  return [
    {
      text: "触发场景",
      children: [
        { text: "用户提到脑图", children: [] },
        { text: "资源标签页", children: [] },
        { text: "PDF 转导图", children: [] },
      ],
    },
    {
      text: "读取能力",
      children: [
        { text: "先解析目标", children: [] },
        { text: "fetch 回读树", children: [] },
        { text: "完整 envelope", children: [] },
      ],
    },
    {
      text: "创建能力",
      children: [
        { text: "outline 写入", children: [] },
        { text: "自动生成 uid", children: [] },
        { text: "保留 sourceRefs", children: [] },
      ],
    },
    {
      text: "权限约束",
      children: [
        { text: "需要 read_write", children: [] },
        { text: "限定资源路径", children: [] },
        { text: "拒绝越权写入", children: [] },
      ],
    },
    {
      text: "写后验证",
      children: [
        { text: "fetch 再确认", children: [] },
        { text: "报告 JSON 路径", children: [] },
        { text: "截图看渲染", children: [] },
      ],
    },
    {
      text: "当前边界",
      children: [
        { text: "非裸树文件", children: [] },
        { text: "apply_ops 受限", children: [] },
        { text: "长文本需压缩", children: [] },
      ],
    },
  ];
}

function collectOutlineTexts(nodes, texts = []) {
  for (const node of nodes) {
    texts.push(String(node.text || ""));
    collectOutlineTexts(Array.isArray(node.children) ? node.children : [], texts);
  }
  return texts;
}

async function main() {
  await fs.mkdir(OUTPUT_DIR, { recursive: true });
  const workspace = await callJson(`${BASE_URL}/api/local-folder/workspaces/default`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-mnote-actor-id": "mnote-e2e",
      "x-mnote-actor-type": "user",
    },
    data: {},
  });
  const rootUri = workspace.workspace?.rootUri || "";
  const rootPath = workspace.workspace?.rootPath || "";
  const workspaceId = workspace.workspace?.manifest?.workspaceId || "local-ws:mnote-e2e:my-space";
  assert(rootUri.startsWith("file://"), `默认本地工作区 rootUri 异常: ${JSON.stringify(workspace)}`);
  assert(rootPath, `默认本地工作区 rootPath 缺失: ${JSON.stringify(workspace)}`);

  const documentName = `MindmapSkillCapabilitySmoke-${Date.now()}.md`;
  const documentId = `local-md:${documentName}`;
  const resourcePath = `maps/task503-mindmap-skill-capabilities-${Date.now()}.mindmap.json`;
  const title = "mnote-mindmap skill 能力";
  await fs.mkdir(path.join(rootPath, "maps"), { recursive: true });
  await fs.writeFile(path.join(rootPath, documentName), "# Mindmap Skill Capability Smoke\n", "utf8");

  const skill = await callMnoteTool({
    toolName: "mnote.skill.read",
    workspaceId,
    documentId,
    rootUri,
    args: { skillId: "mnote-mindmap", agentId: "reasonix" },
  });
  const skillContent = String(skill.content || "");
  assert(skillContent.includes("Mindmap outline format"), "skill 缺少导图格式小节");
  assert(skillContent.includes("Use concise keywords or short phrases"), "skill 缺少短语化节点要求");
  assert(skillContent.includes("Avoid long paragraphs"), "skill 缺少避免长段落要求");

  const outline = skillCapabilityOutline();
  const outlineTexts = collectOutlineTexts(outline);
  assert(outlineTexts.every((text) => text.length > 0 && text.length <= 32), "outline 节点应保持短语化");
  assert(outlineTexts.every((text) => !text.includes("\n")), "outline 节点不能用换行模拟层级");

  const aiAccessScope = {
    permissionLevel: "read_write",
    allowedResourceIds: [resourcePath],
  };
  const created = await callMnoteTool({
    toolName: "mnote.mindmap.create_from_outline",
    workspaceId,
    documentId,
    rootUri,
    idempotencyKey: `idem_${TASK}_${Date.now()}`,
    args: {
      mindmapId: resourcePath,
      resourcePath,
      title,
      outline,
      embedIntoPage: true,
      sourceRefs: [{ kind: "skill", title: "mnote-mindmap", note: TASK }],
      aiAccessScope,
    },
  });
  assert(created.root?.data?.text === title, `创建结果根节点异常: ${JSON.stringify(created.root)}`);
  assert(created.embedResult?.status === "embedded", `创建结果缺少页面绑定: ${JSON.stringify(created.embedResult)}`);
  assert(
    Array.isArray(created.changedFiles) && created.changedFiles.includes(documentName),
    `changedFiles 应包含当前 Markdown 页面: ${JSON.stringify(created.changedFiles)}`,
  );
  const markdownAfterEmbed = await fs.readFile(path.join(rootPath, documentName), "utf8");
  assert(
    markdownAfterEmbed.includes(`[${title}](${resourcePath})`),
    `当前 Markdown 页面未写入 mindmap 链接: ${markdownAfterEmbed}`,
  );

  const fetched = await callMnoteTool({
    toolName: "mnote.mindmap.fetch",
    workspaceId,
    documentId,
    rootUri,
    args: {
      mindmapId: resourcePath,
      resourcePath,
      scope: "full_envelope",
      aiAccessScope,
    },
  });
  const nodeCount = Array.isArray(fetched.nodes) ? fetched.nodes.length : 0;
  assert(fetched.root?.data?.text === title, `回读根节点异常: ${JSON.stringify(fetched.root)}`);
  assert(nodeCount >= 25, `回读节点数不足: ${nodeCount}`);

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
  const page = await context.newPage();
  const events = [];
  page.on("pageerror", (error) => events.push({ kind: "pageerror", text: error.message }));
  page.on("console", (message) => {
    if (message.type() === "error") {
      events.push({ kind: "console", text: message.text() });
    }
  });
  page.on("response", (response) => {
    if (response.url().includes("/api/mindmap/")) {
      events.push({ kind: "mindmap-response", status: response.status(), url: response.url() });
    }
  });
  const viewer = await signInBrowserContext(context);
  const url = `${BASE_URL}/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(`local-file:${resourcePath}`)}?sourceKind=local_folder&rootUri=${encodeURIComponent(rootUri)}&workspaceId=${encodeURIComponent(workspaceId)}`;
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
  await page.locator('[data-testid="mnote-mindmap-editor-root"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await page.waitForFunction(
    (expectedTitle) => {
      const text = document.body.innerText || "";
      const countText = document.querySelector('[data-testid="mindmap-schema-count"]')?.textContent || "";
      return text.includes(expectedTitle) && text.includes("触发场景") && /节点\s*(?:[1-9]|[1-9][0-9]+)/.test(countText);
    },
    title,
    { timeout: UI_TIMEOUT_MS },
  );
  const screenshotPath = path.join(OUTPUT_DIR, "skill-capability-mindmap.png");
  await page.screenshot({ path: screenshotPath, fullPage: true });
  const ui = await page.evaluate(() => ({
    bodyHasTitle: (document.body.innerText || "").includes("mnote-mindmap skill 能力"),
    bodyHasBranch: (document.body.innerText || "").includes("触发场景"),
    bodyHasPermission: (document.body.innerText || "").includes("权限约束"),
    schemaCount: document.querySelector('[data-testid="mindmap-schema-count"]')?.textContent || null,
    rootVisible: Boolean(document.querySelector('[data-testid="mnote-mindmap-editor-root"]')),
    errorText: document.querySelector('[data-testid="leptos-mindmap-error"]')?.textContent || null,
  }));
  await browser.close();
  assert(ui.bodyHasTitle && ui.bodyHasBranch && ui.bodyHasPermission, `导图页面文本缺失: ${JSON.stringify(ui)}`);
  assert(!ui.errorText, `导图页面出现错误: ${ui.errorText}`);

  await writeResult({
    ok: true,
    rootUri,
    workspaceId,
    documentId,
    resourcePath,
    mindmapFile: path.join(rootPath, resourcePath),
    screenshotPath,
    url,
    viewer,
    skillFormatRules: {
      hasFormat: skillContent.includes("Mindmap outline format"),
      hasShortPhraseRule: skillContent.includes("Use concise keywords or short phrases"),
      hasAvoidParagraphRule: skillContent.includes("Avoid long paragraphs"),
    },
    nodeCount,
    embedResult: created.embedResult,
    markdownHasMindmapLink: markdownAfterEmbed.includes(`[${title}](${resourcePath})`),
    firstLevel: (fetched.root?.children || []).map((child) => child?.data?.text),
    ui,
    events,
  });
  console.log(`task503 mindmap skill capability smoke passed: ${RESULT_PATH}`);
}

main().catch(async (error) => {
  await writeResult({
    ok: false,
    error: error instanceof Error ? error.message : String(error),
    stack: error instanceof Error ? error.stack : null,
  }).catch(() => undefined);
  console.error(error instanceof Error ? error.stack : error);
  process.exit(1);
});
