import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

test("chat rows open an archive menu by long press or context menu", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  assert.match(panel, /onPointerDown/);
  assert.match(panel, /onContextMenu/);
  assert.match(panel, /500/);
  assert.match(panel, />\{t\("chats\.archive"\)\}<\/button>/);
  assert.match(panel, /closeMenu\(\); onArchive\(thread\)/);
});

test("mouse clicks do not start the mobile long press menu", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  assert.match(panel, /event\.pointerType === "mouse"/);
  assert.match(panel, /startLongPress\(event, item\)/);
});

test("archive menu closes on outside interaction and before its action", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  assert.match(panel, /document\.addEventListener\("pointerdown"/);
  assert.match(panel, /closeMenu\(\); onArchive\(thread\)/);
});

test("chat context menu offers pin rename and archive actions", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  assert.match(panel, /menu\.thread\.pinned \? t\("chats\.unpin"\) : t\("chats\.pin"\)/);
  assert.match(panel, />\{t\("chats\.rename"\)\}<\/button>/);
  assert.match(panel, />\{t\("chats\.archive"\)\}<\/button>/);
  assert.match(panel, /closeMenu\(\); onPin\(thread\)/);
  assert.match(panel, /closeMenu\(\); const name = window\.prompt/);
});

test("delete is only exposed from archived chats", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  assert.match(panel, /archived && <button[^>]*danger-menuitem/s);
  assert.match(panel, /setDeleteTarget\(thread\)/);
  assert.match(panel, /!archived && <button/);
  assert.match(panel, /closeMenu\(\); onArchive\(thread\)/);
});

test("chat context menu actions use matching Lucide icons", async () => {
  const [panel, icons] = await Promise.all([
    readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8"),
    readFile(new URL("../../shared/ActionIcons.jsx", import.meta.url), "utf8")
  ]);
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.match(icons, /export function HideIcon[\s\S]*<EyeOff/);
  assert.match(icons, /export function PinIcon[\s\S]*pinned \? <PinOff[\s\S]*: <Pin /);
  assert.match(icons, /export function RenameIcon[\s\S]*<PencilLine/);
  assert.match(icons, /export function ArchiveIcon[\s\S]*<Archive/);
  assert.match(panel, /<HideIcon className="chat-context-menu-icon" \/>/);
  assert.match(panel, /<PinIcon pinned=\{menu\.thread\.pinned\} className="chat-context-menu-icon" \/>/);
  assert.match(panel, /<RenameIcon className="chat-context-menu-icon" \/>/);
  assert.match(panel, /<ArchiveIcon className="chat-context-menu-icon" \/>/);
  assert.match(panel, /<TrashIcon className="chat-context-menu-icon" \/>/);
  assert.match(css, /\.chat-context-menu button\{[^}]*display:flex[^}]*gap:10px/);
  assert.match(css, /\.chat-context-menu-icon\{[^}]*width:18px[^}]*height:18px/);
});

test("archived chat rows keep a clean delete icon inside the chat card", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.match(panel, /archived && <button type="button" className="archived-chat-delete"/);
  assert.match(panel, /aria-label=\{`\$\{t\("chats\.delete"\)\}: \$\{threadTitle\(item\)\}`\}/);
  assert.match(panel, /onClick=\{\(\) => setDeleteTarget\(item\)\}/);
  assert.match(panel, /className="archived-chat-delete-icon"/);
  assert.match(css, /\.thread-row-shell\.archived\{[^}]*border:0[^}]*background:transparent[^}]*box-shadow:none/);
  assert.match(css, /\.thread-row-shell\.archived :is\(\.thread-row,\.archived-chat-delete\)\{[^}]*border:0[^}]*background:transparent/);
  assert.match(css, /\.archived-chat-delete\{[^}]*min-width:48px[^}]*min-height:48px/);
});

test("archived chat delete uses a compact neutral line icon", async () => {
  const [panel, icons] = await Promise.all([
    readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8"),
    readFile(new URL("../../shared/ActionIcons.jsx", import.meta.url), "utf8")
  ]);
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.match(panel, /<TrashIcon className="archived-chat-delete-icon" \/>/);
  assert.match(icons, /export function TrashIcon/);
  assert.match(css, /\.archived-chat-delete\{[^}]*color:#b7bac3/);
  assert.match(css, /\.archived-chat-delete:hover\{[^}]*color:#ffb5ba/);
  assert.match(css, /\.archived-chat-delete-icon\{[^}]*width:18px[^}]*height:18px[^}]*stroke-width:1\.8/);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.archived-chat-delete\{[^}]*color:#57606a!important/);
});

