import { describe, expect, it } from 'vitest';
import { makePublic, makeQuestion, makeRound, makeSnapshot } from './fixtures';
import { currentPublic, isPaused, lateJoinOpen, sceneKey, tvScreenFor } from './screen';

const reveal = { questionId: 'q-1', correctOptionId: 'd', distribution: { a: 1, d: 3 }, answeredCount: 4, correctCount: 3, fastest: [] };

describe('tvScreenFor', () => {
  it('shows the denied screen whatever state is cached', () => {
    const pub = makePublic();
    expect(tvScreenFor({ status: 'denied', snapshot: makeSnapshot(pub), public: pub })).toBe('denied');
  });

  it('shows "connecting" until the first snapshot arrives, even while reconnecting', () => {
    expect(tvScreenFor({ status: 'connecting', snapshot: null, public: null })).toBe('connecting');
    expect(tvScreenFor({ status: 'reconnecting', snapshot: null, public: null })).toBe('connecting');
  });

  it('keeps showing the last known scene while reconnecting', () => {
    const pub = makePublic({ round: makeRound({ phase: 'OPEN' }), question: makeQuestion() });
    expect(tvScreenFor({ status: 'reconnecting', snapshot: makeSnapshot(pub), public: pub })).toBe('open');
  });

  it('is in attract mode with no active session, and between rounds', () => {
    expect(tvScreenFor({ status: 'connected', snapshot: makeSnapshot(null), public: null })).toBe('attract');
    const idle = makePublic({ round: null, nextRoundAt: 2_000_000 });
    expect(tvScreenFor({ status: 'connected', snapshot: makeSnapshot(idle), public: idle })).toBe('attract');
  });

  it('shows the lobby while a championship waits and the champion when it completes', () => {
    const lobby = makePublic({ mode: 'CHAMPIONSHIP', status: 'WAITING' });
    expect(tvScreenFor({ status: 'connected', snapshot: null, public: lobby })).toBe('lobby');
    const done = makePublic({ mode: 'CHAMPIONSHIP', status: 'COMPLETED', round: makeRound({ phase: 'ROUND_OVER' }) });
    expect(tvScreenFor({ status: 'connected', snapshot: null, public: done })).toBe('complete');
  });

  it('maps every round phase to its scene', () => {
    const at = (round: ReturnType<typeof makeRound>, question = makeQuestion(), extra = {}) =>
      tvScreenFor({ status: 'connected', snapshot: null, public: makePublic({ round, question, ...extra }) });
    expect(at(makeRound({ phase: 'INTRO', qIndex: -1 }), undefined)).toBe('intro');
    expect(at(makeRound({ phase: 'PENDING' }), makeQuestion({ prompt: null, options: null, openedAt: null, closesAt: null }))).toBe('pending');
    expect(at(makeRound({ phase: 'OPEN' }))).toBe('open');
    expect(at(makeRound({ phase: 'LOCKED' }))).toBe('locked');
    expect(at(makeRound({ phase: 'REVEALED' }), makeQuestion(), { reveal })).toBe('revealed');
    expect(at(makeRound({ phase: 'ROUND_OVER' }), undefined)).toBe('roundOver');
  });

  it('never shows a reveal scene without matching reveal data', () => {
    const revealedNoData = makePublic({ round: makeRound({ phase: 'REVEALED' }), question: makeQuestion(), reveal: null });
    expect(tvScreenFor({ status: 'connected', snapshot: null, public: revealedNoData })).toBe('locked');
    const otherQuestion = makePublic({ round: makeRound({ phase: 'REVEALED' }), question: makeQuestion({ id: 'q-2' }), reveal });
    expect(tvScreenFor({ status: 'connected', snapshot: null, public: otherQuestion })).toBe('locked');
  });

  it('holds on the teaser if an OPEN question arrives without its prompt', () => {
    const pub = makePublic({ round: makeRound({ phase: 'OPEN' }), question: makeQuestion({ prompt: null, options: null }) });
    expect(tvScreenFor({ status: 'connected', snapshot: null, public: pub })).toBe('pending');
  });

  it('prefers the live public view over the snapshot copy', () => {
    const stale = makePublic({ round: makeRound({ phase: 'OPEN' }), question: makeQuestion() });
    const live = makePublic({ round: makeRound({ phase: 'LOCKED' }), question: makeQuestion() });
    expect(currentPublic({ snapshot: makeSnapshot(stale), public: live })).toBe(live);
    expect(tvScreenFor({ status: 'connected', snapshot: makeSnapshot(stale), public: live })).toBe('locked');
    expect(currentPublic({ snapshot: makeSnapshot(stale), public: null })).toBe(stale);
  });
});

describe('isPaused', () => {
  it('is true only for a paused session', () => {
    expect(isPaused(makePublic({ status: 'PAUSED' }))).toBe(true);
    expect(isPaused(makePublic({ status: 'RUNNING' }))).toBe(false);
    expect(isPaused(null)).toBe(false);
  });
});

describe('lateJoinOpen', () => {
  it('stays open until question 3 opens', () => {
    expect(lateJoinOpen(makeRound({ phase: 'INTRO', qIndex: -1 }))).toBe(true);
    expect(lateJoinOpen(makeRound({ phase: 'OPEN', qIndex: 1 }))).toBe(true);
    expect(lateJoinOpen(makeRound({ phase: 'PENDING', qIndex: 2 }))).toBe(true);
    expect(lateJoinOpen(makeRound({ phase: 'OPEN', qIndex: 2 }))).toBe(false);
    expect(lateJoinOpen(makeRound({ phase: 'REVEALED', qIndex: 5 }))).toBe(false);
  });

  it('is closed for the finalists-only Final Rush and once the round is over', () => {
    expect(lateJoinOpen(makeRound({ kind: 'FINAL_RUSH', phase: 'PENDING', qIndex: 0 }))).toBe(false);
    expect(lateJoinOpen(makeRound({ phase: 'ROUND_OVER', qIndex: 1 }))).toBe(false);
  });
});

describe('sceneKey', () => {
  it('keeps OPEN and LOCKED on one node so only the lock beat animates', () => {
    const pub = makePublic({ round: makeRound(), question: makeQuestion() });
    expect(sceneKey('open', pub)).toBe(sceneKey('locked', pub));
    expect(sceneKey('revealed', pub)).not.toBe(sceneKey('open', pub));
  });

  it('changes per question', () => {
    const q1 = makePublic({ round: makeRound(), question: makeQuestion({ id: 'q-1' }) });
    const q2 = makePublic({ round: makeRound(), question: makeQuestion({ id: 'q-2' }) });
    expect(sceneKey('pending', q1)).not.toBe(sceneKey('pending', q2));
  });
});
