import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { assertCloudWebDistFresh, buildCloudWebDist, readCloudWebAppSource } from "./dist-contract.ts";

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = path.resolve(rootDir, "../..");

const requiredFiles = Object.freeze([
  "index.html",
  "src/main.tsx",
  "src/App.tsx",
  "src/components/layout/ActivityRail.tsx",
  "src/components/auth/LoginShell.tsx",
  "src/components/sessions/SessionSidebar.tsx",
  "src/components/conversation/ConversationPanel.tsx",
  "src/components/command-bar/CommandBar.tsx",
  "src/components/hwpod/HwpodNodeOpsSidebar.tsx",
  "src/components/performance/PerformanceView.tsx",
  "src/components/settings/SettingsView.tsx",
  "src/components/skills/SkillsView.tsx",
  "src/hooks/useAuth.ts",
  "src/services/api/client.ts",
  "src/state/workbench.ts",
  "src/types/domain.ts",
  "src/styles/workbench.css",
  "favicon.svg",
  "favicon.ico",
  "help.md"
]);

for (const file of requiredFiles) {
  if (!fs.existsSync(path.resolve(rootDir, file))) throw new Error(`missing React web asset: ${file}`);
}
console.log("hwlab-cloud-web check: React module assets present");

const html = readWeb("index.html");
const appTsx = readWeb("src/App.tsx");
const apiClientSource = readWeb("src/services/api/client.ts");
const authHookSource = readWeb("src/hooks/useAuth.ts");
const appSource = readCloudWebAppSource(rootDir);
const css = readWeb("src/styles/workbench.css");

