import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); import { runBackfill } from "../backfill-canonical-layer"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; /** * Fixture rows used across multiple tests. */ const SPORT_ID = "sport-golf"; const SEASON_ID = "season-golf-2026"; const GOLF_SEASON = { id: SEASON_ID, sportId: SPORT_ID, scoringPattern: "qualifying_points", } as const; function makeEvent(overrides: Partial> = {}) { return { id: "event-1", sportsSeasonId: SEASON_ID, tournamentId: null, name: "Masters Tournament", eventDate: "2026-04-09", eventType: "tournament", ...overrides, }; } function makeSeasonParticipant( overrides: Partial> = {}, ) { return { id: "sp-1", sportsSeasonId: SEASON_ID, participantId: null, name: "Scottie Scheffler", ...overrides, }; } /** * Builds a fake drizzle db that routes select/insert/update calls based * on the target table. The caller supplies per-table select-result * arrays (one per call to that table's select()). Inserts return the * value they were given, extended with a stub id. Updates are recorded * but otherwise no-op. */ interface TableState { /** Queue of results to return for successive select() calls on this table. */ selects?: unknown[][]; /** Rows that insert().values().returning() should yield. */ insertReturns?: unknown[]; /** Incremented on each update() call. */ updates?: { count: number }; } interface FakeDbOptions { tables: Map; /** Records every insert call: tableRef → rows seen. */ inserts?: Map; /** Records every update call: tableRef → count. */ updateCounts?: Map; } function makeFakeDb(opts: FakeDbOptions) { const { tables, inserts, updateCounts } = opts; // Each select() starts a new chain that ultimately resolves to the // next queued selects[] entry for the passed table. The chain is a // thenable via .limit/.where resolution. const db = { select: vi.fn().mockImplementation(() => { let boundTable: unknown; const chain: Record = { from: vi.fn().mockImplementation((table: unknown) => { boundTable = table; return chain; }), where: vi.fn().mockImplementation(() => chain), limit: vi.fn().mockImplementation(() => chain), // Terminal: make chain thenable so `await chain` yields the queued result. then: (resolve: (v: unknown) => unknown) => { const state = tables.get(boundTable); const queue = state?.selects ?? []; const next = queue.shift() ?? []; return Promise.resolve(next).then(resolve); }, }; return chain; }), insert: vi.fn().mockImplementation((table: unknown) => { return { values: vi.fn().mockImplementation((row: unknown) => { if (inserts) { const seen = inserts.get(table) ?? []; seen.push(row); inserts.set(table, seen); } const state = tables.get(table); const returnRows = state?.insertReturns ?? [ { ...(row as object), id: `generated-${Math.random()}` }, ]; const afterValues = { returning: vi.fn().mockResolvedValue(returnRows), // If the caller doesn't chain .returning(), make it awaitable anyway. then: (resolve: (v: unknown) => unknown) => Promise.resolve(returnRows).then(resolve), }; return afterValues; }), }; }), update: vi.fn().mockImplementation((table: unknown) => { if (updateCounts) { updateCounts.set(table, (updateCounts.get(table) ?? 0) + 1); } return { set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined), }), }; }), }; return db; } beforeEach(() => { vi.clearAllMocks(); }); // ─── 1. empty DB no-op ───────────────────────────────────────────────────── describe("runBackfill - empty DB", () => { it("returns zero counts when no qualifying-points seasons exist", async () => { const tables = new Map([ [schema.sportsSeasons, { selects: [[]] }], ]); vi.mocked(database).mockReturnValue(makeFakeDb({ tables }) as never); const report = await runBackfill({ dryRun: false }); expect(report).toMatchObject({ tournamentsCreated: 0, tournamentsLinked: 0, participantsCreated: 0, participantsLinked: 0, tournamentResultsCreated: 0, surfaceElosCreated: 0, errors: [], }); }); }); // ─── 2. golf window with 4 events creates 4 tournaments ─────────────────── describe("runBackfill - golf tournaments", () => { it("creates 4 canonical tournaments for a golf season with 4 events", async () => { const events = [ makeEvent({ id: "ev-1", name: "Masters Tournament", eventDate: "2026-04-09" }), makeEvent({ id: "ev-2", name: "PGA Championship", eventDate: "2026-05-14" }), makeEvent({ id: "ev-3", name: "U.S. Open", eventDate: "2026-06-18" }), makeEvent({ id: "ev-4", name: "The Open Championship", eventDate: "2026-07-16" }), ]; const inserts = new Map(); // Build per-event insertReturns so each new tournament gets its id. const tables = new Map([ [schema.sportsSeasons, { selects: [[GOLF_SEASON]] }], [ schema.scoringEvents, { // 1st select: unlinked events; 2nd: refetch for results loop. selects: [events, events.map((e, i) => ({ ...e, tournamentId: `t-${i + 1}` }))], }, ], [ schema.tournaments, { // One select-miss per event, all return [] (no existing row). selects: [[], [], [], []], // One insert per event; return sequential ids. insertReturns: [{ id: "t-1" }], }, ], [schema.seasonParticipants, { selects: [[]] }], [schema.eventResults, { selects: [[], [], [], []] }], [schema.seasonParticipantSurfaceElos, { selects: [[]] }], ]); // Because our insertReturns is consumed once per test, re-queue per event // by overriding insert behaviour via the fake db options. const tournamentIds = ["t-1", "t-2", "t-3", "t-4"]; let insertIdx = 0; const db: ReturnType = makeFakeDb({ tables, inserts }); // Override insert for tournaments specifically to return sequential ids. db.insert.mockImplementation((table: unknown) => ({ values: vi.fn().mockImplementation((row: unknown) => { const seen = inserts.get(table) ?? []; seen.push(row); inserts.set(table, seen); let returnRows: unknown[]; if (table === schema.tournaments) { returnRows = [{ ...(row as object), id: tournamentIds[insertIdx++] }]; } else { returnRows = [{ ...(row as object), id: "x" }]; } return { returning: vi.fn().mockResolvedValue(returnRows), then: (resolve: (v: unknown) => unknown) => Promise.resolve(returnRows).then(resolve), }; }), })); vi.mocked(database).mockReturnValue(db as never); const report = await runBackfill({ dryRun: false }); expect(report.tournamentsCreated).toBe(4); expect(report.tournamentsLinked).toBe(4); expect(inserts.get(schema.tournaments)).toHaveLength(4); expect(report.errors).toEqual([]); }); }); // ─── 3. re-running doesn't duplicate ────────────────────────────────────── describe("runBackfill - idempotence", () => { it("does not create new tournaments when they already exist", async () => { const events = [ makeEvent({ id: "ev-1", name: "Masters Tournament", eventDate: "2026-04-09" }), ]; const existingTournament = { id: "t-existing", sportId: SPORT_ID, name: "Masters Tournament", year: 2026, }; const inserts = new Map(); const updateCounts = new Map(); const tables = new Map([ [schema.sportsSeasons, { selects: [[GOLF_SEASON]] }], [ schema.scoringEvents, { selects: [events, [{ ...events[0], tournamentId: "t-existing" }]], }, ], [schema.tournaments, { selects: [[existingTournament]] }], [schema.seasonParticipants, { selects: [[]] }], [schema.eventResults, { selects: [[]] }], [schema.seasonParticipantSurfaceElos, { selects: [[]] }], ]); vi.mocked(database).mockReturnValue( makeFakeDb({ tables, inserts, updateCounts }) as never, ); const report = await runBackfill({ dryRun: false }); expect(report.tournamentsCreated).toBe(0); expect(report.tournamentsLinked).toBe(1); expect(inserts.get(schema.tournaments)).toBeUndefined(); }); }); // ─── 4. participant backfill ─────────────────────────────────────────────── describe("runBackfill - participants", () => { it("creates canonical participants and links season_participants", async () => { const sps = [ makeSeasonParticipant({ id: "sp-1", name: "Scottie Scheffler" }), makeSeasonParticipant({ id: "sp-2", name: "Rory McIlroy" }), ]; const inserts = new Map(); const updateCounts = new Map(); const pIds = ["p-1", "p-2"]; let pIdx = 0; const tables = new Map([ [schema.sportsSeasons, { selects: [[GOLF_SEASON]] }], [schema.scoringEvents, { selects: [[], []] }], [schema.seasonParticipants, { selects: [sps] }], [schema.participants, { selects: [[], []] }], [schema.eventResults, { selects: [] }], [schema.seasonParticipantSurfaceElos, { selects: [[]] }], ]); const db = makeFakeDb({ tables, inserts, updateCounts }); db.insert.mockImplementation((table: unknown) => ({ values: vi.fn().mockImplementation((row: unknown) => { const seen = inserts.get(table) ?? []; seen.push(row); inserts.set(table, seen); let returnRows: unknown[]; if (table === schema.participants) { returnRows = [{ ...(row as object), id: pIds[pIdx++] }]; } else { returnRows = [{ ...(row as object), id: "x" }]; } return { returning: vi.fn().mockResolvedValue(returnRows), then: (resolve: (v: unknown) => unknown) => Promise.resolve(returnRows).then(resolve), }; }), })); vi.mocked(database).mockReturnValue(db as never); const report = await runBackfill({ dryRun: false }); expect(report.participantsCreated).toBe(2); expect(report.participantsLinked).toBe(2); expect(inserts.get(schema.participants)).toHaveLength(2); expect(updateCounts.get(schema.seasonParticipants)).toBe(2); }); }); // ─── 5. Masters 2026 round-trip ─────────────────────────────────────────── describe("runBackfill - tournament_results from event_results", () => { it("copies placement and rawScore but NOT qualifying_points_awarded", async () => { const events = [ makeEvent({ id: "ev-masters", tournamentId: "t-masters", name: "Masters Tournament", eventDate: "2026-04-09", }), ]; const seasonParticipant = { id: "sp-1", sportsSeasonId: SEASON_ID, participantId: "p-scottie", name: "Scottie Scheffler", }; const eventResult = { id: "er-1", scoringEventId: "ev-masters", seasonParticipantId: "sp-1", placement: 1, rawScore: "-12.00", qualifyingPointsAwarded: "100.00", }; const inserts = new Map(); const tables = new Map([ [schema.sportsSeasons, { selects: [[GOLF_SEASON]] }], [ schema.scoringEvents, { // 1st: no unlinked events (tournamentId already set) // 2nd: refetch — events linked to tournament selects: [[], events], }, ], [schema.tournaments, { selects: [] }], [schema.seasonParticipants, { selects: [[], [seasonParticipant]] }], [schema.participants, { selects: [] }], [schema.eventResults, { selects: [[eventResult]] }], [schema.tournamentResults, { selects: [[]] }], [schema.seasonParticipantSurfaceElos, { selects: [[]] }], ]); vi.mocked(database).mockReturnValue( makeFakeDb({ tables, inserts }) as never, ); const report = await runBackfill({ dryRun: false }); expect(report.tournamentResultsCreated).toBe(1); const trInserts = inserts.get(schema.tournamentResults) ?? []; expect(trInserts).toHaveLength(1); const inserted = trInserts[0] as Record; expect(inserted.tournamentId).toBe("t-masters"); expect(inserted.participantId).toBe("p-scottie"); expect(inserted.placement).toBe(1); expect(inserted.rawScore).toBe("-12.00"); // CRITICAL: qualifying_points_awarded must NOT be copied. expect(inserted).not.toHaveProperty("qualifyingPointsAwarded"); }); }); // ─── 6. Surface elo conflict detection ──────────────────────────────────── describe("runBackfill - surface elo conflicts", () => { it("records a conflict error when two windows disagree on eloHard", async () => { const sp = { id: "sp-1", sportsSeasonId: SEASON_ID, participantId: "p-tennis-1", name: "Carlos Alcaraz", }; const windowElo = { id: "se-1", participantId: "sp-1", sportsSeasonId: SEASON_ID, eloHard: 2050, eloClay: 2100, eloGrass: 2000, worldRanking: 2, }; const existingCanonicalElo = { id: "cpe-1", participantId: "p-tennis-1", eloHard: 2000, // differs! eloClay: 2100, eloGrass: 2000, worldRanking: 2, }; const inserts = new Map(); const tables = new Map([ [schema.sportsSeasons, { selects: [[GOLF_SEASON]] }], [schema.scoringEvents, { selects: [[], []] }], [schema.seasonParticipants, { selects: [[], [sp]] }], [schema.participants, { selects: [] }], [schema.eventResults, { selects: [] }], [schema.seasonParticipantSurfaceElos, { selects: [[windowElo]] }], [schema.participantSurfaceElos, { selects: [[existingCanonicalElo]] }], ]); vi.mocked(database).mockReturnValue( makeFakeDb({ tables, inserts }) as never, ); const report = await runBackfill({ dryRun: false }); expect(report.surfaceElosCreated).toBe(0); expect(report.errors).toHaveLength(1); expect(report.errors[0]).toMatch(/conflict for participant p-tennis-1/); expect(report.errors[0]).toMatch(/eloHard/); // Must not have inserted a conflicting row. expect(inserts.get(schema.participantSurfaceElos)).toBeUndefined(); }); });