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

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

const TASK = "task453-local-folder-page-ai-changed-files-smoke";
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
  || ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium", "/usr/bin/chromium"]
    .find((candidate) => fs.existsSync(candidate));

function fileUrl(localPath) {
  return `file://${localPath}`;
}

function localMdDocumentId(relativePath) {
  return `local-md:${relativePath.replaceAll("/", "~2F")}`;
}

function writeWorkspaceManifest(root, ownerId, workspaceId) {
  fs.mkdirSync(path.join(root, ".mnote"), { recursive: true });
  fs.writeFileSync(
    path.join(root, ".mnote", "workspace.json"),
    `${JSON.stringify({
      workspaceId,
      ownerId,
      createdAt: new Date().toISOString(),
      capabilities: ["local_files", "ai_sessions", "markdown_edit"],
    }, null, 2)}\n`,
    "utf8",
  );
}

async function fetchPageAggregate(page, documentId, rootUri) {
  return await page.evaluate(async ({ id, uri }) => {
    const url = new URL(`/api/page-aggregate/${encodeURIComponent(id)}`, window.location.origin);
    url.searchParams.set("sourceKind", "local_folder");
    url.searchParams.set("rootUri", uri);
    const response = await fetch(url.toString(), { headers: { accept: "application/json" } });
    return {
      ok: response.ok,
      status: response.status,
      payload: await response.json().catch(() => null),
    };
  }, { id: documentId, uri: rootUri });
}

