import { describe, it, expect } from 'vitest'; import { calculateTeamResult, calculateRankings, getCompletionStatus, calculateAllResults, } from '../aggregation.js'; import { Score, Team, TeamResult } from '../../shared/types.js'; function makeScore(jurorId: string, teamId: string, criterionId: string, value: number): Score { return { jurorId, teamId, criterionId, value, updatedAt: new Date().toISOString() }; } describe('calculateTeamResult', () => { it('returns zero total and average when no scores exist', () => { const result = calculateTeamResult('team-a', 'Team A', [], 5); expect(result.total).toBe(0); expect(result.average).toBe(0); expect(result.jurorCount).toBe(0); expect(result.complete).toBe(false); }); it('calculates total as sum of all score values for the team', () => { const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 7), makeScore('juror2', 'team-a', 'c1', 9), makeScore('juror1', 'team-b', 'c1', 10), // different team, should be excluded ]; const result = calculateTeamResult('team-a', 'Team A', scores, 2); expect(result.total).toBe(8 + 7 + 9); }); it('counts only jurors who submitted all criteria', () => { const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 7), makeScore('juror2', 'team-a', 'c1', 9), // only 1 of 2 criteria ]; const result = calculateTeamResult('team-a', 'Team A', scores, 2); expect(result.jurorCount).toBe(1); // only juror1 has all 2 criteria }); it('calculates average as total / jurorCount', () => { const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 6), makeScore('juror2', 'team-a', 'c1', 10), makeScore('juror2', 'team-a', 'c2', 4), ]; const result = calculateTeamResult('team-a', 'Team A', scores, 2); // total = 28, jurorCount = 2, average = 14 expect(result.total).toBe(28); expect(result.jurorCount).toBe(2); expect(result.average).toBe(14); }); it('marks complete when all jurors with scores have all criteria', () => { const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 7), makeScore('juror2', 'team-a', 'c1', 9), makeScore('juror2', 'team-a', 'c2', 6), ]; const result = calculateTeamResult('team-a', 'Team A', scores, 2); expect(result.complete).toBe(true); }); it('marks incomplete when some jurors have partial scores', () => { const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 7), makeScore('juror2', 'team-a', 'c1', 9), // missing c2 ]; const result = calculateTeamResult('team-a', 'Team A', scores, 2); expect(result.complete).toBe(false); }); }); describe('calculateRankings', () => { it('sorts by total descending and assigns ranks', () => { const results: TeamResult[] = [ { teamId: 'a', teamName: 'A', total: 200, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, { teamId: 'b', teamName: 'B', total: 250, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, { teamId: 'c', teamName: 'C', total: 230, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, ]; const ranked = calculateRankings(results); expect(ranked[0].teamId).toBe('b'); expect(ranked[0].rank).toBe(1); expect(ranked[1].teamId).toBe('c'); expect(ranked[1].rank).toBe(2); expect(ranked[2].teamId).toBe('a'); expect(ranked[2].rank).toBe(3); }); it('assigns same rank to ties', () => { const results: TeamResult[] = [ { teamId: 'a', teamName: 'A', total: 250, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, { teamId: 'b', teamName: 'B', total: 240, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, { teamId: 'c', teamName: 'C', total: 240, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, { teamId: 'd', teamName: 'D', total: 230, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, ]; const ranked = calculateRankings(results); expect(ranked[0].rank).toBe(1); // 250 expect(ranked[1].rank).toBe(2); // 240 expect(ranked[2].rank).toBe(2); // 240 (tie) expect(ranked[3].rank).toBe(4); // 230 }); it('handles empty input', () => { const ranked = calculateRankings([]); expect(ranked).toEqual([]); }); it('handles single team', () => { const results: TeamResult[] = [ { teamId: 'a', teamName: 'A', total: 100, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false }, ]; const ranked = calculateRankings(results); expect(ranked[0].rank).toBe(1); }); }); describe('getCompletionStatus', () => { it('returns pending when no scores exist', () => { expect(getCompletionStatus('juror1', 'team-a', [], 5)).toBe('pending'); }); it('returns pending when scores exist for other juror/team', () => { const scores: Score[] = [ makeScore('juror2', 'team-a', 'c1', 8), makeScore('juror1', 'team-b', 'c1', 7), ]; expect(getCompletionStatus('juror1', 'team-a', scores, 5)).toBe('pending'); }); it('returns partial when some but not all criteria scored', () => { const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 7), ]; expect(getCompletionStatus('juror1', 'team-a', scores, 5)).toBe('partial'); }); it('returns complete when all criteria scored', () => { const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 7), makeScore('juror1', 'team-a', 'c3', 9), makeScore('juror1', 'team-a', 'c4', 6), makeScore('juror1', 'team-a', 'c5', 10), ]; expect(getCompletionStatus('juror1', 'team-a', scores, 5)).toBe('complete'); }); }); describe('calculateAllResults', () => { it('calculates results for all teams with rankings', () => { const teams: Team[] = [ { id: 'team-a', number: 1, name: 'Team A', description: '' }, { id: 'team-b', number: 2, name: 'Team B', description: '' }, ]; const scores: Score[] = [ makeScore('juror1', 'team-a', 'c1', 8), makeScore('juror1', 'team-a', 'c2', 7), makeScore('juror1', 'team-b', 'c1', 10), makeScore('juror1', 'team-b', 'c2', 9), ]; const results = calculateAllResults(scores, teams, 2); expect(results[0].teamId).toBe('team-b'); // higher total (19) expect(results[0].rank).toBe(1); expect(results[0].total).toBe(19); expect(results[1].teamId).toBe('team-a'); // lower total (15) expect(results[1].rank).toBe(2); expect(results[1].total).toBe(15); }); it('handles empty scores', () => { const teams: Team[] = [ { id: 'team-a', number: 1, name: 'Team A', description: '' }, ]; const results = calculateAllResults([], teams, 5); expect(results).toHaveLength(1); expect(results[0].total).toBe(0); expect(results[0].rank).toBe(1); }); });