import { describe, it, expect } from "vitest"; import { SIMPLE_16, SIMPLE_32, NCAA_68 } from "~/lib/bracket-templates"; import { calculateFantasyPoints, calculateAveragedPoints, type ScoringRules } from "../scoring-rules"; const DEFAULT_SCORING: ScoringRules = { pointsFor1st: 100, pointsFor2nd: 70, pointsFor3rd: 50, pointsFor4th: 40, pointsFor5th: 25, pointsFor6th: 25, pointsFor7th: 15, pointsFor8th: 15, }; describe("Bracket Integration Tests - Phase 2.7", () => { describe("16-Team Bracket", () => { it("has correct template structure", () => { expect(SIMPLE_16.totalTeams).toBe(16); expect(SIMPLE_16.rounds).toHaveLength(4); expect(SIMPLE_16.scoringStartsAtRound).toBe("Quarterfinals"); // Round of 16 doesn't score const round1 = SIMPLE_16.rounds[0]; expect(round1.name).toBe("Round of 16"); expect(round1.isScoring).toBe(false); expect(round1.matchCount).toBe(8); // Quarterfinals and beyond score const round2 = SIMPLE_16.rounds[1]; expect(round2.name).toBe("Quarterfinals"); expect(round2.isScoring).toBe(true); expect(round2.matchCount).toBe(4); const round3 = SIMPLE_16.rounds[2]; expect(round3.name).toBe("Semifinals"); expect(round3.isScoring).toBe(true); expect(round3.matchCount).toBe(2); const round4 = SIMPLE_16.rounds[3]; expect(round4.name).toBe("Finals"); expect(round4.isScoring).toBe(true); expect(round4.matchCount).toBe(1); }); it("calculates correct points for each placement", () => { // Champion (1st) const champion = calculateFantasyPoints(1, DEFAULT_SCORING); expect(champion).toBe(100); // Runner-up (2nd) const runnerUp = calculateFantasyPoints(2, DEFAULT_SCORING); expect(runnerUp).toBe(70); // Semifinals losers (3rd/4th share) const sfLosers = calculateAveragedPoints([3, 4], DEFAULT_SCORING); expect(sfLosers).toBe(45); // (50 + 40) / 2 // Quarterfinals losers (5th-8th share) const qfLosers = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); expect(qfLosers).toBe(20); // (25 + 25 + 15 + 15) / 4 // Round of 16 losers (early elimination = 0 points per Q20) const earlyElimination = calculateFantasyPoints(0, DEFAULT_SCORING); expect(earlyElimination).toBe(0); }); it("simulates complete 16-team bracket scoring", () => { // Simulate a complete bracket const results = { // Finals champion: 1, // 100 points runnerUp: 2, // 70 points // Semifinals losers (2 teams share 3rd/4th) sfLoser1: 3, // 45 points (avg of 3rd and 4th) sfLoser2: 3, // 45 points // Quarterfinals losers (4 teams share 5th-8th) qfLoser1: 5, // 20 points (avg of 5-8) qfLoser2: 5, // 20 points qfLoser3: 5, // 20 points qfLoser4: 5, // 20 points // Round of 16 losers (8 teams get 0 points) r16Loser1: 0, // 0 points r16Loser2: 0, // 0 points r16Loser3: 0, // 0 points r16Loser4: 0, // 0 points r16Loser5: 0, // 0 points r16Loser6: 0, // 0 points r16Loser7: 0, // 0 points r16Loser8: 0, // 0 points }; // Verify champion expect(calculateFantasyPoints(results.champion, DEFAULT_SCORING)).toBe(100); // Verify runner-up expect(calculateFantasyPoints(results.runnerUp, DEFAULT_SCORING)).toBe(70); // Verify SF losers expect(calculateFantasyPoints(results.sfLoser1, DEFAULT_SCORING)).toBe(50); // Gets 3rd place points expect(calculateFantasyPoints(results.sfLoser2, DEFAULT_SCORING)).toBe(50); // When averaged: (50 + 40) / 2 = 45 expect(calculateAveragedPoints([3, 4], DEFAULT_SCORING)).toBe(45); // Verify QF losers expect(calculateFantasyPoints(results.qfLoser1, DEFAULT_SCORING)).toBe(25); // Gets 5th place points // When averaged: (25 + 25 + 15 + 15) / 4 = 20 expect(calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING)).toBe(20); // Verify Round of 16 losers get 0 points expect(calculateFantasyPoints(results.r16Loser1, DEFAULT_SCORING)).toBe(0); expect(calculateFantasyPoints(results.r16Loser8, DEFAULT_SCORING)).toBe(0); // Total points distributed (if one team drafted all participants) // 100 + 70 + 45 + 45 + 20 + 20 + 20 + 20 + 0*8 = 340 const totalPoints = 100 + 70 + 45 + 45 + 20 + 20 + 20 + 20 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0; expect(totalPoints).toBe(340); }); it("verifies only top 8 finishers score fantasy points", () => { // Winners and top 8 get points expect(calculateFantasyPoints(1, DEFAULT_SCORING)).toBeGreaterThan(0); expect(calculateFantasyPoints(8, DEFAULT_SCORING)).toBeGreaterThan(0); // Round of 16 losers (positions 9-16) get 0 points expect(calculateFantasyPoints(0, DEFAULT_SCORING)).toBe(0); expect(calculateFantasyPoints(9, DEFAULT_SCORING)).toBe(0); expect(calculateFantasyPoints(16, DEFAULT_SCORING)).toBe(0); }); }); describe("32-Team Bracket", () => { it("has correct template structure", () => { expect(SIMPLE_32.totalTeams).toBe(32); expect(SIMPLE_32.rounds).toHaveLength(5); expect(SIMPLE_32.scoringStartsAtRound).toBe("Quarterfinals"); // Round of 32 and Round of 16 don't score const round1 = SIMPLE_32.rounds[0]; expect(round1.name).toBe("Round of 32"); expect(round1.isScoring).toBe(false); expect(round1.matchCount).toBe(16); const round2 = SIMPLE_32.rounds[1]; expect(round2.name).toBe("Round of 16"); expect(round2.isScoring).toBe(false); expect(round2.matchCount).toBe(8); // Quarterfinals and beyond score const round3 = SIMPLE_32.rounds[2]; expect(round3.name).toBe("Quarterfinals"); expect(round3.isScoring).toBe(true); expect(round3.matchCount).toBe(4); const round4 = SIMPLE_32.rounds[3]; expect(round4.name).toBe("Semifinals"); expect(round4.isScoring).toBe(true); const round5 = SIMPLE_32.rounds[4]; expect(round5.name).toBe("Finals"); expect(round5.isScoring).toBe(true); }); it("calculates correct points for each placement", () => { // Same as 16-team for top 8 expect(calculateFantasyPoints(1, DEFAULT_SCORING)).toBe(100); expect(calculateFantasyPoints(2, DEFAULT_SCORING)).toBe(70); expect(calculateAveragedPoints([3, 4], DEFAULT_SCORING)).toBe(45); expect(calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING)).toBe(20); // Early elimination (9th-32nd) = 0 points expect(calculateFantasyPoints(0, DEFAULT_SCORING)).toBe(0); }); it("simulates complete 32-team bracket scoring", () => { // Top 8 finishers const topEight = { champion: 100, runnerUp: 70, sfLoser1: 45, sfLoser2: 45, qfLoser1: 20, qfLoser2: 20, qfLoser3: 20, qfLoser4: 20, }; // 24 early eliminations (Round of 32 + Round of 16) const earlyEliminations = Array(24).fill(0); // Total points const topEightTotal = Object.values(topEight).reduce((sum, pts) => sum + pts, 0); expect(topEightTotal).toBe(340); const earlyEliminationTotal = earlyEliminations.reduce((sum, pts) => sum + pts, 0); expect(earlyEliminationTotal).toBe(0); const grandTotal = topEightTotal + earlyEliminationTotal; expect(grandTotal).toBe(340); }); it("verifies 24 teams eliminated early get 0 points", () => { // Round of 32: 16 losers // Round of 16: 8 losers // Total: 24 teams get 0 points // Only top 8 score const scoringTeams = 8; const nonScoringTeams = 32 - scoringTeams; expect(nonScoringTeams).toBe(24); // All non-scoring teams get 0 points for (let i = 0; i < nonScoringTeams; i++) { expect(calculateFantasyPoints(0, DEFAULT_SCORING)).toBe(0); } }); }); describe("NCAA March Madness (68 Teams) - Elite Eight Scoring", () => { it("has correct template structure", () => { expect(NCAA_68.totalTeams).toBe(68); expect(NCAA_68.rounds).toHaveLength(7); expect(NCAA_68.scoringStartsAtRound).toBe("Elite Eight"); // First Four through Sweet Sixteen don't score expect(NCAA_68.rounds[0].name).toBe("First Four"); expect(NCAA_68.rounds[0].isScoring).toBe(false); expect(NCAA_68.rounds[1].name).toBe("Round of 64"); expect(NCAA_68.rounds[1].isScoring).toBe(false); expect(NCAA_68.rounds[2].name).toBe("Round of 32"); expect(NCAA_68.rounds[2].isScoring).toBe(false); expect(NCAA_68.rounds[3].name).toBe("Sweet Sixteen"); expect(NCAA_68.rounds[3].isScoring).toBe(false); // Elite Eight and beyond score expect(NCAA_68.rounds[4].name).toBe("Elite Eight"); expect(NCAA_68.rounds[4].isScoring).toBe(true); expect(NCAA_68.rounds[5].name).toBe("Final Four"); expect(NCAA_68.rounds[5].isScoring).toBe(true); expect(NCAA_68.rounds[6].name).toBe("Championship"); expect(NCAA_68.rounds[6].isScoring).toBe(true); }); it("verifies only Elite Eight and beyond score points (per Q18)", () => { // Teams eliminated before Elite Eight get 0 points // First Four: 4 losers // Round of 64: 32 losers // Round of 32: 16 losers // Sweet Sixteen: 8 losers // Total: 60 teams get 0 points const nonScoringTeams = 4 + 32 + 16 + 8; expect(nonScoringTeams).toBe(60); // Only Elite Eight and beyond (8 teams) score const scoringTeams = 8; expect(scoringTeams + nonScoringTeams).toBe(68); }); it("simulates complete March Madness bracket scoring", () => { // Top 8 finishers (Elite Eight and beyond) const topEight = { champion: 100, runnerUp: 70, finalFourLoser1: 45, finalFourLoser2: 45, eliteEightLoser1: 20, eliteEightLoser2: 20, eliteEightLoser3: 20, eliteEightLoser4: 20, }; // 60 early eliminations const earlyEliminations = Array(60).fill(0); // Total points const topEightTotal = Object.values(topEight).reduce((sum, pts) => sum + pts, 0); expect(topEightTotal).toBe(340); const earlyEliminationTotal = earlyEliminations.reduce((sum, pts) => sum + pts, 0); expect(earlyEliminationTotal).toBe(0); const grandTotal = topEightTotal + earlyEliminationTotal; expect(grandTotal).toBe(340); }); it("verifies Elite Eight losers share 5th-8th placement", () => { // Elite Eight has 4 matches (Quarterfinals round) // Losers share 5th-8th = (25 + 25 + 15 + 15) / 4 = 20 points each const eliteEightLosers = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); expect(eliteEightLosers).toBe(20); }); it("verifies Final Four losers share 3rd-4th placement", () => { // Final Four has 2 matches (Semifinals round) // Losers share 3rd-4th = (50 + 40) / 2 = 45 points each const finalFourLosers = calculateAveragedPoints([3, 4], DEFAULT_SCORING); expect(finalFourLosers).toBe(45); }); }); describe("Cross-Template Consistency", () => { it("verifies all templates award same total points for top 8", () => { // Regardless of bracket size, top 8 always get the same points const expectedTotal = 100 + 70 + 45 + 45 + 20 + 20 + 20 + 20; // 16-team bracket const team16Total = 100 + 70 + 45 + 45 + 20 + 20 + 20 + 20; expect(team16Total).toBe(expectedTotal); // 32-team bracket const team32Total = 100 + 70 + 45 + 45 + 20 + 20 + 20 + 20; expect(team32Total).toBe(expectedTotal); // NCAA 68-team bracket const ncaaTotal = 100 + 70 + 45 + 45 + 20 + 20 + 20 + 20; expect(ncaaTotal).toBe(expectedTotal); // All should be 340 expect(expectedTotal).toBe(340); }); it("verifies scoring rounds are consistent across templates", () => { // All templates should have exactly 3 scoring rounds const simple16ScoringRounds = SIMPLE_16.rounds.filter((r) => r.isScoring); expect(simple16ScoringRounds).toHaveLength(3); // QF, SF, Finals const simple32ScoringRounds = SIMPLE_32.rounds.filter((r) => r.isScoring); expect(simple32ScoringRounds).toHaveLength(3); // QF, SF, Finals const ncaaScoringRounds = NCAA_68.rounds.filter((r) => r.isScoring); expect(ncaaScoringRounds).toHaveLength(3); // Elite Eight, Final Four, Championship }); it("verifies non-scoring rounds correctly identified", () => { // 16-team: 1 non-scoring round const simple16NonScoring = SIMPLE_16.rounds.filter((r) => !r.isScoring); expect(simple16NonScoring).toHaveLength(1); // 32-team: 2 non-scoring rounds const simple32NonScoring = SIMPLE_32.rounds.filter((r) => !r.isScoring); expect(simple32NonScoring).toHaveLength(2); // NCAA 68: 4 non-scoring rounds const ncaaNonScoring = NCAA_68.rounds.filter((r) => !r.isScoring); expect(ncaaNonScoring).toHaveLength(4); }); }); describe("Placement 0 for Early Eliminations (Q20)", () => { it("placement 0 awards 0 fantasy points", () => { const points = calculateFantasyPoints(0, DEFAULT_SCORING); expect(points).toBe(0); }); it("placement 0 works with custom scoring rules", () => { const customScoring: ScoringRules = { pointsFor1st: 500, pointsFor2nd: 400, pointsFor3rd: 300, pointsFor4th: 200, pointsFor5th: 100, pointsFor6th: 100, pointsFor7th: 50, pointsFor8th: 50, }; const points = calculateFantasyPoints(0, customScoring); expect(points).toBe(0); }); it("early eliminations do not affect averaged points", () => { // If we incorrectly averaged in 0-point placements, we'd get wrong results // Correct: avg(5,6,7,8) = (25+25+15+15)/4 = 20 // Wrong: avg(0,5,6,7,8) = (0+25+25+15+15)/5 = 16 const correct = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); expect(correct).toBe(20); // Should NOT include 0 in averaging const wrong = calculateAveragedPoints([0, 5, 6, 7, 8], DEFAULT_SCORING); expect(wrong).not.toBe(20); expect(wrong).toBe(16); // This would be incorrect scoring }); }); });