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

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 BASE_URL = (process.env.MNOTE_UI_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 PROBE_DOCX_PATH = process.env.MNOTE_ONLYOFFICE_PROBE_DOCX
  || "/tmp/mnote-onlyoffice-manual/mnote-onlyoffice-upload-test.docx";
const OUT_DIR = path.join(process.cwd(), "tmp", "task524-workspace-object-identity-matrix-smoke");
const RESULT_PATH = path.join(OUT_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 documentUrl(root, relativePath) {
  const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
  url.searchParams.set("sourceKind", "local_folder");
  url.searchParams.set("rootUri", fileUrl(root));
  url.searchParams.set("treeView", "filetree");
  return url.toString();
}

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", "markdown_edit", "asset_upload"],
    }, null, 2)}\n`,
    "utf8",
  );
}

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 !== "/auth", { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
  }
}

async function filetreeRowState(page, relativePath) {
  return await page.evaluate((targetRelativePath) => {
    const row = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
      .find((candidate) => candidate instanceof HTMLElement && candidate.getAttribute("data-local-relative-path") === targetRelativePath);
    if (!(row instanceof HTMLElement)) {
      return null;
    }
    const params = new URL(window.location.href).searchParams;
    const workspacePath = window.__mnoteFileTreeRuntime?.readWorkspacePathFromRow?.(row, {
      currentSourceKind: () => params.get("sourceKind") || document.body.dataset.mnoteSourceKind || "local_folder",
      currentRootUri: () => params.get("rootUri") || document.body.dataset.mnoteRootUri || "",
      resolveWorkspaceId: () => document.body.dataset.workspaceId || "",
      rowTitle: (candidate) => candidate.textContent || "",
    }) || null;
    const objectIdentityRaw = row.getAttribute("data-object-identity") || "";
    let objectIdentity = null;
    try {
      objectIdentity = objectIdentityRaw ? JSON.parse(objectIdentityRaw) : null;
    } catch {
      objectIdentity = objectIdentityRaw;
    }
    return {
      rowId: row.getAttribute("data-row-id") || "",
      rowKind: row.getAttribute("data-row-kind") || "",
      documentId: row.getAttribute("data-document-id") || "",
      assetId: row.getAttribute("data-asset-id") || "",
      relativePath: row.getAttribute("data-local-relative-path") || "",
      objectIdentity,
      workspacePath,
      expanded: row.getAttribute("aria-expanded") || "",
    };
  }, relativePath);
}

async function clickFiletreeOpen(page, relativePath) {
  const row = page.locator(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="${relativePath.replace(/"/g, '\\"')}"]`).first();
  await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  await row.locator('[data-rust-action="open"]').click({ timeout: UI_TIMEOUT_MS });
}

async function waitFiletreeRow(page, relativePath) {
  const row = page.locator(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="${relativePath.replace(/"/g, '\\"')}"]`).first();
  await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
}

