import { describe, expect, it } from 'vitest';
import { makePublic, standing } from './fixtures';
import {
  attractPanels,
  attractStandings,
  challengeWinnerNames,
  distributionRows,
  finalistLanes,
  fitDisplaySize,
  OPEN_PROMPT_SIZES,
  optionFontSize,
  predictionRows,
  promptFontSize,
  REVEAL_PROMPT_SIZES,
  rankMoves,
  wholePercentages,
} from './results';

const options = [
  { id: 'a', text: 'Julius Caesar' },
  { id: 'b', text: 'Caligula' },
  { id: 'c', text: 'Nero' },
  { id: 'd', text: 'Augustus' },
];

describe('wholePercentages', () => {
  it('always sums to exactly 100 using the largest remainders', () => {
    expect(wholePercentages([1, 1, 1])).toEqual([34, 33, 33]);
    expect(wholePercentages([1, 2, 1, 3])).toEqual([14, 29, 14, 43]);
    for (const counts of [
      [7, 0, 0, 0],
      [3, 3, 3, 3],
      [5, 9, 13, 1],
      [1, 1, 1, 1, 1, 1, 1],
    ]) {
      expect(wholePercentages(counts).reduce((a, b) => a + b, 0)).toBe(100);
    }
  });

  it('is all zeros when nobody answered, and ignores junk counts', () => {
    expect(wholePercentages([0, 0, 0, 0])).toEqual([0, 0, 0, 0]);
    expect(wholePercentages([Number.NaN, -2, 4])).toEqual([0, 0, 100]);
  });
});

describe('distributionRows', () => {
  it('keeps question order, letters A-D, counts, percents and marks only the correct option', () => {
    const rows = distributionRows(options, { distribution: { a: 1, b: 2, c: 1, d: 3 }, correctOptionId: 'd' });
    expect(rows.map((r) => r.letter)).toEqual(['A', 'B', 'C', 'D']);
    expect(rows.map((r) => r.count)).toEqual([1, 2, 1, 3]);
    expect(rows.map((r) => r.percent)).toEqual([14, 29, 14, 43]);
    expect(rows.filter((r) => r.correct).map((r) => r.id)).toEqual(['d']);
  });

  it('treats options nobody picked as zero', () => {
    const rows = distributionRows(options, { distribution: { b: 4 }, correctOptionId: 'b' });
    expect(rows.map((r) => [r.count, r.percent])).toEqual([
      [0, 0],
      [4, 100],
      [0, 0],
      [0, 0],
    ]);
  });

  it('handles true/false questions', () => {
    const rows = distributionRows(
      [
        { id: 't', text: 'True' },
        { id: 'f', text: 'False' },
      ],
      { distribution: { t: 2, f: 1 }, correctOptionId: 'f' },
    );
    expect(rows.map((r) => `${r.letter}:${r.percent}:${r.correct}`)).toEqual(['A:67:false', 'B:33:true']);
  });
});

describe('font sizing', () => {
  const openBox = { width: 1184, height: 370 };
  const longest = 'In 1982, which band became the first all-female group writing and playing its own songs to top the US album chart?';

  it('uses the biggest size that fits and keeps real prompts at 64 px or more', () => {
    expect(promptFontSize('Short?', openBox)).toBe(92);
    // The longest prompt in the seeded packs (114 chars) still fits at ≥ 64 px beside the corner QR.
    expect(promptFontSize(longest, openBox)).toBeGreaterThanOrEqual(64);
    const size = promptFontSize(longest, openBox);
    const perLine = Math.floor(openBox.width / (size * 0.8));
    expect(Math.ceil(longest.length / perLine) * size * 1.1).toBeLessThanOrEqual(openBox.height);
  });

  it('falls back to the smallest size, never below the spec minimum of 48 px', () => {
    expect(promptFontSize('x'.repeat(600), openBox)).toBe(OPEN_PROMPT_SIZES.at(-1));
    expect(promptFontSize('x'.repeat(600), { width: 1168, height: 250 }, REVEAL_PROMPT_SIZES)).toBe(48);
    expect(Math.min(...OPEN_PROMPT_SIZES, ...REVEAL_PROMPT_SIZES)).toBeGreaterThanOrEqual(48);
  });

  it('shrinks single-line headlines to fit, within bounds', () => {
    expect(fitDisplaySize('Music', 1700, 190, 110)).toBe(190);
    expect(fitDisplaySize('Quizzly Bear', 976, 150, 72)).toBe(Math.floor(976 / (12 * 0.72)));
    expect(fitDisplaySize('A'.repeat(40), 976, 150, 72)).toBe(72);
  });

  it('never drops options below 40 px', () => {
    expect(optionFontSize(options)).toBe(52);
    expect(optionFontSize([{ id: 'a', text: 'y'.repeat(80) }])).toBe(40);
  });
});

