import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest"; import { AutoRacingSimulator } from "../auto-racing-simulator"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/models/participant-season-result", () => ({ getSeasonResults: vi.fn(), })); vi.mock("~/models/participant-expected-value", () => ({ getAllParticipantEVsForSeason: vi.fn(), })); // ─── F1 race points (positions 1–10) ───────────────────────────────────────── const F1_RACE_POINTS: Record = { 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1, }; // ─── Fixtures ───────────────────────────────────────────────────────────────── const DRIVERS = ["d1", "d2", "d3", "d4", "d5"].map((id) => ({ id })); function makeEvent(isComplete: boolean, eventType = "race") { return { isComplete, eventType }; } function makeSeasonResult(participantId: string, currentPoints: string) { return { participant: { id: participantId }, currentPoints }; } function makeEv(participantId: string, sourceOdds: number | null) { return { participantId, sourceOdds }; } function mockDb(events: ReturnType[]) { return { query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue(DRIVERS), }, scoringEvents: { findMany: vi.fn().mockResolvedValue(events), }, }, }; } // ─── Setup ──────────────────────────────────────────────────────────────────── let db: ReturnType; beforeEach(async () => { const { database } = await import("~/database/context"); const { getSeasonResults } = await import("~/models/participant-season-result"); const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); db = mockDb([]); (database as unknown as MockInstance).mockReturnValue(db); (getSeasonResults as unknown as MockInstance).mockResolvedValue([]); (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([]); }); // ─── Tests ──────────────────────────────────────────────────────────────────── describe("AutoRacingSimulator", () => { it("throws when no participants are found", async () => { db.query.seasonParticipants.findMany.mockResolvedValue([]); await expect( new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1") ).rejects.toThrow(/No participants found/); }); describe("pre-season path (remainingRaces === 0)", () => { beforeEach(async () => { const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); // No scoring events → remainingRaces = 0 db = mockDb([]); const { database } = await import("~/database/context"); (database as unknown as MockInstance).mockReturnValue(db); // Heavy favourite: d1 at −500, all others at +1000 (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ makeEv("d1", -500), makeEv("d2", 1000), makeEv("d3", 1000), makeEv("d4", 1000), makeEv("d5", 1000), ]); }); it("returns one result per driver", async () => { const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); expect(results).toHaveLength(5); }); it("normalizes each position column to sum to 1.0", async () => { const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); const keys = [ "probFirst", "probSecond", "probThird", "probFourth", "probFifth", "probSixth", "probSeventh", "probEighth", ] as const; for (const key of keys) { const sum = results.reduce((s, r) => s + r.probabilities[key], 0); expect(sum, `${key} column sum`).toBeCloseTo(1.0, 6); } }); it("heavy favourite ranks first more often than long shots", async () => { const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); const favourite = results.find((r) => r.participantId === "d1"); const longShot = results.find((r) => r.participantId === "d2"); expect(favourite).toBeDefined(); expect(longShot).toBeDefined(); if (!favourite || !longShot) return; expect(favourite.probabilities.probFirst).toBeGreaterThan(longShot.probabilities.probFirst); }); it("drivers without odds get equal fallback probability", async () => { const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([]); const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); // With equal weights all 5 drivers should finish 1st roughly equally for (const r of results) { expect(r.probabilities.probFirst).toBeGreaterThan(0.1); expect(r.probabilities.probFirst).toBeLessThan(0.3); } }); }); describe("in-season path (remainingRaces > 0)", () => { beforeEach(async () => { const { database } = await import("~/database/context"); // 10 completed races, 5 remaining db = mockDb([ ...Array.from({ length: 10 }, () => makeEvent(true)), ...Array.from({ length: 5 }, () => makeEvent(false)), ]); (database as unknown as MockInstance).mockReturnValue(db); }); it("normalizes each position column to sum to 1.0", async () => { const { getSeasonResults } = await import("~/models/participant-season-result"); (getSeasonResults as unknown as MockInstance).mockResolvedValue( DRIVERS.map((d, i) => makeSeasonResult(d.id, String((5 - i) * 50))) ); const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); const keys = [ "probFirst", "probSecond", "probThird", "probFourth", "probFifth", "probSixth", "probSeventh", "probEighth", ] as const; for (const key of keys) { const sum = results.reduce((s, r) => s + r.probabilities[key], 0); expect(sum, `${key} column sum`).toBeCloseTo(1.0, 6); } }); it("standings leader ranks higher than a driver far behind when standings dominate", async () => { // 20/25 races done → seasonProgress = 0.8 → standings weighted 80% const { database } = await import("~/database/context"); db = mockDb([ ...Array.from({ length: 20 }, () => makeEvent(true)), ...Array.from({ length: 5 }, () => makeEvent(false)), ]); (database as unknown as MockInstance).mockReturnValue(db); const { getSeasonResults } = await import("~/models/participant-season-result"); const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); // d1 leads with 400 pts; d2 is a distant 2nd with 50 pts (getSeasonResults as unknown as MockInstance).mockResolvedValue([ makeSeasonResult("d1", "400"), makeSeasonResult("d2", "50"), makeSeasonResult("d3", "40"), makeSeasonResult("d4", "30"), makeSeasonResult("d5", "20"), ]); // Futures odds heavily favour d2 (pretend markets disagree) (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ makeEv("d1", 5000), // very long shot per futures makeEv("d2", -500), // heavy favourite per futures ]); const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); const leader = results.find((r) => r.participantId === "d1"); const distant = results.find((r) => r.participantId === "d2"); expect(leader).toBeDefined(); expect(distant).toBeDefined(); if (!leader || !distant) return; // Standings signal should dominate: d1's massive points lead wins out expect(leader.probabilities.probFirst).toBeGreaterThan(distant.probabilities.probFirst); }); it("falls back to odds for all drivers when no standings data exists", async () => { // totalCurrentPoints = 0 → standings signal disabled, odds take over const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ makeEv("d1", -500), makeEv("d2", 1000), ]); const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); const fav = results.find((r) => r.participantId === "d1"); const longShot = results.find((r) => r.participantId === "d2"); expect(fav).toBeDefined(); expect(longShot).toBeDefined(); if (!fav || !longShot) return; // Without standings, odds-favoured driver should still rank higher expect(fav.probabilities.probFirst).toBeGreaterThan(longShot.probabilities.probFirst); }); it("a driver with 0 points mid-season is not penalized beyond their odds weight", async () => { // Use 2 completed / 20 remaining → early season, standings gap is small const { database } = await import("~/database/context"); db = mockDb([ ...Array.from({ length: 2 }, () => makeEvent(true)), ...Array.from({ length: 20 }, () => makeEvent(false)), ]); (database as unknown as MockInstance).mockReturnValue(db); const { getSeasonResults } = await import("~/models/participant-season-result"); // d1-d4 have a modest lead; d5 is absent (0 pts, new entry) (getSeasonResults as unknown as MockInstance).mockResolvedValue([ makeSeasonResult("d1", "10"), makeSeasonResult("d2", "8"), makeSeasonResult("d3", "6"), makeSeasonResult("d4", "4"), // d5 intentionally absent → falls back to odds weight ]); const { getAllParticipantEVsForSeason } = await import("~/models/participant-expected-value"); (getAllParticipantEVsForSeason as unknown as MockInstance).mockResolvedValue([ makeEv("d5", -500), // strong odds favourite despite 0 pts ]); const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); // d5 should win championships at a non-trivial rate given their strong odds weight const d5 = results.find((r) => r.participantId === "d5"); expect(d5).toBeDefined(); if (!d5) return; expect(d5.probabilities.probFirst).toBeGreaterThan(0.1); }); it("emits a warning when participants are missing from standings", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const { getSeasonResults } = await import("~/models/participant-season-result"); // Only 3 of 5 drivers have standings rows (getSeasonResults as unknown as MockInstance).mockResolvedValue([ makeSeasonResult("d1", "100"), makeSeasonResult("d2", "80"), makeSeasonResult("d3", "60"), ]); await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining("2 participant(s) missing from standings") ); warnSpy.mockRestore(); }); it("schedule_event entries are excluded from race counts", async () => { const { database } = await import("~/database/context"); // 5 real races + 3 schedule_events (should be ignored) db = mockDb([ ...Array.from({ length: 5 }, () => makeEvent(true)), ...Array.from({ length: 3 }, () => makeEvent(false, "schedule_event")), makeEvent(false), // 1 real remaining ]); (database as unknown as MockInstance).mockReturnValue(db); // Should not throw and should use seasonProgress = 5/6 const results = await new AutoRacingSimulator(F1_RACE_POINTS, "f1").simulate("s1"); expect(results).toHaveLength(5); }); }); });