import { describe, expect, it } from 'vitest';
import type { PrivateState, PublicState } from '@pintrush/engine';
import {
  alwaysOnBackOn,
  championshipInvite,
  championshipStage,
  estimateNextRoundAt,
  predictionsCloseAt,
  predictionWindowOpen,
  resolveScreen,
  type ScreenInput,
} from './screen';

type Round = NonNullable<PublicState['round']>;

function round(patch: Partial<Round> = {}): Round {
  return {
    id: 'r1',
    index: 0,
    kind: 'RUSH',
    label: 'The Rush',
    categories: [],
    hasBoard: true,
    questionCount: 10,
    qIndex: 3,
    phase: 'OPEN',
    phaseEndsAt: 50_000,
    finalists: null,
    estimatedEndAt: 200_000,
    ...patch,
  };
}

function pub(patch: Partial<PublicState> = {}): PublicState {
  return {
    sessionId: 's1',
    eventId: 'e1',
    mode: 'ALWAYS_ON',
    status: 'RUNNING',
    round: round(),
    question: null,
    reveal: null,
    answeredCount: 0,
    participantCount: 0,
    playerCount: 0,
    leaderboard: [],
    teamLeaderboard: [],
    soloLeaderboard: [],
    recentJoins: [],
    nextRoundAt: null,
    plan: null,
    lastRound: null,
    pausedAt: null,
    predictions: null,
    ...patch,
  };
}

function me(patch: Partial<PrivateState> = {}): PrivateState {
  return {
    playerId: 'p1',
    nickname: 'Mike',
    status: 'ACTIVE',
    teamId: null,
    teamName: null,
    totals: { points: 0, correct: 0, answered: 0, correctTimeMs: 0, lastCorrectAt: null, lines: 0, fullBoards: 0, rank: 1 },
    inRound: true,
    waitingForNextRound: false,
    board: null,
    currentAnswer: null,
    lastResult: null,
    roundPoints: 0,
    powerUps: { enabled: true, tokens: 0, used: [], available: [], doubleDownOn: null, removedOptionId: null },
    isFinalist: false,
    prediction: null,
    ...patch,
  };
}

const snapshot = { activeEventKind: 'ALWAYS_ON' as const, mySessionActive: true, mySessionId: 's1', activeSessionId: 's1' };

function input(patch: Partial<ScreenInput> = {}): ScreenInput {
  return { status: 'connected', kicked: false, snapshot, public: pub(), me: me(), ...patch };
}

describe('resolveScreen', () => {
  it('maps every round phase to its screen', () => {
    const phases: [Round['phase'], string][] = [
      ['INTRO', 'intro'],
      ['PENDING', 'pending'],
      ['OPEN', 'open'],
      ['LOCKED', 'locked'],
      ['REVEALED', 'revealed'],
      ['ROUND_OVER', 'round-over'],
    ];
    for (const [phase, kind] of phases) {
      expect(resolveScreen(input({ public: pub({ round: round({ phase }) }) })).kind).toBe(kind);
    }
  });

  it('waits for the first snapshot, and handles denied and kicked before anything else', () => {
    expect(resolveScreen(input({ snapshot: null, public: null, me: null })).kind).toBe('connecting');
    expect(resolveScreen(input({ status: 'denied' })).kind).toBe('denied');
    expect(resolveScreen(input({ kicked: true })).kind).toBe('kicked');
    expect(resolveScreen(input({ me: me({ status: 'KICKED' }) })).kind).toBe('kicked');
  });

  it('keeps rendering the last state while reconnecting', () => {
    expect(resolveScreen(input({ status: 'reconnecting' })).kind).toBe('open');
  });

  it('shows idle, lobby, break and complete from the session status', () => {
    expect(resolveScreen(input({ public: null, me: null })).kind).toBe('idle');
    expect(resolveScreen(input({ public: pub({ status: 'WAITING', round: null }) })).kind).toBe('lobby');
    expect(resolveScreen(input({ public: pub({ round: null, nextRoundAt: 99 }) })).kind).toBe('break');
    expect(resolveScreen(input({ public: pub({ status: 'COMPLETED', round: null }) })).kind).toBe('complete');
  });

  it('overlays pause on top of the current screen', () => {
    const model = resolveScreen(input({ public: pub({ status: 'PAUSED', round: round({ phase: 'PENDING' }) }) }));
    expect(model).toMatchObject({ kind: 'pending', paused: true });
  });

  it('sends late joiners to the waiting screen', () => {
    expect(resolveScreen(input({ me: me({ inRound: false, waitingForNextRound: true }) })).kind).toBe('waiting');
  });

  it('lets finalists play the Final Rush and everyone else predict, then watch', () => {
    const intro = pub({ round: round({ kind: 'FINAL_RUSH', hasBoard: false, questionCount: 5, phase: 'INTRO', qIndex: -1 }) });
    expect(resolveScreen(input({ public: intro, me: me({ isFinalist: true }) })).kind).toBe('intro');
    expect(resolveScreen(input({ public: intro, me: me({ inRound: false }) })).kind).toBe('predict');
    const q1Pending = pub({ round: round({ kind: 'FINAL_RUSH', phase: 'PENDING', qIndex: 0 }) });
    expect(resolveScreen(input({ public: q1Pending, me: me({ inRound: false }) })).kind).toBe('predict');
    const q1Open = pub({ round: round({ kind: 'FINAL_RUSH', phase: 'OPEN', qIndex: 0 }) });
    expect(resolveScreen(input({ public: q1Open, me: me({ inRound: false }) })).kind).toBe('watch');
    expect(resolveScreen(input({ public: q1Open, me: me({ isFinalist: true }) })).kind).toBe('open');
  });
});

