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

/**
 * task474-local-folder-empty-trash-ui-smoke.js
 *
 * 用途：验证 local folder trash workbench 的清空垃圾箱 UI 流程：
 *   1. 清空页面垃圾箱（empty documents）
 *   2. 清空资源垃圾箱（empty resources）
 *   断言：行消失、trash-index.json 清理、trash 文件删除、无页面导航
 *
 * 不修改 Rust / SSR 代码。只验证现有行为、记录证据。
 *
 * Usage:
 *   node scripts/task474-local-folder-empty-trash-ui-smoke.js
 *
 * Env:
 *   MNOTE_WEB_SMOKE_BASE_URL    — 3000 地址，默认 http://127.0.0.1:3000
 *   MNOTE_SMOKE_UI_TIMEOUT_MS   — UI 等待超时，默认 30_000
 *   MNOTE_KEEP_SMOKE_TMP        — 设置后保留临时目录
 */

const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { chromium } = require("playwright");

const TASK = "task474-local-folder-empty-trash-ui-smoke";
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
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 writeWorkspaceManifest(root) {
  const metadataDir = path.join(root, ".mnote");
  fs.mkdirSync(metadataDir, { recursive: true });
  fs.writeFileSync(
    path.join(metadataDir, "workspace.json"),
    `${JSON.stringify({
      workspaceId: `local-ws:user_real:${TASK}`,
      ownerId: "user_real",
      createdAt: new Date().toISOString(),
      capabilities: ["local_files", "tree_commands", "markdown_edit", "asset_upload"],
    }, null, 2)}\n`,
    "utf8",
  );
}

/**
 * 编码本地路径段，匹配 Rust encode_local_id_segment 逻辑
 */
function encodeLocalIdSegment(value) {
  let encoded = "";
  for (let i = 0; i < value.length; i++) {
    const ch = value[i];
    const code = value.charCodeAt(i);
    if ((code >= 0x30 && code <= 0x39) || (code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A) || ch === "." || ch === "_" || ch === "-") {
      encoded += ch;
    } else {
      encoded += "~" + code.toString(16).toUpperCase().padStart(2, "0");
    }
  }
  return encoded;
}

/**
 * Tree command 快捷请求
 */
async function postTreeCommand(rootUri, action, documentId) {
  const response = await fetch(`${BASE_URL}/api/tree/commands`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-mnote-actor-id": "user_real",
      "x-mnote-actor-type": "user",
    },
    body: JSON.stringify({
      action,
      sourceKind: "local_folder",
      rootUri,
      documentId,
    }),
  });
  const payload = await response.json().catch(async () => ({ text: await response.text() }));
  assert.equal(response.status, 200, `${action} failed: ${JSON.stringify(payload)}`);
  return payload;
}

/**
 * 快速登录（测试账号）
 */
async function quickLogin(page) {
  await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
  const quickLoginButton = page.getByRole("button", { name: "测试账号快速登录" });
  if (await quickLoginButton.count()) {
    await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
    await page.waitForURL((url) => url.pathname === "/", { timeout: UI_TIMEOUT_MS });
  }
}

/**
 * 构建 trash 页 URL
 */
function trashUrl(rootUri) {
  const url = new URL(`${BASE_URL}/trash`);
  url.searchParams.set("sourceKind", "local_folder");
  url.searchParams.set("rootUri", rootUri);
  return url.toString();
}

/**
 * 打开 trash 页并等待 workbench 可见
 */
