import { describe, expect, it } from 'vitest';
import { challengeWinnerNames, pointsBreakdown, resultAnnouncement, resultKind, totalPoints, wonTableChallenge, type LastResult } from './points';
import { formatClock, formatElapsed, formatPoints, formatShowTime, formatSigned, ordinal, plural, secondsLeft } from './format';
import { friendlyError, needsRejoin } from './errors';

function result(patch: Partial<LastResult> = {}): LastResult {
  return {
    questionId: 'q1',
    correct: true,
    answered: true,
    correctOptionId: 'b',
    points: 100,
    speedBonus: 38,
    doubleDown: null,
    tileActivated: 4,
    newLines: [],
    fullBoard: false,
    tokenGranted: false,
    ...patch,
  };
}

describe('pointsBreakdown', () => {
  it('itemises a correct answer with speed bonus', () => {
    const items = pointsBreakdown(result(), { difficulty: 'STANDARD' });
    expect(items.map((i) => [i.label, i.points])).toEqual([
      ['Correct answer', 100],
      ['Speed bonus', 38],
    ]);
    expect(totalPoints(items)).toBe(138);
  });

  it('adds lines, full board, Double Down, table challenge and the token', () => {
    const items = pointsBreakdown(result({ points: 150, speedBonus: 50, doubleDown: 'WON', newLines: [0, 6], fullBoard: true, tokenGranted: true }), {
      difficulty: 'DIFFICULT',
      tableChallengeWon: true,
    });
    expect(items.map((i) => i.key)).toEqual(['correct', 'speed', 'dd', 'line-0', 'line-6', 'full', 'table', 'token']);
    expect(items[0]?.label).toBe('Correct · hard question');
    expect(items[3]?.label).toBe('Line · Top row');
    expect(items.at(-1)).toMatchObject({ points: null, tone: 'token' });
    expect(totalPoints(items)).toBe(150 + 50 + 200 + 250 + 250 + 500 + 200);
  });

  it('shows the Double Down loss on a wrong answer and nothing else', () => {
    const items = pointsBreakdown(result({ correct: false, points: 0, speedBonus: 0, doubleDown: 'LOST', tileActivated: null }), {
      difficulty: 'STANDARD',
    });
    expect(items).toEqual([{ key: 'dd', label: 'Double Down lost', points: -100, tone: 'penalty' }]);
    expect(totalPoints(items)).toBe(-100);
  });

  it('classifies and announces results without relying on colour', () => {
    expect(resultKind(result())).toBe('correct');
    expect(resultKind(result({ correct: false }))).toBe('incorrect');
    expect(resultKind(result({ correct: false, answered: false }))).toBe('missed');
    const r = result({ newLines: [2], tokenGranted: true });
    expect(resultAnnouncement(r, pointsBreakdown(r, { difficulty: 'STANDARD' }))).toBe('Correct! +388 points. Line complete! You earned a power-up.');
    const miss = result({ correct: false, answered: false, points: 0, speedBonus: 0 });
    expect(resultAnnouncement(miss, [])).toBe("Time's up, no answer.");
  });
});

describe('table challenge', () => {
  it('matches team ids and merged solo ids, only for round participants', () => {
    const me = { teamId: 't1', playerId: 'p1', inRound: true };
    expect(wonTableChallenge(['t1'], me)).toBe(true);
    expect(wonTableChallenge(['t2'], me)).toBe(false);
    expect(wonTableChallenge(['solo:p1'], { ...me, teamId: null })).toBe(true);
    expect(wonTableChallenge(['t1'], { ...me, inRound: false })).toBe(false);
    expect(wonTableChallenge(undefined, me)).toBe(false);
  });

  it('names the winning tables from the public boards', () => {
    const team = { rank: 1, score: 0, members: 3, activeMembers: 3, isSolo: false };
    const boards = {
      teamLeaderboard: [{ ...team, teamId: 't1', name: 'Quizzly Bears' }],
      soloLeaderboard: [{ ...team, teamId: 'solo:p9', name: 'Ana', isSolo: true }],
    };
    expect(challengeWinnerNames(['t1', 'solo:p9', 'gone'], boards)).toEqual(['Quizzly Bears', 'Ana']);
  });
});

describe('format', () => {
  it('formats points, signs and ranks', () => {
    expect(formatPoints(2310)).toBe('2,310');
    expect(formatSigned(250)).toBe('+250');
    expect(formatSigned(-100)).toBe('−100');
    expect(formatSigned(0)).toBe('0');
    expect([1, 2, 3, 4, 11, 12, 13, 21, 22, 103, 111].map(ordinal)).toEqual([
      '1st',
      '2nd',
      '3rd',
      '4th',
      '11th',
      '12th',
      '13th',
      '21st',
      '22nd',
      '103rd',
      '111th',
    ]);
    expect(plural(1, 'player')).toBe('1 player');
    expect(plural(1200, 'player')).toBe('1,200 players');
  });

  it('formats countdowns rounding up', () => {
    expect(secondsLeft(14_001)).toBe(15);
    expect(secondsLeft(-5)).toBe(0);
    expect(formatClock(42_000)).toBe('0:42');
    expect(formatClock(64_100)).toBe('1:05');
    expect(formatClock(3_725_000)).toBe('1:02:05');
    expect(formatElapsed(1234)).toBe('1.2s');
  });

  it('formats show times relative to now in a given zone', () => {
    const tz = 'America/New_York';
    const now = Date.parse('2026-10-29T18:00:00-04:00');
    expect(formatShowTime(Date.parse('2026-10-29T21:00:00-04:00'), now, tz)).toBe('Today 9:00 PM');
    expect(formatShowTime(Date.parse('2026-10-30T20:30:00-04:00'), now, tz)).toBe('Tomorrow 8:30 PM');
    expect(formatShowTime(Date.parse('2026-10-31T21:00:00-04:00'), now, tz)).toBe('Sat 9:00 PM');
    expect(formatShowTime(Date.parse('2026-11-14T21:00:00-05:00'), now, tz)).toBe('Nov 14, 9:00 PM');
  });
});

describe('errors', () => {
  it('turns rate limits into "slow down" and keeps unknown server messages', () => {
    expect(friendlyError({ code: 'RATE_LIMITED', message: 'Slow down a little' })).toMatch(/slow down/i);
    expect(friendlyError({ code: 'TOO_LATE', message: 'x' })).toMatch(/too late/i);
    expect(friendlyError({ code: 'SOMETHING_NEW', message: 'Server says hi' })).toBe('Server says hi');
  });

  it('sends expired or ended sessions back to the join screen', () => {
    expect(needsRejoin('UNAUTHORIZED')).toBe(true);
    expect(needsRejoin('NO_ACTIVE_SESSION')).toBe(true);
    expect(needsRejoin('FORBIDDEN')).toBe(false);
  });
});
