/**
 * Smoke test: v1 system mode + design system ripple
 *
 * Validates the full pipeline:
 *   createEmptyDocument
 *     → applySemanticPalette           (seeds 56 tokens + Light/Dark theme axis)
 *     → buildHeadingV1/buildSettingRowV1/etc. with theme:'system'  (emit $X refs)
 *     → resolveNodeForCanvas(doc, { Mode: 'Light' })  (resolve to light hex)
 *     → resolveNodeForCanvas(doc, { Mode: 'Dark' })   (resolve to dark hex)
 *
 * Run: bun scripts/smoke-v1-pipeline.ts
 *
 * Known gaps surfaced by this test (do NOT fix here — report to controller):
 *   GAP-1: resolveNodeForCanvas does not resolve `fontWeight` on text nodes
 *          (only fontSize/lineHeight/letterSpacing are in the key list at resolve.ts:281)
 *   GAP-2: resolveNodeForCanvas early-exits when `variables === {}` (line 217),
 *          so DEFAULT_PALETTE_FALLBACK is unreachable for un-seeded documents
 */

import {
  createEmptyDocument,
  applySemanticPalette,
  resolveNodeForCanvas,
  buildHeadingV1,
  buildBodyTextV1,
  buildSectionHeaderV1,
  buildSettingRowV1,
} from '@zseven-w/pen-core';
import type { PenNode } from '@zseven-w/pen-types';

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────

type Result = 'PASS' | 'FAIL' | 'KNOWN_GAP';

interface Assertion {
  name: string;
  result: Result;
  detail?: string;
}

const assertions: Assertion[] = [];

function assert(name: string, condition: boolean, detail?: string): void {
  assertions.push({ name, result: condition ? 'PASS' : 'FAIL', detail });
}

/** Document a known gap without failing the test. */
function knownGap(name: string, detail: string): void {
  assertions.push({ name, result: 'KNOWN_GAP', detail });
}

/** Get all fill entries from the top-level `fill` array of a node (not recursive). */
function getTopFills(node: Record<string, unknown>): Array<Record<string, unknown>> {
  const fills = node.fill as Array<Record<string, unknown>> | undefined;
  if (!Array.isArray(fills)) return [];
  return fills;
}

/** Recursively find a named child by role. */
function findByRole(
  node: Record<string, unknown>,
  role: string,
): Record<string, unknown> | undefined {
  if (node.role === role) return node;
  const children = node.children as Array<Record<string, unknown>> | undefined;
  if (!Array.isArray(children)) return undefined;
  for (const child of children) {
    const found = findByRole(child, role);
    if (found) return found;
  }
  return undefined;
}

/** Walk all string values in a node tree; return any that start with '$'. */
function collectRefs(node: Record<string, unknown>): string[] {
  const refs: string[] = [];
  for (const val of Object.values(node)) {
    if (typeof val === 'string' && val.startsWith('$')) refs.push(val);
    if (Array.isArray(val)) {
      for (const item of val) {
        if (item && typeof item === 'object')
          refs.push(...collectRefs(item as Record<string, unknown>));
        if (typeof item === 'string' && item.startsWith('$')) refs.push(item);
      }
    }
    if (val && typeof val === 'object' && !Array.isArray(val)) {
      refs.push(...collectRefs(val as Record<string, unknown>));
    }
  }
  return refs;
}

function hasNoRefs(node: Record<string, unknown>): boolean {
  return collectRefs(node).length === 0;
}

// ─────────────────────────────────────────────────────────────────────────────
// Step 1: Build v1 system-mode nodes
// ─────────────────────────────────────────────────────────────────────────────

console.log('\n══════════════════════════════════════════════════════');
console.log(' STEP 1: Build v1 nodes with theme:"system"');
console.log('══════════════════════════════════════════════════════');

const sectionHeader = buildSectionHeaderV1({
  title: 'Notifications',
  theme: 'system',
}) as unknown as Record<string, unknown>;
const heading = buildHeadingV1({
  content: 'Account',
  level: 'h2',
  theme: 'system',
}) as unknown as Record<string, unknown>;
const bodyText = buildBodyTextV1({
  content: 'Manage your account preferences',
  theme: 'system',
}) as unknown as Record<string, unknown>;

// settingRow with switch (on:true) — no leading_icon so switch is the first child with a fill
const settingRow1 = buildSettingRowV1({
  title: 'Email',
  trailing: { kind: 'switch', on: true },
  theme: 'system',
}) as unknown as Record<string, unknown>;

// settingRow with value trailing
const settingRow3 = buildSettingRowV1({
  title: 'SMS',
  trailing: { kind: 'value', value: 'Off' },
  theme: 'system',
}) as unknown as Record<string, unknown>;

