import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/database/schema", () => ({ sportsSeasons: { id: "ss.id", sportId: "ss.sport_id" }, seasonParticipants: { sportsSeasonId: "p.sports_season_id" }, scoringEvents: { sportsSeasonId: "se.sports_season_id" }, seasonParticipantExpectedValues: { sportsSeasonId: "pev.sports_season_id" }, })); vi.mock("drizzle-orm", () => ({ eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }), sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ type: "sql", strings, values }), lte: (col: unknown, val: unknown) => ({ type: "lte", col, val }), gte: (col: unknown, val: unknown) => ({ type: "gte", col, val }), and: (...args: unknown[]) => ({ type: "and", args }), desc: (col: unknown) => ({ type: "desc", col }), asc: (col: unknown) => ({ type: "asc", col }), })); vi.mock("~/models/qualifying-points", () => ({ getQPConfig: vi.fn(), updateQPConfig: vi.fn(), })); vi.mock("~/models/sports-season-tournament", () => ({ findTournamentsBySportsSeason: vi.fn().mockResolvedValue([]), })); import * as schema from "~/database/schema"; import { cloneSportsSeason, type NewSportsSeason } from "../sports-season"; import { database } from "~/database/context"; import { getQPConfig, updateQPConfig } from "~/models/qualifying-points"; const SOURCE_ID = "source-season-id"; const NEW_ID = "new-season-id"; const sourceSeason = { id: SOURCE_ID, sportId: "sport-1", name: "2025 F1 Season", year: 2025, startDate: "2025-03-16", endDate: "2025-12-07", draftOn: "2025-01-01", draftOff: "2025-03-15", status: "completed", scoringType: "majors", scoringPattern: "season_standings", totalMajors: null, majorsCompleted: 5, qualifyingPointsFinalized: true, eloCalibrationExponent: "0.33", eloMinRating: 1250, eloMaxRating: 1750, simulationStatus: "idle", }; const newSeasonData: NewSportsSeason = { sportId: "sport-1", name: "2026 F1 Season", year: 2026, startDate: "2026-03-16", endDate: "2026-12-07", draftOn: "2026-01-01", draftOff: "2026-03-15", status: "upcoming", simulationStatus: "idle", majorsCompleted: 0, qualifyingPointsFinalized: false, scoringType: "majors", scoringPattern: "season_standings", }; function makeMockDb({ sourceSeason: ss = sourceSeason, participants = [] as object[], scoringEvents = [] as object[], sourceEvRows = [] as object[], insertedSeason = { ...newSeasonData, id: NEW_ID } as object, // New participants returned by the INSERT ... RETURNING — mirrors sources with new IDs by default newParticipantRows = (participants as Array>).map((p, i) => ({ ...p, id: `new-p${i + 1}`, sportsSeasonId: NEW_ID, })) as object[], } = {}) { // Track what was inserted into each table independently of call order const insertedRows: Record = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any const db: any = { query: { sportsSeasons: { findFirst: vi.fn().mockResolvedValue(ss) }, seasonParticipants: { findMany: vi.fn().mockResolvedValue(participants) }, scoringEvents: { findMany: vi.fn().mockResolvedValue(scoringEvents) }, seasonParticipantExpectedValues: { findMany: vi.fn().mockResolvedValue(sourceEvRows) }, }, insert: vi.fn().mockImplementation((table: object) => { let key: string; let returnRows: object[]; if (table === schema.sportsSeasons) { key = "sportsSeasons"; returnRows = [insertedSeason]; } else if (table === schema.seasonParticipants) { key = "participants"; returnRows = newParticipantRows; } else if (table === schema.scoringEvents) { key = "scoringEvents"; returnRows = []; } else { key = "participantExpectedValues"; returnRows = []; } return { values: vi.fn().mockImplementation((vals: unknown) => { insertedRows[key] = vals; return { returning: vi.fn().mockResolvedValue(returnRows) }; }), }; }), // Passes `db` as the transaction object so tracked inserts still work transaction: vi.fn().mockImplementation(async (fn: (tx: typeof db) => Promise) => fn(db)), _insertedRows: insertedRows, }; return db; } beforeEach(() => { vi.clearAllMocks(); }); describe("cloneSportsSeason", () => { it("creates new season with reset fields (status=upcoming, majorsCompleted=0, simulationStatus=idle)", async () => { const db = makeMockDb(); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); expect(db._insertedRows.sportsSeasons).toMatchObject({ name: "2026 F1 Season", year: 2026, status: "upcoming", simulationStatus: "idle", majorsCompleted: 0, qualifyingPointsFinalized: false, }); }); it("merges ELO calibration fields from source season", async () => { const db = makeMockDb(); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); expect(db._insertedRows.sportsSeasons).toMatchObject({ eloCalibrationExponent: "0.33", eloMinRating: 1250, eloMaxRating: 1750, }); }); it("copies participants with name, shortName, externalId — not expectedValue", async () => { const participants = [ { id: "p1", name: "Max Verstappen", shortName: "VER", externalId: "ext-1", expectedValue: "42.5" }, { id: "p2", name: "Lewis Hamilton", shortName: "HAM", externalId: "ext-2", expectedValue: "38.0" }, ]; const db = makeMockDb({ participants }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); const rows = db._insertedRows.participants as object[]; expect(rows).toHaveLength(2); expect(rows[0]).toMatchObject({ sportsSeasonId: NEW_ID, name: "Max Verstappen", shortName: "VER", externalId: "ext-1", }); expect(rows[0]).not.toHaveProperty("expectedValue"); expect(rows[0]).not.toHaveProperty("id"); }); it("skips participant insert when source has no participants", async () => { const db = makeMockDb({ participants: [] }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); expect(db._insertedRows.participants).toBeUndefined(); }); it("copies scoring events with dates shifted by yearDelta", async () => { const scoringEvents = [ { id: "e1", name: "Bahrain GP", eventType: "final_standings", isQualifyingEvent: false, bracketTemplateId: null, scoringStartsAtRound: null, eventDate: "2025-03-16", eventStartsAt: null }, { id: "e2", name: "Abu Dhabi GP", eventType: "final_standings", isQualifyingEvent: false, bracketTemplateId: null, scoringStartsAtRound: null, eventDate: "2025-12-07", eventStartsAt: null }, ]; const db = makeMockDb({ scoringEvents }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); const rows = db._insertedRows.scoringEvents as object[]; expect(rows).toHaveLength(2); expect(rows[0]).toMatchObject({ sportsSeasonId: NEW_ID, name: "Bahrain GP", eventDate: "2026-03-16", isComplete: false, }); expect(rows[1]).toMatchObject({ name: "Abu Dhabi GP", eventDate: "2026-12-07", }); }); it("leaves eventDate null when source event has no date", async () => { const scoringEvents = [ { id: "e1", name: "TBD Event", eventType: "schedule_event", isQualifyingEvent: false, bracketTemplateId: null, scoringStartsAtRound: null, eventDate: null, eventStartsAt: null }, ]; const db = makeMockDb({ scoringEvents }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); const rows = db._insertedRows.scoringEvents as Array<{ eventDate: unknown }>; expect(rows[0].eventDate).toBeNull(); }); it("shifts eventStartsAt timestamp by yearDelta", async () => { const originalDate = new Date("2025-06-15T14:00:00Z"); const scoringEvents = [ { id: "e1", name: "Mid-year Event", eventType: "playoff_game", isQualifyingEvent: false, bracketTemplateId: null, scoringStartsAtRound: null, eventDate: null, eventStartsAt: originalDate }, ]; const db = makeMockDb({ scoringEvents }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); const rows = db._insertedRows.scoringEvents as Array<{ eventStartsAt: Date }>; const shiftedDate = rows[0].eventStartsAt; expect(shiftedDate.getUTCFullYear()).toBe(2026); expect(shiftedDate.getUTCMonth()).toBe(originalDate.getUTCMonth()); expect(shiftedDate.getUTCDate()).toBe(originalDate.getUTCDate()); }); it("copies futures odds as stub EV records matched by externalId", async () => { const participants = [ { id: "src-p1", name: "Max Verstappen", shortName: "VER", externalId: "ext-1" }, { id: "src-p2", name: "Lewis Hamilton", shortName: "HAM", externalId: "ext-2" }, ]; const sourceEvRows = [ { participantId: "src-p1", sportsSeasonId: SOURCE_ID, source: "futures_odds", sourceOdds: -120, sourceElo: null, worldRanking: null }, { participantId: "src-p2", sportsSeasonId: SOURCE_ID, source: "futures_odds", sourceOdds: 300, sourceElo: null, worldRanking: null }, ]; const newParticipantRows = [ { id: "new-p1", name: "Max Verstappen", externalId: "ext-1", sportsSeasonId: NEW_ID }, { id: "new-p2", name: "Lewis Hamilton", externalId: "ext-2", sportsSeasonId: NEW_ID }, ]; const db = makeMockDb({ participants, sourceEvRows, newParticipantRows }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); const evRows = db._insertedRows.participantExpectedValues as Array>; expect(evRows).toHaveLength(2); expect(evRows[0]).toMatchObject({ participantId: "new-p1", sportsSeasonId: NEW_ID, source: "futures_odds", sourceOdds: -120, sourceElo: null, expectedValue: "0", probFirst: "0", }); expect(evRows[1]).toMatchObject({ participantId: "new-p2", sourceOdds: 300 }); }); it("copies Elo ratings and world rankings as stub EV records", async () => { const participants = [ { id: "src-p1", name: "Player A", shortName: "PA", externalId: "ext-1" }, ]; const sourceEvRows = [ { participantId: "src-p1", sportsSeasonId: SOURCE_ID, source: "elo_simulation", sourceOdds: null, sourceElo: 1650, worldRanking: 3 }, ]; const newParticipantRows = [ { id: "new-p1", name: "Player A", externalId: "ext-1", sportsSeasonId: NEW_ID }, ]; const db = makeMockDb({ participants, sourceEvRows, newParticipantRows }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); const evRows = db._insertedRows.participantExpectedValues as Array>; expect(evRows).toHaveLength(1); expect(evRows[0]).toMatchObject({ participantId: "new-p1", source: "elo_simulation", sourceElo: 1650, worldRanking: 3, sourceOdds: null, }); }); it("falls back to name matching when externalId is null", async () => { const participants = [ { id: "src-p1", name: "Player A", shortName: "PA", externalId: null }, ]; const sourceEvRows = [ { participantId: "src-p1", sportsSeasonId: SOURCE_ID, source: "futures_odds", sourceOdds: 200, sourceElo: null, worldRanking: null }, ]; const newParticipantRows = [ { id: "new-p1", name: "Player A", externalId: null, sportsSeasonId: NEW_ID }, ]; const db = makeMockDb({ participants, sourceEvRows, newParticipantRows }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); const evRows = db._insertedRows.participantExpectedValues as Array>; expect(evRows).toHaveLength(1); expect(evRows[0]).toMatchObject({ participantId: "new-p1", sourceOdds: 200 }); }); it("skips EV copy when source has no odds or Elo data", async () => { const participants = [ { id: "src-p1", name: "Player A", shortName: "PA", externalId: "ext-1" }, ]; // EV row exists but has no input data — e.g. only calculated probabilities const sourceEvRows = [ { participantId: "src-p1", sportsSeasonId: SOURCE_ID, source: "manual", sourceOdds: null, sourceElo: null, worldRanking: null }, ]; const db = makeMockDb({ participants, sourceEvRows }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); expect(db._insertedRows.participantExpectedValues).toBeUndefined(); }); it("skips EV copy when source season has no EV rows", async () => { const participants = [ { id: "src-p1", name: "Player A", shortName: "PA", externalId: "ext-1" }, ]; const db = makeMockDb({ participants, sourceEvRows: [] }); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); expect(db._insertedRows.participantExpectedValues).toBeUndefined(); }); it("copies QP config when scoringPattern is qualifying_points", async () => { const qpSeason: NewSportsSeason = { ...newSeasonData, scoringPattern: "qualifying_points", totalMajors: 4, }; const mockQPConfig = [ { placement: 1, points: "20" }, { placement: 2, points: "14" }, ]; vi.mocked(getQPConfig).mockResolvedValue(mockQPConfig as never); vi.mocked(updateQPConfig).mockResolvedValue([] as never); const db = makeMockDb(); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, qpSeason); expect(getQPConfig).toHaveBeenCalledWith(SOURCE_ID, db); expect(updateQPConfig).toHaveBeenCalledWith( NEW_ID, [ { placement: 1, points: 20 }, { placement: 2, points: 14 }, ], db ); }); it("does NOT copy QP config for non-QP seasons", async () => { const db = makeMockDb(); vi.mocked(database).mockReturnValue(db as never); await cloneSportsSeason(SOURCE_ID, newSeasonData); expect(getQPConfig).not.toHaveBeenCalled(); expect(updateQPConfig).not.toHaveBeenCalled(); }); it("throws when source season is not found", async () => { const db = makeMockDb(); db.query.sportsSeasons.findFirst = vi.fn().mockResolvedValue(undefined); vi.mocked(database).mockReturnValue(db as never); await expect(cloneSportsSeason("nonexistent-id", newSeasonData)).rejects.toThrow( "Source season nonexistent-id not found" ); }); });