#!/usr/bin/env node
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");

const REPO_ROOT = path.resolve(__dirname, "..");
const RUNTIME_PATH = path.join(
  REPO_ROOT,
  "rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js",
);
const FILETREE_RUNTIME_PATH = path.join(
  REPO_ROOT,
  "rust/crates/mnote-web/browser/filetree-runtime.js",
);
const FILETREE_CONTEXT_MENU_RUNTIME_PATH = path.join(
  REPO_ROOT,
  "rust/crates/mnote-web/browser/filetree-context-menu-runtime.js",
);

function loadRuntimeFactory() {
  const source = fs
    .readFileSync(RUNTIME_PATH, "utf8")
    .replace(
      "export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {",
      "const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {",
    );
  const context = {
    console,
    CustomEvent: class CustomEvent {
      constructor(type, init) {
        this.type = type;
        this.detail = init && init.detail;
      }
    },
    HTMLElement: class HTMLElement {},
    navigator: {
      clipboard: {
        writeText: async (text) => {
          context.__clipboardText = text;
        },
      },
    },
    document: {
      body: {},
      documentElement: {
        attrs: {},
        setAttribute(name, value) {
          this.attrs[name] = String(value);
        },
      },
      createElement() {
        return {};
      },
      querySelector() {
        return null;
      },
      querySelectorAll() {
        return [];
      },
    },
    window: {
      alert() {},
      confirm() {
        return true;
      },
      dispatchEvent() {},
    },
    URL,
    setTimeout,
    clearTimeout,
  };
  context.globalThis = context;
  vm.createContext(context);
  vm.runInContext(fs.readFileSync(FILETREE_CONTEXT_MENU_RUNTIME_PATH, "utf8"), context, {
    filename: FILETREE_CONTEXT_MENU_RUNTIME_PATH,
  });
  vm.runInContext(
    `${source}\nglobalThis.__factory = createSidebarFileTreeCommandRuntime;`,
    context,
    { filename: RUNTIME_PATH },
  );
  return context;
}

function loadFileTreeRuntime() {
  const context = {
    console,
    HTMLElement: class HTMLElement {},
    document: {},
    window: {},
    URL,
  };
  context.globalThis = context;
  context.window = context;
  vm.createContext(context);
  vm.runInContext(fs.readFileSync(FILETREE_RUNTIME_PATH, "utf8"), context, {
    filename: FILETREE_RUNTIME_PATH,
  });
  return context;
}

async function runCopyId(runtime, context, detail) {
  context.__clipboardText = "";
  runtime.handleTreeContextMenuAction("copy-id", detail, null);
  await new Promise((resolve) => setImmediate(resolve));
  return context.__clipboardText;
}

