import { describe, it, expect, vi, beforeEach } from "vitest"; // Mock external DB-touching dependencies before importing the module under test vi.mock("~/services/probability-updater", () => ({ updateProbabilitiesAfterResult: vi.fn().mockResolvedValue(undefined), })); // ── Minimal db mock ──────────────────────────────────────────────────────── // // processMatchResult calls, in order: // 1. upsertParticipantResult (x2) → db.query.participantResults.findFirst, db.insert // 2. recalculateAffectedLeagues → db.query.seasonSports.findMany (returns [] so loop exits) // 3. updateProbabilitiesAfterResult (mocked above) function makeDb(existingResult?: { id: string; isPartialScore: boolean }) { // Track all inserted values in order const insertedRows: object[] = []; const updatedRows: object[] = []; const insertValues = vi.fn().mockImplementation((values: object) => { insertedRows.push(values); return Promise.resolve(); }); const updateWhere = vi.fn().mockResolvedValue(undefined); const updateSet = vi.fn().mockImplementation((values: object) => { updatedRows.push(values); return { where: updateWhere }; }); // participantResults.findFirst: first call gets existingResult (if any), // subsequent calls return undefined (winner/loser don't have existing rows) let findFirstCallCount = 0; const findFirst = vi.fn().mockImplementation(() => { const result = findFirstCallCount === 0 ? existingResult : undefined; findFirstCallCount++; return Promise.resolve(result); }); return { db: { insert: vi.fn().mockReturnValue({ values: insertValues }), update: vi.fn().mockReturnValue({ set: updateSet }), query: { participantResults: { findFirst }, seasonSports: { findMany: vi.fn().mockResolvedValue([]), // empty → standings loop exits immediately }, sportsSeasons: { findFirst: vi.fn().mockResolvedValue(null) }, scoringEvents: { findFirst: vi.fn().mockResolvedValue(null) }, }, } as any, insertedRows, updatedRows, findFirst, insertValues, updateSet, updateWhere, }; } import { processMatchResult } from "../scoring-calculator"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; const BASE = { winnerId: "winner-1", loserId: "loser-1", sportsSeasonId: "ss-1", bracketTemplateId: null as string | null, }; describe("processMatchResult", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("Quarterfinals (scoring)", () => { it("loser gets position 5 (final), winner gets position 3 (provisional)", async () => { const { db, insertedRows } = makeDb(); await processMatchResult( { ...BASE, round: "Quarterfinals", isScoring: true }, db ); expect(insertedRows).toHaveLength(2); expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 5, isPartialScore: false }); expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 3, isPartialScore: true }); }); it("also works for Elite Eight / Divisional / Conference Semifinals", async () => { for (const round of ["Elite Eight", "Divisional", "Conference Semifinals"]) { const { db, insertedRows } = makeDb(); await processMatchResult({ ...BASE, round, isScoring: true }, db); expect(insertedRows[0]).toMatchObject({ finalPosition: 5, isPartialScore: false }); expect(insertedRows[1]).toMatchObject({ finalPosition: 3, isPartialScore: true }); } }); it("recalculates standings (calls db.query.seasonSports.findMany)", async () => { const { db } = makeDb(); await processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db); expect((db.query.seasonSports.findMany as ReturnType)).toHaveBeenCalled(); }); }); describe("Semifinals (scoring)", () => { it("loser gets position 3 (final), winner gets position 2 (provisional)", async () => { const { db, insertedRows } = makeDb(); await processMatchResult({ ...BASE, round: "Semifinals", isScoring: true }, db); expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 3, isPartialScore: false }); expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 2, isPartialScore: true }); }); it("also works for Final Four / Conference Finals / Conference Championship", async () => { for (const round of ["Final Four", "Conference Finals", "Conference Championship"]) { const { db, insertedRows } = makeDb(); await processMatchResult({ ...BASE, round, isScoring: true }, db); expect(insertedRows[0]).toMatchObject({ finalPosition: 3, isPartialScore: false }); expect(insertedRows[1]).toMatchObject({ finalPosition: 2, isPartialScore: true }); } }); }); describe("Finals / Championship (scoring)", () => { it("loser=2nd (final), winner=1st (final), no third row", async () => { const { db, insertedRows } = makeDb(); await processMatchResult({ ...BASE, round: "Finals", isScoring: true }, db); expect(insertedRows).toHaveLength(2); expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 2, isPartialScore: false }); expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 1, isPartialScore: false }); }); it("handles all Finals aliases the same way", async () => { for (const round of ["Championship", "Super Bowl", "NBA Finals", "Grand Final"]) { const { db, insertedRows } = makeDb(); await processMatchResult({ ...BASE, round, isScoring: true }, db); expect(insertedRows).toHaveLength(2); expect(insertedRows[0]).toMatchObject({ finalPosition: 2, isPartialScore: false }); expect(insertedRows[1]).toMatchObject({ finalPosition: 1, isPartialScore: false }); } }); }); describe("Non-scoring round (isScoring=false)", () => { it("loser eliminated (pos 0 final), winner gets provisional T5 floor", async () => { const { db, insertedRows } = makeDb(); await processMatchResult({ ...BASE, round: "Round of 16", isScoring: false }, db); expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false }); expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 5, isPartialScore: true }); }); }); describe("Unrecognized scoring round", () => { it("gives loser 0 pts and still completes without throwing", async () => { const { db, insertedRows } = makeDb(); await processMatchResult({ ...BASE, round: "Mystery Round", isScoring: true }, db); expect(insertedRows[0]).toMatchObject({ finalPosition: 0, isPartialScore: false }); }); }); describe("AFL rounds", () => { it("Elimination Finals: loser exits at position 7 (final)", async () => { const { db, insertedRows } = makeDb(); await processMatchResult( { ...BASE, round: "Elimination Finals", isScoring: true, bracketTemplateId: "afl_10" }, db ); expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 7, isPartialScore: false }); }); it("Qualifying Finals: loser is still alive (provisional floor 5)", async () => { const { db, insertedRows } = makeDb(); await processMatchResult( { ...BASE, round: "Qualifying Finals", isScoring: true, bracketTemplateId: "afl_10" }, db ); // Loser row is provisional (isPartialScore=true) expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 5, isPartialScore: true }); }); }); describe("Idempotency: already-final row is not un-finalized", () => { it("does not overwrite a finalized row with a partial score", async () => { // findFirst returns a finalized row for the first participant (loser) const existing = { id: "result-1", isPartialScore: false }; const { db, insertedRows, updatedRows } = makeDb(existing); await processMatchResult( { ...BASE, round: "Quarterfinals", isScoring: true }, db ); // The guard in upsertParticipantResult should skip the update when // existing.isPartialScore=false and we're trying to write isPartialScore=true. // The loser row would try to write isPartialScore=false (final) — that IS // allowed (both false). The winner row tries isPartialScore=true — since // there's no existing row for the winner (findFirst returns undefined on // second call), it inserts. // // Key assertion: no update/insert with isPartialScore=true was applied to // the existing finalized row. const finalizedOverwrite = updatedRows.find( (r: any) => r.isPartialScore === true ); expect(finalizedOverwrite).toBeUndefined(); }); }); describe("Probability recalculation", () => { it("always calls updateProbabilitiesAfterResult with the sports season id", async () => { const { db } = makeDb(); await processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db); expect(updateProbabilitiesAfterResult).toHaveBeenCalledWith("ss-1", true); }); it("does not throw even if probability update fails", async () => { (updateProbabilitiesAfterResult as ReturnType).mockRejectedValueOnce( new Error("network error") ); const { db } = makeDb(); // Should not throw — the error is caught and logged await expect( processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db) ).resolves.not.toThrow(); }); }); });