"use strict";

const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");

const repositoryRoot = path.resolve(__dirname, "..", "..");
const webRoot = path.join(
  repositoryRoot,
  "src",
  "MBN_STOCK_WEBVIEW.LegacyParityApp",
  "Web");
const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
const playout = fs.readFileSync(path.join(webRoot, "playout-ui.js"), "utf8");

function luminance(hex) {
  const channels = [1, 3, 5].map(index =>
    Number.parseInt(hex.slice(index, index + 2), 16) / 255);
  const linear = channels.map(channel => channel <= 0.04045
    ? channel / 12.92
    : ((channel + 0.055) / 1.055) ** 2.4);
  return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
}

function contrast(left, right) {
  const bright = Math.max(luminance(left), luminance(right));
  const dark = Math.min(luminance(left), luminance(right));
  return (bright + 0.05) / (dark + 0.05);
}

test("operator inputs retain a visible three-to-one boundary", () => {
  const match = styles.match(/--ui-border-strong:\s*(#[0-9a-f]{6})/i);
  assert.ok(match, "strong control border token must exist");
  assert.ok(contrast(match[1], "#ffffff") >= 3,
    `control border contrast was ${contrast(match[1], "#ffffff").toFixed(2)}:1`);
});

test("playlist exposes independent candidate, selection, and PGM states", () => {
  assert.match(app, /element\.dataset\.active = isCandidate \? "true" : "false"/);
  assert.match(app, /"aria-selected",\s*presentation\.isSelected \? "true" : "false"/s);
  assert.match(app, /if \(presentation\.isOnAir\) accessibleStates\.push\("현재 PGM"\)/);
  assert.match(app, /if \(isCandidate\) accessibleStates\.push\("송출 후보"\)/);
  assert.match(app, /if \(presentation\.isSelected\) accessibleStates\.push\("삭제 선택됨"\)/);

  for (const selector of [
    '.playlist-data-row[data-active="true"]:not(.on-air)',
    '.playlist-data-row[aria-selected="true"]:not(.on-air)',
    '.playlist-data-row[data-active="true"][aria-selected="true"]:not(.on-air)',
    '.playlist-data-row.on-air',
    '.playlist-data-row.on-air[data-active="true"]',
    '.playlist-data-row.on-air[aria-selected="true"]',
    '.playlist-data-row.on-air[data-active="true"][aria-selected="true"]'
  ]) {
    assert.ok(styles.includes(selector), `${selector} must have an explicit visual state`);
  }
  assert.match(styles, /\.playlist-data-row\.on-air\s*\{[^}]*background:\s*#fdc8d2/i);
});

test("keyboard focus, high contrast, and scroll affordances remain visible", () => {
  assert.match(styles,
    /\.playlist-data-row:focus-visible[\s\S]*?outline:\s*3px solid var\(--ui-accent\)/);
  assert.match(styles, /@media \(forced-colors:\s*active\)/);
  assert.match(styles,
    /\.playlist-data-row\[data-active="true"\]\s*\{\s*border-top:\s*3px solid Highlight/);
  assert.match(styles,
    /\.playlist-data-row\[aria-selected="true"\]\s*\{\s*border-right:\s*3px solid Highlight/);
  assert.match(styles, /\.playlist-data-row\.on-air\s*\{\s*border-left:\s*4px double Highlight/);
  assert.match(styles, /scrollbar-width:\s*auto/);
  assert.match(styles, /scrollbar-gutter:\s*stable/);
  assert.match(styles, /scroll-padding-top:\s*29px/);
});

test("playout controls publish their real keyboard shortcuts", () => {
  assert.match(markup,
    /id="playout-take-in"[^>]*aria-label="TAKE IN"[^>]*aria-keyshortcuts="F8"/s);
  assert.match(markup,
    /id="playout-take-out"[^>]*aria-label="TAKE OUT"[^>]*aria-keyshortcuts="Escape"/s);
  assert.match(markup, /id="playout-next"[^>]*aria-label="NEXT"/s);
});

test("dry-run mode reads as information and clipped status keeps its full tooltip", () => {
  assert.match(playout,
    /connection\.classList\.toggle\("dry-run", playout\.mode === "dryRun"\)/);
  assert.match(styles, /\.connection-state\.dry-run:not\(\.outcome-unknown\)\s*\{/);
  assert.match(playout, /status\.title = status\.textContent/);
});

test("appearance themes expose explicit light, dark, and Windows-following palettes", () => {
  assert.match(styles,
    /:root\[data-color-theme="light"\],[\s\S]*?body\[data-color-theme="light"\]\s*\{\s*color-scheme:\s*light/);
  assert.match(styles,
    /:root\[data-color-theme="dark"\],[\s\S]*?body\[data-color-theme="dark"\]\s*\{[\s\S]*?color-scheme:\s*dark/);
  assert.match(styles,
    /@media \(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?:root\[data-color-theme="system"\],[\s\S]*?body\[data-color-theme="system"\]/);
  assert.match(styles, /--ui-brand:\s*#f47b20/i);
  assert.match(styles, /\.brand-wordmark-plate\s*\{[^}]*background:\s*#fff/s);
  assert.match(styles, /\.brand-wordmark\s*\{[^}]*filter:\s*none/s);
  assert.match(styles,
    /\.playlist-state-legend \.legend-selected\s*\{\s*color:\s*var\(--ui-accent\)/);
  assert.match(styles,
    /\.playlist-state-legend \.legend-next\s*\{\s*color:\s*var\(--ui-warning\)/);
  assert.match(styles,
    /\.playlist-state-legend \.legend-on-air\s*\{\s*color:\s*var\(--ui-danger\)/);
});

test("dark palette keeps text, actions, and semantic states readable", () => {
  const match = styles.match(
    /:root\[data-color-theme="dark"\],[\s\S]*?body\[data-color-theme="dark"\]\s*\{([\s\S]*?)\n\}/);
  assert.ok(match, "explicit dark palette must exist");
  const token = name => {
    const value = match[1].match(new RegExp(`--${name}:\\s*(#[0-9a-f]{6})`, "i"));
    assert.ok(value, `${name} dark token must exist`);
    return value[1];
  };

  assert.ok(contrast(token("ui-text"), token("ui-surface")) >= 7);
  assert.ok(contrast(token("ui-text-muted"), token("ui-surface")) >= 4.5);
  assert.ok(contrast(token("ui-accent"), token("ui-canvas")) >= 4.5);
  assert.ok(contrast(token("ui-on-accent"), token("ui-accent")) >= 4.5);
  assert.ok(contrast(token("ui-on-accent"), token("ui-success")) >= 4.5);
  assert.ok(contrast(token("ui-danger"), token("ui-danger-soft")) >= 4.5);
  assert.ok(contrast(token("ui-warning"), token("ui-warning-soft")) >= 4.5);
  assert.match(styles,
    /\.playlist-data-row\.on-air\s*\{[^}]*background:\s*var\(--ui-on-air\)[^}]*var\(--ui-on-air-edge\)/s);
});

test("light palette keeps hints and small semantic labels readable", () => {
  const match = styles.match(/:root\s*\{([\s\S]*?)\n\}/);
  assert.ok(match, "default light palette must exist");
  const token = name => {
    const value = match[1].match(new RegExp(`--${name}:\\s*(#[0-9a-f]{6})`, "i"));
    assert.ok(value, `${name} light token must exist`);
    return value[1];
  };

  assert.ok(contrast(token("ui-text-subtle"), token("ui-surface")) >= 4.5);
  assert.ok(contrast(token("ui-text-subtle"), token("ui-surface-subtle")) >= 4.5);
  assert.ok(contrast(token("ui-success"), token("ui-success-soft")) >= 4.5);
  assert.ok(contrast(token("ui-warning"), token("ui-warning-soft")) >= 4.5);
});

test("visual card mode creates a responsive graphic-action grid without touching operator rows", () => {
  const start = styles.indexOf("/* View modes affect graphic actions only.");
  const end = styles.indexOf("/* End view-mode presentation. */", start);
  assert.ok(start >= 0 && end > start, "view-mode CSS must have an auditable boundary");
  const viewModeRules = styles.slice(start, end);

  for (const mode of ["automatic", "compact", "cards"]) {
    assert.ok(viewModeRules.includes(`data-view-mode="${mode}"`),
      `${mode} mode must have explicit presentation rules`);
  }
  assert.match(viewModeRules, /\[data-visual-card-list="true"\]\s*\{/);
  assert.match(viewModeRules, /grid-template-columns:\s*minmax\(0, 1fr\)/);
  assert.match(viewModeRules,
    /@container operator-workspace \(min-width:\s*520px\)[\s\S]*?repeat\(2, minmax\(0, 1fr\)\)/);
  assert.match(viewModeRules,
    /@container operator-workspace \(min-width:\s*820px\)[\s\S]*?repeat\(3, minmax\(0, 1fr\)\)/);
  assert.match(viewModeRules,
    /data-visual-card-context="comparison-actions"[\s\S]*?repeat\(auto-fit, minmax\(min\(230px, 100%\), 1fr\)\)/);
  assert.match(viewModeRules,
    /data-visual-card-context="theme-actions"[\s\S]*?repeat\(2, minmax\(0, 1fr\)\)/);
  assert.doesNotMatch(viewModeRules,
    /data-visual-card-context="expert-actions"[\s\S]{0,180}?repeat\([23],/);
  assert.match(viewModeRules, /button\.visual-card\s*\{[\s\S]*?min-height:\s*62px/);
  assert.doesNotMatch(viewModeRules, /grid-auto-rows:\s*minmax\(62px/);
  assert.match(viewModeRules,
    /\.comparison-actions\s*\{\s*display:\s*block;/);
  assert.match(viewModeRules,
    /@container operator-workspace \(max-width:\s*759px\)[\s\S]*?\.comparison-actions\s*\{\s*display:\s*grid;/);
  assert.doesNotMatch(viewModeRules, /\.visual-card-(?:kicker|preview|state)/,
    "generic previews, repeated category labels, and decorative state tiles add no information");
  assert.match(viewModeRules,
    /button\.visual-card:is\(\.selected,[\s\S]*?box-shadow:\s*inset 3px 0 var\(--ui-accent\)/);
  assert.match(viewModeRules,
    /summary\[data-tree-root\]\.tree-node-selected/);
  assert.match(viewModeRules,
    /summary\[data-tree-root\]:focus-visible\s*\{[\s\S]*?outline:\s*3px solid var\(--ui-accent\)/);
  assert.match(viewModeRules,
    /button\.visual-card:is\(\.unavailable, :disabled\):is\(/);
  assert.match(viewModeRules, /@container operator-workspace \(max-width:\s*759px\)/);
  assert.match(viewModeRules, /body\[data-view-mode="compact"\]/);
  assert.doesNotMatch(viewModeRules,
    /(?:#|\.)(?:stock-results|stock-result|cut-list|cut-row|playlist-row|playlist-data-row|playlist-grid|operator-catalog-draft|operator-catalog-results|operator-catalog-stock-results|overseas-period-actions|overseas-result-area)\b/);
  assert.doesNotMatch(viewModeRules, /grid-auto-flow:\s*dense|\border\s*:/);
  const cardButtonRule = viewModeRules.match(/button\.visual-card\s*\{([\s\S]*?)\n\}/);
  assert.ok(cardButtonRule, "visual cards need one stable geometry rule");
  assert.doesNotMatch(cardButtonRule[1], /transform\s*:/,
    "hover or selection must not move the double-click hit target");
});

test("appearance controls and accessibility preferences retain clear feedback", () => {
  assert.match(styles, /\.settings-choice-list\s*\{/);
  assert.match(styles, /\.settings-choice-row\s*\{/);
  assert.match(styles, /\.settings-choice-row select\s*\{/);
  assert.match(styles,
    /@media \(forced-colors:\s*active\)[\s\S]*?\.settings-choice-row\s*\{\s*border-bottom-color:\s*CanvasText/);
  assert.match(styles,
    /@media \(prefers-reduced-motion:\s*reduce\)[\s\S]*?\.settings-switch::after\s*\{\s*transition:\s*none !important/);
});

test("workspace header keeps one concise title and the live native connection state", () => {
  assert.match(markup,
    /class="brand"[\s\S]*?class="brand-wordmark-plate"[\s\S]*?<img class="brand-wordmark" src="Brand\/logo\.png" width="176" height="43"[\s\S]*?alt="매일경제TV" draggable="false">[\s\S]*?class="brand-divider" aria-hidden="true"[\s\S]*?class="brand-product">V-STOCK<\/span>/);
  assert.match(markup,
    /class="brand-wordmark-fallback">매일경제TV<\/span>/);
  assert.match(styles,
    /\.brand-wordmark-plate\.wordmark-fallback-active \.brand-wordmark-fallback\s*\{[^}]*visibility:\s*visible/);
  assert.match(styles,
    /\.brand-wordmark-plate\.wordmark-fallback-active \.brand-wordmark\s*\{[^}]*display:\s*none/);
  assert.doesNotMatch(markup, /class="brand-symbol"|class="brand-lockup"|>VRi</);
  assert.match(markup,
    /class="workspace-heading"[\s\S]*?class="workspace-heading-icon"[\s\S]*?<div class="workspace-heading-copy">[\s\S]*?<h1 id="workspace-title">[^<]+<\/h1>/);
  assert.doesNotMatch(markup, /<small>현재 작업<\/small>/);
  assert.match(markup,
    /class="playout-connection-summary">[\s\S]*?id="playout-connection-state"/);
  assert.doesNotMatch(markup, /class="playout-connection-copy"/);
  assert.doesNotMatch(markup, /<strong>Tornado2<\/strong>/);
  assert.match(markup,
    /id="playout-connection-state"[^>]*role="status"[^>]*aria-label="Tornado2 연결 상태"[^>]*aria-live="polite"[^>]*aria-atomic="true">미연결<\/div>/);
  assert.match(styles,
    /\.workspace-header::before,\s*\.playlist-heading::before\s*\{\s*display:\s*none;/);
  assert.match(styles, /\.connection-state::before\s*\{/);
  assert.match(styles, /\.connection-state\.connected::before\s*\{/);
  assert.match(styles,
    /\.connection-state\.dry-run:not\(\.outcome-unknown\)::before\s*\{/);
  assert.match(styles,
    /\.workspace-surface\s*\{[^}]*grid-template-rows:\s*66px minmax\(0, 1fr\)/);
  assert.match(playout,
    /const nextConnectionText = connectionLabel\(playout\);[\s\S]*?if \(connection\.textContent !== nextConnectionText\)[\s\S]*?connection\.textContent = nextConnectionText/);
});
