import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest"; import { countSeasonRaces, hasRaceRun } from "../season-races"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); const NOW = new Date("2026-08-17T12:00:00.000Z"); const TODAY = "2026-08-17"; interface EventRow { eventType: string; isComplete: boolean; eventDate: string | null; eventStartsAt: Date | null; } function makeEvent(overrides: Partial = {}): EventRow { return { eventType: "schedule_event", isComplete: false, eventDate: null, eventStartsAt: null, ...overrides, }; } async function mockEvents(events: EventRow[]) { const { database } = await import("~/database/context"); (database as unknown as MockInstance).mockReturnValue({ query: { scoringEvents: { findMany: vi.fn().mockResolvedValue(events), }, }, }); } beforeEach(async () => { await mockEvents([]); }); describe("hasRaceRun", () => { it("trusts isComplete when an admin has set it", () => { expect( hasRaceRun( { isComplete: true, eventDate: "2026-12-31", eventStartsAt: null }, NOW, TODAY ) ).toBe(true); }); it("prefers eventStartsAt over eventDate", () => { // Started yesterday and long finished, even though eventDate is unset. expect( hasRaceRun( { isComplete: false, eventDate: null, eventStartsAt: new Date("2026-08-16T18:00:00.000Z"), }, NOW, TODAY ) ).toBe(true); expect( hasRaceRun( { isComplete: false, eventDate: TODAY, eventStartsAt: new Date("2026-08-17T18:00:00.000Z"), }, NOW, TODAY ) ).toBe(false); }); it("does not call a race run the moment it goes green", () => { // Declaring the finale finished at the green flag would publish the // pre-race leader as champion at 100%, from standings without that race. const greenFlag = new Date(NOW.getTime() - 30 * 60 * 1000); expect( hasRaceRun({ isComplete: false, eventDate: TODAY, eventStartsAt: greenFlag }, NOW, TODAY) ).toBe(false); }); it("counts a race run once it has had time to finish", () => { const greenFlag = new Date(NOW.getTime() - 7 * 60 * 60 * 1000); expect( hasRaceRun({ isComplete: false, eventDate: TODAY, eventStartsAt: greenFlag }, NOW, TODAY) ).toBe(true); }); it("still honours isComplete for a race that just started", () => { const greenFlag = new Date(NOW.getTime() - 30 * 60 * 1000); expect( hasRaceRun({ isComplete: true, eventDate: TODAY, eventStartsAt: greenFlag }, NOW, TODAY) ).toBe(true); }); it("treats a past date as run even when nobody marked it complete", () => { expect( hasRaceRun( { isComplete: false, eventDate: "2026-08-16", eventStartsAt: null }, NOW, TODAY ) ).toBe(true); }); it("treats a race happening today as still upcoming", () => { expect( hasRaceRun({ isComplete: false, eventDate: TODAY, eventStartsAt: null }, NOW, TODAY) ).toBe(false); }); it("treats an undated row as upcoming", () => { expect( hasRaceRun({ isComplete: false, eventDate: null, eventStartsAt: null }, NOW, TODAY) ).toBe(false); }); }); describe("countSeasonRaces", () => { it("counts schedule_event rows as races", async () => { // This is the whole bug: a season_standings calendar is stored as // schedule_event rows, and the simulator used to skip them. await mockEvents([ makeEvent({ eventDate: "2026-03-01" }), makeEvent({ eventDate: "2026-04-01" }), makeEvent({ eventDate: "2026-09-01" }), ]); expect(await countSeasonRaces("s1", NOW)).toEqual({ completed: 2, remaining: 1, total: 3, }); }); it("excludes the final_standings scoring row", async () => { await mockEvents([ makeEvent({ eventDate: "2026-03-01" }), makeEvent({ eventType: "final_standings", eventDate: "2026-11-01" }), ]); expect(await countSeasonRaces("s1", NOW)).toEqual({ completed: 1, remaining: 0, total: 1, }); }); it("counts other event types too, whichever type the admin used", async () => { await mockEvents([ makeEvent({ eventType: "major_tournament", eventDate: "2026-03-01" }), makeEvent({ eventType: "playoff_game", eventDate: "2026-09-01" }), ]); expect(await countSeasonRaces("s1", NOW)).toEqual({ completed: 1, remaining: 1, total: 2, }); }); it("returns zeroes when the season has no events", async () => { expect(await countSeasonRaces("s1", NOW)).toEqual({ completed: 0, remaining: 0, total: 0, }); }); it("counts a realistic late-season IndyCar calendar", async () => { const calendar = [ ...Array.from({ length: 15 }, (_, i) => makeEvent({ eventDate: `2026-0${((i % 6) + 3)}-0${(i % 9) + 1}` }) ), // The next race goes green in a few hours — still remaining. makeEvent({ eventStartsAt: new Date("2026-08-17T18:00:00.000Z") }), makeEvent({ eventStartsAt: new Date("2026-08-30T18:00:00.000Z") }), makeEvent({ eventType: "final_standings" }), ]; await mockEvents(calendar); expect(await countSeasonRaces("s1", NOW)).toEqual({ completed: 15, remaining: 2, total: 17, }); }); });