// Session 63 (A1-S4) — the VOICE v1.1 template engine. Numbers only from
// pipeline JSON; lint enforces the banned list + no-exclamation law; record
// claims obey n≥20; Stark lines are SELECTED from the committed library.

const media = require('../../src/services/mediaEngine');
const { lintVoice, segmentThread, starkLine, morningWire, signal, theSettle, receipt, streakWatch, archetypeWatch, lineDispatch } = media;

describe('lintVoice — the banned list is law', () => {
  test.each([
    ['This one is a lock!', 2],           // "lock" + exclamation
    ['🔥 free money, who’s tailing', 3],
    ['We’re so back 🚀', 2],
  ])('%s → %i violations', (text, n) => {
    expect(lintVoice(text).length).toBeGreaterThanOrEqual(n);
  });
  test('clean wire copy passes', () => {
    expect(lintVoice('THE SETTLE — Jul 11\nA-tier: 6-2\nvyndr.app/ledger')).toEqual([]);
  });
});

describe('every template output passes its own lint', () => {
  const NOW = '2026-07-12T14:05:00.000Z';
  const GRADE = {
    player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over',
    grade: 'A', confidence: 78, archetype: 'BOMBER',
    gradedAt: { line: 1.5, odds: -120, timestamp: NOW },
  };

  test('morning wire: real counts, stark from the library, no bans', () => {
    const text = morningWire({ dateIso: NOW, counts: { mlb: 15, wnba: 3, nba: 0 }, loudest: GRADE });
    expect(text).toContain('15 MLB.');
    expect(text).toContain('3 WNBA.');
    expect(text).not.toContain('0 NBA');           // zero sports never listed
    expect(text).toContain('Aaron Judge');
    expect(text).toContain('vyndr.app');
    expect(lintVoice(text)).toEqual([]);
  });

  test('signal: player + line + grade + ET timestamp with real odds', () => {
    const text = signal(GRADE);
    expect(text).toContain('Aaron Judge — TB o1.5 · A');
    expect(text).toContain('BOMBER read.');
    expect(text).toContain('at -120');
    expect(lintVoice(text)).toEqual([]);
  });

  test('the settle: counts always, percentages NEVER under n≥20', () => {
    const agg = { settled: 8, hits: 5, misses: 3, pushes: 0, hit_pct: null, pending: 16, by_tier: { A: { settled: 4, hits: 3, misses: 1, hit_pct: null } } };
    const misses = [{ player_name: 'A’ja Wilson', stat: 'points', line: 23.5, side: 'over' }];
    const text = theSettle({ aggregate: agg, misses, dateIso: NOW });
    expect(text).toContain('All grades: 5-3');
    expect(text).toContain('A-tier: 3-1');
    expect(text).not.toContain('%');                 // no percentage below the gate
    expect(text).toContain('Record building — 8 settled');
    expect(text).toContain('Wilson PTS o23.5 ❌');   // misses BY NAME
    expect(lintVoice(text)).toEqual([]);
  });

  test('receipt: lock beside close beside result', () => {
    const row = {
      player_name: 'Aaron Judge', stat: 'total_bases', line: 1.5, side: 'over', grade: 'A',
      graded_at: NOW, locked_odds: '-120', closing_line: 1.5, closing_odds: '-145',
      clv: 0, clv_result: 'flat', outcome: 'hit', actual_value: 2, settled_at: NOW,
    };
    const text = receipt(row);
    expect(text).toContain('Posted');
    expect(text).toContain('Judge TB o1.5 · A · -120');
    expect(text).toContain('Close: o1.5 at -145');
    expect(text).toContain('Result: 2 ✅');
    expect(lintVoice(text)).toEqual([]);
  });

  test('streak watch: lens reads only, absent lens rows dropped', () => {
    const rows = [
      { player: 'Brice Turang', lens: { read: '12-game on-base streak; tonight @ Pittsburgh Pirates' } },
      { player: 'No Lens Guy', lens: { read: null } },
    ];
    const text = streakWatch(rows, NOW);
    expect(text).toContain('Turang');
    expect(text).not.toContain('No Lens Guy');
    expect(lintVoice(text)).toEqual([]);
  });

  test('archetype watch needs ≥2 of a kind, else null', () => {
    expect(archetypeWatch([GRADE], NOW)).toBeNull();
    const two = archetypeWatch([GRADE, { ...GRADE, player: 'Shohei Ohtani' }], NOW);
    expect(two).toContain('BOMBER watch: 2');
    expect(lintVoice(two)).toEqual([]);
  });

  test('line dispatch from a real movement row', () => {
    const text = lineDispatch({ ...GRADE, movement: { kind: 'steam', delta: 1, currentLine: 2.5 } }, NOW);
    expect(text).toContain('o1.5 → o2.5 ▲');
    expect(text).toContain('Steam.');
    expect(lintVoice(text)).toEqual([]);
  });
});