assert.match(html, /<div id="root"><\/div>/u, "index.html must only expose React mount root");
assert.match(html, /src="\/src\/main\.tsx"/u, "index.html must load React TSX entry");
assert.match(html, /HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP/u, "workspace bootstrap must start during HTML parse for the workspace first screen");
assert.match(html, /new URLSearchParams\(window\.location\.search\)\.get\("projectId"\)/u, "workspace bootstrap must honor the URL projectId before the React bundle loads");
assert.doesNotMatch(html, /prj_device_pod_workbench/u, "workspace bootstrap must not hard-code the retired device-pod project id");
assert.doesNotMatch(html, /workbench-shell|hwpod-node-sidebar|command-input|conversation-list|session-tabs/u, "index.html must not contain business DOM shell");
for (const removed of ["app.ts", "app-conversation.ts", "app-helpers.ts", "app-skills.ts", "styles.css", "web-types.d.ts"]) {
  assert.equal(fs.existsSync(path.join(rootDir, removed)), false, `${removed} must not remain as a legacy runtime path`);
}
assertIncludes(appSource, "createRoot", "React root must be used");
assertIncludes(appSource, "React.StrictMode", "React StrictMode must wrap the app");
assertIncludes(appSource, "useWorkbenchStore", "state hook must own workbench state");
assertIncludes(appSource, "fetchJson", "API client must own fetch calls");
assertIncludes(appSource, "HwpodNodeOpsSidebar", "HWPOD node-ops sidebar component must exist");
assertIncludes(appSource, "CommandBar", "command bar component must exist");
assertIncludes(appSource, "PerformanceView", "performance view component must exist");
assertIncludes(appSource, "SkillsView", "skills component must exist");
assertIncludes(appSource, "SettingsView", "settings component must exist");
assertIncludes(appTsx, "const workspaceRoute = route === \"workspace\"", "React runtime must explicitly identify workspace route before starting workbench data flows");
assertIncludes(authHookSource, "consumeEarlyWorkspaceBootstrap", "auth hook must consume the HTML-started workspace bootstrap instead of issuing a second first-screen bootstrap");
assertIncludes(apiClientSource, "adapter: (): Promise<ApiResult<LiveProbePayload>> => fetchJson(\"/v1/rpc/system.health\", { method: \"POST\"", "adapter health must use POST /v1/rpc/system.health");
assertIncludes(appSource, "api.hwpodNodeOpsHealth()", "Workbench live probe must call HWPOD node-ops through Cloud Web/API");
assertIncludes(apiClientSource, "\"/v1/hwpod-node-ops\"", "API client must expose HWPOD node-ops route");
assertIncludes(readRepo("internal/dev-entrypoint/cloud-web-routes.mjs"), "normalizedPath.startsWith(\"/v1/rpc/\")", "Cloud Web must proxy REST RPC bridge routes to cloud-api");
assertIncludes(appSource, "id=\"command-input\"", "React runtime must render command input selector");
assertIncludes(appSource, "id=\"conversation-list\"", "React runtime must render conversation list selector");
assertIncludes(appSource, "id=\"hwpod-node-sidebar\"", "React runtime must render HWPOD node sidebar selector");
assertIncludes(appSource, "id=\"hwpod-event-scroll\"", "React runtime must render HWPOD node-ops stream selector");
assertIncludes(appSource, "id=\"logout-button\"", "React runtime must render logout selector");
assertIncludes(appSource, "route === \"performance\"", "React runtime must route top-level performance panel");
assertIncludes(readRepo("internal/cloud/server.ts"), "/v1/web-performance/summary", "cloud-api must expose WebUI performance summary API");
assert.doesNotMatch(appSource, /document\.getElementById\((?!["']root["'])|querySelector\(["']#(?:command-input|conversation-list|hwpod-node-sidebar)/u, "major UI must not be driven by global DOM element lookups");
assert.doesNotMatch(appSource, /\bconst\s+el\s*=/u, "global el map must not return");

for (const directory of ["components/layout", "components/auth", "components/sessions", "components/conversation", "components/command-bar", "components/hwpod", "components/performance", "components/settings", "components/skills", "hooks", "services/api", "state", "types", "styles"]) {
  assert.ok(fs.existsSync(path.join(rootDir, "src", directory)), `missing React module boundary src/${directory}`);
}
assert.match(css, /@media \(max-width: 720px\)/u, "mobile layout contract must be explicit");
assert.match(css, /grid-template-columns:\s*56px\s+minmax\(0,\s*1fr\)/u, "390px mobile layout must not keep three squeezed columns");
assert.match(css, /\.hwpod-event-scroll\s*\{[\s\S]*?overscroll-behavior:\s*contain;/u, "HWPOD node-ops stream scroll must be contained");
assert.match(css, /\.hwpod-event-panel\s*\{[\s\S]*?max-height:\s*min\(360px,\s*38dvh\);/u, "HWPOD node-ops stream panel must have a bounded own scroll area");
assert.match(css, /\.session-tabs\s*\{[\s\S]*?overflow-x:\s*hidden;/u, "session sidebar must not horizontally scroll");
assert.match(css, /\.message-body p\s*\{\s*white-space:\s*pre-wrap;\s*\}/u, "final response markdown paragraphs must preserve soft line breaks");
assert.match(css, /@keyframes trace-update-pulse/u, "trace updates must have an explicit visual pulse");
assert.match(css, /@keyframes trace-working-border/u, "running trace frame must show an explicit working animation");
assert.match(css, /\.message-trace-body\s*\{[\s\S]*?max-height:\s*calc\(4 \* 1\.35em \+ 14px\);/u, "trace/tool output body must be capped to about four lines with internal scroll");
assert.doesNotMatch(css, /\.message-final-response[^{}]*\{[^}]*max-height\s*:/u, "final response must not be height-capped; only trace output keeps its own height cap");
assert.doesNotMatch(appSource, /message-trace-storage-key/u, "trace summary must not expose raw trace ids as visible chips");
assert.doesNotMatch(appSource, /last ·/u, "trace summary must not show raw last-event labels");

const cloudApiServer = readRepo("internal/cloud/server.ts");
assertIncludes(cloudApiServer, "/v1/hwpod-node-ops", "cloud-api must expose HWPOD node-ops forwarder");
assertIncludes(cloudApiServer, "/v1/skills", "cloud-api must expose Skills REST routes");

await buildCloudWebDist(rootDir);
console.log("hwlab-cloud-web check: dist bundle built by Vite");
await assertCloudWebDistFresh(rootDir);
console.log("hwlab-cloud-web check: dist freshness verified");
console.log("hwlab-cloud-web check ok: React + strict TypeScript module boundaries and runtime selectors are present");

function readWeb(relativePath: string): string { return fs.readFileSync(path.resolve(rootDir, relativePath), "utf8"); }
function readRepo(relativePath: string): string { return fs.readFileSync(path.resolve(repoRoot, relativePath), "utf8"); }
function assertIncludes(source: string, term: string, message: string): void { assert.ok(source.includes(term), message); }
