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

test("header notification indicator opens a dismissible short-lived alert center", async () => {
  const [bar, center, css, copy] = await Promise.all([
    readFile(new URL("../context/ContextBar.jsx", import.meta.url), "utf8"),
    readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8"),
    readFile(new URL("./notifications.css", import.meta.url), "utf8"),
    readFile(new URL("../../shared/i18n.js", import.meta.url), "utf8")
  ]);

  assert.match(bar, /chatProgressLabel\(session, t\)/);
  assert.doesNotMatch(bar, /notifications\.length > 0 \? t\("notification\.count"/);
  assert.match(bar, /<NotificationCenter notifications=\{notifications\}/);
  assert.match(center, /aria-label=\{t\("notification\.openNotifications"\)\}/);
  assert.match(center, /context-project-indicator--notification/);
  assert.match(center, /notification-center-panel/);
  assert.match(center, /const hasAlert = otherNotifications\.length > 0 \|\| Boolean\(notificationError\) \|\| refreshErrors\.length > 0/);
  assert.doesNotMatch(center, /disabled=\{!hasContent\}/);
  assert.doesNotMatch(center, /if \(!hasContent\) setOpen\(false\)/);
  assert.match(center, /!hasContent && <p className="notification-center-empty">\{t\("notification\.empty"\)\}<\/p>/);
  assert.match(center, /hasAlert \? "context-project-indicator context-project-indicator--notification" : liveSessions\.length > 0 \? "context-project-indicator context-project-indicator--active" : "context-project-indicator context-project-indicator--complete"/);
  assert.match(center, /otherNotifications\.map/);
  assert.match(center, /accountDisplayName/);
  assert.match(center, /notification-title/);
  assert.match(center, /<small>/);
  assert.match(center, /\^\(\.\*\?\)\\s·\\s\(5h\|Weekly\|5시간\|주간\)/);
  assert.match(center, /onOpen\(notification\)/);
  assert.match(center, /onClear/);
  assert.match(center, /useOutsideDismiss/);
  assert.match(css, /\.notification-center\{[^}]*display:contents/s);
  assert.match(css, /\.notification-center-trigger\{[^}]*position:absolute[^}]*width:44px/s);
  assert.doesNotMatch(css, /\.notification-center-trigger\{[^}]*inset:0[^}]*width:100%/s);
  assert.match(css, /\.notification-center-trigger \.context-project-indicator\{[^}]*position:static[^}]*transform:none/s);
  assert.match(css, /\.notification-center-panel\{[^}]*position:absolute[^}]*top:100%/s);
  assert.match(css, /\.notification-center-panel\{[^}]*width:100%/s);
  assert.match(css, /\.notification-center-panel\{[^}]*--notification-visible-items:5[^}]*max-height:min\(calc\(50px \+ \(var\(--notification-item-height\) \* var\(--notification-visible-items\)\)\),65dvh\)/s);
  assert.match(css, /\.context-project-indicator--notification\{[^}]*#ef5350/s);
  assert.match(css, /\.notification-center-item\{[^}]*min-height:48px/s);
  assert.doesNotMatch(css, /\.notification-center-item\{[^}]*min-height:var\(--notification-item-height\)/s);
  assert.match(css, /\.notification-title\{[^}]*display:flex/s);
  assert.match(css, /\.notification-title small\{[^}]*font-size:10px/s);
  assert.match(css, /\.notification-center-item span\{[^}]*-webkit-line-clamp:2/s);
  assert.match(css, /\.notification-center-item span\{[^}]*white-space:normal/s);
  assert.match(css, /\.notification-center-list\{[^}]*min-height:0[^}]*overflow-y:auto[^}]*overscroll-behavior:contain/s);
  assert.match(css, /\.notification-center-empty\{[^}]*display:grid[^}]*min-height:48px[^}]*margin:0[^}]*place-items:center[^}]*color:#aeb4be[^}]*font-size:13px/s);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.notification-center-empty\{[^}]*color:#57606a!important/s);
  assert.match(copy, /"notification\.empty": "No notifications"/);
  assert.match(copy, /"notification\.empty": "알림 없음"/);
});

