import { describe, it, expect } from "vitest"; /** * Tiebreaker Logic Tests * Phase 4.1: Test comprehensive tiebreaker scenarios * * Tiebreaker order: * 1. Total points (higher is better) * 2. Number of 1st place finishes * 3. Number of 2nd place finishes * 4. Continue through all placements (3rd, 4th, ..., 8th) * * Note: These are unit tests that test the tiebreaker logic in isolation. * The actual recalculateStandings function is tested through integration tests * in actual league scenarios. */ /** * Helper function to simulate compareTeamsForRanking logic * This mirrors the private function in scoring-calculator.ts */ function compareTeamsForRanking( teamA: { totalPoints: number; placementCounts: Record; }, teamB: { totalPoints: number; placementCounts: Record; } ): number { // First compare by total points (descending) if (teamA.totalPoints !== teamB.totalPoints) { return teamB.totalPoints - teamA.totalPoints; } // If points are tied, apply tiebreaker cascade for (let placement = 1; placement <= 8; placement++) { const countA = teamA.placementCounts[placement] || 0; const countB = teamB.placementCounts[placement] || 0; if (countA !== countB) { return countB - countA; // More of this placement is better } } // Completely tied return 0; } /** * Helper function to simulate assignRanks logic * This mirrors the private function in scoring-calculator.ts */ function assignRanks( teams: Array<{ teamId: string; totalPoints: number; placementCounts: Record; }> ): Map { // Sort teams by ranking criteria const sorted = [...teams].toSorted(compareTeamsForRanking); const ranks = new Map(); let currentRank = 1; for (let i = 0; i < sorted.length; i++) { const team = sorted[i]; // Check if this team is tied with the previous team if (i > 0) { const prevTeam = sorted[i - 1]; const comparison = compareTeamsForRanking(team, prevTeam); if (comparison !== 0) { // Not tied, advance rank currentRank = i + 1; } // If comparison === 0, keep the same rank (tie) } ranks.set(team.teamId, currentRank); } return ranks; } describe("Tiebreaker Logic", () => { describe("Basic Point Sorting", () => { it("should rank teams by total points descending", () => { const teams = [ { teamId: "team-a", totalPoints: 100, placementCounts: {} }, { teamId: "team-b", totalPoints: 200, placementCounts: {} }, { teamId: "team-c", totalPoints: 150, placementCounts: {} }, ]; const ranks = assignRanks(teams); expect(ranks.get("team-b")).toBe(1); // Highest points expect(ranks.get("team-c")).toBe(2); expect(ranks.get("team-a")).toBe(3); // Lowest points }); }); describe("First Place Tiebreaker", () => { it("should break tie using 1st place count", () => { // Both teams have 215 points total, different 1st place counts const teams = [ { teamId: "team-a", totalPoints: 215, placementCounts: { 1: 2, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 1 }, // 2 firsts }, { teamId: "team-b", totalPoints: 215, placementCounts: { 1: 1, 2: 1, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 1 }, // 1 first }, ]; const ranks = assignRanks(teams); // Team A should rank higher due to more 1st place finishes expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(2); }); it("should handle multiple teams with same points but different 1st place counts", () => { const teams = [ { teamId: "team-a", totalPoints: 215, placementCounts: { 1: 2, 8: 1 } as Record, // 2 firsts }, { teamId: "team-b", totalPoints: 215, placementCounts: { 1: 1, 2: 1, 8: 1 } as Record, // 1 first }, { teamId: "team-c", totalPoints: 215, placementCounts: { 2: 2, 3: 1 } as Record, // 0 firsts }, ]; const ranks = assignRanks(teams); // Ranked by 1st place count expect(ranks.get("team-a")).toBe(1); // 2 firsts expect(ranks.get("team-b")).toBe(2); // 1 first expect(ranks.get("team-c")).toBe(3); // 0 firsts }); }); describe("Second Place Tiebreaker", () => { it("should break tie using 2nd place count when 1st place count is tied", () => { const teams = [ { teamId: "team-a", totalPoints: 240, placementCounts: { 1: 1, 2: 2, 8: 1 } as Record, // 1 first, 2 seconds }, { teamId: "team-b", totalPoints: 240, placementCounts: { 1: 1, 3: 2, 8: 1 } as Record, // 1 first, 0 seconds }, ]; const ranks = assignRanks(teams); expect(ranks.get("team-a")).toBe(1); // More 2nd places expect(ranks.get("team-b")).toBe(2); }); }); describe("Cascading Tiebreakers", () => { it("should cascade through all placement counts when needed", () => { // Tiebreaker goes deep: same points, same 1st-3rd, different 4th const teams = [ { teamId: "team-a", totalPoints: 160, placementCounts: { 2: 1, 3: 1, 4: 1 } as Record, // 0×1st, 1×2nd, 1×3rd, 1×4th }, { teamId: "team-b", totalPoints: 160, placementCounts: { 2: 1, 3: 1, 5: 2 } as Record, // 0×1st, 1×2nd, 1×3rd, 0×4th, 2×5th }, ]; const ranks = assignRanks(teams); // Team A wins on 4th place count expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(2); }); }); describe("Complete Ties", () => { it("should assign same rank to teams with identical scores and placements", () => { const teams = [ { teamId: "team-a", totalPoints: 220, placementCounts: { 1: 1, 2: 1, 3: 1 }, }, { teamId: "team-b", totalPoints: 220, placementCounts: { 1: 1, 2: 1, 3: 1 }, }, ]; const ranks = assignRanks(teams); // Both should have rank 1 expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(1); }); it("should handle 3-way ties correctly", () => { const teams = [ { teamId: "team-a", totalPoints: 170, placementCounts: { 1: 1, 2: 1 } }, { teamId: "team-b", totalPoints: 170, placementCounts: { 1: 1, 2: 1 } }, { teamId: "team-c", totalPoints: 170, placementCounts: { 1: 1, 2: 1 } }, ]; const ranks = assignRanks(teams); // All three should have rank 1 expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(1); expect(ranks.get("team-c")).toBe(1); }); it("should skip rank numbers after ties", () => { // 2 teams tied for 1st, next team should be 3rd const teams = [ { teamId: "team-a", totalPoints: 300, placementCounts: { 1: 3 } as Record }, { teamId: "team-b", totalPoints: 300, placementCounts: { 1: 3 } as Record }, // Tied { teamId: "team-c", totalPoints: 210, placementCounts: { 2: 3 } as Record }, ]; const ranks = assignRanks(teams); expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(1); expect(ranks.get("team-c")).toBe(3); // Skips 2 }); }); describe("Edge Cases", () => { it("should handle season with single team", () => { const teams = [{ teamId: "solo", totalPoints: 100, placementCounts: { 1: 1 } }]; const ranks = assignRanks(teams); expect(ranks.size).toBe(1); expect(ranks.get("solo")).toBe(1); }); it("should handle teams with zero points", () => { const teams = [ { teamId: "team-a", totalPoints: 100, placementCounts: { 1: 1 } as Record }, { teamId: "team-b", totalPoints: 0, placementCounts: {} as Record }, ]; const ranks = assignRanks(teams); expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(2); }); it("should handle all teams with same score", () => { const teams = [ { teamId: "team-a", totalPoints: 100, placementCounts: { 1: 1 } }, { teamId: "team-b", totalPoints: 100, placementCounts: { 1: 1 } }, { teamId: "team-c", totalPoints: 100, placementCounts: { 1: 1 } }, ]; const ranks = assignRanks(teams); // All should be rank 1 expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(1); expect(ranks.get("team-c")).toBe(1); }); }); describe("Complex Tiebreaker Scenarios", () => { it("should handle 8-way cascade ending in complete tie", () => { // Two teams identical through all 8 placements const teams = [ { teamId: "team-a", totalPoints: 450, placementCounts: { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1 }, }, { teamId: "team-b", totalPoints: 450, placementCounts: { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1 }, }, ]; const ranks = assignRanks(teams); expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(1); // Perfect tie }); it("should break tie at 8th place (last tiebreaker)", () => { // Identical through 1st-7th, different at 8th const teams = [ { teamId: "team-a", totalPoints: 100, placementCounts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 7 }, // 7×8th }, { teamId: "team-b", totalPoints: 100, placementCounts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 6 }, // 6×8th }, ]; const ranks = assignRanks(teams); // Team A wins with more 8th place finishes expect(ranks.get("team-a")).toBe(1); expect(ranks.get("team-b")).toBe(2); }); }); });