import { describe, it, expect, vi, beforeEach } from "vitest"; /** * Tests for calculateTeamProjectedScore * * Key behaviours under test: * - Finalized bracket participants use calculateBracketPoints (averaged), not raw points * - Partial-score participants (still alive) contribute floor points to actualPoints * and their incremental EV (EV - floor) to projectedPoints * - EV < floor is clamped to 0 extra EV (Math.max guard) * - Non-bracket finalized participants use calculateFantasyPoints (raw) * - Pending participants (no result) contribute full EV to projectedPoints */ vi.mock("~/services/ev-calculator", () => ({ calculateEV: vi.fn(), })); vi.mock("../participant-expected-value", () => ({ getParticipantEV: vi.fn(), })); import { calculateTeamProjectedScore, calculateTeamScore } from "../scoring-calculator"; import { calculateEV } from "~/services/ev-calculator"; import { getParticipantEV } from "../participant-expected-value"; // Standard scoring rules used across tests const SCORING = { pointsFor1st: 100, pointsFor2nd: 70, pointsFor3rd: 50, pointsFor4th: 40, pointsFor5th: 25, pointsFor6th: 25, pointsFor7th: 15, pointsFor8th: 15, }; // Derived bracket averages for assertions const AVG_5_TO_8 = (25 + 25 + 15 + 15) / 4; // 20 const AVG_3_TO_4 = (50 + 40) / 2; // 45 /** Build a minimal pick object matching what the query returns */ function makePick( participantId: string, sportsSeasonId: string, scoringPattern: string, result: { finalPosition: number | null; isPartialScore: boolean } | null ) { return { pickNumber: 1, round: 1, participant: { id: participantId, sportsSeasonId, results: result ? [result] : [], sportsSeason: { scoringPattern }, }, }; } /** Build a mock DB that returns the given picks and no bracketTemplateId */ function makeDb( picks: ReturnType[], seasonResults = picks.flatMap((pick) => pick.participant.results .filter((result) => result.finalPosition !== null) .map((result) => ({ sportsSeasonId: pick.participant.sportsSeasonId, finalPosition: result.finalPosition, })) ) ) { return { query: { seasons: { findFirst: vi.fn().mockResolvedValue(SCORING), }, draftPicks: { findMany: vi.fn().mockResolvedValue(picks), }, scoringEvents: { findFirst: vi.fn().mockResolvedValue({ bracketTemplateId: null }), }, seasonParticipantResults: { findMany: vi.fn().mockResolvedValue(seasonResults), }, }, } as any; } const DUMMY_EV_ROW = { probFirst: "0.1", probSecond: "0.1", probThird: "0.1", probFourth: "0.1", probFifth: "0.1", probSixth: "0.1", probSeventh: "0.2", probEighth: "0.2", } as any; beforeEach(() => { vi.clearAllMocks(); }); describe("calculateTeamProjectedScore", () => { describe("finalized bracket participant", () => { it("uses bracket-averaged points, not raw position value", async () => { // QF loser: finalPosition=5, isPartialScore=false // calculateBracketPoints(5) = avg(5,6,7,8) = 20, NOT raw pointsFor5th = 25 const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: false }), ]); vi.mocked(getParticipantEV).mockResolvedValue(null); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(AVG_5_TO_8); // 20, not 25 expect(result.projectedPoints).toBe(AVG_5_TO_8); expect(result.participantsFinished).toBe(1); expect(result.participantsRemaining).toBe(0); }); it("averages 3rd/4th for bracket semi-final losers", async () => { const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 3, isPartialScore: false }), ]); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(AVG_3_TO_4); // 45 expect(result.projectedPoints).toBe(AVG_3_TO_4); }); it("uses raw points for non-bracket (e.g. season_standings) finalized participant", async () => { // Non-bracket 3rd place should use pointsFor3rd = 50, not averaged const db = makeDb([ makePick("p1", "ss1", "season_standings", { finalPosition: 3, isPartialScore: false }), ]); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(50); // raw pointsFor3rd }); it("splits finalized QP tie points by shared fantasy slots", async () => { const db = makeDb( [ makePick("p1", "ss1", "qualifying_points", { finalPosition: 2, isPartialScore: false }), ], [ { sportsSeasonId: "ss1", finalPosition: 2 }, { sportsSeasonId: "ss1", finalPosition: 2 }, ] ); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(60); // (70 + 50) / 2 expect(result.projectedPoints).toBe(60); }); }); describe("partial-score bracket participant (still alive)", () => { it("floor counts as actual; incremental EV above floor counts toward projected", async () => { // Won QF: finalPosition=3 (floor = avg[3,4] = 45), isPartialScore=true // EV = 60 → incremental = 60 - 45 = 15 → projected = 45 + 15 = 60 const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 3, isPartialScore: true }), ]); vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW); vi.mocked(calculateEV).mockReturnValue(60); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(AVG_3_TO_4); // 45 floor expect(result.projectedPoints).toBe(60); // 45 + (60 - 45) // Partial participants are NOT counted as finished — they're still competing expect(result.participantsFinished).toBe(0); expect(result.participantsRemaining).toBe(1); }); it("clamps EV contribution to 0 when EV drops below floor (Math.max guard)", async () => { // Won SF: finalPosition=2 (floor = 70), isPartialScore=true // EV = 50 (below floor) → incremental = max(0, 50-70) = 0 → projected = 70 const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 2, isPartialScore: true }), ]); vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW); vi.mocked(calculateEV).mockReturnValue(50); // EV < floor const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(70); // floor = 2nd place = 70 expect(result.projectedPoints).toBe(70); // no negative EV contribution }); it("partial-score floor for non-scoring round (pos 5) contributes 20 pts", async () => { // Won Round of 16 (non-scoring): finalPosition=5, isPartialScore=true → floor=20 const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }), ]); vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW); vi.mocked(calculateEV).mockReturnValue(35); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(AVG_5_TO_8); // 20 expect(result.projectedPoints).toBe(35); // 20 + (35 - 20) }); }); describe("pending participant (no result)", () => { it("contributes 0 to actualPoints and full EV to projectedPoints", async () => { const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", null), ]); vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW); vi.mocked(calculateEV).mockReturnValue(30); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(0); expect(result.projectedPoints).toBe(30); expect(result.participantsFinished).toBe(0); expect(result.participantsRemaining).toBe(1); }); it("contributes 0 projected when no EV data exists", async () => { const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", null), ]); vi.mocked(getParticipantEV).mockResolvedValue(null); const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(0); expect(result.projectedPoints).toBe(0); }); }); describe("mixed team (calculateTeamProjectedScore)", () => { it("correctly combines finalized, partial, and pending participants", async () => { // Pick 1: bracket finalized 1st place → actual=100, no EV // Pick 2: bracket partial pos 5 (floor=20) → actual=20, EV=35 → +15 projected // Pick 3: pending → actual=0, EV=25 → +25 projected // Total: actual=120, projected=120+15+25=160 const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 1, isPartialScore: false }), makePick("p2", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }), makePick("p3", "ss1", "playoff_bracket", null), ]); vi.mocked(getParticipantEV) .mockResolvedValueOnce(DUMMY_EV_ROW) // p2 — partial .mockResolvedValueOnce(DUMMY_EV_ROW); // p3 — pending vi.mocked(calculateEV) .mockReturnValueOnce(35) // p2 EV .mockReturnValueOnce(25); // p3 EV const result = await calculateTeamProjectedScore("team1", "season1", db); expect(result.actualPoints).toBe(120); // 100 + 20 expect(result.projectedPoints).toBe(160); // 120 + 15 + 25 expect(result.participantsFinished).toBe(1); expect(result.participantsRemaining).toBe(2); // partial + pending }); }); }); describe("calculateTeamScore", () => { // calculateTeamScore writes totalPoints, placementCounts, and participantsCompleted // to the teamStandings table. Partial-score participants (still alive in bracket) // count toward placementCounts using their current position (best available signal), // but must NOT be counted as completed — they're still in progress. it("partial-score participant contributes floor to totalPoints and placementCounts, but not participantsCompleted", async () => { // QF survivor: finalPosition=5 (floor=20), isPartialScore=true const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }), ]); const result = await calculateTeamScore("team1", "season1", db); expect(result.totalPoints).toBe(AVG_5_TO_8); // 20 floor in score expect(result.participantsCompleted).toBe(0); // still alive → not completed expect(result.placementCounts[5]).toBe(1); // current position counts toward tiebreaker expect(result.participantsTotal).toBe(1); }); it("finalized bracket participant counts toward placementCounts and participantsCompleted", async () => { // QF loser: finalPosition=5, isPartialScore=false const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: false }), ]); const result = await calculateTeamScore("team1", "season1", db); expect(result.totalPoints).toBe(AVG_5_TO_8); // 20 bracket-averaged expect(result.participantsCompleted).toBe(1); expect(result.placementCounts[5]).toBe(1); }); it("eliminated in non-scoring round (finalPosition=0) counts as completed", async () => { // Round of 16 loser: 0 pts, fully eliminated (isPartialScore=false) const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 0, isPartialScore: false }), ]); const result = await calculateTeamScore("team1", "season1", db); expect(result.totalPoints).toBe(0); // no points for pre-bracket elimination expect(result.participantsCompleted).toBe(1); // done — should not count as remaining expect(result.participantsTotal).toBe(1); expect(result.placementCounts[1]).toBe(0); // no placement badge }); it("mixed team: partial counts in totalPoints and placementCounts; finalized also counts as completed", async () => { // p1: finalized 1st → totalPoints +100, placementCounts[1]++, completed++ // p2: partial pos 5 (floor 20) → totalPoints +20, placementCounts[5]++, not completed const db = makeDb([ makePick("p1", "ss1", "playoff_bracket", { finalPosition: 1, isPartialScore: false }), makePick("p2", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }), ]); const result = await calculateTeamScore("team1", "season1", db); expect(result.totalPoints).toBe(100 + AVG_5_TO_8); // 120 expect(result.participantsCompleted).toBe(1); expect(result.participantsTotal).toBe(2); expect(result.placementCounts[1]).toBe(1); expect(result.placementCounts[5]).toBe(1); // partial position counts toward tiebreaker }); it("splits finalized QP tie points across the 8th-place cutoff", async () => { const db = makeDb( [ makePick("p1", "ss1", "qualifying_points", { finalPosition: 8, isPartialScore: false }), ], [ { sportsSeasonId: "ss1", finalPosition: 8 }, { sportsSeasonId: "ss1", finalPosition: 8 }, ] ); const result = await calculateTeamScore("team1", "season1", db); expect(result.totalPoints).toBe(7.5); // (15 + 0) / 2 expect(result.participantsCompleted).toBe(1); expect(result.placementCounts[8]).toBe(1); }); });