test("project title, chat status, and notification indicator have separate hit areas", async () => {
  const [bar, center, contextCss, notificationCss] = await Promise.all([
    readFile(new URL("../context/ContextBar.jsx", import.meta.url), "utf8"),
    readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8"),
    readFile(new URL("../context/context.css", import.meta.url), "utf8"),
    readFile(new URL("./notifications.css", import.meta.url), "utf8")
  ]);

  assert.match(bar, /className="context-project-button context-project-title"[^>]*onClick=\{onChatOpen\}/);
  assert.match(center, /className="context-project-status"[^>]*onClick=\{onStatusOpen\}/);
  assert.match(bar, /onStatusOpen=\{onChatOpen\}/);
  assert.match(bar, /<NotificationCenter[^>]*currentStatus=\{chatProgressLabel\(session, t\)\}/s);
  assert.match(contextCss, /\.context-project-title\{[^}]*grid-area:title/s);
  assert.match(contextCss, /\.context-project-status\{[^}]*grid-area:status[^}]*min-height:0/s);
  assert.match(contextCss, /\.context-project\{[^}]*grid-template-rows:auto auto[^}]*gap:2px 0!important[^}]*padding:12px 0!important/s);
  assert.match(contextCss, /\.context-project-title\{[^}]*padding-top:0/s);
  assert.match(contextCss, /\.context-project-status\{[^}]*align-items:center[^}]*padding:0 0 0 12px/s);
  assert.match(contextCss, /@media\(max-width:640px\)\{\.context-project\{padding:10px 0!important\}\}/s);
  assert.doesNotMatch(notificationCss, /\.context-project\{[^}]*padding/s);
  assert.match(notificationCss, /\.notification-center-trigger\{[^}]*position:absolute[^}]*top:0[^}]*right:0[^}]*bottom:0[^}]*width:44px/s);
});

test("header shows current chat status and adds slash alerts only when present", async () => {
  const [app, bar, center, css, copy] = await Promise.all([
    readFile(new URL("../../App.jsx", import.meta.url), "utf8"),
    readFile(new URL("../context/ContextBar.jsx", import.meta.url), "utf8"),
    readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8"),
    readFile(new URL("./notifications.css", import.meta.url), "utf8"),
    readFile(new URL("../../shared/i18n.js", import.meta.url), "utf8")
  ]);

  assert.match(bar, /currentStatus=\{chatProgressLabel\(session, t\)\}/);
  assert.match(center, /otherActiveSessions\(sessions, currentSession\)/);
  assert.match(center, /notification\.sessionId !== currentSession\?\.id/);
  assert.match(app, /const notificationCurrentSession = app\.tab === "chat" && !app\.newChatOpen && !app\.chatTransition \? displaySession : null/);
  assert.match(app, /useNotifications\(\{ accounts: app\.accounts, currentSession: notificationCurrentSession, recoveryVersion: connectivity\.recoveryVersion \}\)/);
  assert.match(center, /notification\.otherChatAlerts/);
  assert.match(center, /otherChatAlertLabel && <><span className="notification-center-trigger-separator"/);
  assert.match(css, /\.notification-center-trigger-label\{/);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.notification-center-trigger\{[^}]*background:transparent!important[^}]*background-color:transparent!important/s);
  assert.match(copy, /"notification\.otherChatAlerts": "\{count\} alerts"/);
  assert.match(copy, /"notification\.otherChatAlerts": "알림 \{count\}개"/);
  assert.doesNotMatch(copy, /notification\.otherChatAlertsNone/);
});

test("session notification click preserves the execution account and opens that session", async () => {
  const app = await readFile(new URL("../../App.jsx", import.meta.url), "utf8");

  assert.match(app, /useNotifications\(\{ accounts: app\.accounts, currentSession: notificationCurrentSession, recoveryVersion: connectivity\.recoveryVersion \}\)/);
  assert.match(app, /if \(notificationClickAction\(notification\.type\) !== "open-session"\)/);
  assert.match(app, /request\(`\/sessions\/\$\{encodeURIComponent\(notification\.sessionId\)\}`\)/);
  assert.doesNotMatch(app, /app\.selectAccount\(result\.session\.accountId\)/);
  assert.match(app, /app\.openSession\(result\.session, undefined, \{ reveal: true \}\)/);
  assert.match(app, /notifications\.dismissSession\(notification\.sessionId\)/);
  assert.match(app, /app\.openSession\(result\.session, undefined, \{ reveal: true \}\);\s*notifications\.dismissSession\(notification\.sessionId\)/s);
});

