import { describe, expect, it } from 'vitest';
import { hostView, privateView, publicView, type RoundPlanItem } from '../src/index';
import { Game, boardSetup, finalRushSetup } from './helpers';

function game(players = ['p1', 'p2']): Game {
  const g = Game.create();
  g.start();
  for (const p of players) g.join(p);
  return g;
}

const hasAnswerKey = (v: unknown): boolean => JSON.stringify(v).includes('"correctOptionId"');

describe('publicView', () => {
  it('INTRO: round info, no question, no reveal', () => {
    const g = game();
    g.startRound(boardSetup('r1'));
    const v = publicView(g.state);
    expect(v.round).toMatchObject({ id: 'r1', index: 0, kind: 'RUSH', hasBoard: true, questionCount: 10, qIndex: -1, phase: 'INTRO', finalists: null });
    expect(v.round!.categories).toHaveLength(9);
    expect(v.question).toBeNull();
    expect(v.reveal).toBeNull();
    expect(v.participantCount).toBe(2);
    expect(hasAnswerKey(v)).toBe(false);
  });

  it('PENDING shows the category teaser only; OPEN shows prompt and options but never the answer', () => {
    const g = game();
    g.startRound(boardSetup('r1'));
    g.advanceTo('PENDING', 0);
    const pending = publicView(g.state);
    expect(pending.question).toEqual({
      id: 'r1-q0',
      index: 0,
      category: 'pop',
      categoryLabel: 'POP',
      difficulty: 'STANDARD',
      type: 'MULTIPLE_CHOICE',
      isWildcard: false,
      prompt: null,
      options: null,
      opensAt: g.round.phaseEndsAt,
      openedAt: null,
      closesAt: null,
    });
    g.advanceTo('OPEN', 0);
    const open = publicView(g.state);
    expect(open.question!.prompt).toBe('Prompt for r1-q0?');
    expect(open.question!.options).toEqual(['a', 'b', 'c', 'd'].map((id) => ({ id, text: `Option ${id} of r1-q0` })));
    expect(open.question!.openedAt).toBe(g.round.openedAt);
    expect(open.question!.closesAt).toBe(g.round.closesAt);
    expect(open.reveal).toBeNull();
    expect(hasAnswerKey(open)).toBe(false);
    g.later(1_000);
    g.answer('p1', 'a');
    const answered = publicView(g.state);
    expect(answered.answeredCount).toBe(1);
    expect(hasAnswerKey(answered)).toBe(false);
    expect(JSON.stringify(answered)).not.toContain('p1:'); // no per-player answer data
    // Leaderboard does not move before reveal.
    expect(answered.leaderboard.every((s) => s.points === 0)).toBe(true);
  });

  it('LOCKED still hides the answer; REVEALED shows it; ROUND_OVER hides question and reveal again', () => {
    const g = game(['p1']);
    g.startRound(boardSetup('r1'));
    g.advanceTo('LOCKED', 0);
    expect(publicView(g.state).reveal).toBeNull();
    expect(hasAnswerKey(publicView(g.state))).toBe(false);
    g.advanceTo('REVEALED', 0);
    const revealed = publicView(g.state);
    expect(revealed.reveal).toMatchObject({ questionId: 'r1-q0', correctOptionId: 'a' });
    g.advanceTo('ROUND_OVER');
    const over = publicView(g.state);
    expect(over.question).toBeNull();
    expect(over.reveal).toBeNull();
    expect(over.answeredCount).toBe(0);
  });

  it('marks the wildcard question', () => {
    const g = game(['p1']);
    g.startRound(boardSetup('r1'));
    g.advanceTo('OPEN', 9);
    expect(publicView(g.state).question).toMatchObject({ isWildcard: true, index: 9 });
  });

  it('estimatedEndAt matches the real end when every question runs its full window', () => {
    const g = game(['p1']);
    g.startRound(boardSetup('r1', { tweak: (i) => (i === 3 ? { difficulty: 'DIFFICULT' } : {}) }));
    const estimates = [publicView(g.state).round!.estimatedEndAt];
    for (const [phase, q] of [['PENDING', 0], ['OPEN', 3], ['LOCKED', 5], ['REVEALED', 9], ['ROUND_OVER', undefined]] as const) {
      g.advanceTo(phase, q);
      estimates.push(publicView(g.state).round!.estimatedEndAt);
    }
    g.runUntil((x) => x.state.round === null);
    const end = g.eventsOf('RoundCompleted')[0]!.at;
    for (const e of estimates) expect(e).toBe(end);
  });

  it('leaderboards, solo board, recent joins and player count', () => {
    const g = Game.create();
    g.start();
    g.join('a1', 'a1', { id: 'A', name: 'Aces' });
    g.join('a2', 'a2', { id: 'A', name: 'Aces' });
    for (let i = 0; i < 10; i++) g.join(`s${i}`);
    g.must({ type: 'KICK_PLAYER', now: g.now, playerId: 's0' });
    const v = publicView(g.state);
    expect(v.playerCount).toBe(11);
    expect(v.leaderboard).toHaveLength(10);
    expect(v.recentJoins).toEqual(['s9', 's8', 's7', 's6', 's5', 's4', 's3', 's2']);
    expect(v.teamLeaderboard.map((t) => t.teamId)).toEqual(['A']);
    expect(v.soloLeaderboard).toHaveLength(9);
    const merged = Game.create({ settings: { mergeSolos: true } });
    merged.join('x1');
    expect(publicView(merged.state).soloLeaderboard).toBeNull();
    expect(publicView(merged.state).teamLeaderboard).toEqual([expect.objectContaining({ teamId: 'solo:x1', isSolo: true })]);
  });

  it('championship plan progress, pause marker and last round', () => {
    const plan: RoundPlanItem[] = [
      { kind: 'RUSH', packKey: 'test', label: 'The Rush', segment: 0 },
      { kind: 'FINAL_RUSH', packKey: 'test', label: 'Final Rush', segment: 1 },
    ];
    const g = Game.create({ mode: 'CHAMPIONSHIP', plan });
    expect(publicView(g.state).plan).toEqual({ index: 0, total: 2, label: 'The Rush', nextLabel: 'Final Rush' });
    g.join('p1');
    g.start();
    g.startRound(boardSetup('r1'));
    expect(publicView(g.state).plan).toEqual({ index: 0, total: 2, label: 'The Rush', nextLabel: 'Final Rush' });
    g.runUntil((x) => x.state.round === null);
    const between = publicView(g.state);
    expect(between.plan).toEqual({ index: 1, total: 2, label: 'Final Rush', nextLabel: null });
    expect(between.lastRound).toMatchObject({ id: 'r1', kind: 'RUSH' });
    expect(between.nextRoundAt).toBe(g.state.nextRoundAt);
    g.must({ type: 'PAUSE', now: g.now });
    expect(publicView(g.state).pausedAt).toBe(g.now);
    expect(publicView(Game.create().state).plan).toBeNull();
  });
});

