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

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

const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const ROOT_URI = process.env.MNOTE_KNOWLEDGE_RAG_ROOT_URI || "file:///mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space";
const WORKSPACE_ID = process.env.MNOTE_KNOWLEDGE_RAG_WORKSPACE_ID || "local-ws:mnote-e2e:my-space";
const QUERY = process.env.MNOTE_KNOWLEDGE_RAG_PYRROLIDINE_QUERY || "吡咯烷";
const EXPECTED_RESOURCE = process.env.MNOTE_KNOWLEDGE_RAG_EXPECTED_RESOURCE
  || "有机合成中的保护基/[OCR]_有机合成中的保护基-酚羰基羧基巯基的保护_20250201_1908.layered_删减-2025-02-04 18-59-42.docx";
const TOP_N = Number(process.env.MNOTE_KNOWLEDGE_RAG_TOP_N || 5);
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task543-knowledge-rag-pyrrolidine-top5-locator-smoke");
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
  || ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
    .find((candidate) => fs.existsSync(candidate));

function normalizeText(value) {
  return String(value || "")
    .replace(/<[^>]+>/g, " ")
    .replace(/[#*_`~>\[\](){}]+/g, " ")
    .replace(/[\u200B-\u200D\uFEFF]/g, "")
    .replace(/\s+/g, " ")
    .trim();
}

function compactText(value) {
  return normalizeText(value).replace(/[0-9０-９]+/g, "").replace(/[\s\p{P}\p{S}]+/gu, "");
}

function longestCommonSubstringLength(left, right) {
  const a = compactText(left);
  const b = compactText(right);
  if (!a || !b) return 0;
  const shorter = a.length <= b.length ? a : b;
  const longer = a.length <= b.length ? b : a;
  for (let len = Math.min(80, shorter.length); len >= 6; len -= 1) {
    for (let start = 0; start + len <= shorter.length; start += 1) {
      if (longer.includes(shorter.slice(start, start + len))) return len;
    }
  }
  return 0;
}

async function signIn(context) {
  const auth = await context.request.post(`${BASE_URL}/api/auth`, {
    data: {
      action: "auth:signIn",
      args: {
        provider: "password",
        params: {
          email: "mnote.e2e@example.com",
          password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
          flow: "signIn",
        },
      },
    },
  });
  assert(auth.ok(), `登录失败: ${auth.status()} ${await auth.text()}`);
}

async function apiSearch(context) {
  const response = await context.request.post(`${BASE_URL}/api/knowledge-rag/search`, {
    data: {
      rootUri: ROOT_URI,
      workspaceId: WORKSPACE_ID,
      query: QUERY,
      mode: "exact",
      topK: 20,
      chunkTopK: 20,
      includeChunkContent: true,
      sourcePaths: [EXPECTED_RESOURCE],
    },
  });
  assert(response.ok(), `knowledge-rag search 失败: ${response.status()} ${await response.text()}`);
  const payload = await response.json();
  const results = Array.isArray(payload.results) ? payload.results : [];
  assert(results.length >= TOP_N, `资料库结果不足 ${TOP_N} 条: ${JSON.stringify(payload, null, 2).slice(0, 3000)}`);
  assert(Array.isArray(payload.citations) && payload.citations.length >= TOP_N, `资料库结果缺少 citations[]: ${JSON.stringify(payload, null, 2).slice(0, 3000)}`);
  results.slice(0, TOP_N).forEach((item, index) => {
    assert(item.displayQuote && item.locatorEvidenceText, `第 ${index + 1} 条缺少 displayQuote/locatorEvidenceText: ${JSON.stringify(item, null, 2)}`);
    assert(!/(?:<\/?e(?:q(?:uation)?)?\b|<\/?drawing\b|format=["']?latex|\blatex\b)/i.test(String(item.displayQuote || "")), `第 ${index + 1} 条 displayQuote 仍暴露原始公式/绘图标记: ${item.displayQuote}`);
  });
  const blockIds = results.map((item) => item?.locator?.blockId).filter(Boolean);
  assert.equal(new Set(blockIds).size, blockIds.length, `搜索结果仍有同段落重复: ${JSON.stringify(blockIds)}`);
  return results.slice(0, TOP_N);
}

async function openSearchPanel(page, navigate = false) {
  if (navigate) {
    await page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
    await page.evaluate(() => {
      window.localStorage?.setItem("mnote.search.collapseSourcesDefault.v1", "0");
    });
  }
  const inputVisible = await page.locator('[data-testid="wolai-search-input"]').isVisible().catch(() => false);
  if (!inputVisible) {
    await page.waitForSelector('[data-mnote-action="open-search-modal"]', { timeout: UI_TIMEOUT_MS });
    await page.click('[data-mnote-action="open-search-modal"]');
  }
  await page.waitForSelector('[data-testid="wolai-search-input"]', { timeout: UI_TIMEOUT_MS });
  await page.fill('[data-testid="wolai-search-input"]', QUERY);
  await page.evaluate(() => {
    const button = document.querySelector('[data-search-switch="knowledge"]');
    if (button instanceof HTMLElement) {
      button.setAttribute("aria-checked", "true");
      button.classList.add("is-on");
    }
    document.querySelector('[data-testid="wolai-search-input"]')
      ?.dispatchEvent(new Event("input", { bubbles: true }));
  });
  await page.waitForFunction(
    (topN) => document.querySelectorAll('[data-testid="wolai-search-result-row"]').length >= topN,
    TOP_N,
    { timeout: UI_TIMEOUT_MS },
  );
}

async function clickAndAuditRow(page, index, apiResult) {
  const rows = await page.$$('[data-testid="wolai-search-result-row"]');
  assert(rows[index], `缺少第 ${index + 1} 条搜索结果`);
  const rowText = await rows[index].evaluate((node) => node.textContent || "");
  assert(
    !/(?:<\/?equation\b|format=["']?latex|<\/?drawing\b|\blatex\b)/i.test(rowText),
    `第 ${index + 1} 条搜索结果仍暴露原始公式/绘图标记: ${rowText}`,
  );
  const locator = await rows[index].evaluate((node) => {
    const raw = node.getAttribute("data-evidence-locator") || "";
    try {
      return raw ? JSON.parse(raw) : null;
    } catch (_) {
      return null;
    }
  });
  assert(locator?.blockId, `第 ${index + 1} 条缺少 locator blockId: ${rowText}`);
  assert.equal(locator.blockId, apiResult?.locator?.blockId, `第 ${index + 1} 条 UI/API blockId 不一致`);

  await rows[index].click();
  await page.waitForFunction(
    ({ expectedResource, expectedBlockId }) => {
      const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
      return panel && !panel.hidden && panel.getAttribute("data-mnote-evidence-block-id") === expectedBlockId;
    },
    { expectedResource: EXPECTED_RESOURCE, expectedBlockId: locator.blockId },
    { timeout: UI_TIMEOUT_MS },
  );
  await page.waitForTimeout(1200);
  await page.waitForFunction(
    (expectedResource) => {
      const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
      const frame = panel?.querySelector("iframe.mnote-resource-tab-frame");
      const doc = frame?.contentDocument;
      return doc?.documentElement?.getAttribute("data-mnote-office-evidence-applied") === "true";
    },
    EXPECTED_RESOURCE,
    { timeout: UI_TIMEOUT_MS },
  );

  const state = await page.evaluate((expectedResource) => {
    const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
    const frame = panel?.querySelector("iframe.mnote-resource-tab-frame");
    const doc = frame?.contentDocument;
    const highlighted = doc?.querySelector('[data-mnote-office-evidence-target="true"]');
    return {
      panelBlockId: panel?.getAttribute("data-mnote-evidence-block-id") || "",
      panelEvidenceText: panel?.getAttribute("data-mnote-evidence-text") || "",
      iframeApplied: doc?.documentElement?.getAttribute("data-mnote-office-evidence-applied") || "",
      highlightedText: highlighted?.textContent || "",
      highlightedTag: highlighted?.tagName || "",
      highlightedTop: highlighted ? Math.round(highlighted.getBoundingClientRect().top) : null,
    };
  }, EXPECTED_RESOURCE);

  const evidenceText = locator?.openAction?.params?.evidenceText || apiResult?.locator?.openAction?.params?.evidenceText || apiResult?.quote || rowText;
  const overlap = longestCommonSubstringLength(state.highlightedText, evidenceText);
  const compactHighlighted = compactText(state.highlightedText);
  const compactEvidence = compactText(evidenceText);
  assert.equal(state.panelBlockId, locator.blockId, `第 ${index + 1} 条 panel blockId 不一致: ${JSON.stringify(state, null, 2)}`);
  assert(state.highlightedText.includes(QUERY), `第 ${index + 1} 条定位高亮未包含 query: ${JSON.stringify(state, null, 2)}`);
  assert(
    normalizeText(state.highlightedText).length >= 24 || state.highlightedTag === "P",
    `第 ${index + 1} 条仍是短词高亮，不是段落级高亮: ${JSON.stringify(state, null, 2)}`,
  );
  assert(
    overlap >= Math.min(10, Math.max(4, compactEvidence.length))
      || compactHighlighted.includes(compactEvidence)
      || compactEvidence.includes(compactHighlighted),
    `第 ${index + 1} 条搜索结果与实际定位上下文不一致: ${JSON.stringify({ rowText, evidenceText, overlap, state }, null, 2)}`,
  );

  return {
    index: index + 1,
    rowText: normalizeText(rowText).slice(0, 220),
    apiBlockId: apiResult?.locator?.blockId || "",
    uiBlockId: locator.blockId,
    panelBlockId: state.panelBlockId,
    highlightedText: normalizeText(state.highlightedText).slice(0, 220),
    highlightedTag: state.highlightedTag,
    overlap,
  };
}

async function main() {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  const browser = await chromium.launch({ headless: true, executablePath: CHROMIUM_EXECUTABLE_PATH });
  const context = await browser.newContext({ viewport: { width: 1360, height: 900 } });
  try {
    await signIn(context);
    const topResults = await apiSearch(context);
    const page = await context.newPage();
    const pageErrors = [];
    const consoleErrors = [];
    page.on("pageerror", (error) => pageErrors.push(String(error?.stack || error?.message || error)));
    page.on("console", (message) => {
      if (["error", "warning"].includes(message.type())) consoleErrors.push(`${message.type()}: ${message.text()}`);
    });
    await openSearchPanel(page, true);

    const audits = [];
    for (let index = 0; index < TOP_N; index += 1) {
      await openSearchPanel(page, false);
      audits.push(await clickAndAuditRow(page, index, topResults[index]));
    }
    await page.screenshot({ path: path.join(OUTPUT_DIR, "top5-after-last-click.png"), fullPage: true });

    const result = {
      ok: true,
      baseUrl: BASE_URL,
      rootUri: ROOT_URI,
      workspaceId: WORKSPACE_ID,
      query: QUERY,
      topN: TOP_N,
      audits,
      pageErrors,
      consoleErrors,
    };
    fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
    console.log(JSON.stringify(result, null, 2));
  } finally {
    await browser.close().catch(() => undefined);
  }
}

main().catch((error) => {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  fs.writeFileSync(
    path.join(OUTPUT_DIR, "failure.json"),
    `${JSON.stringify({ ok: false, error: error.stack || error.message || String(error) }, null, 2)}\n`,
    "utf8",
  );
  console.error(error.stack || error.message || String(error));
  process.exit(1);
});