test("archived chat deletion uses the themed app confirmation dialog", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.doesNotMatch(panel, /window\.confirm/);
  assert.match(panel, /const closeDeleteDialog = useBackLayer\(Boolean\(deleteTarget\),[\s\S]*?"delete-chat"\)/);
  assert.match(panel, /chat-confirm-backdrop--exit[\s\S]*onPointerDown=\{\(event\) => \{ if \(event\.target === event\.currentTarget\) closeDeleteDialog\(\); \}\}/);
  assert.match(panel, /chat-confirm-dialog--exit[\s\S]*role="dialog" aria-modal="true" aria-labelledby="chat-delete-title" aria-describedby="chat-delete-description"/);
  assert.match(panel, /<strong className="chat-confirm-thread">\{threadTitle\(deleteTarget\)\}<\/strong>/);
  assert.match(panel, /className="chat-confirm-cancel"[^>]*autoFocus/);
  assert.match(panel, /className="chat-confirm-delete"[^>]*onClick=\{\(\) => confirmDelete\(\)\}/);
  assert.match(css, /\.chat-confirm-backdrop\{[^}]*position:fixed[^}]*background:rgba\(0,0,0,\.66\)/);
  assert.match(css, /\.chat-confirm-dialog\{[^}]*width:min\(100%,430px\)[^}]*background:#181a1f/);
  assert.match(css, /\.chat-confirm-delete\{[^}]*background:#c93c45[^}]*color:#fff/);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.chat-confirm-dialog\{[^}]*background:#fff!important/);
});

test("delete confirmation closes before waiting for the delete request", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const handler = panel.match(/function confirmDelete\(\) \{[\s\S]*?\n  \}/)?.[0] ?? "";

  assert.ok(handler.indexOf("deleteMotion.startExit()") >= 0);
  assert.ok(handler.indexOf("deleteMotion.startExit()") < handler.indexOf("void onDelete(target)"));
});

test("consecutive archived chat deletes are not blocked by a global pending state", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");

  assert.doesNotMatch(panel, /deletePending/);
  assert.match(panel, /deleteMotion\.startExit\(\);\s*void onDelete\(target\)/);
});

test("archived chat search uses the full available width", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.match(panel, /className=\{archived \? "chat-search-bar archived" : "chat-search-bar"\}/);
  assert.match(css, /\.chat-search-bar\.archived\{[^}]*grid-template-columns:minmax\(0,1fr\)/);
});

test("archived cards use one seamless surface in dark and light themes", async () => {
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.match(css, /\.thread-row-shell\.archived\{[^}]*border:0[^}]*background:transparent[^}]*box-shadow:none/);
  assert.match(css, /\.thread-row-shell\.archived :is\(\.thread-row,\.archived-chat-delete\)\{[^}]*border:0[^}]*border-radius:0[^}]*background:transparent[^}]*box-shadow:none/);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.thread-row-shell\.archived\{border:0!important;background:transparent!important;background-color:transparent!important;box-shadow:none!important/);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.thread-row-shell\.archived :is\(\.thread-row,\.archived-chat-delete\)\{[^}]*background:transparent!important[^}]*box-shadow:none!important/);
});

test("initial archived chat loading reuses the current chat transition spinner", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.match(panel, /const initialArchiveLoading = archived && loading && threads\.length === 0/);
  assert.match(panel, /initialArchiveLoading \? <div className="archive-loading" role="status" aria-busy="true"/);
  assert.match(panel, /<span className="chat-transition-spinner" aria-hidden="true"><\/span>/);
  assert.match(panel, /\{loading && !initialArchiveLoading && <p className="thread-state">/);
  assert.match(css, /\.archive-loading\{[^}]*display:grid[^}]*place-items:center/);
});

test("chats panel exposes archive mode navigation", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  assert.match(panel, /archived \? <header className="project-chat-header"><h2>\{t\("chats\.archived"\)\}<\/h2><\/header> : <h2>\{t\("chats\.recent"\)\}<\/h2>/);
  assert.match(panel, /closeArchive/);
  assert.doesNotMatch(panel, /archive-link-button/);
});

test("chat-row context menus stay right-aligned while project menus open below their card", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");
  assert.match(panel, /menu\.kind === "project" \? \{ top: menu\.y, left: menu\.x, right: "auto" \} : \{ top: menu\.y \}/);
  assert.match(panel, /anchorRect\.bottom \+ 6/);
  assert.match(panel, /menu\.anchorRight - menuWidth/);
  assert.match(css, /\.chat-context-menu\{[^}]*right:14px/);
  assert.doesNotMatch(css, /\.chat-context-menu\{[^}]*transform:/);
});

test("chat context menu is compact, stays inside the viewport, and keeps a consistent light-theme surface", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");

  assert.match(panel, /useLayoutEffect/);
  assert.match(panel, /\{ height: menuHeight, width: menuWidth \} = menuRef\.current\.getBoundingClientRect\(\)/);
  assert.match(css, /\.chat-context-menu\{[^}]*width:fit-content[^}]*min-width:140px[^}]*max-width:calc\(100vw - 28px\)[^}]*padding:4px/);
  assert.match(css, /\.chat-context-menu button\{[^}]*min-height:48px[^}]*padding:8px 10px[^}]*white-space:nowrap/);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.chat-context-menu\{[^}]*background:#fff!important/);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.chat-context-menu button\{[^}]*background:transparent!important/);
});

test("chat list text cannot be selected during a long press", async () => {
  const panel = await readFile(new URL("./ChatsPanel.jsx", import.meta.url), "utf8");
  const css = await readFile(new URL("./chats.css", import.meta.url), "utf8");
  assert.doesNotMatch(panel, /onSelectStart=/);
  assert.match(css, /\.chats-panel\{[^}]*user-select:none/);
  assert.match(css, /\.chats-panel input\{[^}]*user-select:text/);
});
