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

const assert = require("node:assert/strict");
const fs = require("node:fs");
const fsp = require("node:fs/promises");
const os = require("node:os");
const path = require("node:path");
const JSZip = require("jszip");
const XLSX = require("@e965/xlsx");
const { chromium } = require("playwright");

const ROOT = path.resolve(__dirname, "..");
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 OUT_DIR = path.join(ROOT, "tmp", "task501-office-preview-light-viewer-smoke");
const TEST_ACCOUNT = "mnote-e2e";
const TEST_EMAIL = "mnote.e2e@example.com";
const TEST_PASSWORD = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!";
const WORKSPACE_ID = "local-ws:mnote-e2e:office-preview-smoke";
const DOCUMENT_ID = "local-md:office-preview-smoke.md";

function resolveChromiumExecutablePath() {
  const explicit = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || "";
  if (explicit && fs.existsSync(explicit)) return explicit;
  return [
    "/usr/bin/google-chrome-stable",
    "/usr/bin/google-chrome",
    "/snap/bin/chromium",
    "/usr/bin/chromium",
    "/usr/bin/chromium-browser",
  ].find((candidate) => fs.existsSync(candidate)) || "";
}

function officePreviewUrl(root, fileName, fileType) {
  const fileUrl = new URL("/api/local-folder/files/open", BASE_URL);
  fileUrl.searchParams.set("rootUri", `file://${root}`);
  fileUrl.searchParams.set("path", fileName);

  const url = new URL("/office-preview", BASE_URL);
  url.searchParams.set("fileUrl", `${fileUrl.pathname}${fileUrl.search}`);
  url.searchParams.set("fileName", fileName);
  url.searchParams.set("fileType", fileType);
  url.searchParams.set("workspaceId", WORKSPACE_ID);
  url.searchParams.set("sourceKind", "local_folder");
  url.searchParams.set("rootUri", `file://${root}`);
  url.searchParams.set("documentId", DOCUMENT_ID);
  return url.toString();
}