describe('stark layer — selected, never generated; deterministic', () => {
  test('same date → same line; comes from the committed library', () => {
    const a = starkLine('morning', '2026-07-12T10:00:00Z');
    const b = starkLine('morning', '2026-07-12T22:00:00Z');
    expect(a).toBe(b);
    expect(media.__internals.STARK.morning).toContain(a);
  });
  test('the whole library passes lint', () => {
    for (const lines of Object.values(media.__internals.STARK)) {
      if (!Array.isArray(lines)) continue;
      for (const l of lines) expect(lintVoice(l)).toEqual([]);
    }
  });
});

describe('segmentThread — ≤280 per tweet on line boundaries', () => {
  test('splits long threads and preserves every line', () => {
    const long = Array.from({ length: 30 }, (_, i) => `Line ${i} of the settle with some padding text`).join('\n');
    const tweets = segmentThread(long);
    expect(tweets.length).toBeGreaterThan(1);
    for (const t of tweets) expect(t.length).toBeLessThanOrEqual(280);
    expect(tweets.join('\n')).toBe(long);
  });
});

describe('deskService — pack assembly from pipeline data only', () => {
  const { assembleDeskPack } = require('../../src/services/deskService');
  test('assembles formats + data brief from injected caches', async () => {
    const NOW = '2026-07-12T14:05:00.000Z';
    const store = {
      'snapshot:mlb:latest': { grades: [
        { player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A', confidence: 80, archetype: 'BOMBER', gradedAt: { line: 1.5, odds: -120, timestamp: NOW } },
        { player: 'Shohei Ohtani', stat_type: 'home_runs', line: 0.5, direction: 'over', grade: 'A', confidence: 76, archetype: 'BOMBER', gradedAt: { line: 0.5, odds: 130, timestamp: NOW } },
      ] },
    };
    const pack = await assembleDeskPack({
      cacheGet: async (k) => store[k] ?? null,
      loadRosterLogs: async () => [],
      ledger: { getModelAggregate: async () => ({ settled: 0, hits: 0, misses: 0, pushes: 0, hit_pct: null, pending: 24 }), __internals: { isConfigured: () => false } },
      fetchSettled: async () => [],
      now: () => NOW,
    });
    expect(pack.formats.morning_wire[0].text).toContain('2 MLB.');
    expect(pack.formats.signals.length).toBe(2);
    expect(pack.formats.archetype_watch[0].text).toContain('BOMBER watch: 2');
    // variants: wire + numbers-only when a stark line was present
    expect(pack.formats.morning_wire.length).toBeGreaterThanOrEqual(1);
    expect(pack.data_brief.top_signals[0].player).toBe('Aaron Judge');
    expect(pack.data_brief.record.pending).toBe(24);
    // every emitted text passes lint
    const all = JSON.stringify(pack.formats);
    expect(all).not.toContain('!');
  });
});

describe('ghostPublisher — drafts only, env-gated, no deps', () => {
  const ghost = require('../../src/services/ghostPublisher');
  test('JWT is a valid HS256 three-parter with the key id', () => {
    const t = ghost.ghostJwt('abc123:6465616462656566', 1_752_000_000);
    const [h, p] = t.split('.');
    const header = JSON.parse(Buffer.from(h, 'base64url').toString());
    const payload = JSON.parse(Buffer.from(p, 'base64url').toString());
    expect(header.kid).toBe('abc123');
    expect(payload.aud).toBe('/admin/');
    expect(payload.exp - payload.iat).toBe(300);
  });
  test('unconfigured → no-op, never a throw', async () => {
    const r = await ghost.saveDraft({ title: 't', html: '<p>x</p>' });
    expect(r.ok).toBe(false);
    expect(r.reason).toBe('ghost not configured');
  });
  test('posts DRAFT status only', async () => {
    let body = null;
    const r = await ghost.saveDraft({ title: 't', html: '<p>x</p>' }, {
      force: true, url: 'https://blog.test', adminKey: 'id:6162',
      fetchImpl: async (url, opts) => { body = JSON.parse(opts.body); return { ok: true, json: async () => ({ posts: [{ id: 'p1' }] }) }; },
    });
    expect(r.ok).toBe(true);
    expect(body.posts[0].status).toBe('draft'); // NOTHING auto-publishes
  });
});