describe('rankMoves', () => {
  it('reports places climbed and points gained since the previous board', () => {
    const before = [standing(1, 'Bea', 500), standing(2, 'Hal', 400), standing(3, 'Cid', 300)];
    const after = [standing(1, 'Cid', 700), standing(2, 'Bea', 500), standing(3, 'Hal', 400)];
    const moves = rankMoves(before, after);
    expect(moves.get('p-Cid')).toEqual({ places: 2, gained: 400, isNew: false });
    expect(moves.get('p-Bea')).toEqual({ places: -1, gained: 0, isNew: false });
  });

  it('flags new entries only when there is a previous board', () => {
    const after = [standing(1, 'Ann', 150)];
    expect(rankMoves([], after).get('p-Ann')).toEqual({ places: 0, gained: 0, isNew: true });
    expect(rankMoves(null, after).get('p-Ann')).toEqual({ places: 0, gained: 0, isNew: false });
  });
});

describe('Final Rush helpers', () => {
  const finalists = [
    { playerId: 'p-Bea', nickname: 'Bea' },
    { playerId: 'p-Hal', nickname: 'Hal' },
    { playerId: 'p-Cid', nickname: 'Cid' },
  ];

  it('turns prediction shares into whole percentages in seed order', () => {
    expect(predictionRows(finalists, { 'p-Bea': 0.5, 'p-Hal': 0.333, 'p-Cid': 0.167 }).map((r) => r.percent)).toEqual([50, 33, 17]);
    expect(predictionRows(finalists, null).map((r) => r.percent)).toEqual([0, 0, 0]);
    expect(predictionRows(null, { x: 1 })).toEqual([]);
  });

  it('builds race lanes from the leaderboard, ordered by rank, scaled to the leader', () => {
    const lanes = finalistLanes(finalists, [standing(1, 'Cid', 1200), standing(2, 'Bea', 600), standing(3, 'Hal', 300)]);
    expect(lanes.map((l) => l.nickname)).toEqual(['Cid', 'Bea', 'Hal']);
    expect(lanes.map((l) => l.progress)).toEqual([1, 0.5, 0.25]);
  });
});

describe('challengeWinnerNames', () => {
  it('maps winning team ids to names and skips unknown ids', () => {
    const pub = makePublic({
      teamLeaderboard: [{ rank: 1, teamId: 't1', name: 'Quizzly Bears', score: 400, members: 3, activeMembers: 3, isSolo: false }],
    });
    expect(challengeWinnerNames(pub, ['t1', 'ghost'])).toEqual(['Quizzly Bears']);
    expect(challengeWinnerNames(pub, undefined)).toEqual([]);
  });
});

describe('attract panels', () => {
  it('only rotates through panels that have real data', () => {
    expect(attractPanels({ nightlyTop: [], lastRound: null })).toEqual(['howTo']);
    const lastRound = { id: 'r', index: 0, kind: 'RUSH' as const, label: 'The Rush', completedAt: 1, podium: [{ playerId: 'p', nickname: 'Bea', points: 900 }] };
    expect(attractPanels({ nightlyTop: [standing(1, 'Bea', 900)], lastRound })).toEqual(['leaderboard', 'lastRound', 'howTo']);
    expect(attractPanels({ nightlyTop: [], lastRound: { ...lastRound, podium: [] } })).toEqual(['howTo']);
  });

  it("prefers tonight's always-on board, falling back to the active session's", () => {
    const pub = makePublic({ leaderboard: [standing(1, 'Hal', 100)] });
    expect(attractStandings([standing(1, 'Bea', 900)], pub).map((s) => s.nickname)).toEqual(['Bea']);
    expect(attractStandings([], pub).map((s) => s.nickname)).toEqual(['Hal']);
    expect(attractStandings(undefined, null)).toEqual([]);
  });
});
