// S6 (A1 board) — ROW-GRAMMAR source locks (specs/ROW-GRAMMAR.md).
// Density is unlimited when grammar is fixed: every element is DATA with ONE
// meaning in the SAME slot on every row of its type. These tests fail if a
// slot moves, a color changes meaning, or truncation sneaks onto a name.

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 strip = read('components/vyndr/StatStrip.tsx');
const spec = fs.readFileSync(path.join(__dirname, '..', '..', 'specs', 'ROW-GRAMMAR.md'), 'utf8');

/** Assert `markers` appear in this exact order in `src`, starting at `from`. */
function assertOrder(src, markers, from = 0) {
  let cursor = from;
  for (const m of markers) {
    const i = src.indexOf(m, cursor);
    expect({ marker: m, found: i >= 0 }).toEqual({ marker: m, found: true });
    cursor = i + m.length;
  }
  return cursor;
}

describe('ROW-GRAMMAR §2 — canonical prop-row slot order (snapshot mode)', () => {
  test('stat+line → market context (dot, movement) → model output (revision, grade) → outcome → actions → provenance', () => {
    // Anchor on the graded row's stat+side+line span (unique to it).
    const base = strip.indexOf("<span style={{ color: '#fff' }}>{p.stat} {p.side}{p.line}</span>");
    expect(base).toBeGreaterThan(-1);
    assertOrder(strip, [
      '<BestPriceDot p={p} />',          // slot 4a — best available price
      '<MovementChip p={p} />',          // slot 4b — market movement
      'p.revisedFrom',                   // slot 5a — revision strikethrough
      '<GradeBadge grade={p.grade}',     // slot 5b — the current grade
      '<LiveTracker p={p} />',           // slot 6a — live proto-outcome (S11)
      '<OutcomeChip p={p} />',           // slot 6b — settled result
      '<ParlayBtn p={p} />',             // slot 7a — action
      '<BookItTeaser p={p} />',          // slot 7b — action
      'Graded {p.gradedAt.ago}',         // slot 8  — provenance, always last
    ], base);
  });

  test('sub-line order: last-10 dots → sparkline → current-line delta', () => {
    const base = strip.indexOf('ROW-GRAMMAR sub-line');
    expect(base).toBeGreaterThan(-1);
    assertOrder(strip, [
      '<DotStrip dots={p.last10Dots} />',
      '<LineSparkline p={p} />',
      'Current {p.delta.currentLine}',
    ], base);
  });

  test('strip header: identity run (name → team → archetype) then viability chips', () => {
    const base = strip.indexOf('// compact');
    expect(base).toBeGreaterThan(-1);
    assertOrder(strip, [
      '<PlayerName',
      '<TeamChip team={team}',  // Rev 3 — team chip anchors the abbr
      '<ArchetypeBadge archetype={archetype.primary}',
      '<ViabilityChips lineup={lineup} injury={injury} />',
    ], base);
  });

  test('live/settled/dead rows suppress actions (the bet window is over) — S11 amendment', () => {
    expect(strip).toContain('{!p.outcome && !p.dead && !p.live && <ParlayBtn p={p} />}');
    expect(strip).toContain('{!p.outcome && !p.dead && !p.live && <BookItTeaser p={p} />}');
  });
});

describe('ROW-GRAMMAR §3 — one meaning per color', () => {
  const section = (start, end) => {
    const a = strip.indexOf(start);
    const b = end ? strip.indexOf(end, a) : strip.length;
    expect(a).toBeGreaterThan(-1);
    return strip.slice(a, b === -1 ? strip.length : b);
  };

  test('sparkline is green/amber/dim — never red (nothing has settled)', () => {
    const src = section('export function LineSparkline', 'export function ViabilityChips');
    expect(src).toContain("'var(--g-a)'");
    expect(src).toContain("'var(--amber)'");
    expect(src).not.toContain('--miss');
  });

  test('dot strip: filled = signal green, hollow = dim (never red/amber)', () => {
    const src = section('export function DotStrip', 'export function LineSparkline');
    expect(src).toContain('var(--g-a');
    expect(src).toContain('var(--text-2');
    expect(src).not.toContain('--miss');
    expect(src).not.toContain('--amber');
  });

  test('movement chips keep STEAM amber / VALUE green (QA.20 family)', () => {
    const src = section('function MovementChip', 'export interface StripArchetype');
    expect(src).toContain("steam ? 'var(--amber");
    expect(src).not.toContain('--miss');
  });

  test('live TRACKING mark is green/amber only — an in-progress prop is never red (S11)', () => {
    const src = section('export function LiveTracker', 'export function DotStrip');
    expect(src).toContain('var(--g-a');
    expect(src).toContain('var(--amber');
    expect(src).not.toContain('--miss');
    // The label never implies re-grading: TRACKING, read locked pre-game.
    expect(src).toContain('TRACKING — read locked pre-game');
    // Data → mono.
    expect(src).toContain('className="mono"');
  });
});

describe('ROW-GRAMMAR §1.4 — no truncation of names/times/pitchers', () => {
  test('StatStrip never ellipsizes', () => {
    expect(strip).not.toMatch(/textOverflow|text-overflow|ellipsis/);
  });
  test('SearchModal result names wrap, never truncate', () => {
    const sm = read('components/vyndr/SearchModal.tsx');
    expect(sm).not.toMatch(/textOverflow|ellipsis/);
  });
});

describe('ROW-GRAMMAR — the law is written down and wired', () => {
  test('spec exists with the canonical slot order and color law', () => {
    expect(spec).toContain('CANONICAL PROP-ROW SLOT ORDER');
    expect(spec).toContain('one meaning per color');
    expect(spec).toContain('never');
    expect(spec).not.toMatch(/!/); // VOICE — no exclamation points
  });
  test('data marks are mono', () => {
    // DotStrip + sub-line render with the mono class (data never sans).
    const dot = strip.slice(strip.indexOf('export function DotStrip'), strip.indexOf('export function LineSparkline'));
    expect(dot).toContain('className="mono"');
  });
});