test("a visible chat automatically dismisses its completed alert", async () => {
  const notifications = await readFile(new URL("./useNotifications.js", import.meta.url), "utf8");

  assert.match(notifications, /dismissSessionNotificationState\(current, currentSession\?\.id, \{ types: \["completed"\] \}\)/);
});

test("completed alerts fall back to their captured chat title before the slow chat list", async () => {
  const center = await readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8");

  assert.match(center, /activeSessionTitle\(relatedSession \?\? \{\}, threads, t\("notification\.untitledChat"\), notification\.conversationTitle\)/);
});

test("notification and active-session clicks reveal chat from settings details", async () => {
  const [app, controller] = await Promise.all([
    readFile(new URL("../../App.jsx", import.meta.url), "utf8"),
    readFile(new URL("../../app/useAppController.js", import.meta.url), "utf8")
  ]);

  assert.equal((app.match(/app\.openSession\(result\.session, undefined, \{ reveal: true \}\)/g) ?? []).length, 2);
  assert.match(controller, /openSession: \(session, requestId, \{ reveal = false \} = \{\}\)/);
  assert.match(controller, /writeSavedView\(undefined, reveal \? "chat"/);
  assert.match(controller, /dispatch\(\{ type: "session\.opened", session, requestId, reveal \}\)/);
});

test("failed session notification open keeps the notification and exposes a safe retry error", async () => {
  const [app, center] = await Promise.all([
    readFile(new URL("../../App.jsx", import.meta.url), "utf8"),
    readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8")
  ]);

  assert.match(app, /catch \(error\) \{\s*notifications\.failOpen\(notification\.id\)/s);
  assert.match(app, /<ContextBar[^>]*notificationError=\{notifications\.openError\}/s);
  assert.match(app, /<ContextBar[^>]*notificationRefreshErrors=\{notifications\.refreshErrors\}/s);
  assert.match(center, /notificationError &&/);
  assert.match(center, /refreshErrors\.map/);
  assert.match(center, /t\("notification\.openFailed"\)/);
  assert.match(center, /t\("notification\.sessionsRefreshFailed"\)/);
  assert.match(center, /t\("notification\.usageRefreshFailed"/);
});

test("header activity center lists alerts above live sessions without a duplicate alerts heading", async () => {
  const [app, bar, center, css, copy] = await Promise.all([
    readFile(new URL("../../App.jsx", import.meta.url), "utf8"),
    readFile(new URL("../context/ContextBar.jsx", import.meta.url), "utf8"),
    readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8"),
    readFile(new URL("./notifications.css", import.meta.url), "utf8"),
    readFile(new URL("../../shared/i18n.js", import.meta.url), "utf8")
  ]);

  assert.match(app, /<ContextBar[^>]*sessions=\{notifications\.sessions\}[^>]*currentSession=\{notificationCurrentSession\}/s);
  assert.match(app, /async function handleActiveSessionOpen\(session\)/);
  assert.match(app, /request\(`\/sessions\/\$\{encodeURIComponent\(session\.id\)\}`\)/);
  assert.match(app, /onActiveSessionOpen=\{handleActiveSessionOpen\}/);
  assert.match(bar, /<NotificationCenter[^>]*sessions=\{sessions\}[^>]*currentSession=\{currentSession\}/s);
  assert.match(center, /otherActiveSessions\(sessions, currentSession\)/);
  assert.match(center, /const hasContent = liveSessions\.length > 0 \|\| hasAlert/);
  assert.doesNotMatch(center, /disabled=\{!hasContent\}/);
  assert.ok(center.indexOf("otherNotifications.map") < center.indexOf("liveSessions.map"));
  assert.doesNotMatch(center, /t\("notification\.alertsTitle"\)/);
  assert.match(center, /t\("notification\.activeTitle"\)/);
  assert.match(center, /liveSessions\.map/);
  assert.match(center, /activeSessionTitle\(session, threads, t\("notification\.untitledChat"\)\)/);
  assert.match(center, /onActiveSessionOpen\(session\)/);
  assert.match(css, /\.notification-center-section-title\{/);
  assert.match(css, /\.notification-center-item--active\{/);
  assert.match(copy, /"notification\.activeTitle": "In progress"/);
  assert.match(copy, /"notification\.activeTitle": "진행 중"/);
});

test("activity rows lead with the project and put the chat title beside the status", async () => {
  const center = await readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8");

  assert.match(center, /<b>\{session\.projectLabel \|\| t\("notification\.unknownProject"\)\}<\/b>/);
  assert.match(center, /<b>\{session\.projectLabel \|\| t\("notification\.unknownProject"\)\}<\/b><small>\{activeSessionStatus\(session\)\}<\/small>/);
  assert.match(center, /<span>\{activeSessionTitle\(session, threads, t\("notification\.untitledChat"\)\)\}<\/span>/);
  assert.match(center, /const relatedSession = sessionForNotification\(notification\)/);
  assert.match(center, /notification\.type === "completed"/);
  assert.match(center, /<b>\{relatedSession\?\.projectLabel \|\| notification\.detail \|\| t\("notification\.unknownProject"\)\}<\/b>/);
  assert.match(center, /<small>\{notificationTitle\(notification\)\}<\/small>/);
  assert.match(center, /activeSessionTitle\(relatedSession \?\? \{\}, threads, t\("notification\.untitledChat"\), notification\.conversationTitle\)/);
});

test("activity row status keeps its width while a long project name is ellipsized", async () => {
  const [center, css] = await Promise.all([
    readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8"),
    readFile(new URL("./notifications.css", import.meta.url), "utf8")
  ]);

  assert.match(center, /notification-center-item--completed/);
  assert.match(css, /\.notification-center-item--active \.notification-title b,[^}]*\.notification-center-item--completed \.notification-title b\{[^}]*flex:1 1 auto[^}]*min-width:0[^}]*overflow:hidden[^}]*text-overflow:ellipsis/s);
  assert.match(css, /\.notification-center-item--active \.notification-title small,[^}]*\.notification-center-item--completed \.notification-title small\{[^}]*flex:0 0 auto/s);
});

