import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest"; import { NCAAFootballSimulator } from "../ncaa-football-simulator"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); // Use real implementations of pure math functions to avoid mock call accumulation // across 50k-iteration simulations. Only mock convertFuturesToElo (complex behavior). vi.mock("~/services/probability-engine", async (importOriginal) => { const actual = await importOriginal() as Record; return { ...actual, convertFuturesToElo: vi.fn((oddsInput: { participantId: string; odds: number }[]) => { return new Map(oddsInput.map((o, i) => [o.participantId, 1600 - i * 50])); }), }; }); // ─── Helpers ────────────────────────────────────────────────────────────────── const TEAM_IDS = Array.from({ length: 12 }, (_, i) => `team-${i + 1}`); /** Build EV rows with sourceElo ratings (descending — team-1 is best). */ function makeEvRows( ids: string[], opts: { includeOdds?: boolean } = {} ) { return ids.map((participantId, i) => ({ participantId, sourceElo: 1700 - i * 50, // 1700, 1650, 1600, … sourceOdds: opts.includeOdds ? (i === 0 ? -200 : 300 + i * 50) : null, })); } /** Build participant rows. */ function makeParticipants(ids: string[]) { return ids.map((id) => ({ id })); } // ─── Tests ──────────────────────────────────────────────────────────────────── describe("NCAAFootballSimulator", () => { let mockDb: { select: MockInstance; }; // select() returns a chain: .from().where() → resolves to an array // We need two calls: one for participants, one for EV rows. let selectCallCount: number; beforeEach(async () => { selectCallCount = 0; const { database } = await import("~/database/context"); mockDb = { select: vi.fn(), }; (database as unknown as MockInstance).mockReturnValue(mockDb); }); function setupMockDb( participantIds: string[], opts: { includeOdds?: boolean } = {} ) { const participants = makeParticipants(participantIds); const evRows = makeEvRows(participantIds, opts); mockDb.select.mockImplementation(() => { const callIndex = selectCallCount++; const data = callIndex === 0 ? participants : evRows; return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data), }), }; }); } it("returns one result per participant", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); expect(results).toHaveLength(12); }); it("all probabilities are between 0 and 1", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); for (const r of results) { const p = r.probabilities; expect(p.probFirst).toBeGreaterThanOrEqual(0); expect(p.probFirst).toBeLessThanOrEqual(1); expect(p.probSecond).toBeGreaterThanOrEqual(0); expect(p.probSecond).toBeLessThanOrEqual(1); } }); it("top-rated team has highest champion probability", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); const byId = new Map(results.map((r) => [r.participantId, r])); const team1Prob = byId.get("team-1")!.probabilities.probFirst; const team12Prob = byId.get("team-12")!.probabilities.probFirst; expect(team1Prob).toBeGreaterThan(team12Prob); }); it("probFirst sums to approximately 1.0 across all teams", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); const total = results.reduce((sum, r) => sum + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probSecond sums to approximately 1.0 across all teams", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); const total = results.reduce((sum, r) => sum + r.probabilities.probSecond, 0); expect(total).toBeCloseTo(1.0, 1); }); it("probThird equals probFourth for every team (tied SF placement)", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); for (const r of results) { expect(r.probabilities.probThird).toBeCloseTo(r.probabilities.probFourth, 10); } }); it("probFifth through probEighth are equal for every team (tied QF placement)", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); for (const r of results) { const p = r.probabilities; expect(p.probFifth).toBeCloseTo(p.probSixth, 10); expect(p.probSixth).toBeCloseTo(p.probSeventh, 10); expect(p.probSeventh).toBeCloseTo(p.probEighth, 10); } }); it("weakest team has very low champion probability", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); // team-12 (Elo 1150) vs team-1 (Elo 1700): team-12 should rarely win const byId = new Map(results.map((r) => [r.participantId, r])); expect(byId.get("team-12")!.probabilities.probFirst).toBeLessThan(0.02); }); it("source is cfp_monte_carlo", async () => { setupMockDb(TEAM_IDS); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); for (const r of results) { expect(r.source).toBe("cfp_monte_carlo"); } }); it("throws when no participants found", async () => { mockDb.select.mockReturnValue({ from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]), }), }); const sim = new NCAAFootballSimulator(); await expect(sim.simulate("season-1")).rejects.toThrow(/No participants found/); }); it("throws when fewer than 12 participants are provided", async () => { const elevenTeams = TEAM_IDS.slice(0, 11); let call = 0; mockDb.select.mockImplementation(() => { const data = call++ === 0 ? makeParticipants(elevenTeams) : makeEvRows(elevenTeams); return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) }; }); const sim = new NCAAFootballSimulator(); await expect(sim.simulate("season-1")).rejects.toThrow(/12 participants/); }); it("uses futures odds when sourceOdds present (does not throw)", async () => { setupMockDb(TEAM_IDS, { includeOdds: true }); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); // Should still return 12 results with valid probabilities expect(results).toHaveLength(12); const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0); expect(total).toBeCloseTo(1.0, 1); }); it("teams outside top 12 have all-zero probabilities (15-team season)", async () => { // 15 participants — only top 12 enter the bracket, bottom 3 stay at 0 EV const fifteenTeams = Array.from({ length: 15 }, (_, i) => `team-${i + 1}`); let call = 0; mockDb.select.mockImplementation(() => { const data = call++ === 0 ? makeParticipants(fifteenTeams) : makeEvRows(fifteenTeams); return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) }; }); const sim = new NCAAFootballSimulator(); const results = await sim.simulate("season-1"); expect(results).toHaveLength(15); for (const id of ["team-13", "team-14", "team-15"]) { const r = results.find((x) => x.participantId === id)!; expect(r.probabilities.probFirst).toBe(0); expect(r.probabilities.probSecond).toBe(0); expect(r.probabilities.probThird).toBe(0); expect(r.probabilities.probEighth).toBe(0); } }); });