import { describe, expect, it } from 'vitest';
import { createRng, selectRoundQuestions, shuffle, type QuestionInput } from '../src/index';
import { CATS, MUSIC_CATS, makePack, mcQuestion } from './helpers';

const pack = makePack(CATS, 6, 'q');

describe('board rounds (RUSH / MUSIC_MAYHEM / POWER_ROUND)', () => {
  it('one question per category (in seeded order) then a wildcard-eligible wildcard', () => {
    for (const kind of ['RUSH', 'MUSIC_MAYHEM', 'POWER_ROUND'] as const) {
      const qs = selectRoundQuestions({ kind, pool: pack.questions, categories: CATS, seed: `s-${kind}` });
      expect(qs).toHaveLength(10);
      expect(new Set(qs.slice(0, 9).map((q) => q.category))).toEqual(new Set(CATS.map((c) => c.key)));
      expect(qs[9]!.wildcardEligible).toBe(true);
      expect(new Set(qs.map((q) => q.id)).size).toBe(10);
    }
  });

  it('is deterministic per seed and independent of pool order', () => {
    const a = selectRoundQuestions({ kind: 'RUSH', pool: pack.questions, categories: CATS, seed: 'same' });
    const b = selectRoundQuestions({ kind: 'RUSH', pool: shuffle(pack.questions, createRng('reorder')), categories: CATS, seed: 'same' });
    expect(b.map((q) => q.id)).toEqual(a.map((q) => q.id));
  });

  it('category order is a seeded shuffle, not always the pack order', () => {
    const orders = new Set<string>();
    for (let i = 0; i < 10; i++) {
      orders.add(selectRoundQuestions({ kind: 'RUSH', pool: pack.questions, categories: CATS, seed: `order-${i}` }).slice(0, 9).map((q) => q.category).join());
    }
    expect(orders.size).toBeGreaterThan(5);
  });

  it('prefers questions not excluded, and falls back to excluded ones when a category runs dry', () => {
    const first = selectRoundQuestions({ kind: 'RUSH', pool: pack.questions, categories: CATS, seed: 'a' });
    const second = selectRoundQuestions({ kind: 'RUSH', pool: pack.questions, categories: CATS, seed: 'b', exclude: first.map((q) => q.id) });
    expect(second.some((q) => first.some((f) => f.id === q.id))).toBe(false);
    // Exclude every 'pop' question: pop still gets one (an excluded one), others stay fresh.
    const popIds = pack.questions.filter((q) => q.category === 'pop').map((q) => q.id);
    const third = selectRoundQuestions({ kind: 'RUSH', pool: pack.questions, categories: CATS, seed: 'c', exclude: popIds });
    const pop = third.slice(0, 9).find((q) => q.category === 'pop')!;
    expect(popIds).toContain(pop.id);
  });

  it('wildcard falls back to any unused question when no wildcard-eligible one is left', () => {
    const pool = CATS.flatMap((c) => [mcQuestion(`${c.key}-1`, c.key), mcQuestion(`${c.key}-2`, c.key)]);
    const qs = selectRoundQuestions({ kind: 'RUSH', pool, categories: CATS, seed: 'w' });
    expect(qs).toHaveLength(10);
    expect(qs[9]!.wildcardEligible).toBeUndefined();
    expect(qs.slice(0, 9).map((q) => q.id)).not.toContain(qs[9]!.id);
  });

  it('throws a clear error when impossible', () => {
    const nine = CATS.map((c) => mcQuestion(`${c.key}-only`, c.key));
    expect(() => selectRoundQuestions({ kind: 'RUSH', pool: nine, categories: CATS, seed: 'x' })).toThrow(/wildcard/);
    const missing = pack.questions.filter((q) => q.category !== 'geo');
    expect(() => selectRoundQuestions({ kind: 'RUSH', pool: missing, categories: CATS, seed: 'x' })).toThrow(/geo/);
    expect(() => selectRoundQuestions({ kind: 'RUSH', pool: pack.questions, categories: CATS.slice(0, 8), seed: 'x' })).toThrow(/9 distinct categories/);
  });

  it('ignores questions from other packs’ categories', () => {
    const music = makePack(MUSIC_CATS, 2, 'm');
    const qs = selectRoundQuestions({ kind: 'MUSIC_MAYHEM', pool: [...pack.questions, ...music.questions], categories: MUSIC_CATS, seed: 'mm' });
    expect(qs.every((q) => q.id.startsWith('m-'))).toBe(true);
  });
});

describe('TABLE_WARS', () => {
  it('10 mixed questions, at most 2 per category, fresh first', () => {
    const qs = selectRoundQuestions({ kind: 'TABLE_WARS', pool: pack.questions, categories: CATS, seed: 'tw' });
    expect(qs).toHaveLength(10);
    expect(new Set(qs.map((q) => q.id)).size).toBe(10);
    const counts = new Map<string, number>();
    for (const q of qs) counts.set(q.category, (counts.get(q.category) ?? 0) + 1);
    expect(Math.max(...counts.values())).toBeLessThanOrEqual(2);
    const again = selectRoundQuestions({ kind: 'TABLE_WARS', pool: pack.questions, categories: CATS, seed: 'tw2', exclude: qs.map((q) => q.id) });
    expect(again.some((q) => qs.includes(q))).toBe(false);
  });
  it('throws when the category cap makes 10 impossible', () => {
    const four = makePack(CATS.slice(0, 4), 5, 'f');
    expect(() => selectRoundQuestions({ kind: 'TABLE_WARS', pool: four.questions, categories: CATS, seed: 'x' })).toThrow(/TABLE_WARS/);
  });
});

describe('FINAL_RUSH', () => {
  it('5 questions, DIFFICULT preferred, varied categories', () => {
    const qs = selectRoundQuestions({ kind: 'FINAL_RUSH', pool: pack.questions, categories: CATS, seed: 'fr' });
    expect(qs).toHaveLength(5);
    expect(qs.every((q) => q.difficulty === 'DIFFICULT')).toBe(true);
    expect(new Set(qs.map((q) => q.category)).size).toBe(5);
  });
  it('tops up with STANDARD when there are too few difficult questions, then reuses excluded ones', () => {
    const pool: QuestionInput[] = [
      mcQuestion('d1', 'pop', { difficulty: 'DIFFICULT' }),
      mcQuestion('d2', 'hist', { difficulty: 'DIFFICULT' }),
      ...['s1', 's2', 's3', 's4'].map((id) => mcQuestion(id, 'sci')),
    ];
    const qs = selectRoundQuestions({ kind: 'FINAL_RUSH', pool, categories: CATS, seed: 'fr' });
    expect(qs.slice(0, 2).map((q) => q.id).sort()).toEqual(['d1', 'd2']);
    expect(qs).toHaveLength(5);
    const excluded = selectRoundQuestions({ kind: 'FINAL_RUSH', pool, categories: CATS, seed: 'fr', exclude: ['d1', 'd2', 's1', 's2', 's3', 's4'] });
    expect(excluded).toHaveLength(5);
    expect(() => selectRoundQuestions({ kind: 'FINAL_RUSH', pool: pool.slice(0, 4), categories: CATS, seed: 'fr' })).toThrow(/FINAL_RUSH/);
  });
});