test("notification alerts swipe left to dismiss only that alert", async () => {
  const [app, bar, center, css] = await Promise.all([
    readFile(new URL("../../App.jsx", import.meta.url), "utf8"),
    readFile(new URL("../context/ContextBar.jsx", import.meta.url), "utf8"),
    readFile(new URL("./NotificationCenter.jsx", import.meta.url), "utf8"),
    readFile(new URL("./notifications.css", import.meta.url), "utf8")
  ]);

  assert.match(app, /onNotificationDismiss=\{notifications\.dismiss\}/);
  assert.match(bar, /onNotificationDismiss/);
  assert.match(bar, /onDismiss=\{onNotificationDismiss\}/);
  assert.match(center, /function DismissibleNotificationItem/);
  assert.match(center, /onPointerDown=\{handlePointerDown\}/);
  assert.match(center, /onPointerMove=\{handlePointerMove\}/);
  assert.match(center, /onPointerUp=\{handlePointerEnd\}/);
  assert.match(center, /if \(suppressClickRef\.current\)/);
  assert.match(center, /onDismiss\(notification\.id\)/);
  assert.match(center, /className="notification-center-dismiss-action"/);
  assert.match(center, /aria-label=\{t\("notification\.dismiss"\)\}/);
  assert.match(center, /function handlePointerCancel[\s\S]*suppressClickRef\.current = false/);
  assert.doesNotMatch(center, /useEffect\(\(\) => \{ if \(!hasContent\) setOpen\(false\); \}, \[hasContent\]\)/);
  assert.ok(center.indexOf("otherNotifications.map") < center.indexOf("<DismissibleNotificationItem"));
  assert.ok(center.indexOf("<DismissibleNotificationItem") < center.indexOf("liveSessions.map"));
  assert.match(css, /\.notification-center-swipe\{[^}]*overflow:hidden/s);
  assert.match(css, /\.notification-center-swipe-item\{[^}]*touch-action:pan-y/s);
  assert.match(css, /\.notification-center-dismiss-action\{[^}]*min-height:48px/s);
  assert.match(css, /\.notification-center-dismiss-action:focus-visible\{[^}]*opacity:1/s);
  assert.match(css, /html\[data-theme="light"\] \.app-shell \.notification-center-dismiss-action\{[^}]*background:#a83232!important[^}]*background-color:#a83232!important/s);
  assert.match(css, /transform:translate3d\(var\(--notification-swipe-offset\),0,0\)/);
});