async function expandFiletreeFolder(page, relativePath) {
  const row = page.locator(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="${relativePath.replace(/"/g, '\\"')}"]`).first();
  await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
  const expanded = await row.getAttribute("aria-expanded");
  if (expanded === "true") return;
  await row.locator('[data-rust-action="open"]').click({ timeout: UI_TIMEOUT_MS });
  await page.waitForFunction(
    (targetRelativePath) => {
      const candidate = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
        .find((row) => row instanceof HTMLElement && row.getAttribute("data-local-relative-path") === targetRelativePath);
      return candidate && candidate.getAttribute("aria-expanded") === "true";
    },
    relativePath,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function activeSnapshotEntry(page, expectedRelativePath, expectedKind) {
  return await page.waitForFunction(
    ({ expectedRelativePath, expectedKind }) => {
      const snapshot = window.__mnoteOpenEditorsSnapshot || null;
      const liveSnapshot = window.__mnoteDocumentPaneRuntime?.getOpenEditorsSnapshot?.() || snapshot;
      const resources = Array.isArray(liveSnapshot?.resourceEditors) ? liveSnapshot.resourceEditors : [];
      const entries = Array.isArray(liveSnapshot?.editors) ? liveSnapshot.editors.concat(resources) : resources;
      const activeObjectIdentity = String(liveSnapshot?.activeObjectIdentity || "");
      const active = entries.find((entry) =>
        entry
        && (entry.active === true || entry.objectIdentity === activeObjectIdentity)
        && (!expectedRelativePath || entry.workspacePath?.relativePath === expectedRelativePath || entry.path === expectedRelativePath)
        && (!expectedKind || entry.kind === expectedKind || entry.editorKind === expectedKind || entry.workspacePath?.resourceKind === expectedKind)
      );
      return active || null;
    },
    { expectedRelativePath, expectedKind },
    { timeout: UI_TIMEOUT_MS },
  );
}

function assertWorkspacePathMatchesRow(label, rowState, snapshotEntry, expected) {
  assert(rowState, `${label}: 缺少 FileTree row state`);
  assert(rowState.workspacePath, `${label}: FileTree row 缺少 workspacePath: ${JSON.stringify(rowState)}`);
  assert(snapshotEntry, `${label}: 缺少 OpenEditorsSnapshot entry`);
  assert.equal(rowState.workspacePath.schema, "mnote.workspace_path.v1", `${label}: row workspacePath schema`);
  assert.equal(snapshotEntry.workspacePath?.schema, "mnote.workspace_path.v1", `${label}: snapshot workspacePath schema`);
  assert.equal(rowState.workspacePath.relativePath, expected.relativePath, `${label}: row relativePath`);
  assert.equal(snapshotEntry.workspacePath?.relativePath, expected.relativePath, `${label}: snapshot relativePath`);
  assert.equal(snapshotEntry.workspacePath?.sourceKind, rowState.workspacePath.sourceKind, `${label}: sourceKind 应一致`);
  assert.equal(snapshotEntry.workspacePath?.rootUri, rowState.workspacePath.rootUri, `${label}: rootUri 应一致`);
  assert.equal(snapshotEntry.workspacePath?.documentId, rowState.workspacePath.documentId, `${label}: documentId 应一致`);
  if (expected.assetId) {
    assert.equal(rowState.workspacePath.assetId, expected.assetId, `${label}: row assetId`);
    assert.equal(snapshotEntry.workspacePath?.assetId, expected.assetId, `${label}: snapshot assetId`);
  }
  if (expected.resourceKind) {
    assert.equal(rowState.workspacePath.resourceKind, expected.resourceKind, `${label}: row resourceKind`);
    assert.equal(snapshotEntry.workspacePath?.resourceKind, expected.resourceKind, `${label}: resourceKind`);
  }
  if (expected.objectKind) {
    assert.equal(rowState.workspacePath.objectIdentity?.objectKind, expected.objectKind, `${label}: row objectKind`);
    assert.equal(snapshotEntry.workspacePath?.objectIdentity?.objectKind, expected.objectKind, `${label}: snapshot objectKind`);
  }
  if (rowState.workspacePath.objectIdentity && typeof rowState.workspacePath.objectIdentity === "object"
    && snapshotEntry.workspacePath?.objectIdentity && typeof snapshotEntry.workspacePath.objectIdentity === "object") {
    assert.deepStrictEqual(
      snapshotEntry.workspacePath.objectIdentity,
      rowState.workspacePath.objectIdentity,
      `${label}: snapshot objectIdentity 应与 row objectIdentity 对齐`,
    );
  }
}

async function main() {
  fs.mkdirSync(OUT_DIR, { recursive: true });
  const actorId = "mnote-e2e";
  const workspaceId = `local-ws:${actorId}:task524`;
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task524-identity-"));
  const rootUri = fileUrl(root);
  fs.mkdirSync(path.join(root, "Page"), { recursive: true });
  fs.mkdirSync(path.join(root, "docs"), { recursive: true });
  writeWorkspaceManifest(root, actorId, workspaceId);
  fs.writeFileSync(path.join(root, "Page.md"), "# Page\n\nTask524 page\n", "utf8");
  fs.writeFileSync(path.join(root, "docs", "Plan.md"), "# Plan\n\nTask524 plan\n", "utf8");
  fs.writeFileSync(path.join(root, "Page", "notes.txt"), "Task524 text resource\n", "utf8");
  fs.writeFileSync(path.join(root, "Page", "map.mindmap.json"), JSON.stringify({
    root: { data: { text: "Task524 Mindmap" }, children: [] },
  }, null, 2), "utf8");
  if (fs.existsSync(PROBE_DOCX_PATH)) {
    fs.copyFileSync(PROBE_DOCX_PATH, path.join(root, "Page", "office.docx"));
  } else {
    fs.writeFileSync(path.join(root, "Page", "office.docx"), "task524 office probe\n", "utf8");
  }

  const browser = await chromium.launch({
    headless: process.env.HEADFUL !== "1",
    ...(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();
  const result = {
    ok: false,
    task: "task524-workspace-object-identity-matrix-smoke",
    baseUrl: BASE_URL,
    root,
    rootUri,
    checks: [],
  };

  try {
    await quickLogin(page);
    await page.goto(documentUrl(root, "Page.md"), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
    await page.locator(".document-pane[data-pane-role=\"primary\"] .editor-surface .ProseMirror").first().waitFor({
      state: "visible",
      timeout: UI_TIMEOUT_MS,
    });
    await page.waitForFunction(() => Boolean(window.__mnoteFileTreeRuntime?.readWorkspacePathFromRow), null, {
      timeout: UI_TIMEOUT_MS,
    });
    await page.waitForFunction(() => Boolean(window.__mnoteOpenEditorsSnapshot), null, {
      timeout: UI_TIMEOUT_MS,
    });
    await page.evaluate(() => {
      window.__task524IdentityEvents = [];
      window.addEventListener("tree.filetree.open", (event) => {
        window.__task524IdentityEvents.push({ type: "tree.filetree.open", detail: event.detail || null });
      });
      window.addEventListener("tree.asset.open", (event) => {
        window.__task524IdentityEvents.push({ type: "tree.asset.open", detail: event.detail || null });
      });
    });

    const pageRow = await filetreeRowState(page, "Page.md");
    const pageEntry = await page.evaluate(() => {
      const snapshot = window.__mnoteDocumentPaneRuntime?.getOpenEditorsSnapshot?.() || window.__mnoteOpenEditorsSnapshot || null;
      return snapshot?.groups?.primary?.editors?.find((entry) => entry.kind === "page" && entry.documentId === "local-md:Page.md") || null;
    });
    assertWorkspacePathMatchesRow("page", pageRow, pageEntry, {
      relativePath: "Page.md",
      resourceKind: "page",
      objectKind: "page",
    });
    result.checks.push({ kind: "page", row: pageRow, snapshot: pageEntry });

    const folderRow = await filetreeRowState(page, "docs");
    assert(folderRow, "folder: 缺少 docs row");
    assert.equal(folderRow.workspacePath?.schema, "mnote.workspace_path.v1", "folder row workspacePath schema");
    assert.equal(folderRow.workspacePath?.relativePath, "docs", "folder row relativePath");
    assert.equal(folderRow.workspacePath?.documentId, "local-dir:docs", "folder row documentId");
    assert.equal(folderRow.workspacePath?.objectIdentity?.objectKind, "index", "folder row objectKind");
    const beforeFolderUrl = page.url();
    await clickFiletreeOpen(page, "docs");
    await page.waitForFunction(() => {
      const row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="docs"]');
      return row && row.getAttribute("aria-expanded") === "true";
    }, null, { timeout: UI_TIMEOUT_MS });
    assert.equal(page.url(), beforeFolderUrl, "FileTree folder open 只应展开，不应改 URL");
    result.checks.push({ kind: "folder", row: folderRow });

    await expandFiletreeFolder(page, "Page");

    const resources = [
      { label: "text", relativePath: "Page/notes.txt", kind: "text", resourceKind: "attachment", objectKind: "attachment" },
      { label: "mindmap", relativePath: "Page/map.mindmap.json", kind: "mindmap", resourceKind: "mindmap", objectKind: "mindmap" },
      { label: "office", relativePath: "Page/office.docx", kind: "office", resourceKind: "only_office", objectKind: "only_office" },
    ];

    for (const resource of resources) {
      await waitFiletreeRow(page, resource.relativePath);
      const rowState = await filetreeRowState(page, resource.relativePath);
      assert(rowState, `${resource.label}: 缺少 FileTree row`);
      await clickFiletreeOpen(page, resource.relativePath);
      const entryHandle = await activeSnapshotEntry(page, resource.relativePath, resource.kind);
      const entry = await entryHandle.jsonValue();
      assertWorkspacePathMatchesRow(resource.label, rowState, entry, {
        relativePath: resource.relativePath,
        assetId: rowState.workspacePath.assetId,
        resourceKind: resource.resourceKind,
        objectKind: resource.objectKind,
      });
      assert.equal(entry.kind, resource.kind, `${resource.label}: editor kind`);
      const eventDetail = await page.evaluate((relativePath) => {
        const events = Array.isArray(window.__task524IdentityEvents) ? window.__task524IdentityEvents : [];
        return [...events].reverse().find((event) => event?.detail?.workspacePath?.relativePath === relativePath) || null;
      }, resource.relativePath);
      assert(eventDetail, `${resource.label}: 应捕获真实 tree.asset.open 事件`);
      assert.equal(eventDetail.detail.workspacePath?.sourceKind, rowState.workspacePath.sourceKind, `${resource.label}: event sourceKind`);
      assert.equal(eventDetail.detail.workspacePath?.rootUri, rowState.workspacePath.rootUri, `${resource.label}: event rootUri`);
      assert.equal(eventDetail.detail.workspacePath?.documentId, rowState.workspacePath.documentId, `${resource.label}: event documentId`);
      assert.equal(eventDetail.detail.workspacePath?.assetId, rowState.workspacePath.assetId, `${resource.label}: event assetId`);
      assert.deepStrictEqual(eventDetail.detail.workspacePath?.objectIdentity, rowState.workspacePath.objectIdentity, `${resource.label}: event objectIdentity`);
      const url = new URL(page.url());
      const resourceTab = url.searchParams.get("resourceTab") || "";
      assert(
        resourceTab === `primary::${entry.objectIdentity}`,
        `${resource.label}: URL resourceTab 应等于 active snapshot objectIdentity: ${JSON.stringify({ resourceTab, entry })}`,
      );
      const activeTabIdentity = await page.evaluate(() => {
        const activeTab = document.querySelector(".mnote-main-tab.is-active");
        return activeTab?.getAttribute("data-mnote-object-identity") || "";
      });
      if (activeTabIdentity) {
        assert.equal(activeTabIdentity, entry.objectIdentity, `${resource.label}: active tab identity`);
      }
      result.checks.push({ kind: resource.label, row: rowState, snapshot: entry, resourceTab, activeTabIdentity });
    }

    result.ok = true;
    fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
    console.log(JSON.stringify(result, null, 2));
  } catch (error) {
    result.error = error && error.stack ? error.stack : String(error);
    fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
    throw error;
  } finally {
    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
  }
}

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