async function main() {
  const fileTreeContext = loadFileTreeRuntime();
  class FakeRow extends fileTreeContext.HTMLElement {
    constructor(attrs, textContent = "") {
      super();
      this.attrs = attrs;
      this.textContent = textContent;
    }
    getAttribute(name) {
      return this.attrs[name] || "";
    }
    querySelector() {
      return null;
    }
  }
  const readWorkspacePath = (attrs, textContent = "") =>
    fileTreeContext.window.__mnoteFileTreeRuntime.readWorkspacePathFromRow(
      new FakeRow(attrs, textContent),
      {
        currentSourceKind: () => attrs.__sourceKind || "local_folder",
        currentRootUri: () => attrs.__rootUri || "file:///mnt/Data1T/mnote",
        resolveWorkspaceId: () => "local-folder",
        rowTitle: (row) => row.textContent,
      },
    );
  const workspacePath = readWorkspacePath({
    "data-row-id": "local:asset:assets/report.pdf",
    "data-row-kind": "asset",
    "data-document-id": "local-md:README.md",
    "data-asset-id": "local-file:assets/report.pdf",
    "data-local-relative-path": "assets/report.pdf",
    "data-object-identity": JSON.stringify({
      objectKind: "attachment",
      documentId: "local-md:README.md",
      assetId: "local-file:assets/report.pdf",
    }),
  }, "report.pdf");
  assert.equal(workspacePath.schema, "mnote.workspace_path.v1");
  assert.equal(workspacePath.workspaceId, "local-folder");
  assert.equal(workspacePath.sourceKind, "local_folder");
  assert.equal(workspacePath.rootUri, "file:///mnt/Data1T/mnote");
  assert.equal(workspacePath.relativePath, "assets/report.pdf");
  assert.equal(workspacePath.documentId, "local-md:README.md");
  assert.equal(workspacePath.assetId, "local-file:assets/report.pdf");
  assert.equal(workspacePath.objectKind, "attachment");
  assert.equal(workspacePath.resourceKind, "attachment");
  assert.equal(workspacePath.objectIdentity.assetId, "local-file:assets/report.pdf");

  const markdownWorkspacePath = readWorkspacePath({
    "data-row-id": "local:markdown:docs/Page.md",
    "data-row-kind": "markdown",
    "data-document-id": "local-md:docs~2FPage.md",
    "data-local-relative-path": "docs/Page.md",
    "data-object-identity": JSON.stringify({
      objectKind: "page",
      documentId: "local-md:docs~2FPage.md",
      assetId: null,
    }),
  }, "Page.md");
  assert.equal(markdownWorkspacePath.relativePath, "docs/Page.md");
  assert.equal(markdownWorkspacePath.objectKind, "page");
  assert.equal(markdownWorkspacePath.isLocalFolder, true);

  const folderWorkspacePath = readWorkspacePath({
    "data-row-id": "local:folder:docs",
    "data-row-kind": "folder",
    "data-document-id": "local-dir:docs",
    "data-local-relative-path": "docs",
    "data-object-identity": JSON.stringify({
      objectKind: "index",
      documentId: "local-dir:docs",
      assetId: null,
    }),
  }, "docs");
  assert.equal(folderWorkspacePath.relativePath, "docs");
  assert.equal(folderWorkspacePath.objectKind, "index");

  const mindmapWorkspacePath = readWorkspacePath({
    "data-row-id": "local:asset:Page/思维导图.json",
    "data-row-kind": "asset",
    "data-asset-id": "local-file:Page/思维导图.json",
    "data-local-relative-path": "Page/思维导图.json",
    "data-object-identity": JSON.stringify({
      objectKind: "mindmap",
      documentId: "local-md:Page~2FPage.md",
      assetId: "local-file:Page/思维导图.json",
    }),
  }, "思维导图.json");
  assert.equal(mindmapWorkspacePath.objectKind, "mindmap");
  assert.equal(mindmapWorkspacePath.objectIdentity.assetId, "local-file:Page/思维导图.json");

  const officeWorkspacePath = readWorkspacePath({
    "data-row-id": "local:asset:Page/report.docx",
    "data-row-kind": "asset",
    "data-asset-id": "local-file:Page/report.docx",
    "data-local-relative-path": "Page/report.docx",
    "data-object-identity": JSON.stringify({
      objectKind: "only_office",
      documentId: "local-md:Page~2FPage.md",
      assetId: "local-file:Page/report.docx",
    }),
  }, "report.docx");
  assert.equal(officeWorkspacePath.objectKind, "only_office");
  assert.equal(officeWorkspacePath.objectIdentity.assetId, "local-file:Page/report.docx");

  const nonLocalWorkspacePath = readWorkspacePath({
    __sourceKind: "cloud",
    __rootUri: "",
    "data-row-id": "doc:cloud-page",
    "data-row-kind": "document",
    "data-document-id": "local-cloud-compatible-id",
    "data-object-identity": JSON.stringify({
      objectKind: "page",
      documentId: "local-cloud-compatible-id",
      assetId: null,
    }),
  }, "cloud");
  assert.equal(nonLocalWorkspacePath.isLocalFolder, false, "sourceKind 非 local_folder 时不应仅因 local-* documentId 误判成本地文件夹");

  const context = loadRuntimeFactory();
  class FakeCommandBadge extends context.HTMLElement {
    constructor(kind) {
      super();
      this.kind = kind;
    }
    getAttribute(name) {
      return name === "data-kind" ? this.kind : "";
    }
  }
  class FakeCommandRow extends context.HTMLElement {
    constructor(attrs, textContent = "", badgeKind = "") {
      super();
      this.attrs = attrs;
      this.textContent = textContent;
      this.badge = badgeKind ? new FakeCommandBadge(badgeKind) : null;
    }
    getAttribute(name) {
      return this.attrs[name] || "";
    }
    querySelector(selector) {
      return selector === ".tree-kind-badge" ? this.badge : null;
    }
  }
  const runtime = context.__factory({
    currentSourceKind: () => "local_folder",
    currentRootUri: () => "file:///mnt/Data1T/mnote",
    cssEscape: (value) => String(value).replace(/"/g, '\\"'),
    fileTreeRuntimeFunction: () => null,
    localFilePathFromAssetId: (assetId) =>
      String(assetId || "").replace(/^local-file:/, ""),
    resolveWorkspaceId: () => "local-folder",
    runtimeState: { activeTreeContextMenu: null },
    selectedSidebarFileTreeSelection: { selectedRowIds: new Set(), focusedRowId: null },
  });
  assert.equal(
    runtime.classifySidebarFileTreeAsset(new FakeCommandRow({ "data-object-kind": "table" }, "table.asset", "table")),
    "table",
    "table objectKind/iconKind 行应归为 table 资源",
  );
  const dirtyCommandRow = new FakeCommandRow({
    "data-row-id": "local:markdown:docs/Page.md",
    "data-row-kind": "markdown",
    "data-document-id": "local-md:docs~2FPage.md",
    "data-local-relative-path": "docs/Page.md",
  }, "Page.md");
  const commandContextRuntime = context.window.__mnoteFileTreeContextMenuRuntime;
  const dirtyContext = commandContextRuntime.buildFileTreeCommandContext(
    dirtyCommandRow,
    {
      schema: "mnote.open_editors_snapshot.v1",
      editors: [
        {
          documentId: "local-md:docs~2FPage.md",
          dirtyState: "dirty",
          workspacePath: { relativePath: "docs/Page.md" },
        },
      ],
    },
    null,
    {
      currentSourceKind: () => "local_folder",
      workspaceReadonly: () => false,
      selection: { selectedRowIds: new Set(["local:markdown:docs/Page.md"]) },
      queryRowById: () => dirtyCommandRow,
    },
  );
  assert.equal(dirtyContext["workspace.sourceKind"], "local_folder");
  assert.equal(dirtyContext["tree.selectionCount"], 1);
  assert.equal(dirtyContext["tree.selectionResourceKind"], "page");
  assert.equal(dirtyContext["tree.targetResourceKind"], "page");
  assert.equal(dirtyContext["editor.dirty"], true);
  assert.equal(dirtyContext["editor.dirtyState"], "dirty");
  assert.equal(dirtyContext["ai.canWrite"], false);
  assert.equal(
    commandContextRuntime.evaluateSidebarFileTreeWhen(dirtyContext, "!workspace.readonly && !editor.dirty"),
    false,
    "dirty 目标下菜单与快捷键写操作应被统一 command context 禁用",
  );
  const readonlyContext = commandContextRuntime.buildFileTreeCommandContext(
    dirtyCommandRow,
    { editors: [] },
    null,
    {
      currentSourceKind: () => "local_folder",
      workspaceReadonly: () => true,
      selection: { selectedRowIds: new Set(["local:markdown:docs/Page.md"]) },
      queryRowById: () => dirtyCommandRow,
    },
  );
  assert.equal(readonlyContext["workspace.readonly"], true);
  assert.equal(
    commandContextRuntime.evaluateSidebarFileTreeWhen(readonlyContext, "!workspace.readonly"),
    false,
    "readonly 目标下菜单与快捷键写操作应被统一 command context 禁用",
  );
  const cleanTargetRow = new FakeCommandRow({
    "data-row-id": "local:markdown:docs/Clean.md",
    "data-row-kind": "markdown",
    "data-document-id": "local-md:docs~2FClean.md",
    "data-local-relative-path": "docs/Clean.md",
  }, "Clean.md");
  const selectedRows = new Map([
    ["local:markdown:docs/Clean.md", cleanTargetRow],
    ["local:markdown:docs/Page.md", dirtyCommandRow],
  ]);
  const multiSelectDirtyContext = commandContextRuntime.buildFileTreeCommandContext(
    cleanTargetRow,
    {
      editors: [
        {
          documentId: "local-md:docs~2FPage.md",
          dirtyState: "Dirty",
          workspacePath: { relativePath: "docs/Page.md" },
        },
      ],
    },
    null,
    {
      currentSourceKind: () => "local_folder",
      workspaceReadonly: () => false,
      selection: { selectedRowIds: new Set(["local:markdown:docs/Clean.md", "local:markdown:docs/Page.md"]) },
      queryRowById: (rowId) => selectedRows.get(rowId),
    },
  );
  assert.equal(multiSelectDirtyContext["tree.selectionCount"], 2);
  assert.equal(multiSelectDirtyContext["tree.selectionResourceKind"], "page");
  assert.equal(multiSelectDirtyContext["editor.dirty"], true);
  assert.equal(multiSelectDirtyContext["editor.dirtyState"], "dirty");
  assert.equal(
    commandContextRuntime.evaluateSidebarFileTreeWhen(multiSelectDirtyContext, "!workspace.readonly && !editor.dirty"),
    false,
    "多选中任一非 target 行 dirty 时，菜单与快捷键写操作也必须禁用",
  );
  const dirtyAssetRow = new FakeCommandRow({
    "data-row-id": "local:asset:assets/report.pdf",
    "data-row-kind": "asset",
    "data-asset-id": "local-file:assets/report.pdf",
    "data-local-relative-path": "assets/report.pdf",
  }, "report.pdf");
  const dirtyAssetContext = commandContextRuntime.buildFileTreeCommandContext(
    dirtyAssetRow,
    {
      resourceEditors: [
        {
          assetId: "local-file:assets/report.pdf",
          dirtyState: "ExternalModified",
          workspacePath: { relativePath: "assets/report.pdf", assetId: "local-file:assets/report.pdf" },
        },
      ],
    },
    null,
    {
      currentSourceKind: () => "local_folder",
      workspaceReadonly: () => false,
      selection: { selectedRowIds: new Set(["local:asset:assets/report.pdf"]) },
      queryRowById: () => dirtyAssetRow,
    },
  );
  assert.equal(dirtyAssetContext["tree.selectionResourceKind"], "asset");
  assert.equal(dirtyAssetContext["tree.targetIsAsset"], true);
  assert.equal(dirtyAssetContext["editor.dirty"], true);
  assert.equal(dirtyAssetContext["editor.dirtyState"], "external_modified");
  assert.equal(
    commandContextRuntime.evaluateSidebarFileTreeWhen(dirtyAssetContext, "!workspace.readonly && !editor.dirty"),
    false,
    "dirty resource tab 对应 asset 的 rename/delete 也必须禁用",
  );

  assert.equal(
    await runCopyId(runtime, context, {
      contextKind: "filetree",
      rowKind: "folder",
      documentId: "local-dir:docs",
      rowId: "local:folder:docs",
      workspacePath: folderWorkspacePath,
      title: "docs",
    }),
    "/mnt/Data1T/mnote/docs",
    "复制本地文件夹页面 ID 应得到绝对路径",
  );

  assert.equal(
    await runCopyId(runtime, context, {
      contextKind: "filetree",
      rowKind: "markdown",
      documentId: "local-md:docs~2FPage.md",
      rowId: "local:markdown:docs~2FPage.md",
      workspacePath: markdownWorkspacePath,
      title: "Page",
    }),
    "/mnt/Data1T/mnote/docs/Page.md",
    "复制本地 Markdown 页面 ID 应得到绝对路径",
  );

  assert.equal(
    await runCopyId(runtime, context, {
      contextKind: "filetree",
      rowKind: "asset",
      assetId: "local-file:assets/report.pdf",
      rowId: "local:asset:assets/report.pdf",
      workspacePath,
      title: "report.pdf",
    }),
    "/mnt/Data1T/mnote/assets/report.pdf",
    "复制本地资源 ID 应得到绝对路径",
  );

  console.log(
    JSON.stringify(
      {
        ok: true,
        copied: [
          "/mnt/Data1T/mnote/docs",
          "/mnt/Data1T/mnote/docs/Page.md",
          "/mnt/Data1T/mnote/assets/report.pdf",
        ],
      },
      null,
      2,
    ),
  );
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