async function waitForEditorText(page, expected) {
  await page.waitForFunction(
    (text) => {
      const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
      return (editor?.textContent || "").includes(text);
    },
    expected,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function typeDirtyText(page, text) {
  const editor = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first();
  await editor.click({ timeout: UI_TIMEOUT_MS });
  await page.keyboard.type(text, { delay: 8 });
  await waitForEditorText(page, text.trim());
}

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

async function conflictEnvelope(page, documentId) {
  return await page.evaluate((docId) => {
    const snapshot = window.__mnoteDebugDocumentSessions?.snapshot?.();
    if (!snapshot) return null;
    const session = snapshot.sessions.find((item) => item.documentId === docId);
    return session?.lastExternalConflictEnvelope || null;
  }, documentId);
}

async function main() {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  const suffix = Date.now().toString(36);
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-local-ai-changed-files-"));
  const documentId = localMdDocumentId("README.md");
  const dirtyDocumentId = localMdDocumentId("Dirty.md");
  const actorId = "user_real";
  const sessionId = `mnote_local_ai_changed_${suffix}`;
  const runId = `run_local_ai_changed_${suffix}`;
  const dirtySessionId = `mnote_local_ai_dirty_${suffix}`;
  const dirtyRunId = `run_local_ai_dirty_${suffix}`;
  const marker = `LOCAL-AI-CHANGED-FILES-${suffix}`;
  const dirtyMarker = `LOCAL-AI-DIRTY-FILES-${suffix}`;
  const dirtyLocalToken = `LOCAL-UNSAVED-DIRTY-${suffix}`;
  const readmePath = path.join(root, "README.md");
  const dirtyPath = path.join(root, "Dirty.md");
  const captured = [];
  let currentScenario = "clean";

  const browser = await chromium.launch({
    headless: true,
    ...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
  });
  const context = await browser.newContext({
    viewport: { width: 1440, height: 960 },
    extraHTTPHeaders: {
      "x-mnote-actor-id": actorId,
      "x-mnote-actor-type": "user",
    },
  });
  const page = await context.newPage();

  try {
    const workspaceId = `local-ws:${actorId}:task453`;
    writeWorkspaceManifest(root, actorId, workspaceId);
    fs.writeFileSync(readmePath, `# Local AI Changed Files\n初始内容 ${suffix}\n`, "utf8");
    fs.writeFileSync(dirtyPath, `# Dirty AI Changed Files\n初始 dirty 内容 ${suffix}\n`, "utf8");
    const rootUri = fileUrl(root);
    const scenarioConfig = () => currentScenario === "dirty"
      ? {
        sessionId: dirtySessionId,
        runId: dirtyRunId,
        documentId: dirtyDocumentId,
        filePath: dirtyPath,
        relativePath: "Dirty.md",
        marker: dirtyMarker,
        message: "已修改本地 Dirty。",
      }
      : {
        sessionId,
        runId,
        documentId,
        filePath: readmePath,
        relativePath: "README.md",
        marker,
        message: "已修改本地 README。",
      };

    await page.route("**/api/ai-agent/run", async (route) => {
      throw new Error(`页面 AI 不应请求旧 /api/ai-agent/run: ${route.request().url()}`);
    });
    await page.route("**/api/documents/save", async (route) => {
      throw new Error(`local-first AI smoke 不应请求 compat /api/documents/save: ${route.request().url()}`);
    });
    await page.route("**/api/hermes/client/gateway/health**", async (route) => {
      await route.fulfill({
        status: 200,
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          ok: true,
          gateway: { ok: true, status: "mocked" },
          profile: { name: "reasonix", modelConfigured: true, apiKeyConfigured: true },
          suggestions: [],
        }),
      });
    });
    await page.route("**/api/hermes/client/tools**", async (route) => {
      await route.fulfill({
        status: 200,
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ ok: true, tools: [] }),
      });
    });
    await page.route("**/api/hermes/client/profiles", async (route) => {
      await route.fulfill({
        status: 200,
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          ok: true,
          active: "reasonix",
          profiles: [{ name: "reasonix", label: "Reasonix", modelConfigured: true, apiKeyConfigured: true }],
        }),
      });
    });
    await page.route("**/api/hermes/client/sessions", async (route) => {
      const scenario = scenarioConfig();
      captured.push({ kind: "session", method: route.request().method(), body: route.request().postData() || "" });
      await route.fulfill({
        status: 200,
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          ok: true,
          sessionId: scenario.sessionId,
          title: "本地 changed files",
          traceId: `trace_local_changed_${suffix}`,
          persistence: "local_ai_session_jsonl",
          sessionStorage: "local_private",
        }),
      });
    });
    await page.route("**/api/hermes/client/sessions/*/resume", async (route) => {
      const scenario = scenarioConfig();
      captured.push({ kind: "session-resume", method: route.request().method(), body: "" });
      await route.fulfill({
        status: 200,
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          ok: true,
          sessionId: scenario.sessionId,
          session: { sessionId: scenario.sessionId, messages: [] },
          runtime: {
            sessionId: scenario.sessionId,
            runId: scenario.runId,
            status: "completed",
            profile: "reasonix",
            documentId: scenario.documentId,
            traceId: `trace_local_changed_resume_${suffix}`,
          },
        }),
      });
    });
    await page.route("**/api/hermes/client/runs", async (route) => {
      const scenario = scenarioConfig();
      captured.push({ kind: "run", method: route.request().method(), body: route.request().postData() || "" });
      await route.fulfill({
        status: 200,
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          ok: true,
          sessionId: scenario.sessionId,
          runId: scenario.runId,
          events: [],
          traceId: `trace_local_changed_run_${suffix}`,
          persistence: "local_ai_session_jsonl",
          sessionStorage: "local_private",
        }),
      });
    });
    await page.route("**/api/hermes/client/events/*", async (route) => {
      const scenario = scenarioConfig();
      captured.push({ kind: "events", method: route.request().method(), body: "" });
      fs.appendFileSync(scenario.filePath, `\nAI 写入标记：${scenario.marker}\n`, "utf8");
      await route.fulfill({
        status: 200,
        headers: { "content-type": "text/event-stream; charset=utf-8" },
        body:
          `data: ${JSON.stringify({ event: "message.delta", run_id: scenario.runId, session_id: scenario.sessionId, delta: scenario.message })}\n\n` +
          `data: ${JSON.stringify({
            event: "run.completed",
            run_id: scenario.runId,
            session_id: scenario.sessionId,
            output: scenario.message,
            agentAudit: {
              eventId: `audit_local_changed_${currentScenario}_${suffix}`,
              actorId,
              actorType: "user",
              agentKind: "reasonix",
              rootUri,
              diffSummary: "1 changed file(s)",
              changedFiles: [
                {
                  path: scenario.relativePath,
                  changeType: "modified",
                  summary: `追加 ${scenario.marker}`,
                  hashBefore: "111",
                  hashAfter: "222",
                  modifiedBeforeMs: 10,
                  modifiedAfterMs: 20,
                },
              ],
            },
          })}\n\n`,
      });
    });

    const documentUrl = new URL(`${BASE_URL}/documents/${encodeURIComponent(documentId)}`);
    documentUrl.searchParams.set("sourceKind", "local_folder");
    documentUrl.searchParams.set("rootUri", rootUri);
    await page.goto(documentUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
    await page.waitForFunction(
      () => (document.body.textContent || "").includes("Local AI Changed Files"),
      null,
      { timeout: UI_TIMEOUT_MS },
    );
    await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({
      state: "attached",
      timeout: UI_TIMEOUT_MS,
    }).catch(() => undefined);

    await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
    await page.locator("[data-page-ai-input]").fill(`请修改 README 并记录 changed files ${marker}`, { timeout: UI_TIMEOUT_MS });
    await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
    await page.waitForFunction(
      () => {
        const drawerText = document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "";
        return drawerText.includes("agent.changed_files") && drawerText.includes("README.md");
      },
      null,
      { timeout: UI_TIMEOUT_MS },
    );

    const cards = await page.$$eval("[data-page-ai-tool-card]", (nodes) =>
      nodes.map((node) => ({
        status: node.getAttribute("data-page-ai-tool-status"),
        text: node.textContent || "",
      })),
    );
    assert(
      cards.some((card) =>
        card.status === "completed"
        && card.text.includes("agent.changed_files")
        && card.text.includes("README.md")
        && card.text.includes(marker)
        && card.text.includes("hash 111→222")
        && card.text.includes("reasonix/user/user_real")
      ),
      `本地 AI changed files 工具卡未显示 README.md 与 diff 摘要: ${JSON.stringify(cards)}`,
    );
    const diskText = fs.readFileSync(readmePath, "utf8");
    assert(diskText.includes(marker), "本地 README.md 未写入 smoke 标记");
    const aggregate = await fetchPageAggregate(page, documentId, rootUri);
    assert.equal(aggregate.ok, true, `Page Aggregate 应能读取 local_folder 文档: ${JSON.stringify(aggregate)}`);
    assert(
      JSON.stringify(aggregate.payload || {}).includes(marker),
      `Page Aggregate 应读回 AI 写入标记: ${JSON.stringify(aggregate)}`,
    );
    await waitForEditorText(page, marker);
    const editorState = await page.evaluate(() => {
      const root = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
      const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
      const conflictPanel = document.querySelector('[data-testid="mnote-editor-conflict-panel"]');
      return {
        status: root?.getAttribute("data-runtime-editor-status") || "",
        text: editor?.textContent || "",
        conflictVisible: Boolean(conflictPanel && conflictPanel.getClientRects().length > 0),
      };
    });
    assert.notEqual(editorState.status, "external-change-conflict", `clean AI 写入不应触发冲突态: ${JSON.stringify(editorState)}`);
    assert.equal(editorState.conflictVisible, false, `clean AI 写入不应显示冲突面板: ${JSON.stringify(editorState)}`);
    assert(captured.some((entry) => entry.kind === "run"), "未捕获 page AI run 请求");
    const runBody = JSON.parse(captured.find((entry) => entry.kind === "run")?.body || "{}");
    assert.equal(runBody.documentId, documentId, `Hermes run 应携带 local documentId: ${JSON.stringify(runBody)}`);
    assert.equal(runBody.sourceKind, "local_folder", `Hermes run 应携带 local_folder sourceKind: ${JSON.stringify(runBody)}`);
    assert.equal(runBody.rootUri, rootUri, `Hermes run 应携带 rootUri: ${JSON.stringify(runBody)}`);
    assert.equal(runBody.editorTarget?.schema, "mnote.ai_editor_target.v1", `Hermes run 应携带 AI editor target: ${JSON.stringify(runBody)}`);
    assert.equal(runBody.editorTarget?.source, "open_editors_snapshot", `AI editor target 应来自 OpenEditorsSnapshot: ${JSON.stringify(runBody.editorTarget)}`);
    assert.equal(runBody.editorTarget?.documentId, documentId, `AI editor target 应指向当前文档: ${JSON.stringify(runBody.editorTarget)}`);
    assert.equal(runBody.runTargetSnapshot?.schema, "mnote.page_ai_run_target_snapshot.v1", `Hermes run 应携带冻结 target snapshot: ${JSON.stringify(runBody.runTargetSnapshot)}`);
    assert.equal(runBody.runTargetSnapshot?.editorTarget?.documentId, documentId, `冻结 target snapshot 应指向发起 run 时的文档: ${JSON.stringify(runBody.runTargetSnapshot)}`);
    assert.equal(runBody.runTargetSnapshot?.editorTarget?.workspacePath?.rootUri, rootUri, `冻结 target snapshot 应保留发起 run 时的 rootUri: ${JSON.stringify(runBody.runTargetSnapshot)}`);
    assert.equal(runBody.pageContext?.aiContext?.runTargetSnapshot?.editorTarget?.documentId, documentId, `pageContext.aiContext 应保留冻结 target snapshot: ${JSON.stringify(runBody.pageContext?.aiContext)}`);
    assert.equal(runBody.pageContext?.aiContext?.activeEditorTarget?.source, "open_editors_snapshot", `pageContext.aiContext 应包含 activeEditorTarget: ${JSON.stringify(runBody.pageContext?.aiContext)}`);
    assert.equal(runBody.pageContext?.aiContext?.openEditorsSnapshot?.activeEditor?.documentId, documentId, `aiContext.openEditorsSnapshot 应包含 active editor: ${JSON.stringify(runBody.pageContext?.aiContext?.openEditorsSnapshot)}`);
    assert(runBody.pageContext?.aiContext?.openEditorsSnapshot?.groups?.primary, `aiContext.openEditorsSnapshot 应保留 primary group: ${JSON.stringify(runBody.pageContext?.aiContext?.openEditorsSnapshot)}`);
    assert(runBody.pageContext?.aiContext?.openEditorsSnapshot?.groups?.secondary, `aiContext.openEditorsSnapshot 应保留 secondary group: ${JSON.stringify(runBody.pageContext?.aiContext?.openEditorsSnapshot)}`);
    const cleanCapturedKinds = captured.map((entry) => entry.kind);

    captured.length = 0;
    await page.evaluate(({ otherRootUri, otherWorkspaceId }) => {
      const runtime = window.__mnoteDocumentPaneRuntime;
      if (!runtime || typeof runtime.getOpenEditorsSnapshot !== "function") {
        throw new Error("missing_open_editors_snapshot_runtime");
      }
      window.__task453OriginalOpenEditorsSnapshot = runtime.getOpenEditorsSnapshot.bind(runtime);
      runtime.getOpenEditorsSnapshot = () => {
        const snapshot = window.__task453OriginalOpenEditorsSnapshot();
        const cloned = JSON.parse(JSON.stringify(snapshot || {}));
        const poisonEditor = (entry) => {
          if (!entry || typeof entry !== "object") return;
          entry.workspaceId = otherWorkspaceId;
          entry.workspacePath = Object.assign({}, entry.workspacePath || {}, {
            workspaceId: otherWorkspaceId,
            rootUri: otherRootUri,
            sourceKind: "local_folder",
          });
        };
        poisonEditor(cloned.activeEditor);
        (cloned.editors || []).forEach((entry) => {
          if (entry && entry.active) poisonEditor(entry);
        });
        Object.values(cloned.groups || {}).forEach((group) => {
          (group.editors || []).forEach((entry) => {
            if (entry && entry.active) poisonEditor(entry);
          });
        });
        return cloned;
      };
    }, { otherRootUri: "file:///tmp/mnote-task453-other-root", otherWorkspaceId: "local-ws:user_real:other" });
    await page.locator("[data-page-ai-input]").fill(`请错误写入其他 workspace ${marker}`, { timeout: UI_TIMEOUT_MS });
    await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
    await page.waitForFunction(
      () => {
        const drawerText = document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "";
        return drawerText.includes("AI target 与当前本地工作区 rootUri 不一致")
          || drawerText.includes("AI target 与当前 workspaceId 不一致");
      },
      null,
      { timeout: UI_TIMEOUT_MS },
    );
    assert(!captured.some((entry) => entry.kind === "run"), `跨 workspace target 不应发起 Hermes run: ${JSON.stringify(captured)}`);
    await page.evaluate(() => {
      const runtime = window.__mnoteDocumentPaneRuntime;
      if (runtime && window.__task453OriginalOpenEditorsSnapshot) {
        runtime.getOpenEditorsSnapshot = window.__task453OriginalOpenEditorsSnapshot;
      }
      delete window.__task453OriginalOpenEditorsSnapshot;
    });

    currentScenario = "dirty";
    captured.length = 0;
    const dirtyUrl = new URL(`${BASE_URL}/documents/${encodeURIComponent(dirtyDocumentId)}`);
    dirtyUrl.searchParams.set("sourceKind", "local_folder");
    dirtyUrl.searchParams.set("rootUri", rootUri);
    await page.goto(dirtyUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
    await waitForEditorText(page, "Dirty AI Changed Files");
    await typeDirtyText(page, ` ${dirtyLocalToken}`);
    await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
    await page.locator("[data-page-ai-input]").fill(`请修改 Dirty 并记录 changed files ${dirtyMarker}`, { timeout: UI_TIMEOUT_MS });
    await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
    await page.waitForFunction(
      () => {
        const drawerText = document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "";
        return drawerText.includes("目标文档存在未保存或外部变更状态");
      },
      null,
      { timeout: UI_TIMEOUT_MS },
    );
    assert(!captured.some((entry) => entry.kind === "run"), `dirty buffer 下 Page AI 不应发起 Hermes run: ${JSON.stringify(captured)}`);
    const dirtyDiskText = fs.readFileSync(dirtyPath, "utf8");
    assert(!dirtyDiskText.includes(dirtyMarker), "dirty buffer 阻断后磁盘不应出现 AI 写入标记");
    const dirtyEditorState = await page.evaluate(() => {
      const root = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
      const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
      const conflictPanel = document.querySelector('[data-testid="mnote-editor-conflict-panel"]');
      return {
        status: root?.getAttribute("data-runtime-editor-status") || "",
        text: editor?.textContent || "",
        conflictVisible: Boolean(conflictPanel && conflictPanel.getClientRects().length > 0),
      };
    });
    assert(dirtyEditorState.text.includes(dirtyLocalToken), `dirty buffer 阻断后本地未保存内容应仍在编辑器中: ${JSON.stringify(dirtyEditorState)}`);
    assert.equal(dirtyEditorState.conflictVisible, false, `dirty buffer 阻断不应生成冲突面板: ${JSON.stringify(dirtyEditorState)}`);
    const dirtyAggregate = await fetchPageAggregate(page, dirtyDocumentId, rootUri);
    assert(
      !JSON.stringify(dirtyAggregate.payload || {}).includes(dirtyMarker),
      `dirty buffer 阻断后 Page Aggregate 不应读回 AI 写入标记: ${JSON.stringify(dirtyAggregate)}`,
    );

    const result = {
      ok: true,
      root,
      documentId,
      sessionId,
      runId,
      marker,
      aggregateRevision: aggregate.payload?.result?.body?.revision ?? aggregate.payload?.body?.revision ?? null,
      editorStatus: editorState.status,
      dirtyDocumentId,
      dirtyRunId,
      dirtyMarker,
      dirtyBlocked: true,
      dirtyEditorStatus: dirtyEditorState.status,
      capturedKinds: cleanCapturedKinds,
      dirtyCapturedKinds: captured.map((entry) => entry.kind),
      resultPath: RESULT_PATH,
    };
    fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
    console.log(JSON.stringify(result, null, 2));
  } finally {
    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
    fs.rmSync(root, { recursive: true, force: true });
  }
}

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