// settingRow with leading_icon (for breadth of testing)
const settingRow4 = buildSettingRowV1({
  title: 'Marketing',
  leading_icon: 'mail',
  trailing: { kind: 'chevron' },
  theme: 'system',
}) as unknown as Record<string, unknown>;

const settingRow2 = buildSettingRowV1({
  title: 'Push Notifications',
  trailing: { kind: 'switch', on: false },
  theme: 'system',
}) as unknown as Record<string, unknown>;

const allV1Nodes = [
  sectionHeader,
  settingRow1,
  settingRow2,
  settingRow3,
  settingRow4,
  heading,
  bodyText,
];

// Inspect raw v1 output using role-based search (precise, not depth-first first-fill)
const headingFill = getTopFills(heading);
const switchNode = findByRole(settingRow1, 'setting-row-switch');
const switchFill = switchNode ? getTopFills(switchNode) : [];
const valueFill = findByRole(settingRow3, 'setting-row-value');

console.log('\nRaw heading node:');
console.log(`  fontSize:   ${String(heading.fontSize)}`);
console.log(`  fontWeight: ${String(heading.fontWeight)}`);
console.log(`  lineHeight: ${String(heading.lineHeight)}`);
console.log(`  fill:       ${JSON.stringify(headingFill)}`);

console.log('\nRaw settingRow1 switch fill (role=setting-row-switch):');
console.log(`  fill: ${JSON.stringify(switchFill)}`);

console.log('\nRaw settingRow3 trailing value fill (role=setting-row-value):');
console.log(`  fill: ${JSON.stringify(getTopFills(valueFill ?? {}))}`);

// Collect all $refs across v1 nodes
const allRefs = allV1Nodes.flatMap((n) => collectRefs(n));
const uniqueRefs = [...new Set(allRefs)].sort();
console.log(`\nAll unique $refs found across v1 nodes (${uniqueRefs.length}):`);
for (const ref of uniqueRefs) console.log(`  ${ref}`);

// ── Assertions: Step 1 ──────────────────────────────────────────────────────

assert(
  'heading.fontSize is $type-h2-size ref (system mode)',
  heading.fontSize === '$type-h2-size',
  `got: ${String(heading.fontSize)}`,
);

assert(
  'heading.fontWeight is $type-h2-weight ref (system mode)',
  heading.fontWeight === '$type-h2-weight',
  `got: ${String(heading.fontWeight)}`,
);

assert(
  'heading.lineHeight is $type-h2-line-height ref (system mode)',
  heading.lineHeight === '$type-h2-line-height',
  `got: ${String(heading.lineHeight)}`,
);

const headingFillColor = headingFill[0]?.color as string | undefined;
assert(
  'heading fill[0].color is $color-text-primary ref',
  headingFillColor === '$color-text-primary',
  `got: ${String(headingFillColor)}`,
);

const switchFillColor = switchFill[0]?.color as string | undefined;
assert(
  'settingRow1 switch fill[0].color is $color-accent ref (on:true)',
  switchFillColor === '$color-accent',
  `got: ${String(switchFillColor)}`,
);

const valueFillColor = valueFill
  ? (getTopFills(valueFill)[0]?.color as string | undefined)
  : undefined;
assert(
  'settingRow3 trailing value fill[0].color is $color-text-muted ref',
  valueFillColor === '$color-text-muted',
  `got: ${String(valueFillColor)}`,
);

assert(
  'v1 nodes contain multiple $refs (system mode active)',
  allRefs.length > 5,
  `found ${allRefs.length} total refs`,
);

assert(
  'bodyText.fontSize is literal number 16 (body not yet using system typography tokens)',
  typeof bodyText.fontSize === 'number' && bodyText.fontSize === 16,
  `got: ${typeof bodyText.fontSize} ${String(bodyText.fontSize)}`,
);

// ─────────────────────────────────────────────────────────────────────────────
// Step 2: Mount into doc + seed palette
// ─────────────────────────────────────────────────────────────────────────────

console.log('\n══════════════════════════════════════════════════════');
console.log(' STEP 2: Mount nodes into doc + applySemanticPalette');
console.log('══════════════════════════════════════════════════════');

const rawDoc = createEmptyDocument();
const seededDoc = applySemanticPalette(rawDoc);

// Inject nodes into the active page's root frame children
const activePage = seededDoc.pages?.[0];
if (!activePage) throw new Error('No active page in document');
const rootFrame = activePage.children[0] as Record<string, unknown>;
rootFrame.children = allV1Nodes;

