/** * 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) => ({ where: (where: unknown) => ({ returning: () => { const values = whereValues(where); const row = rows.find((r) => values.includes(r.id)); if (row) Object.assign(row, data); return Promise.resolve([row]); }, }), }), })), }; 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"); }); });