describe('championship invite', () => {
  const champ = { activeEventKind: 'CHAMPIONSHIP' as const, mySessionActive: false, mySessionId: 's1', activeSessionId: 'c1' };

  it('is a banner while the phone is still mid-round in its own session', () => {
    const i = input({ snapshot: champ });
    expect(championshipInvite(i)).toBe('banner');
    expect(resolveScreen(i)).toMatchObject({ kind: 'open', invite: 'banner' });
  });

  it('takes over the screen between rounds or when the phone has no session', () => {
    expect(resolveScreen(input({ snapshot: champ, public: pub({ round: null }) })).kind).toBe('invite');
    expect(resolveScreen(input({ snapshot: { ...champ, mySessionId: null }, public: null, me: null })).kind).toBe('invite');
    // A paused always-on round can't be finished, so the invite takes over.
    expect(resolveScreen(input({ snapshot: champ, public: pub({ status: 'PAUSED' }) })).kind).toBe('invite');
  });

  it('is not offered once the phone plays the championship', () => {
    expect(championshipInvite(input({ snapshot: { ...champ, mySessionActive: true } }))).toBe('none');
  });
});

describe('always-on back on after a finished session', () => {
  const champDone = pub({ mode: 'CHAMPIONSHIP', status: 'COMPLETED', round: null, sessionId: 'c1' });
  const alwaysOnActive = { activeEventKind: 'ALWAYS_ON' as const, mySessionActive: false, mySessionId: 'c1', activeSessionId: 'a1' };

  it('offers "Keep playing" once tonight’s always-on is the active session again', () => {
    const i = input({ snapshot: alwaysOnActive, public: champDone });
    expect(alwaysOnBackOn(i)).toBe(true);
    expect(resolveScreen(i)).toMatchObject({ kind: 'complete', backOn: true, invite: 'none' });
  });

  it('also covers a finished always-on night when the next one runs', () => {
    const lastNight = pub({ status: 'COMPLETED', round: null, sessionId: 'old' });
    expect(resolveScreen(input({ snapshot: { ...alwaysOnActive, mySessionId: 'old' }, public: lastNight })).backOn).toBe(true);
  });

  it('keeps the final standings alone during the results window and when nothing runs', () => {
    // Results window: the finished championship is still the active session.
    const resultsWindow = { activeEventKind: 'CHAMPIONSHIP' as const, mySessionActive: true, mySessionId: 'c1', activeSessionId: 'c1' };
    expect(resolveScreen(input({ snapshot: resultsWindow, public: champDone })).backOn).toBe(false);
    const idleRoom = { activeEventKind: null, mySessionActive: false, mySessionId: 'c1', activeSessionId: null };
    expect(resolveScreen(input({ snapshot: idleRoom, public: champDone })).backOn).toBe(false);
  });

  it('is never set while the phone plays the active session', () => {
    expect(resolveScreen(input({ public: pub({ status: 'COMPLETED', round: null }) })).backOn).toBe(false);
    expect(resolveScreen(input()).backOn).toBe(false);
  });
});

describe('championship stage', () => {
  it('reads "lobby" until the active session is running', () => {
    expect(championshipStage({ activeSessionStatus: 'WAITING' })).toBe('lobby');
    expect(championshipStage({ activeSessionStatus: 'RUNNING' })).toBe('live');
    expect(championshipStage({ activeSessionStatus: 'PAUSED' })).toBe('live');
  });
});

describe('estimates', () => {
  it('prefers the server next-round time and falls back to the round end plus the break', () => {
    expect(estimateNextRoundAt(pub({ round: null, nextRoundAt: 123 }))).toBe(123);
    expect(estimateNextRoundAt(pub())).toBe(200_000 + 60_000);
    const champ = pub({ mode: 'CHAMPIONSHIP', plan: { index: 0, total: 9, label: 'The Rush', nextLabel: 'The Rush' } });
    expect(estimateNextRoundAt(champ)).toBe(200_000 + 20_000);
    expect(estimateNextRoundAt({ ...champ, plan: { index: 1, total: 9, label: 'The Rush', nextLabel: 'Music Mayhem' } })).toBe(230_000);
    expect(estimateNextRoundAt({ ...champ, plan: { index: 8, total: 9, label: 'Final Rush', nextLabel: null } })).toBeNull();
  });

  it('closes predictions when the first question opens', () => {
    const intro = round({ kind: 'FINAL_RUSH', phase: 'INTRO', qIndex: -1, phaseEndsAt: 10_000 });
    expect(predictionWindowOpen(intro)).toBe(true);
    expect(predictionsCloseAt(intro, null)).toBe(14_000);
    const pending = round({ kind: 'FINAL_RUSH', phase: 'PENDING', qIndex: 0, phaseEndsAt: 20_000 });
    expect(predictionsCloseAt(pending, null)).toBe(20_000);
    expect(predictionWindowOpen(round({ phase: 'PENDING', qIndex: 1 }))).toBe(false);
    expect(predictionsCloseAt(round({ phase: 'OPEN', qIndex: 0 }), null)).toBeNull();
  });
});