async function writeDocx(filePath) {
  const zip = new JSZip();
  zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>`);
  zip.folder("_rels").file(".rels", `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>`);
  zip.folder("word").file("document.xml", `<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:body>
    <w:p><w:r><w:t>MNote DOCX light preview works</w:t></w:r></w:p>
    <w:p><w:r><w:t>第二行测试内容</w:t></w:r></w:p>
    <w:sectPr/>
  </w:body>
</w:document>`);
  await fsp.writeFile(filePath, await zip.generateAsync({ type: "nodebuffer" }));
}

function writeXlsx(filePath) {
  const workbook = XLSX.utils.book_new();
  const sheet = XLSX.utils.aoa_to_sheet([
    ["Name", "Value"],
    ["MNote XLSX light preview works", "ok"],
    ["中文", "正常"],
  ]);
  XLSX.utils.book_append_sheet(workbook, sheet, "Sheet1");
  XLSX.writeFile(workbook, filePath);
}

async function writePptx(filePath) {
  const zip = new JSZip();
  zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
  <Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
  <Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
  <Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
  <Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
</Types>`);
  zip.folder("_rels").file(".rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
</Relationships>`);
  zip.folder("ppt").file("presentation.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
  <p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
  <p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst>
  <p:sldSz cx="12192000" cy="6858000" type="wide"/>
  <p:notesSz cx="6858000" cy="9144000"/>
  <p:defaultTextStyle>
    <a:defPPr><a:defRPr lang="zh-CN"/></a:defPPr>
    <a:lvl1pPr marL="0" algn="l"><a:defRPr sz="1800" lang="zh-CN"/></a:lvl1pPr>
  </p:defaultTextStyle>
</p:presentation>`);
  zip.folder("ppt").folder("_rels").file("presentation.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
</Relationships>`);
  zip.folder("ppt").folder("slideMasters").file("slideMaster1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
  <p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>
  <p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
</p:sldMaster>`);
  zip.folder("ppt").folder("slideMasters").folder("_rels").file("slideMaster1.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
</Relationships>`);
  zip.folder("ppt").folder("slideLayouts").file("slideLayout1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank">
  <p:cSld name="Blank"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>
</p:sldLayout>`);
  zip.folder("ppt").folder("slideLayouts").folder("_rels").file("slideLayout1.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
</Relationships>`);
  zip.folder("ppt").folder("slides").file("slide1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
  <p:cSld>
    <p:spTree>
      <p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
      <p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>
      <p:sp>
        <p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
        <p:spPr><a:xfrm><a:off x="914400" y="914400"/><a:ext cx="10363200" cy="1371600"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
        <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="zh-CN" sz="4400"/><a:t>MNote PPTX light preview works</a:t></a:r></a:p></p:txBody>
      </p:sp>
    </p:spTree>
  </p:cSld>
</p:sld>`);
  zip.folder("ppt").folder("slides").folder("_rels").file("slide1.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
</Relationships>`);
  zip.folder("ppt").folder("theme").file("theme1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="MNote">
  <a:themeElements>
    <a:clrScheme name="MNote"><a:dk1><a:srgbClr val="000000"/></a:dk1><a:lt1><a:srgbClr val="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F2937"/></a:dk2><a:lt2><a:srgbClr val="F8FAFC"/></a:lt2><a:accent1><a:srgbClr val="2563EB"/></a:accent1><a:accent2><a:srgbClr val="22A559"/></a:accent2><a:accent3><a:srgbClr val="E0525B"/></a:accent3><a:accent4><a:srgbClr val="F59E0B"/></a:accent4><a:accent5><a:srgbClr val="8B5CF6"/></a:accent5><a:accent6><a:srgbClr val="14B8A6"/></a:accent6><a:hlink><a:srgbClr val="2563EB"/></a:hlink><a:folHlink><a:srgbClr val="7C3AED"/></a:folHlink></a:clrScheme>
    <a:fontScheme name="MNote"><a:majorFont><a:latin typeface="Arial"/></a:majorFont><a:minorFont><a:latin typeface="Arial"/></a:minorFont></a:fontScheme>
    <a:fmtScheme name="MNote"><a:fillStyleLst/><a:lnStyleLst/><a:effectStyleLst/><a:bgFillStyleLst/></a:fmtScheme>
  </a:themeElements>
</a:theme>`);
  await fsp.writeFile(filePath, await zip.generateAsync({ type: "nodebuffer" }));
}

async function signIn(context) {
  const payload = (params) => ({
    action: "auth:signIn",
    args: {
      provider: "password",
      params,
    },
  });
  let response = await context.request.fetch(`${BASE_URL}/api/auth`, {
    method: "POST",
    data: payload({
      account: TEST_ACCOUNT,
      password: TEST_PASSWORD,
      flow: "signIn",
    }),
  });
  if (response.ok()) return;
  response = await context.request.fetch(`${BASE_URL}/api/auth`, {
    method: "POST",
    data: payload({
      email: TEST_EMAIL,
      username: TEST_ACCOUNT,
      name: TEST_ACCOUNT,
      password: TEST_PASSWORD,
      flow: "signUp",
    }),
  });
  assert(response.ok(), `测试账号登录失败: ${response.status()} ${await response.text()}`);
}

async function setPageWidthPreferences(context, root, updates) {
  const response = await context.request.fetch(`${BASE_URL}/api/ui/preferences`, {
    method: "PUT",
    data: {
      documentId: DOCUMENT_ID,
      workspaceId: WORKSPACE_ID,
      sourceKind: "local_folder",
      rootUri: `file://${root}`,
      updates,
    },
  });
  assert(response.ok(), `页面宽度偏好写入失败: ${response.status()} ${await response.text()}`);
}