describe('privateView', () => {
  it('returns null for unknown players and the player’s own state otherwise', () => {
    const g = game();
    expect(privateView(g.state, 'nobody')).toBeNull();
    g.must({ type: 'SET_TEAM', now: g.now, playerId: 'p1', team: { id: 't', name: 'Tapsters' } });
    g.startRound(boardSetup('r1'));
    const v = privateView(g.state, 'p1')!;
    expect(v).toMatchObject({ playerId: 'p1', nickname: 'p1', status: 'ACTIVE', teamId: 't', teamName: 'Tapsters', inRound: true, waitingForNextRound: false });
    expect(v.totals).toMatchObject({ points: 0, rank: 1 });
    expect(v.board!.tiles).toHaveLength(9);
    expect(v.board!.categoryLabels).toMatchObject({ pop: 'POP', gen: 'GEN' });
    expect(v.currentAnswer).toBeNull();
    expect(v.lastResult).toBeNull();
    expect(v.powerUps).toEqual({ enabled: true, tokens: 0, used: [], available: [], doubleDownOn: null, removedOptionId: null });
  });

  it('shows the player’s own answer, and the correct option only after reveal', () => {
    const g = game();
    g.startRound(boardSetup('r1'));
    g.advanceTo('OPEN', 0);
    g.later(800);
    g.answer('p1', 'b');
    const open = privateView(g.state, 'p1')!;
    expect(open.currentAnswer).toEqual({ questionId: 'r1-q0', optionId: 'b', wildcardPosition: null });
    expect(hasAnswerKey(open)).toBe(false);
    g.later(200);
    g.answer('p2', 'a');
    expect(hasAnswerKey(privateView(g.state, 'p2'))).toBe(false); // LOCKED
    g.advanceTo('REVEALED', 0);
    expect(privateView(g.state, 'p1')!.lastResult).toEqual({
      questionId: 'r1-q0',
      correct: false,
      answered: true,
      correctOptionId: 'a',
      points: 0,
      speedBonus: 0,
      doubleDown: null,
      tileActivated: null,
      newLines: [],
      fullBoard: false,
      tokenGranted: false,
    });
    const p2 = privateView(g.state, 'p2')!.lastResult!;
    expect(p2).toMatchObject({ correct: true, points: 100, tileActivated: g.round.boards['p2']!.tiles.find((t) => t.category === 'pop')!.position });
    expect(privateView(g.state, 'p2')!.roundPoints).toBe(100 + p2.speedBonus);
  });

  it('reports new lines, full board and token grants in lastResult', () => {
    const g = game(['p1']);
    g.startRound(boardSetup('r1'));
    g.strategy = ({ question }) => ({ optionId: question.correctOptionId, delayMs: 500 });
    let sawLine = false;
    for (let q = 0; q < 9; q++) {
      g.advanceTo('REVEALED', q);
      const r = privateView(g.state, 'p1')!.lastResult!;
      expect(r.correct).toBe(true);
      if (r.newLines.length > 0) {
        sawLine = true;
        expect(g.round.boards['p1']!.lines).toEqual(expect.arrayContaining(r.newLines));
      }
      if (q === 8) {
        expect(r.fullBoard).toBe(true);
        expect(r.newLines.length).toBeGreaterThan(0);
      } else {
        expect(r.fullBoard).toBe(false);
      }
    }
    expect(sawLine).toBe(true);
    const grants = g.eventsOf('PowerUpGranted').length;
    expect(grants).toBe(3);
  });

  it('kicked players have no rank and are out of the round', () => {
    const g = game();
    g.startRound(boardSetup('r1'));
    g.must({ type: 'KICK_PLAYER', now: g.now, playerId: 'p2' });
    expect(privateView(g.state, 'p2')!.totals.rank).toBeNull();
    expect(privateView(g.state, 'p2')!.inRound).toBe(false);
    expect(privateView(g.state, 'p2')!.waitingForNextRound).toBe(false);
  });

  it('Final Rush: finalists flagged, predictions visible as shares', () => {
    const plan: RoundPlanItem[] = [{ kind: 'FINAL_RUSH', packKey: 'test', label: 'Final Rush', segment: 0 }];
    const g = Game.create({ mode: 'CHAMPIONSHIP', plan });
    for (let i = 0; i < 8; i++) g.join(`p${i}`);
    g.start();
    g.startRound(finalRushSetup('fr'));
    const finalists = g.round.finalists!;
    expect(finalists).toEqual(['p0', 'p1', 'p2', 'p3', 'p4']);
    g.must({ type: 'PREDICT', now: g.now, playerId: 'p5', finalistId: 'p2', idempotencyKey: 'a' });
    g.must({ type: 'PREDICT', now: g.now, playerId: 'p6', finalistId: 'p2', idempotencyKey: 'b' });
    g.must({ type: 'PREDICT', now: g.now, playerId: 'p7', finalistId: 'p0', idempotencyKey: 'c' });
    expect(publicView(g.state).predictions).toEqual({ p0: 0.333, p1: 0, p2: 0.667, p3: 0, p4: 0 });
    expect(publicView(g.state).round!.finalists).toEqual(finalists.map((id) => ({ playerId: id, nickname: id })));
    expect(privateView(g.state, 'p0')).toMatchObject({ isFinalist: true, inRound: true, prediction: null });
    expect(privateView(g.state, 'p5')).toMatchObject({ isFinalist: false, inRound: false, waitingForNextRound: false, prediction: 'p2' });
  });
});

describe('hostView', () => {
  it('public view plus roster (including kicked players), teams and plan progress', () => {
    const g = game();
    g.join('bot', 'Beer Bot', null, true);
    g.must({ type: 'SET_TEAM', now: g.now, playerId: 'p1', team: { id: 't', name: 'Tapsters' } });
    g.must({ type: 'KICK_PLAYER', now: g.now, playerId: 'p2' });
    const h = hostView(g.state);
    expect(h.public.sessionId).toBe('session-1');
    expect(h.players.map((p) => [p.playerId, p.status, p.teamName, p.isBot])).toEqual([
      ['p1', 'ACTIVE', 'Tapsters', false],
      ['p2', 'KICKED', null, false],
      ['bot', 'ACTIVE', null, true],
    ]);
    expect(h.teams).toEqual([{ teamId: 't', name: 'Tapsters', members: 1 }]);
    expect(h).toMatchObject({ roundsCompleted: 0, planIndex: 0, planLength: null });
  });
});
