/** * Unit Tests: Round-Robin Schedule Generator * * Tests the correctness of the round-robin scheduling algorithm */ import { describe, test, expect } from 'bun:test'; import { generateRoundRobin, validateScheduleInput, expectedRounds, expectedMatchups, } from '@/lib/schedule-generator'; // Helper to create team pairings from simple IDs function createTeams(count: number): { player1Id: number; player2Id: number }[] { const teams = []; for (let i = 0; i < count; i++) { // Create teams with unique player IDs // Team 1: players 1, 2 // Team 2: players 3, 4 // etc. teams.push({ player1Id: i * 2 + 1, player2Id: i * 2 + 2, }); } return teams; } describe('Round-Robin Schedule Generator', () => { describe('generateRoundRobin', () => { test('should return empty array for fewer than 2 teams', () => { expect(generateRoundRobin([])).toEqual([]); expect(generateRoundRobin([{ player1Id: 1, player2Id: 2 }])).toEqual([]); }); test('should generate correct schedule for 2 teams', () => { const rounds = generateRoundRobin([ { player1Id: 1, player2Id: 2 }, { player1Id: 3, player2Id: 4 }, ]); expect(rounds).toHaveLength(1); expect(rounds[0].roundNumber).toBe(1); expect(rounds[0].matchups).toHaveLength(1); expect(rounds[0].matchups[0]).toEqual({ player1P1Id: 1, player1P2Id: 2, player2P1Id: 3, player2P2Id: 4, }); }); test('should generate N-1 rounds for N even teams', () => { const teams = createTeams(4); const rounds = generateRoundRobin(teams); expect(rounds).toHaveLength(3); }); test('should generate N rounds for N odd teams (with bye)', () => { const teams = createTeams(3); const rounds = generateRoundRobin(teams); expect(rounds).toHaveLength(3); }); test('each team plays every other team exactly once (even)', () => { const teams = createTeams(4); const rounds = generateRoundRobin(teams); // Collect all pairings as sorted tuples const pairings = new Set(); for (const round of rounds) { for (const matchup of round.matchups) { const key = [ matchup.player1P1Id, matchup.player1P2Id, matchup.player2P1Id, matchup.player2P2Id, ].sort().join('-'); pairings.add(key); } } // 4 teams = 6 unique pairings expect(pairings.size).toBe(6); }); test('each team plays every other team exactly once (odd)', () => { const teams = createTeams(5); const rounds = generateRoundRobin(teams); const pairings = new Set(); for (const round of rounds) { for (const matchup of round.matchups) { const key = [ matchup.player1P1Id, matchup.player1P2Id, matchup.player2P1Id, matchup.player2P2Id, ].sort().join('-'); pairings.add(key); } } // 5 teams = 10 unique pairings expect(pairings.size).toBe(10); }); test('each team plays exactly once per round (even teams)', () => { const teams = createTeams(6); const rounds = generateRoundRobin(teams); for (const round of rounds) { const teamsInRound = new Set(); for (const matchup of round.matchups) { teamsInRound.add([matchup.player1P1Id, matchup.player1P2Id].sort().join('-')); teamsInRound.add([matchup.player2P1Id, matchup.player2P2Id].sort().join('-')); } // Each team appears exactly once expect(teamsInRound.size).toBe(6); } }); test('each team plays at most once per round (odd teams)', () => { const teams = createTeams(5); const rounds = generateRoundRobin(teams); for (const round of rounds) { const teamsInRound = new Set(); for (const matchup of round.matchups) { teamsInRound.add([matchup.player1P1Id, matchup.player1P2Id].sort().join('-')); teamsInRound.add([matchup.player2P1Id, matchup.player2P2Id].sort().join('-')); } // 5 teams, one has bye each round, so 4 play expect(teamsInRound.size).toBe(4); } }); test('should handle 8 teams (typical euchre tournament)', () => { const teams = createTeams(8); const rounds = generateRoundRobin(teams); expect(rounds).toHaveLength(7); const totalMatchups = rounds.reduce((sum, r) => sum + r.matchups.length, 0); expect(totalMatchups).toBe(28); }); test('round numbers should be sequential starting from 1', () => { const teams = createTeams(6); const rounds = generateRoundRobin(teams); rounds.forEach((round, idx) => { expect(round.roundNumber).toBe(idx + 1); }); }); }); describe('validateScheduleInput', () => { test('should reject empty team list', () => { const result = validateScheduleInput([]); expect(result.valid).toBe(false); expect(result.error).toContain('At least 2'); }); test('should reject single team', () => { const result = validateScheduleInput([{ player1Id: 1, player2Id: 2 }]); expect(result.valid).toBe(false); }); test('should reject duplicate team pairings', () => { const result = validateScheduleInput([ { player1Id: 1, player2Id: 2 }, { player1Id: 3, player2Id: 4 }, { player1Id: 1, player2Id: 2 }, // Duplicate ]); expect(result.valid).toBe(false); expect(result.error).toContain('Duplicate'); }); test('should accept valid team list', () => { const result = validateScheduleInput(createTeams(4)); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); }); describe('expectedRounds', () => { test('should return 0 for fewer than 2 teams', () => { expect(expectedRounds(0)).toBe(0); expect(expectedRounds(1)).toBe(0); }); test('should return N-1 for even N', () => { expect(expectedRounds(4)).toBe(3); expect(expectedRounds(6)).toBe(5); expect(expectedRounds(8)).toBe(7); }); test('should return N for odd N', () => { expect(expectedRounds(3)).toBe(3); expect(expectedRounds(5)).toBe(5); expect(expectedRounds(7)).toBe(7); }); }); describe('expectedMatchups', () => { test('should return 0 for fewer than 2 teams', () => { expect(expectedMatchups(0)).toBe(0); expect(expectedMatchups(1)).toBe(0); }); test('should return N*(N-1)/2 for N teams', () => { expect(expectedMatchups(4)).toBe(6); expect(expectedMatchups(6)).toBe(15); expect(expectedMatchups(8)).toBe(28); }); }); });