async function verifyPreview(page, root, item) {
  const consoleMessages = [];
  page.on("console", (message) => {
    consoleMessages.push(`${message.type()}: ${message.text()}`);
  });
  page.on("pageerror", (error) => {
    consoleMessages.push(`pageerror: ${error && error.stack ? error.stack : error.message}`);
  });

  await page.goto(officePreviewUrl(root, item.fileName, item.fileType), {
    waitUntil: "domcontentloaded",
    timeout: UI_TIMEOUT_MS,
  });
  await page.waitForFunction(
    () => {
      const status = document.documentElement.getAttribute("data-mnote-office-preview-status");
      return status && status !== "加载中";
    },
    null,
    { timeout: UI_TIMEOUT_MS },
  );
  const status = await page.locator("html").getAttribute("data-mnote-office-preview-status");
  const text = await page.locator("#mnote-office-viewer").innerText({ timeout: UI_TIMEOUT_MS });
  const layout = await page.evaluate(() => {
    const viewer = document.querySelector("#mnote-office-viewer");
    const tableWrap = document.querySelector(".mnote-office-table-wrap");
    const docxPage = document.querySelector(".docx-wrapper > section.docx, .mnote-docx-wrapper > section.mnote-docx");
    const pptxStage = document.querySelector(".mnote-office-pptx-stage");
    const pptxSlide = document.querySelector(".pptx-preview-slide-wrapper");
    const rectOf = (node) => {
      if (!(node instanceof Element)) return null;
      const rect = node.getBoundingClientRect();
      return {
        left: rect.left,
        width: rect.width,
      };
    };
    return {
      viewportWidth: window.innerWidth,
      pageWidthContentType: document.documentElement.getAttribute("data-page-width-content-type"),
      pageWidthMode: document.documentElement.getAttribute("data-page-width-resolved-mode"),
      toolbarCount: document.querySelectorAll(".mnote-office-toolbar").length,
      viewer: rectOf(viewer),
      tableWrap: rectOf(tableWrap),
      docxPage: rectOf(docxPage),
      pptxStage: rectOf(pptxStage),
      pptxSlide: rectOf(pptxSlide),
    };
  });
  const screenshot = path.join(OUT_DIR, `${item.fileType}.png`);
  await page.screenshot({ path: screenshot, fullPage: true });

  assert.equal(status, "完成", `${item.fileType} 预览状态应完成`);
  assert(text.includes(item.expected), `${item.fileType} 预览应包含样本文本: ${item.expected}`);
  assert.equal(consoleMessages.length, 0, `${item.fileType} 预览不应产生控制台错误: ${consoleMessages.join("\n")}`);
  assert.equal(layout.toolbarCount, 0, "轻量预览页不应显示顶部状态条");
  if (item.fileType === "docx") {
    assert.equal(layout.pageWidthContentType, "word", "DOCX 应使用 Word 宽度类型");
    assert.equal(layout.pageWidthMode, item.expectedMode || "wide", "DOCX 应使用指定页面宽度");
    assert(layout.viewer && layout.viewer.width >= Math.min(layout.viewportWidth, 1180) - 24, "DOCX 预览容器应接近宽版宽度");
    assert(layout.viewer.left > 0 && layout.viewer.left < 80, "DOCX 宽版容器应居中而不是左侧小块");
    assert(layout.docxPage && layout.docxPage.width >= layout.viewportWidth * 0.75, "DOCX 页面应放大到接近可视宽度");
  } else if (item.fileType === "pptx") {
    assert.equal(layout.pageWidthContentType, "ppt", "PPTX 应使用 PPT 宽度类型");
    assert.equal(layout.pageWidthMode, item.expectedMode || "wide", "PPTX 应使用指定页面宽度");
    if ((item.expectedMode || "wide") === "full") {
      assert(layout.viewer && layout.viewer.width >= layout.viewportWidth - 2, "PPTX 全宽模式应占满页面宽度");
      assert(layout.pptxStage && layout.pptxStage.width >= layout.viewportWidth - 40, "PPTX 舞台应随全宽容器放大");
    } else {
      assert(layout.viewer && layout.viewer.width >= Math.min(layout.viewportWidth, 1180) - 24, "PPTX 预览容器应接近宽版宽度");
    }
  } else {
    assert.equal(layout.pageWidthContentType, "excel", `${item.fileType} 应使用 Excel 宽度类型`);
    assert.equal(layout.pageWidthMode, item.expectedMode || "full", `${item.fileType} 应使用指定页面宽度`);
    assert(layout.viewer && layout.viewer.left <= 1, `${item.fileType} 预览容器应从页面左侧开始`);
    assert(layout.viewer.width >= layout.viewportWidth - 2, `${item.fileType} 预览容器应占满页面宽度`);
    assert(layout.tableWrap && layout.tableWrap.width >= layout.viewportWidth - 40, `${item.fileType} 表格容器应接近全宽`);
  }
  return { fileType: item.fileType, status, text: text.slice(0, 300), layout, screenshot };
}

