import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest"; import { EPLSimulator, eplWinProbability, simEplMatch } from "../epl-simulator"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/models/regular-season-standings", () => ({ getRegularSeasonStandings: vi.fn(), })); vi.mock("~/models/simulator", () => ({ getSportsSeasonSimulatorConfig: vi.fn(), })); const EPL_TEAMS = [ "Arsenal", "Manchester City", "Liverpool", "Chelsea", "Tottenham Hotspur", "Manchester United", "Newcastle United", "Aston Villa", "Brighton & Hove Albion", "West Ham United", "Crystal Palace", "Fulham", "Everton", "Bournemouth", "Brentford", "Wolverhampton Wanderers", "Nottingham Forest", "Leeds United", "Burnley", "Sunderland", ]; const PARTICIPANT_ROWS = EPL_TEAMS.map((name, i) => ({ id: `team-${i + 1}`, name })); const EV_ROWS = PARTICIPANT_ROWS.map((team, i) => ({ participantId: team.id, sourceElo: 1760 - i * 25, sourceOdds: null, })); function mockSelects(participantRows = PARTICIPANT_ROWS, evRows = EV_ROWS) { let selectCallCount = 0; return vi.fn().mockImplementation(() => { selectCallCount++; if (selectCallCount === 1) { return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(participantRows) }) }; } return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(evRows) }) }; }); } describe("EPL match helpers", () => { it("returns 0.5 win probability for equal Elo ratings", () => { expect(eplWinProbability(1500, 1500, 400)).toBeCloseTo(0.5, 6); }); it("supports a configurable match parity factor", () => { const standard = eplWinProbability(1700, 1500, 400); const higherParity = eplWinProbability(1700, 1500, 1000); expect(standard).toBeGreaterThan(higherParity); expect(higherParity).toBeGreaterThan(0.5); }); it("equal Elo teams draw at a realistic rate", () => { let draws = 0; const N = 5000; for (let i = 0; i < N; i++) { if (simEplMatch(1500, 1500, { matchParityFactor: 400 }) === "draw") draws++; } const drawRate = draws / N; expect(drawRate).toBeGreaterThan(0.21); expect(drawRate).toBeLessThan(0.31); }); }); describe("EPLSimulator.simulate()", () => { let mockDb: { select: MockInstance }; beforeEach(async () => { const { database } = await import("~/database/context"); const { getRegularSeasonStandings } = await import("~/models/regular-season-standings"); const { getSportsSeasonSimulatorConfig } = await import("~/models/simulator"); mockDb = { select: mockSelects() }; (database as unknown as MockInstance).mockReturnValue(mockDb); (getRegularSeasonStandings as unknown as MockInstance).mockResolvedValue([]); (getSportsSeasonSimulatorConfig as unknown as MockInstance).mockResolvedValue({ config: { iterations: 500, matchParityFactor: 400 }, }); }); it("throws if no participants found", async () => { mockDb.select = mockSelects([], []); await expect(new EPLSimulator().simulate("season-1")).rejects.toThrow(/No participants found/); }); it("returns one result per EPL club and normalizes top-eight columns", async () => { const results = await new EPLSimulator().simulate("season-1"); expect(results).toHaveLength(20); const keys = [ "probFirst", "probSecond", "probThird", "probFourth", "probFifth", "probSixth", "probSeventh", "probEighth", ] as const; for (const key of keys) { const colSum = results.reduce((sum, result) => sum + result.probabilities[key], 0); expect(colSum, `${key} column sum`).toBeCloseTo(1.0, 8); } for (const result of results) { expect(result.source).toBe("epl_standings_monte_carlo"); for (const value of Object.values(result.probabilities)) { expect(value).toBeGreaterThanOrEqual(0); expect(value).toBeLessThanOrEqual(1); } } }); it("higher Elo teams have stronger title probability", async () => { const results = await new EPLSimulator().simulate("season-1"); const top = results.find((result) => result.participantId === "team-1"); const bottom = results.find((result) => result.participantId === "team-20"); if (!top || !bottom) throw new Error("Expected test teams not found"); expect(top.probabilities.probFirst).toBeGreaterThan(bottom.probabilities.probFirst); }); it("uses current EPL standings when present", async () => { const { getRegularSeasonStandings } = await import("~/models/regular-season-standings"); (getRegularSeasonStandings as unknown as MockInstance).mockResolvedValue([ { participantId: "team-20", wins: 25, ties: 5, losses: 0, gamesPlayed: 30, tablePoints: 80, goalsFor: 80, goalsAgainst: 20, goalDifference: 60, leagueRank: 1, }, ...PARTICIPANT_ROWS.slice(0, 19).map((team) => ({ participantId: team.id, wins: 8, ties: 5, losses: 17, gamesPlayed: 30, tablePoints: 29, goalsFor: 35, goalsAgainst: 55, goalDifference: -20, leagueRank: 10, })), ]); const results = await new EPLSimulator().simulate("season-1"); const leader = results.find((result) => result.participantId === "team-20"); if (!leader) throw new Error("Expected standings leader not found"); expect(leader.probabilities.probFirst).toBeGreaterThan(0.95); }); it("throws when Elo/projected points are missing", async () => { mockDb.select = mockSelects(PARTICIPANT_ROWS, []); await expect(new EPLSimulator().simulate("season-1")).rejects.toThrow(/Missing Elo\/projected points/); }); });