import { describe, it, expect, vi, beforeEach } from "vitest"; // Mock the database context before importing any model vi.mock("~/database/context", () => ({ database: vi.fn(), })); import { database } from "~/database/context"; import { getSurfaceEloMap, batchUpsertSurfaceElos, getSurfaceElosForSeason } from "../surface-elo"; const mockDb = { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), insert: vi.fn().mockReturnThis(), values: vi.fn().mockReturnThis(), onConflictDoUpdate: vi.fn().mockReturnThis(), }; beforeEach(() => { vi.clearAllMocks(); (database as ReturnType).mockReturnValue(mockDb); // Reset chain to return mock itself (supports fluent chaining) mockDb.select.mockReturnValue(mockDb); mockDb.from.mockReturnValue(mockDb); mockDb.where.mockReturnValue(mockDb); mockDb.innerJoin.mockReturnValue(mockDb); mockDb.orderBy.mockReturnValue(mockDb); mockDb.insert.mockReturnValue(mockDb); mockDb.values.mockReturnValue(mockDb); mockDb.onConflictDoUpdate.mockResolvedValue(undefined); }); describe("getSurfaceEloMap", () => { it("returns an empty Map when no records exist", async () => { mockDb.where.mockResolvedValue([]); const result = await getSurfaceEloMap("season-1"); expect(result).toBeInstanceOf(Map); expect(result.size).toBe(0); }); it("returns a Map keyed by seasonParticipantId", async () => { mockDb.where.mockResolvedValue([ { seasonParticipantId: "p1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 }, { seasonParticipantId: "p2", worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: null }, ]); const result = await getSurfaceEloMap("season-1"); expect(result.size).toBe(2); expect(result.get("p1")).toEqual({ worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 }); expect(result.get("p2")).toEqual({ worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: null }); }); it("preserves null surface Elos (does not coerce to 0)", async () => { mockDb.where.mockResolvedValue([ { seasonParticipantId: "p1", worldRanking: null, eloHard: null, eloClay: null, eloGrass: null }, ]); const result = await getSurfaceEloMap("season-1"); const entry = result.get("p1"); expect(entry?.worldRanking).toBeNull(); expect(entry?.eloHard).toBeNull(); expect(entry?.eloClay).toBeNull(); expect(entry?.eloGrass).toBeNull(); }); }); describe("batchUpsertSurfaceElos", () => { it("does nothing when given an empty array", async () => { await batchUpsertSurfaceElos([]); expect(mockDb.insert).not.toHaveBeenCalled(); }); it("calls insert with the correct values on the per-window table", async () => { // The function first upserts to the per-window table, then reads // season_participants for canonical-id lookup. Make the follow-up select // return empty so the canonical write path short-circuits and we only // observe the per-window insert in this assertion. mockDb.where.mockResolvedValue([]); const inputs = [ { participantId: "p1", sportsSeasonId: "s1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 }, { participantId: "p2", sportsSeasonId: "s1", worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: undefined }, ]; await batchUpsertSurfaceElos(inputs); expect(mockDb.insert).toHaveBeenCalledOnce(); expect(mockDb.values).toHaveBeenCalledOnce(); const passedValues = mockDb.values.mock.calls[0][0] as Array<{ participantId: string; eloHard: number | null; eloClay: number | null; eloGrass: number | null; }>; expect(passedValues).toHaveLength(2); expect(passedValues[0].participantId).toBe("p1"); expect(passedValues[0].eloHard).toBe(1800); expect(passedValues[1].eloHard).toBeNull(); expect(passedValues[1].eloGrass).toBeNull(); // undefined → null }); it("uses onConflictDoUpdate for upsert behaviour", async () => { mockDb.where.mockResolvedValue([]); // no canonical links; only per-window write await batchUpsertSurfaceElos([ { participantId: "p1", sportsSeasonId: "s1", worldRanking: 5, eloHard: 2000 }, ]); expect(mockDb.onConflictDoUpdate).toHaveBeenCalledOnce(); }); it("mirrors writes to canonical participant_surface_elos when linked", async () => { // season_participants lookup returns canonical links for p1 only. mockDb.where.mockResolvedValue([ { id: "p1", canonicalId: "canon-1" }, ]); await batchUpsertSurfaceElos([ { participantId: "p1", sportsSeasonId: "s1", worldRanking: 5, eloHard: 2000 }, ]); // One per-window insert, one canonical insert → two inserts total. expect(mockDb.insert).toHaveBeenCalledTimes(2); expect(mockDb.onConflictDoUpdate).toHaveBeenCalledTimes(2); const canonicalValues = mockDb.values.mock.calls[1][0] as Array<{ participantId: string; eloHard: number | null; }>; expect(canonicalValues).toHaveLength(1); expect(canonicalValues[0].participantId).toBe("canon-1"); expect(canonicalValues[0].eloHard).toBe(2000); }); }); describe("getSurfaceElosForSeason", () => { it("returns records joined with participant names", async () => { mockDb.orderBy.mockResolvedValue([ { id: "rec-1", participantId: "p1", sportsSeasonId: "s1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600, updatedAt: new Date("2025-01-01"), participantName: "Carlos Alcaraz", }, ]); const result = await getSurfaceElosForSeason("s1"); expect(result).toHaveLength(1); expect(result[0].participantName).toBe("Carlos Alcaraz"); expect(result[0].eloHard).toBe(1800); }); it("returns empty array when no Elo records exist", async () => { mockDb.orderBy.mockResolvedValue([]); const result = await getSurfaceElosForSeason("s1"); expect(result).toEqual([]); }); });