console.log(`\nDoc themes after applySemanticPalette: ${JSON.stringify(seededDoc.themes)}`);
console.log(`Doc variables count: ${Object.keys(seededDoc.variables ?? {}).length}`);

assert(
  'doc has Mode theme axis after applySemanticPalette',
  Array.isArray(seededDoc.themes?.Mode) &&
    (seededDoc.themes?.Mode as string[]).includes('Light') &&
    (seededDoc.themes?.Mode as string[]).includes('Dark'),
  `got: ${JSON.stringify(seededDoc.themes)}`,
);

assert(
  'doc has 56 semantic palette variables',
  Object.keys(seededDoc.variables ?? {}).length === 56,
  `got: ${Object.keys(seededDoc.variables ?? {}).length}`,
);

// ─────────────────────────────────────────────────────────────────────────────
// Step 3: Resolve in Light theme
// ─────────────────────────────────────────────────────────────────────────────

console.log('\n══════════════════════════════════════════════════════');
console.log(' STEP 3: Resolve in Light theme');
console.log('══════════════════════════════════════════════════════');

const vars = seededDoc.variables ?? {};
const lightTheme = { Mode: 'Light' };
const darkTheme = { Mode: 'Dark' };

const lightHeading = resolveNodeForCanvas(
  heading as unknown as PenNode,
  vars,
  lightTheme,
) as unknown as Record<string, unknown>;
const lightRow1 = resolveNodeForCanvas(
  settingRow1 as unknown as PenNode,
  vars,
  lightTheme,
) as unknown as Record<string, unknown>;

const lightHeadingFontSize = lightHeading.fontSize;
const lightHeadingFill = getTopFills(lightHeading)[0]?.color as string | undefined;
const lightSwitchNode = findByRole(lightRow1, 'setting-row-switch');
const lightSwitchFill = lightSwitchNode
  ? (getTopFills(lightSwitchNode)[0]?.color as string | undefined)
  : undefined;

console.log(`\nLight-resolved heading.fontSize: ${String(lightHeadingFontSize)}`);
console.log(`Light-resolved heading.fontWeight: ${String(lightHeading.fontWeight)}`);
console.log(`Light-resolved heading.lineHeight: ${String(lightHeading.lineHeight)}`);
console.log(`Light-resolved heading fill[0].color: ${String(lightHeadingFill)}`);
console.log(`Light-resolved switch fill[0].color: ${String(lightSwitchFill)}`);

assert(
  'Light: heading.fontSize resolves to number 20 ($type-h2-size)',
  typeof lightHeadingFontSize === 'number' && lightHeadingFontSize === 20,
  `got: ${typeof lightHeadingFontSize} ${String(lightHeadingFontSize)}`,
);

assert(
  'Light: heading.lineHeight resolves to number 1.25 ($type-h2-line-height)',
  typeof lightHeading.lineHeight === 'number' && lightHeading.lineHeight === 1.25,
  `got: ${typeof lightHeading.lineHeight} ${String(lightHeading.lineHeight)}`,
);

assert(
  'Light: heading fill[0].color resolves to #0F172A (light text-primary)',
  lightHeadingFill === '#0F172A',
  `got: ${String(lightHeadingFill)}`,
);

assert(
  'Light: switch fill[0].color resolves to #2563EB (light accent)',
  lightSwitchFill === '#2563EB',
  `got: ${String(lightSwitchFill)}`,
);

// GAP-1 fixed (commit 52f9549b): fontWeight now in resolve.ts text-key list.
// Verify it stays fixed.
const fontWeightAfterLight = lightHeading.fontWeight;
assert(
  'GAP-1 fixed: heading.fontWeight resolves to number (was $ref before 52f9549b)',
  typeof fontWeightAfterLight === 'number' && fontWeightAfterLight === 600,
  `got: ${String(fontWeightAfterLight)} (expected 600 for h2)`,
);

// The heading still has $type-h2-weight unresolved → refs remain
const lightHeadingRefs = collectRefs(lightHeading);
console.log(`\nRemaining $refs in light-resolved heading: ${JSON.stringify(lightHeadingRefs)}`);
assert(
  'Light: fill and fontSize refs resolved (no $color-* or $type-h*-size refs remain)',
  !lightHeadingRefs.some((r) => r.startsWith('$color-') || r === '$type-h2-size'),
  `remaining: ${JSON.stringify(lightHeadingRefs)}`,
);

// ─────────────────────────────────────────────────────────────────────────────
// Step 4: Resolve in Dark theme
// ─────────────────────────────────────────────────────────────────────────────

console.log('\n══════════════════════════════════════════════════════');
console.log(' STEP 4: Resolve in Dark theme');
console.log('══════════════════════════════════════════════════════');

