import { describe, it, expect } from "vitest"; import { calculateSplitQualifyingPoints, DEFAULT_QP_VALUES, hasProcessedQualifyingPlacement, } from "../qualifying-points"; describe("Qualifying Points Configuration", () => { describe("DEFAULT_QP_VALUES", () => { it("should have 16 placement values", () => { expect(DEFAULT_QP_VALUES).toHaveLength(16); }); it("should have correct QP values for top placements", () => { expect(DEFAULT_QP_VALUES[0]).toEqual({ placement: 1, points: 20 }); expect(DEFAULT_QP_VALUES[1]).toEqual({ placement: 2, points: 14 }); expect(DEFAULT_QP_VALUES[2]).toEqual({ placement: 3, points: 10 }); expect(DEFAULT_QP_VALUES[3]).toEqual({ placement: 4, points: 8 }); }); it("should have 5 QP for 5th and 6th place", () => { expect(DEFAULT_QP_VALUES[4]).toEqual({ placement: 5, points: 5 }); expect(DEFAULT_QP_VALUES[5]).toEqual({ placement: 6, points: 5 }); }); it("should have 3 QP for 7th and 8th place", () => { expect(DEFAULT_QP_VALUES[6]).toEqual({ placement: 7, points: 3 }); expect(DEFAULT_QP_VALUES[7]).toEqual({ placement: 8, points: 3 }); }); it("should have 2 QP for 9th-12th place", () => { expect(DEFAULT_QP_VALUES[8]).toEqual({ placement: 9, points: 2 }); expect(DEFAULT_QP_VALUES[9]).toEqual({ placement: 10, points: 2 }); expect(DEFAULT_QP_VALUES[10]).toEqual({ placement: 11, points: 2 }); expect(DEFAULT_QP_VALUES[11]).toEqual({ placement: 12, points: 2 }); }); it("should have 1 QP for 13th-16th place", () => { expect(DEFAULT_QP_VALUES[12]).toEqual({ placement: 13, points: 1 }); expect(DEFAULT_QP_VALUES[13]).toEqual({ placement: 14, points: 1 }); expect(DEFAULT_QP_VALUES[14]).toEqual({ placement: 15, points: 1 }); expect(DEFAULT_QP_VALUES[15]).toEqual({ placement: 16, points: 1 }); }); it("should be sorted by placement (ascending)", () => { for (let i = 0; i < DEFAULT_QP_VALUES.length - 1; i++) { expect(DEFAULT_QP_VALUES[i].placement).toBeLessThan( DEFAULT_QP_VALUES[i + 1].placement ); } }); it("should have descending point values for higher placements", () => { expect(DEFAULT_QP_VALUES[0].points).toBeGreaterThan(DEFAULT_QP_VALUES[1].points); expect(DEFAULT_QP_VALUES[1].points).toBeGreaterThan(DEFAULT_QP_VALUES[2].points); expect(DEFAULT_QP_VALUES[2].points).toBeGreaterThan(DEFAULT_QP_VALUES[3].points); }); }); describe("QP Accumulation Logic", () => { it("should accumulate QP across multiple events", () => { // Simulating participant earning QP from 4 majors const event1QP = 20; // 1st place const event2QP = 14; // 2nd place const event3QP = 10; // 3rd place const event4QP = 8; // 4th place const totalQP = event1QP + event2QP + event3QP + event4QP; expect(totalQP).toBe(52); }); it("should handle decimal QP values", () => { const qp1 = 20.5; const qp2 = 14.25; const total = qp1 + qp2; expect(total).toBe(34.75); }); it("should handle zero QP awards", () => { const event1QP = 20; const event2QP = 0; // Finished beyond 16th const total = event1QP + event2QP; expect(total).toBe(20); }); }); describe("QP Ranking Logic", () => { it("should rank participants by total QP (highest first)", () => { const standings = [ { name: "Player A", qp: 44 }, { name: "Player B", qp: 50 }, { name: "Player C", qp: 38 }, ]; const sorted = standings.toSorted((a, b) => b.qp - a.qp); expect(sorted[0].name).toBe("Player B"); // 50 QP expect(sorted[1].name).toBe("Player A"); // 44 QP expect(sorted[2].name).toBe("Player C"); // 38 QP }); it("should handle ties in QP totals", () => { const standings = [ { name: "Player A", qp: 30 }, { name: "Player B", qp: 30 }, { name: "Player C", qp: 20 }, ]; const sorted = standings.toSorted((a, b) => b.qp - a.qp); // Both players with 30 QP should be before Player C expect(sorted[2].name).toBe("Player C"); expect(sorted[0].qp).toBe(30); expect(sorted[1].qp).toBe(30); }); }); describe("QP to Fantasy Placement Conversion", () => { it("should assign top 8 QP leaders to fantasy placements 1-8", () => { const qpRankings = [50, 44, 38, 30, 25, 20, 15, 10, 5, 2]; const top8 = qpRankings.slice(0, 8); expect(top8).toHaveLength(8); expect(top8[0]).toBe(50); // 1st place expect(top8[7]).toBe(10); // 8th place }); it("should assign 0 fantasy points to participants beyond top 8", () => { const qpRankings = [50, 44, 38, 30, 25, 20, 15, 10, 5, 2]; const beyond8 = qpRankings.slice(8); expect(beyond8).toHaveLength(2); // These would get 0 fantasy points const fantasyPoints = beyond8.map(() => 0); expect(fantasyPoints).toEqual([0, 0]); }); it("should handle ties at the cutoff (8th place)", () => { // Scenario: Two players tied for 8th place QP const qpRankings = [ { name: "P1", qp: 50 }, // 1st { name: "P2", qp: 44 }, // 2nd { name: "P3", qp: 38 }, // 3rd { name: "P4", qp: 30 }, // 4th { name: "P5", qp: 25 }, // 5th { name: "P6", qp: 20 }, // 6th { name: "P7", qp: 15 }, // 7th { name: "P8", qp: 10 }, // Tied for 8th { name: "P9", qp: 10 }, // Tied for 8th ]; // Both should get placement 8 const tied8th = qpRankings.filter(p => p.qp === 10); expect(tied8th).toHaveLength(2); // In implementation, both would share 8th place fantasy placement const placement = 8; expect(placement).toBe(8); }); }); describe("Tie Handling During QP Awards", () => { const pointsByPlacement = new Map( DEFAULT_QP_VALUES.map(({ placement, points }) => [placement, points]) ); it("rounds a 4-way golf tie for 15th to the nearest hundredth", () => { expect(calculateSplitQualifyingPoints(15, 4, pointsByPlacement)).toBe(0.5); }); it("should correctly split QP among tied participants", () => { // Scenario: 7 players tied for 14th place // They occupy positions 14-20 // Positions 14, 15, 16 get 1 QP each = 3 total QP // Positions 17-20 get 0 QP // Total: 3 QP divided by 7 players = 3/7 = 0.428571... per player const tieCount = 7; const startPosition = 14; // Calculate total QP for positions 14-20 const qpMap: Record = { 14: 1, 15: 1, 16: 1, 17: 0, 18: 0, 19: 0, 20: 0, }; let totalQP = 0; for (let pos = startPosition; pos < startPosition + tieCount; pos++) { totalQP += qpMap[pos] || 0; } const qpPerPlayer = totalQP / tieCount; expect(totalQP).toBe(3); expect(calculateSplitQualifyingPoints(14, 7, pointsByPlacement)).toBe(0.43); expect(qpPerPlayer).toBeCloseTo(0.428571, 5); }); it("should handle 2-way tie for 5th place", () => { // 2 players tied for 5th occupy positions 5 and 6 // Position 5: 5 QP, Position 6: 5 QP // Total: 10 QP / 2 players = 5 QP each const tieCount = 2; const startPosition = 5; const qpMap: Record = { 5: 5, 6: 5 }; let totalQP = 0; for (let pos = startPosition; pos < startPosition + tieCount; pos++) { totalQP += qpMap[pos]; } const qpPerPlayer = totalQP / tieCount; expect(totalQP).toBe(10); expect(qpPerPlayer).toBe(5); }); it("should handle 3-way tie for 1st place", () => { // 3 players tied for 1st occupy positions 1, 2, 3 // Position 1: 20 QP, Position 2: 14 QP, Position 3: 10 QP // Total: 44 QP / 3 players = 14.666... QP each const tieCount = 3; const startPosition = 1; const qpMap: Record = { 1: 20, 2: 14, 3: 10 }; let totalQP = 0; for (let pos = startPosition; pos < startPosition + tieCount; pos++) { totalQP += qpMap[pos]; } const qpPerPlayer = totalQP / tieCount; expect(totalQP).toBe(44); expect(calculateSplitQualifyingPoints(1, 3, pointsByPlacement)).toBe(14.67); expect(qpPerPlayer).toBeCloseTo(14.666667, 5); }); }); describe("Reprocessing Events", () => { it("should handle reprocessing by removing old QP before adding new QP", () => { // Scenario: Admin enters results, processes QP, then realizes there's an error // They fix the placements and reprocess // Initial processing const participant1InitialQP = 20; // 1st place const participant2InitialQP = 14; // 2nd place let totalQP1 = participant1InitialQP; let totalQP2 = participant2InitialQP; // Admin realizes participant 1 should have been 3rd, not 1st // Reprocessing: subtract old QP, add new QP totalQP1 = totalQP1 - participant1InitialQP + 10; // Remove 20, add 10 (3rd place) totalQP2 = totalQP2 - participant2InitialQP + 20; // Remove 14, add 20 (moved to 1st) expect(totalQP1).toBe(10); // Should have 10 QP (3rd place) expect(totalQP2).toBe(20); // Should have 20 QP (1st place) }); it("should not increment majorsCompleted when reprocessing", () => { // First processing let majorsCompleted = 0; const wasAlreadyProcessed = false; if (!wasAlreadyProcessed) { majorsCompleted += 1; } expect(majorsCompleted).toBe(1); // Reprocessing (wasAlreadyProcessed = true) const reprocessing = true; if (!reprocessing) { majorsCompleted += 1; } expect(majorsCompleted).toBe(1); // Should still be 1, not 2 }); }); describe("Processed event detection", () => { it("treats a placed zero-QP result as already processed", () => { expect( hasProcessedQualifyingPlacement([ { placement: 20, qualifyingPointsAwarded: "0.00" }, ]) ).toBe(true); }); it("does not treat filler zero-QP rows without placements as processed", () => { expect( hasProcessedQualifyingPlacement([ { placement: null, qualifyingPointsAwarded: "0" }, ]) ).toBe(false); }); it("does not treat unprocessed placed rows as processed", () => { expect( hasProcessedQualifyingPlacement([ { placement: 15, qualifyingPointsAwarded: null }, ]) ).toBe(false); }); }); describe("Scoring Workflow", () => { it("should process 4 majors and calculate final standings", () => { // Simulating a full season with 4 majors const participants = [ { name: "Tiger Woods", qp: 0, events: 0 }, { name: "Rory McIlroy", qp: 0, events: 0 }, { name: "Scottie Scheffler", qp: 0, events: 0 }, ]; // Event 1: The Masters participants[0].qp += 20; // Tiger wins participants[0].events++; participants[1].qp += 14; // Rory 2nd participants[1].events++; participants[2].qp += 10; // Scottie 3rd participants[2].events++; // Event 2: PGA Championship participants[1].qp += 20; // Rory wins participants[1].events++; participants[0].qp += 14; // Tiger 2nd participants[0].events++; participants[2].qp += 10; // Scottie 3rd participants[2].events++; // Event 3: U.S. Open participants[2].qp += 20; // Scottie wins participants[2].events++; participants[0].qp += 14; // Tiger 2nd participants[0].events++; participants[1].qp += 10; // Rory 3rd participants[1].events++; // Event 4: The Open participants[0].qp += 20; // Tiger wins participants[0].events++; participants[1].qp += 14; // Rory 2nd participants[1].events++; participants[2].qp += 10; // Scottie 3rd participants[2].events++; // Final standings participants.sort((a, b) => b.qp - a.qp); expect(participants[0].name).toBe("Tiger Woods"); expect(participants[0].qp).toBe(68); // 20+14+14+20 expect(participants[0].events).toBe(4); expect(participants[1].name).toBe("Rory McIlroy"); expect(participants[1].qp).toBe(58); // 14+20+10+14 expect(participants[1].events).toBe(4); expect(participants[2].name).toBe("Scottie Scheffler"); expect(participants[2].qp).toBe(50); // 10+10+20+10 expect(participants[2].events).toBe(4); }); }); });