async function main() {
  await fsp.mkdir(OUT_DIR, { recursive: true });
  const root = await fsp.mkdtemp(path.join(os.tmpdir(), "mnote-office-preview-"));
  await fsp.mkdir(path.join(root, ".mnote"), { recursive: true });
  await fsp.writeFile(
    path.join(root, ".mnote", "workspace.json"),
    `${JSON.stringify({
      workspaceId: WORKSPACE_ID,
      ownerId: TEST_ACCOUNT,
      createdAt: new Date().toISOString(),
      capabilities: ["local_files", "markdown_edit", "asset_upload"],
    }, null, 2)}\n`,
    "utf8",
  );
  await writeDocx(path.join(root, "sample.docx"));
  writeXlsx(path.join(root, "sample.xlsx"));
  await writePptx(path.join(root, "sample.pptx"));
  await fsp.writeFile(
    path.join(root, "sample.csv"),
    "Name,Value\nMNote CSV light preview works,ok\n中文,正常\n",
    "utf8",
  );

  const executablePath = resolveChromiumExecutablePath();
  const browser = await chromium.launch({
    headless: process.env.HEADFUL !== "1",
    ...(executablePath ? { executablePath } : {}),
  });
  const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
  try {
    await signIn(context);
    await setPageWidthPreferences(context, root, {
      "pageWidth.word": { mode: "wide", custom: null },
      "pageWidth.excel": { mode: "full", custom: null },
      "pageWidth.ppt": { mode: "wide", custom: null },
    });
    const page = await context.newPage();
    const results = [];
    for (const item of [
      { fileName: "sample.docx", fileType: "docx", expected: "MNote DOCX light preview works" },
      { fileName: "sample.xlsx", fileType: "xlsx", expected: "MNote XLSX light preview works" },
      { fileName: "sample.pptx", fileType: "pptx", expected: "MNote PPTX light preview works" },
      { fileName: "sample.csv", fileType: "csv", expected: "MNote CSV light preview works" },
    ]) {
      results.push(await verifyPreview(page, root, item));
    }
    await setPageWidthPreferences(context, root, {
      "pageWidth.ppt": { mode: "full", custom: null },
    });
    results.push(await verifyPreview(page, root, {
      fileName: "sample.pptx",
      fileType: "pptx",
      expected: "MNote PPTX light preview works",
      expectedMode: "full",
    }));
    const result = { ok: true, baseUrl: BASE_URL, root, results };
    await fsp.writeFile(path.join(OUT_DIR, "result.json"), `${JSON.stringify(result, null, 2)}\n`, "utf8");
    console.log(JSON.stringify(result, null, 2));
  } finally {
    await browser.close();
  }
}

main().catch(async (error) => {
  await fsp.mkdir(OUT_DIR, { recursive: true });
  await fsp.writeFile(
    path.join(OUT_DIR, "result.json"),
    `${JSON.stringify({ ok: false, baseUrl: BASE_URL, error: error && error.stack ? error.stack : String(error) }, null, 2)}\n`,
    "utf8",
  );
  console.error(error && error.stack ? error.stack : error);
  process.exit(1);
});