const darkHeading = resolveNodeForCanvas(
  heading as unknown as PenNode,
  vars,
  darkTheme,
) as unknown as Record<string, unknown>;
const darkRow1 = resolveNodeForCanvas(
  settingRow1 as unknown as PenNode,
  vars,
  darkTheme,
) as unknown as Record<string, unknown>;

const darkHeadingFontSize = darkHeading.fontSize;
const darkHeadingFill = getTopFills(darkHeading)[0]?.color as string | undefined;
const darkSwitchNode = findByRole(darkRow1, 'setting-row-switch');
const darkSwitchFill = darkSwitchNode
  ? (getTopFills(darkSwitchNode)[0]?.color as string | undefined)
  : undefined;

console.log(`\nDark-resolved heading.fontSize: ${String(darkHeadingFontSize)}`);
console.log(`Dark-resolved heading fill[0].color: ${String(darkHeadingFill)}`);
console.log(`Dark-resolved switch fill[0].color: ${String(darkSwitchFill)}`);

// Side-by-side comparison table
console.log('\n┌─────────────────────────────┬──────────────────┬──────────────────┐');
console.log('│ Field                       │ Light            │ Dark             │');
console.log('├─────────────────────────────┼──────────────────┼──────────────────┤');
console.log(
  `│ heading.fontSize            │ ${String(lightHeadingFontSize).padEnd(16)} │ ${String(darkHeadingFontSize).padEnd(16)} │`,
);
console.log(
  `│ heading fill (text-primary) │ ${String(lightHeadingFill).padEnd(16)} │ ${String(darkHeadingFill).padEnd(16)} │`,
);
console.log(
  `│ switch fill (accent)        │ ${String(lightSwitchFill).padEnd(16)} │ ${String(darkSwitchFill).padEnd(16)} │`,
);
console.log('└─────────────────────────────┴──────────────────┴──────────────────┘');

assert(
  'Dark: heading.fontSize resolves to number 20 (typography is theme-agnostic)',
  typeof darkHeadingFontSize === 'number' && darkHeadingFontSize === 20,
  `got: ${typeof darkHeadingFontSize} ${String(darkHeadingFontSize)}`,
);

assert(
  'Dark: heading fill[0].color resolves to #F1F5F9 (dark text-primary)',
  darkHeadingFill === '#F1F5F9',
  `got: ${String(darkHeadingFill)}`,
);

assert(
  'Dark: switch fill[0].color resolves to #60A5FA (dark accent)',
  darkSwitchFill === '#60A5FA',
  `got: ${String(darkSwitchFill)}`,
);

assert(
  'Light vs Dark heading fill.color are DIFFERENT (color theme ripple works)',
  lightHeadingFill !== darkHeadingFill,
  `light=${String(lightHeadingFill)}, dark=${String(darkHeadingFill)}`,
);

assert(
  'Light vs Dark heading.fontSize are SAME (numeric is theme-agnostic)',
  lightHeadingFontSize === darkHeadingFontSize,
  `light=${String(lightHeadingFontSize)}, dark=${String(darkHeadingFontSize)}`,
);

assert(
  'Light vs Dark switch fill.color are DIFFERENT (accent ripple works)',
  lightSwitchFill !== darkSwitchFill,
  `light=${String(lightSwitchFill)}, dark=${String(darkSwitchFill)}`,
);

// ─────────────────────────────────────────────────────────────────────────────
// Step 5: Fallback path (un-seeded doc)
// ─────────────────────────────────────────────────────────────────────────────

console.log('\n══════════════════════════════════════════════════════');
console.log(' STEP 5: Fallback path — un-seeded doc (no palette)');
console.log('══════════════════════════════════════════════════════');

// GAP-2: resolveNodeForCanvas exits early when variables === {} (line 217)
// The DEFAULT_PALETTE_FALLBACK is never reached. We verify this is the actual
// behavior and document it as a known gap.
const unseededDoc = createEmptyDocument();
const unseededVars = unseededDoc.variables ?? {};

const fallbackLightHeading = resolveNodeForCanvas(heading as unknown as PenNode, unseededVars, {
  Mode: 'Light',
}) as unknown as Record<string, unknown>;
const fallbackLightFill = getTopFills(fallbackLightHeading)[0]?.color as string | undefined;
const fallbackLightSize = fallbackLightHeading.fontSize;

console.log(`\nUn-seeded vars count: ${Object.keys(unseededVars).length}`);
console.log(`Fallback Light heading fill[0].color: ${String(fallbackLightFill)}`);
console.log(`Fallback Light heading.fontSize: ${String(fallbackLightSize)}`);
console.log(`(Post-52f9549b: empty-vars doc reaches DEFAULT_PALETTE_FALLBACK)`);

