// VYNDR 2.0 design system — Phase A (tokens/CSS/fonts/glitch) + Phase B
// (shared components). The .tsx components can't run under the plain-JS Jest
// config (no Babel/TS transform), so component contracts are asserted against
// their source text; the helper LOGIC is exercised directly via the CommonJS
// vyndrTokens module that the components actually import.

const fs = require('fs');
const path = require('path');

const WEB = path.join(__dirname, '..', '..', 'web', 'src');
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');

const css = read('app/globals.css');
const layout = read('app/layout.tsx');
const tokens = require('../../web/src/lib/vyndrTokens');

describe('Phase A — design tokens (§2)', () => {
  const REQUIRED_TOKENS = [
    '--bg-0', '--bg-1', '--bg-2', '--bg-3', '--border', '--border-hi',
    '--text-0', '--text-1', '--text-2',
    '--g-ap', '--g-a', '--g-b', '--g-c', '--g-d',
    '--s-nba', '--s-mlb', '--s-wnba', '--s-soccer',
    '--acc-0', '--acc-1', '--amber', '--amber-glow',
    '--live', '--hit', '--miss',
    '--glitch', '--scan-op', '--grade-hero', '--sans', '--mono',
  ];
  it.each(REQUIRED_TOKENS)('defines %s in :root', (token) => {
    expect(css).toMatch(new RegExp(`${token.replace(/[-]/g, '\\-')}\\s*:`));
  });

  it('uses the exact §2 grade hex values', () => {
    expect(css).toContain('--g-ap: #00ffb8');
    expect(css).toContain('--g-a:  #00d4a0');
    expect(css).toContain('--g-b:  #F0F0F0');
    expect(css).toContain('--g-c:  #B8BCC8');
    expect(css).toContain('--g-d:  #FF4757');
  });

  it('sets --sans to Inter and --mono to JetBrains Mono', () => {
    // Session 41 — fonts are self-hosted via next/font, so the families now
    // come through the --font-* variables (with the literal names kept as
    // fallbacks for any non-next/font context).
    expect(css).toMatch(/--sans:\s*var\(--font-sans\),\s*'Inter'/);
    expect(css).toMatch(/--mono:\s*var\(--font-mono\),\s*'JetBrains Mono'/);
  });

  it('sets glitch intensity to the §2 baseline of 1 and scan-op to 0.04', () => {
    expect(css).toMatch(/--glitch:\s*1;/);
    expect(css).toMatch(/--scan-op:\s*0\.04;/);
  });
});