async function openTrash(page, rootUri) {
  await page.goto(trashUrl(rootUri), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
  await page.locator('[data-testid="mnote-trash-workbench"][data-trash-source-kind="local_folder"]').waitFor({
    state: "visible",
    timeout: UI_TIMEOUT_MS,
  });
}

/**
 * 读取 trash-index.json 的 entries map
 */
function readTrashIndex(root) {
  const indexPath = path.join(root, ".mnote", "trash-index.json");
  if (!fs.existsSync(indexPath)) return {};
  const raw = fs.readFileSync(indexPath, "utf8");
  const data = JSON.parse(raw);
  return data.entries || data || {};
}

/**
 * 读取 trash-index.json 中指定 entry
 */
function getTrashEntry(root, entryId) {
  const entries = readTrashIndex(root);
  return entries[entryId] || null;
}

/**
 * 从 trash-index.json 获取 entry 的 trashPath
 */
function getTrashFilePath(root, entryId) {
  const entry = getTrashEntry(root, entryId);
  if (!entry) return null;
  return entry.trashRelativePath || entry.trash_relative_path || entry.trashFilePath || entry.trashed_file_path || null;
}

async function main() {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), `mnote-${TASK}-`));
  const rootUri = fileUrl(root);

  // ── 测试数据：两个松散 Markdown 页面和非 md 资源 ──
  const mdRelativePath = "docs/test-page.md";
  const mdDocId = `local-md:${encodeLocalIdSegment(mdRelativePath)}`; // local-md:docs~2Ftest-page.md
  const mdSourcePath = path.join(root, mdRelativePath);

  const md2RelativePath = "docs/second-page.md";
  const md2DocId = `local-md:${encodeLocalIdSegment(md2RelativePath)}`; // local-md:docs~2Fsecond-page.md
  const md2SourcePath = path.join(root, md2RelativePath);

  const assetRelativePath = "docs/resource.txt";
  const assetId = `local-file:${assetRelativePath}`; // local-file:docs/resource.txt
  const assetSourcePath = path.join(root, assetRelativePath);

  // 准备 workspace
  fs.mkdirSync(path.join(root, "docs"), { recursive: true });
  writeWorkspaceManifest(root);
  fs.writeFileSync(path.join(root, "README.md"), "# Local Root\n", "utf8");
  fs.writeFileSync(mdSourcePath, "# Test Page\n\nPage content for empty-trash smoke.\n", "utf8");
  fs.writeFileSync(md2SourcePath, "# Second Page\n\nSecond page content for multi-purge smoke.\n", "utf8");
  fs.writeFileSync(assetSourcePath, "resource body for empty-trash smoke\n", "utf8");

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

  // 导航事件记录
  const navigationEvents = [];
  page.on("framenavigated", (frame) => {
    if (frame === page.mainFrame()) {
      navigationEvents.push({ url: frame.url(), timestamp: Date.now() });
    }
  });

  const evidence = {
    ok: false,
    task: TASK,
    baseUrl: BASE_URL,
    root,
    mdRelativePath,
    mdDocId,
    md2RelativePath,
    md2DocId,
    assetRelativePath,
    assetId,
    steps: [],
    emptyDocuments: {},
    emptyResources: {},
    navEvents: [],
  };

  try {
    // ════════════════════════════════════════════
    // 步骤 1：登录并归档两个条目到垃圾箱
    // ════════════════════════════════════════════
    await quickLogin(page);

    // 归档 Markdown 页面
    const mdArchiveResult = await postTreeCommand(rootUri, "delete", mdDocId);
    assert.equal(mdArchiveResult.result?.execution?.resourceKind, "markdown",
      `MD archive resourceKind should be markdown, got: ${JSON.stringify(mdArchiveResult)}`);
    assert.equal(fs.existsSync(mdSourcePath), false, "archive 后源 Markdown 文件应移入垃圾箱");
    const mdTrashPath = mdArchiveResult.result?.execution?.trashPath
      || mdArchiveResult.trashPath
      || getTrashFilePath(root, mdDocId);
    assert.ok(mdTrashPath, `MD archive 应返回 trashPath, got: ${JSON.stringify(mdArchiveResult)}`);

    // 归档第二个 Markdown 页面，覆盖清空页面垃圾箱多条目串行 purge
    const md2ArchiveResult = await postTreeCommand(rootUri, "delete", md2DocId);
    assert.equal(md2ArchiveResult.result?.execution?.resourceKind, "markdown",
      `MD2 archive resourceKind should be markdown, got: ${JSON.stringify(md2ArchiveResult)}`);
    assert.equal(fs.existsSync(md2SourcePath), false, "archive 后第二个 Markdown 源文件应移入垃圾箱");
    const md2TrashPath = md2ArchiveResult.result?.execution?.trashPath
      || md2ArchiveResult.trashPath
      || getTrashFilePath(root, md2DocId);
    assert.ok(md2TrashPath, `MD2 archive 应返回 trashPath, got: ${JSON.stringify(md2ArchiveResult)}`);

    // 归档非 md 资源
    const assetArchiveResult = await postTreeCommand(rootUri, "delete", assetId);
    assert.equal(assetArchiveResult.result?.execution?.canonicalCommand, "tree.resource.archive",
      `Asset archive should be tree.resource.archive, got: ${JSON.stringify(assetArchiveResult)}`);
    assert.equal(fs.existsSync(assetSourcePath), false, "archive 后源文件应移入垃圾箱");
    const assetTrashPath = assetArchiveResult.result?.execution?.trashPath
      || assetArchiveResult.trashPath
      || getTrashFilePath(root, assetId);
    assert.ok(assetTrashPath, `Asset archive 应返回 trashPath, got: ${JSON.stringify(assetArchiveResult)}`);

    evidence.steps.push({
      step: 1,
      label: "通过 tree command 归档 Markdown 页面和非 md 资源到垃圾箱",
      ok: true,
      mdTrashPath,
      md2TrashPath,
      assetTrashPath,
    });

    // 验证 trash-index.json 包含三个 entry
    const entriesBefore = readTrashIndex(root);
    assert.ok(entriesBefore[mdDocId], `trash-index 应包含 MD entry: ${mdDocId}`);
    assert.ok(entriesBefore[md2DocId], `trash-index 应包含第二个 MD entry: ${md2DocId}`);
    assert.ok(entriesBefore[assetId], `trash-index 应包含 asset entry: ${assetId}`);

    // ════════════════════════════════════════════
    // 步骤 2：打开 trash 页，验证两行可见
    // ════════════════════════════════════════════
    const navBeforeTrashOpen = navigationEvents.length;
    await openTrash(page, rootUri);
    const trashOpenNavDelta = navigationEvents.length - navBeforeTrashOpen;

    // MD 行（页面垃圾箱）
    const mdTrashRow = page.locator(`article[data-trash-row="local"][data-trash-entry-id="${mdDocId}"]`);
    await mdTrashRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const mdResourceKind = await mdTrashRow.getAttribute("data-resource-kind");
    assert.equal(mdResourceKind, "markdown", `MD trash row data-resource-kind 应为 "markdown", 实际: ${mdResourceKind}`);

    const md2TrashRow = page.locator(`article[data-trash-row="local"][data-trash-entry-id="${md2DocId}"]`);
    await md2TrashRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const md2ResourceKind = await md2TrashRow.getAttribute("data-resource-kind");
    assert.equal(md2ResourceKind, "markdown", `第二个 MD trash row data-resource-kind 应为 "markdown", 实际: ${md2ResourceKind}`);

    // Asset 行（资源垃圾箱）
    const assetTrashRow = page.locator(`article[data-trash-row="local"][data-trash-entry-id="${assetId}"]`);
    await assetTrashRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const assetResourceKind = await assetTrashRow.getAttribute("data-resource-kind");
    assert.equal(assetResourceKind, "local_file", `Asset trash row data-resource-kind 应为 "local_file", 实际: ${assetResourceKind}`);

    // 验证按钮 enabled 状态
    const emptyDocsButton = page.locator('[data-trash-action="local-empty-documents"]');
    const emptyResourcesButton = page.locator('[data-trash-action="local-empty-resources"]');
    await emptyDocsButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    await emptyResourcesButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const docsButtonDisabled = await emptyDocsButton.isDisabled();
    const resourcesButtonDisabled = await emptyResourcesButton.isDisabled();
    assert.equal(docsButtonDisabled, false, "存在页面时清空页面垃圾箱按钮应 enabled");
    assert.equal(resourcesButtonDisabled, false, "存在资源时清空资源垃圾箱按钮应 enabled");

    evidence.steps.push({
      step: 2,
      label: "打开 trash 页，两行可见，按钮 enabled",
      ok: true,
      trashOpenNavDelta,
    });

    // ════════════════════════════════════════════
    // 步骤 3：清空页面垃圾箱
    // ════════════════════════════════════════════
    const navBeforeEmptyDocs = navigationEvents.length;

    // 确认 dialog：清空后无法恢复，确定继续吗？
    page.once("dialog", (dialog) => {
      evidence.emptyDocuments.dialogText = dialog.message();
      dialog.accept();
    });
    await emptyDocsButton.click({ timeout: UI_TIMEOUT_MS });

    // 等待 document rows 消失（refresh() 替换 innerHTML 后文档行应被移除）
    // 由于 refresh() 以 fetch+DOMParser 方式替换 innerHTML，原来 DOM 引用会变为 detached
    await page.waitForFunction(
      (id) => !document.querySelector(`[data-trash-entry-id="${id}"]`),
      mdDocId,
      { timeout: UI_TIMEOUT_MS },
    );
    await page.waitForFunction(
      (id) => !document.querySelector(`[data-trash-entry-id="${id}"]`),
      md2DocId,
      { timeout: UI_TIMEOUT_MS },
    );

    // 现在重新获取 DOM 引用验证
    const mdRowAfterEmptyDocs = page.locator(`article[data-trash-row="local"][data-trash-entry-id="${mdDocId}"]`);
    const mdRowCountAfterEmptyDocs = await mdRowAfterEmptyDocs.count();
    assert.equal(mdRowCountAfterEmptyDocs, 0, "清空页面垃圾箱后 MD 行应消失");
    const md2RowAfterEmptyDocs = page.locator(`article[data-trash-row="local"][data-trash-entry-id="${md2DocId}"]`);
    const md2RowCountAfterEmptyDocs = await md2RowAfterEmptyDocs.count();
    assert.equal(md2RowCountAfterEmptyDocs, 0, "清空页面垃圾箱后第二个 MD 行应消失");

    // 文档行消失后，资源行应仍然存在
    const assetRowAfterEmptyDocs = page.locator(`article[data-trash-row="local"][data-trash-entry-id="${assetId}"]`);
    const assetRowCountAfterEmptyDocs = await assetRowAfterEmptyDocs.count();
    assert.equal(assetRowCountAfterEmptyDocs, 1, "清空页面垃圾箱后资源行应仍然存在");

    // 验证按钮状态：文档按钮 disabled，资源按钮仍 enabled
    const docsButtonDisabledAfter = await page.locator('[data-trash-action="local-empty-documents"]').isDisabled();
    assert.equal(docsButtonDisabledAfter, true, "清空页面垃圾箱后 empty-documents 按钮应 disabled");

    // 验证文件系统：trash 文件和 trash-index
    const mdEntryAfterEmptyDocs = getTrashEntry(root, mdDocId);
    assert.equal(mdEntryAfterEmptyDocs, null, `清空页面垃圾箱后 trash-index 不应包含 MD entry: ${mdDocId}`);
    const md2EntryAfterEmptyDocs = getTrashEntry(root, md2DocId);
    assert.equal(md2EntryAfterEmptyDocs, null, `清空页面垃圾箱后 trash-index 不应包含第二个 MD entry: ${md2DocId}`);

    const resolvedMdTrashPath = path.resolve(root, mdTrashPath);
    assert.equal(fs.existsSync(resolvedMdTrashPath), false, `清空页面垃圾箱后 trash 文件应删除: ${resolvedMdTrashPath}`);
    const resolvedMd2TrashPath = path.resolve(root, md2TrashPath);
    assert.equal(fs.existsSync(resolvedMd2TrashPath), false, `清空页面垃圾箱后第二个 trash 文件应删除: ${resolvedMd2TrashPath}`);

    // 导航断言：清空操作不应触发页面导航
    const navAfterEmptyDocs = navigationEvents.length;
    const emptyDocsNavDelta = navAfterEmptyDocs - navBeforeEmptyDocs;
    assert.equal(emptyDocsNavDelta, 0,
      `清空页面垃圾箱后应无页面导航，实际发生 ${emptyDocsNavDelta} 次`);

    evidence.emptyDocuments = {
      ok: true,
      navDelta: emptyDocsNavDelta,
      dialogAccepted: true,
      mdEntryClean: mdEntryAfterEmptyDocs === null,
      md2EntryClean: md2EntryAfterEmptyDocs === null,
      mdFileGone: !fs.existsSync(resolvedMdTrashPath),
      md2FileGone: !fs.existsSync(resolvedMd2TrashPath),
    };
    evidence.steps.push({
      step: 3,
      label: "清空页面垃圾箱——按钮 click、accept confirm、行消失、trash-index 清理、文件删除、无导航",
      ok: true,
      navDelta: emptyDocsNavDelta,
    });

    // ════════════════════════════════════════════
    // 步骤 4：清空资源垃圾箱
    // ════════════════════════════════════════════
    const navBeforeEmptyResources = navigationEvents.length;

    const resourceButtonAfterEmptyDocs = page.locator('[data-trash-action="local-empty-resources"]');
    await resourceButtonAfterEmptyDocs.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
    const resButtonDisabledAfterDocs = await resourceButtonAfterEmptyDocs.isDisabled();
    assert.equal(resButtonDisabledAfterDocs, false, "资源行仍存在时 empty-resources 按钮应 enabled");

    // 接受确认 dialog
    page.once("dialog", (dialog) => {
      evidence.emptyResources.dialogText = dialog.message();
      dialog.accept();
    });
    await resourceButtonAfterEmptyDocs.click({ timeout: UI_TIMEOUT_MS });

    // 等待资源行消失
    await page.waitForFunction(
      (id) => !document.querySelector(`[data-trash-entry-id="${id}"]`),
      assetId,
      { timeout: UI_TIMEOUT_MS },
    );

    const assetRowAfterEmptyResources = page.locator(`article[data-trash-row="local"][data-trash-entry-id="${assetId}"]`);
    const assetRowCountAfterEmptyResources = await assetRowAfterEmptyResources.count();
    assert.equal(assetRowCountAfterEmptyResources, 0, "清空资源垃圾箱后资源行应消失");

    // 验证按钮状态
    const resButtonDisabledAfter = await page.locator('[data-trash-action="local-empty-resources"]').isDisabled();
    assert.equal(resButtonDisabledAfter, true, "清空资源垃圾箱后 empty-resources 按钮应 disabled");

    // 验证文件系统
    const assetEntryAfterEmptyResources = getTrashEntry(root, assetId);
    assert.equal(assetEntryAfterEmptyResources, null, `清空资源垃圾箱后 trash-index 不应包含 asset entry: ${assetId}`);

    const resolvedAssetTrashPath = path.resolve(root, assetTrashPath);
    assert.equal(fs.existsSync(resolvedAssetTrashPath), false, `清空资源垃圾箱后 trash 文件应删除: ${resolvedAssetTrashPath}`);

    // 导航断言
    const navAfterEmptyResources = navigationEvents.length;
    const emptyResourcesNavDelta = navAfterEmptyResources - navBeforeEmptyResources;
    assert.equal(emptyResourcesNavDelta, 0,
      `清空资源垃圾箱后应无页面导航，实际发生 ${emptyResourcesNavDelta} 次`);

    evidence.emptyResources = {
      ok: true,
      navDelta: emptyResourcesNavDelta,
      dialogAccepted: true,
      assetEntryClean: assetEntryAfterEmptyResources === null,
      assetFileGone: !fs.existsSync(resolvedAssetTrashPath),
    };
    evidence.steps.push({
      step: 4,
      label: "清空资源垃圾箱——按钮 click、accept confirm、行消失、trash-index 清理、文件删除、无导航",
      ok: true,
      navDelta: emptyResourcesNavDelta,
    });

    // ════════════════════════════════════════════
    // 步骤 5：最终验证——trash-index.json 完全空
    // ════════════════════════════════════════════
    const entriesAfter = readTrashIndex(root);
    const entryKeys = Object.keys(entriesAfter);
    assert.equal(entryKeys.length, 0, `两次清空后 trash-index 应为空，实际有 ${entryKeys.length} 个 entry: ${JSON.stringify(entryKeys)}`);

    // 验证原始源文件不应恢复
    assert.equal(fs.existsSync(mdSourcePath), false, "清空垃圾箱后 MD 源文件不应恢复");
    assert.equal(fs.existsSync(md2SourcePath), false, "清空垃圾箱后第二个 MD 源文件不应恢复");
    assert.equal(fs.existsSync(assetSourcePath), false, "清空垃圾箱后资源源文件不应恢复");

    evidence.steps.push({
      step: 5,
      label: "最终验证：trash-index 为空、源文件未恢复",
      ok: true,
      entryKeys,
    });

    // 收集完整导航事件
    evidence.navEvents = navigationEvents.map((e) => ({
      url: e.url.length > 120 ? e.url.substring(0, 120) + "…" : e.url,
      timestamp: e.timestamp,
    }));
    evidence.ok = true;

    console.log(JSON.stringify({ ok: true, resultPath: RESULT_PATH, steps: evidence.steps.length }, null, 2));
  } catch (error) {
    evidence.error = error instanceof Error ? error.stack || error.message : String(error);
    evidence.pageUrl = page.url();
    evidence.pageText = await page.locator('[data-testid="mnote-trash-workbench"]').innerText({ timeout: 3000 }).catch(() => "");
    throw error;
  } finally {
    // 写结果文件
    fs.mkdirSync(OUTPUT_DIR, { recursive: true });
    fs.writeFileSync(RESULT_PATH, `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
    console.log(JSON.stringify({ ok: evidence.ok, resultPath: RESULT_PATH, error: evidence.error || null }, null, 2));

    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
    if (!process.env.MNOTE_KEEP_SMOKE_TMP) {
      fs.rmSync(root, { recursive: true, force: true });
    }
  }
}

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