// GAP-2 fixed (commit 52f9549b): empty vars no longer early-exits;
// DEFAULT_PALETTE_FALLBACK fires per spec §5.3 equivalence contract.
assert(
  'GAP-2 fixed: empty-vars doc resolves color ref via DEFAULT_PALETTE_FALLBACK',
  fallbackLightFill === '#0F172A',
  `fill=${String(fallbackLightFill)} (expected #0F172A for $color-text-primary light)`,
);
assert(
  'GAP-2 fixed: empty-vars doc resolves typography ref via DEFAULT_PALETTE_FALLBACK',
  typeof fallbackLightSize === 'number' && fallbackLightSize === 20,
  `fontSize=${String(fallbackLightSize)} (expected 20 for h2)`,
);

// Demonstrate that fallback DOES work when vars has at least one non-palette entry
// (proving the code path itself works — the guard is the only issue)
const artificialVars = { dummy: { type: 'number' as const, value: 1 } };
const artificialFallbackHeading = resolveNodeForCanvas(
  heading as unknown as PenNode,
  artificialVars,
  { Mode: 'Light' },
) as unknown as Record<string, unknown>;
const artificialFallbackFill = getTopFills(artificialFallbackHeading)[0]?.color as
  | string
  | undefined;
const artificialFallbackSize = artificialFallbackHeading.fontSize;

console.log(`\nArtificial non-empty vars (triggers fallback path):`);
console.log(`  heading fill[0].color: ${String(artificialFallbackFill)}`);
console.log(`  heading.fontSize: ${String(artificialFallbackSize)}`);

assert(
  'DEFAULT_PALETTE_FALLBACK works when vars is non-empty (fill → #0F172A)',
  artificialFallbackFill === '#0F172A',
  `got: ${String(artificialFallbackFill)}`,
);

assert(
  'DEFAULT_PALETTE_FALLBACK works when vars is non-empty (fontSize → 20)',
  typeof artificialFallbackSize === 'number' && artificialFallbackSize === 20,
  `got: ${typeof artificialFallbackSize} ${String(artificialFallbackSize)}`,
);

// Fallback comparison table
console.log('\n┌─────────────────────────────┬──────────────────┬──────────────────┐');
console.log('│ Field                       │ Seeded           │ Artificial-FB    │');
console.log('├─────────────────────────────┼──────────────────┼──────────────────┤');
console.log(
  `│ Light heading fill.color    │ ${String(lightHeadingFill).padEnd(16)} │ ${String(artificialFallbackFill).padEnd(16)} │`,
);
console.log(
  `│ Light heading.fontSize      │ ${String(lightHeadingFontSize).padEnd(16)} │ ${String(artificialFallbackSize).padEnd(16)} │`,
);
console.log('└─────────────────────────────┴──────────────────┴──────────────────┘');

assert(
  'Seeded == Fallback for Light fill (fallback byte-equiv to seeded)',
  lightHeadingFill === artificialFallbackFill,
  `seeded=${String(lightHeadingFill)}, fallback=${String(artificialFallbackFill)}`,
);

assert(
  'Seeded == Fallback for Light fontSize (fallback byte-equiv to seeded)',
  lightHeadingFontSize === artificialFallbackSize,
  `seeded=${String(lightHeadingFontSize)}, fallback=${String(artificialFallbackSize)}`,
);

// ─────────────────────────────────────────────────────────────────────────────
// Summary
// ─────────────────────────────────────────────────────────────────────────────

console.log('\n══════════════════════════════════════════════════════');
console.log(' ASSERTION SUMMARY');
console.log('══════════════════════════════════════════════════════');

let passed = 0;
let failed = 0;
let gaps = 0;
for (const a of assertions) {
  let icon: string;
  if (a.result === 'PASS') {
    icon = '✓';
    passed++;
  } else if (a.result === 'FAIL') {
    icon = '✗';
    failed++;
  } else {
    icon = '⚠';
    gaps++;
  }
  const detail = a.detail ? `  (${a.detail})` : '';
  console.log(`${icon} [${a.result}] ${a.name}${detail}`);
}

console.log(
  `\n${passed}/${assertions.length - gaps} assertions passed, ${failed} failed, ${gaps} known gap(s) documented`,
);

if (failed > 0) {
  console.error('\n[SMOKE TEST FAILED] — see FAIL assertions above');
  process.exit(1);
} else {
  console.log('\n[SMOKE TEST PASSED] Full pipeline OK (known gaps documented above)');
}
