/** * Advancing an AFL Wildcard Round winner into the Elimination Finals. * * The two winners are re-seeded by ladder position — 5th hosts the lower-ranked winner, * 6th the higher-ranked one — so the destination is not a fixed crossover from a given * Wildcard match, and results can be recorded in either order. */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { AFL_10 } from "~/lib/bracket-templates"; interface MatchRow { id: string; scoringEventId: string; round: string; matchNumber: number; participant1Id: string | null; participant2Id: string | null; isComplete: boolean; winnerId: string | null; loserId: string | null; } let rows: MatchRow[] = []; /** * The literal values drizzle put in a where clause (`eq(col, value)`), which is all this * mock needs to tell one lookup from another — there is no query engine behind it. */ function whereValues(node: unknown, depth = 0): string[] { if (!node || depth > 10) return []; if (Array.isArray(node)) return node.flatMap((child) => whereValues(child, depth + 1)); if (typeof node !== "object") return []; const obj = node as Record; const own = typeof obj.value === "string" ? [obj.value] : []; return [...own, ...whereValues(obj.queryChunks, depth + 1)]; } const db = { query: { playoffMatches: { findFirst: vi.fn(({ where }: { where: unknown }) => { const values = whereValues(where); return Promise.resolve(rows.find((r) => values.includes(r.id))); }), findMany: vi.fn(({ where }: { where: unknown }) => { const values = whereValues(where); return Promise.resolve( rows .filter((r) => values.includes(r.scoringEventId) && values.includes(r.round)) .toSorted((a, b) => a.matchNumber - b.matchNumber) ); }), }, }, update: vi.fn(() => ({ set: (data: Partial) => { const applyTo = (where: unknown) => { const values = whereValues(where); const row = rows.find((r) => values.includes(r.id)); if (row) Object.assign(row, data); return row; }; // Advancement writes through the query builder with and without .returning(). return { where: (where: unknown) => { const applied = Promise.resolve([applyTo(where)]); return Object.assign(applied, { returning: () => applied }); }, }; }, })), // No rollback: the tests assert the writes that were attempted, in order. transaction: vi.fn((fn: (tx: typeof db) => Promise) => fn(db)), }; vi.mock("~/database/context", () => ({ database: () => db })); const { advanceWinnerTemplate } = await import("../playoff-match"); const EVENT = "event-1"; /** Ladder seed n → participant id. */ const seed = (n: number) => `seed-${n}`; /** A freshly generated afl_10 Wildcard Round (7v10, 8v9) and Elimination Finals (5, 6). */ function bracket(): MatchRow[] { const base = { scoringEventId: EVENT, isComplete: false, winnerId: null, loserId: null }; return [ { ...base, id: "wc1", round: "Wildcard Round", matchNumber: 1, participant1Id: seed(7), participant2Id: seed(10) }, { ...base, id: "wc2", round: "Wildcard Round", matchNumber: 2, participant1Id: seed(8), participant2Id: seed(9) }, { ...base, id: "ef1", round: "Elimination Finals", matchNumber: 1, participant1Id: seed(5), participant2Id: null }, { ...base, id: "ef2", round: "Elimination Finals", matchNumber: 2, participant1Id: seed(6), participant2Id: null }, ]; } function row(id: string): MatchRow { const found = rows.find((r) => r.id === id); if (!found) throw new Error(`No such match ${id}`); return found; } /** Record a Wildcard result the way setMatchWinner does, then advance it. */ async function winWildcard(id: string, winnerId: string) { const match = row(id); match.winnerId = winnerId; match.loserId = match.participant1Id === winnerId ? match.participant2Id : match.participant1Id; match.isComplete = true; await advanceWinnerTemplate(id, winnerId, AFL_10); } describe("AFL Wildcard Round advancement", () => { beforeEach(() => { rows = bracket(); }); it("sends 5th the lower-ranked winner and 6th the higher-ranked one", async () => { await winWildcard("wc1", seed(7)); await winWildcard("wc2", seed(8)); expect(row("ef1").participant2Id).toBe(seed(8)); expect(row("ef2").participant2Id).toBe(seed(7)); }); it("re-seeds when the lower seed wins through", async () => { // The reported bug: 10th beating 7th used to be crossed straight to 6th, leaving // 5th with the better survivor. await winWildcard("wc1", seed(10)); await winWildcard("wc2", seed(8)); expect(row("ef1").participant2Id).toBe(seed(10)); expect(row("ef2").participant2Id).toBe(seed(8)); }); it("re-seeds a 9th-placed winner above a 10th-placed one", async () => { await winWildcard("wc1", seed(10)); await winWildcard("wc2", seed(9)); expect(row("ef1").participant2Id).toBe(seed(10)); expect(row("ef2").participant2Id).toBe(seed(9)); }); it("places the same pairings whichever result is entered first", async () => { await winWildcard("wc2", seed(8)); await winWildcard("wc1", seed(10)); expect(row("ef1").participant2Id).toBe(seed(10)); expect(row("ef2").participant2Id).toBe(seed(8)); }); it("places the 7v10 winner immediately, since its slot is settled either way", async () => { await winWildcard("wc1", seed(7)); expect(row("ef2").participant2Id).toBe(seed(7)); expect(row("ef1").participant2Id).toBeNull(); }); it("holds an 8v9 winner back until the 7v10 game is decided", async () => { // 8th and 9th sit between 7th and 10th, so placing one now could need undoing. await winWildcard("wc2", seed(8)); expect(row("ef1").participant2Id).toBeNull(); expect(row("ef2").participant2Id).toBeNull(); }); it("does not disturb a winner it already placed", async () => { await winWildcard("wc1", seed(7)); await winWildcard("wc2", seed(9)); expect(row("ef2").participant2Id).toBe(seed(7)); expect(row("ef1").participant2Id).toBe(seed(9)); }); it("refuses to overwrite a slot already holding someone else", async () => { row("ef1").participant2Id = "stranger"; await expect(winWildcard("wc1", seed(10))).rejects.toThrow(/already filled/); expect(row("ef1").participant2Id).toBe("stranger"); }); it("moves the winner when a recorded Wildcard result is corrected", async () => { await winWildcard("wc1", seed(7)); expect(row("ef2").participant2Id).toBe(seed(7)); // The result was wrong: 10th won. 7th must not be left alive in the other slot. await winWildcard("wc1", seed(10)); expect(row("ef1").participant2Id).toBe(seed(10)); expect(row("ef2").participant2Id).toBeNull(); }); it("re-seeds a pairing left behind by the old fixed crossover", async () => { // Pre-fix state: the 7v10 winner was crossed to 6th whatever its ladder position. row("wc1").winnerId = seed(10); row("wc1").loserId = seed(7); row("wc1").isComplete = true; row("ef2").participant2Id = seed(10); await winWildcard("wc2", seed(8)); expect(row("ef1").participant2Id).toBe(seed(10)); expect(row("ef2").participant2Id).toBe(seed(8)); }); it("swaps both winners when re-resolving an already-placed pair", async () => { row("wc1").winnerId = seed(10); row("wc1").loserId = seed(7); row("wc1").isComplete = true; row("ef2").participant2Id = seed(10); row("ef1").participant2Id = seed(8); await winWildcard("wc2", seed(8)); expect(row("ef1").participant2Id).toBe(seed(10)); expect(row("ef2").participant2Id).toBe(seed(8)); }); it("leaves the bracket alone when the pairings are already right", async () => { await winWildcard("wc1", seed(7)); await winWildcard("wc2", seed(8)); db.transaction.mockClear(); await winWildcard("wc2", seed(8)); expect(db.transaction).not.toHaveBeenCalled(); expect(row("ef1").participant2Id).toBe(seed(8)); expect(row("ef2").participant2Id).toBe(seed(7)); }); });