describe('Phase A — fonts (§2)', () => {
  it('self-hosts Inter + JetBrains Mono via next/font (Session 41 — no CDN <link>)', () => {
    expect(layout).toContain("from 'next/font/google'");
    expect(layout).toContain('Inter(');
    expect(layout).toContain('JetBrains_Mono(');
    expect(layout).not.toMatch(/href=["'][^"']*fonts\.googleapis\.com/);
  });
});

describe('Phase A — glitch + animation keyframes (§4, ported verbatim)', () => {
  const KEYFRAMES = [
    'wm-tear', 'glitch-shift-r', 'glitch-shift-b', 'wm-caret', 'head-tear',
    'crt-sweep', 'crt-sweep-local', 'phosphor-pulse', 'grade-reveal',
    'ticker-scroll', 'live-pulse', 'flash-up', 'flash-down', 'ekg-scroll',
    'node-pulse', 'synapse-travel', 'count-tick', 'factor-ignite',
    'toast-in', 'sheet-up', 'cmd-in', 'fade-in', 'scan-drift',
  ];
  it.each(KEYFRAMES)('defines @keyframes %s', (name) => {
    expect(css).toContain(`@keyframes ${name}`);
  });

  it('floors fade-in at the visible state (opacity .6, never 0)', () => {
    expect(css).toMatch(/@keyframes fade-in\s*\{\s*from\s*\{\s*opacity:\s*0\.6;/);
  });
});

describe('Phase A — scanline texture + reduced motion (§4, §10)', () => {
  it('has the .scanlines::after CRT texture class', () => {
    expect(css).toContain('.scanlines::after');
    expect(css).toMatch(/repeating-linear-gradient\(0deg, transparent 50%, rgba\(0,0,0,1\) 50%\)/);
    expect(css).toMatch(/opacity: var\(--scan-op\)/);
  });

  it('respects prefers-reduced-motion and the in-app data-motion toggle', () => {
    expect(css).toContain('@media (prefers-reduced-motion: reduce)');
    expect(css).toContain('html[data-motion="reduced"]');
  });

  it('ships the a11y preference layer (contrast / text / colorblind / readable font)', () => {
    expect(css).toContain('html[data-contrast="high"]');
    expect(css).toContain('html[data-text="xl"]');
    expect(css).toContain('html[data-cb="1"]');
    expect(css).toContain('html[data-font="readable"]');
  });
});

describe('Phase B — token helpers (vyndrTokens)', () => {
  it('gradeColor returns the correct CSS variable for each grade tier', () => {
    expect(tokens.gradeColor('A+')).toBe('var(--g-ap)');
    expect(tokens.gradeColor('A')).toBe('var(--g-a)');
    expect(tokens.gradeColor('A-')).toBe('var(--g-a)');
    expect(tokens.gradeColor('B+')).toBe('var(--g-b)');
    expect(tokens.gradeColor('B')).toBe('var(--g-b)');
    expect(tokens.gradeColor('C')).toBe('var(--g-c)');
    expect(tokens.gradeColor('D')).toBe('var(--g-d)');
  });

  it('gradeColor falls back to primary text for unknown grades', () => {
    expect(tokens.gradeColor('Z')).toBe('var(--text-0)');
  });

  it('gradeHex returns the exact §2 hex for each grade', () => {
    expect(tokens.gradeHex('A+')).toBe('#00ffb8');
    expect(tokens.gradeHex('A')).toBe('#00d4a0');
    expect(tokens.gradeHex('B')).toBe('#f0f0f0');
    expect(tokens.gradeHex('C')).toBe('#b8bcc8');
    expect(tokens.gradeHex('D')).toBe('#ff4757');
  });

  it('SPORT map carries every sport with its token color and label', () => {
    expect(tokens.SPORT.nba).toEqual({ label: 'NBA', color: 'var(--s-nba)', hex: '#e94b3c' });
    expect(tokens.SPORT.mlb.hex).toBe('#1e90ff');
    expect(tokens.SPORT.wnba.hex).toBe('#f7944a');
    expect(tokens.SPORT.soccer.hex).toBe('#3ddc84');
  });

  it('gradeBadgeSize maps variants and passes raw numbers through; hero is 80–120px', () => {
    expect(tokens.gradeBadgeSize('sm')).toBe(16);
    expect(tokens.gradeBadgeSize('lg')).toBe(48);
    expect(tokens.gradeBadgeSize(72)).toBe(72);
    const hero = tokens.gradeBadgeSize('hero');
    expect(hero).toBeGreaterThanOrEqual(80);
    expect(hero).toBeLessThanOrEqual(120);
  });
});

describe('Phase B — shared component contracts (§5)', () => {
  const comp = (name) => read(path.join('components', 'vyndr', name));

  it('Wordmark renders the green R, caret, VYNDR data-text and BETA pill', () => {
    const src = comp('Wordmark.tsx');
    expect(src).toContain('data-text="VYNDR"');
    expect(src).toContain('className="wm-r"');
    expect(src).toContain('wm-cursor');
    expect(src).toContain('wm-beta');
  });

  it('GradeBadge colors by grade token and supports the hero size', () => {
    const src = comp('GradeBadge.tsx');
    expect(src).toContain('gradeColor(grade)');
    expect(src).toContain('gradeBadgeSize(size)');
    expect(src).toMatch(/'hero'/);
  });

  it('SportBadge derives its color from the SPORT token map', () => {
    expect(comp('SportBadge.tsx')).toContain('SPORT[sport');
  });

  it('VBtn defines all five variants', () => {
    const src = comp('VBtn.tsx');
    ['primary', 'outline', 'ghost', 'amber', 'danger'].forEach((v) => {
      expect(src).toContain(`${v}:`);
    });
    expect(src).toContain("background: 'var(--g-a)'"); // primary = grade-green filled
  });

  it('Card is a Level-1 (bg-1) surface with the scanline skin', () => {
    const src = comp('Card.tsx');
    expect(src).toContain("background: 'var(--bg-1)'");
    expect(src).toContain('scanlines');
  });

  it('TerminalInput uses a phosphor cursor while empty', () => {
    expect(comp('TerminalInput.tsx')).toContain('phosphor-cursor');
  });

  it('Sparkline renders an SVG path from its data points', () => {
    const src = comp('Sparkline.tsx');
    expect(src).toContain('<svg');
    expect(src).toContain('<path');
  });

  it('Ticker RESTS on each item (DS5 punctuated stillness) — no infinite marquee', () => {
    // DS5 (Part 4, #7) retired the continuous .ticker-track marquee: the ticker
    // now holds each ranked item ≥4s and pulses only on change. See
    // ds5PricingStates.test.js for the full motion lock.
    const src = comp('Ticker.tsx');
    expect(src).not.toContain('ticker-track');
    expect(src).toContain('ticker-rest');
  });
});
