/** * Tests for getUpcomingEventsForDraftedParticipants logic. * * We test the exported function indirectly by exercising the pure * business-logic paths (all-compete vs bracket, date filtering, * deduplication, empty inputs) using a vi.mock of the database context * so no real DB is required. */ import { describe, it, expect, vi, beforeEach } from "vitest"; // ── DB mock ──────────────────────────────────────────────────────────────── // Must be declared before the import that triggers the module graph. const mockDb = { query: { scoringEvents: { findMany: vi.fn() }, }, selectDistinct: vi.fn(), }; vi.mock("~/database/context", () => ({ database: () => mockDb, })); vi.mock("~/database/schema", () => ({ scoringEvents: { sportsSeasonId: "se.sports_season_id", isComplete: "se.is_complete", eventDate: "se.event_date", eventType: "se.event_type" }, playoffMatches: { scoringEventId: "pm.scoring_event_id", participant1Id: "pm.participant1_id", participant2Id: "pm.participant2_id", round: "pm.round", matchNumber: "pm.match_number", isComplete: "pm.is_complete" }, playoffMatchGames: { playoffMatchId: "pmg.playoff_match_id", scheduledAt: "pmg.scheduled_at", gameNumber: "pmg.game_number" }, })); vi.mock("drizzle-orm", () => ({ eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }), and: (...args: unknown[]) => ({ type: "and", args }), asc: (col: unknown) => ({ type: "asc", col }), desc: (col: unknown) => ({ type: "desc", col }), gte: (col: unknown, val: unknown) => ({ type: "gte", col, val }), lte: (col: unknown, val: unknown) => ({ type: "lte", col, val }), or: (...args: unknown[]) => ({ type: "or", args }), inArray: (col: unknown, arr: unknown) => ({ type: "inArray", col, arr }), isNotNull: (col: unknown) => ({ type: "isNotNull", col }), })); import { getUpcomingEventsForDraftedParticipants } from "../scoring-event"; // ── Helpers ──────────────────────────────────────────────────────────────── const DATE_FROM = "2025-04-01"; const DATE_TO = "2025-04-30"; const SPORTS_SEASON_ID = "ss-1"; function makeParticipant(id: string, name: string) { return { id, name }; } function makeScoringEvent(overrides: Partial<{ id: string; name: string; eventDate: string | null; eventStartsAt: Date | null; eventType: string; sportsSeasonId: string; isComplete: boolean; }> = {}) { return { id: overrides.id ?? "event-1", name: overrides.name ?? "Test Event", eventDate: overrides.eventDate ?? "2025-04-10", eventStartsAt: overrides.eventStartsAt ?? null, eventType: overrides.eventType ?? "schedule_event", sportsSeasonId: overrides.sportsSeasonId ?? SPORTS_SEASON_ID, isComplete: overrides.isComplete ?? false, }; } // ── All-compete patterns ─────────────────────────────────────────────────── describe("getUpcomingEventsForDraftedParticipants — all-compete sports", () => { beforeEach(() => { vi.clearAllMocks(); // selectDistinct won't be called for all-compete mockDb.selectDistinct.mockReturnValue({ from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue([]) }); }); it("returns [] when draftedParticipants is empty", async () => { const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "season_standings", [], DATE_FROM, DATE_TO ); expect(result).toEqual([]); expect(mockDb.query.scoringEvents.findMany).not.toHaveBeenCalled(); }); it("returns all events with all drafted participants attached", async () => { const participants = [makeParticipant("p1", "Verstappen"), makeParticipant("p2", "Hamilton")]; const events = [makeScoringEvent({ id: "e1", name: "Australian GP" })]; mockDb.query.scoringEvents.findMany.mockResolvedValue(events); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO ); expect(result).toHaveLength(1); expect(result[0].name).toBe("Australian GP"); expect(result[0].relevantParticipants).toEqual(participants); }); it("works for qualifying_points pattern", async () => { const participants = [makeParticipant("g1", "Scheffler")]; mockDb.query.scoringEvents.findMany.mockResolvedValue([makeScoringEvent({ id: "e1", name: "Masters" })]); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "qualifying_points", participants, DATE_FROM, DATE_TO ); expect(result).toHaveLength(1); }); it("unknown pattern (e.g. golf_qualifying_points) falls through to bracket path, not all-compete", async () => { // golf_qualifying_points is a simulatorType, not a scoringPattern. // Unknown patterns are treated as bracket sports (selectDistinct path). const participants = [makeParticipant("g1", "McIlroy")]; const selectChain = { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue([]) }; mockDb.selectDistinct.mockReturnValue(selectChain); await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "golf_qualifying_points", participants, DATE_FROM, DATE_TO ); // Should NOT call the all-compete findMany path expect(mockDb.query.scoringEvents.findMany).not.toHaveBeenCalled(); // Should call the bracket selectDistinct path expect(mockDb.selectDistinct).toHaveBeenCalled(); }); it("returns multiple events sorted by eventDate", async () => { const participants = [makeParticipant("p1", "Verstappen")]; const events = [ makeScoringEvent({ id: "e2", name: "Miami GP", eventDate: "2025-04-20" }), makeScoringEvent({ id: "e1", name: "Australian GP", eventDate: "2025-04-06" }), ]; // findMany is ordered by asc(eventDate) in the real query; simulate same order mockDb.query.scoringEvents.findMany.mockResolvedValue([events[1], events[0]]); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO ); expect(result[0].name).toBe("Australian GP"); expect(result[1].name).toBe("Miami GP"); }); }); // ── Bracket patterns ─────────────────────────────────────────────────────── describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { // Main query chain: resolves on orderBy (has leftJoin, orderBy) function makeSelectChain(rows: unknown[]) { const chain = { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue(rows), }; return chain; } // Second query chain (max game lookup): resolves on where (no leftJoin, no orderBy) function makeMaxGameChain(rows: unknown[]) { const chain = { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockResolvedValue(rows), }; return chain; } beforeEach(() => { vi.clearAllMocks(); // Default for the second selectDistinct call (max-game-number lookup). // Individual tests override the first call with mockReturnValueOnce. mockDb.selectDistinct.mockReturnValue(makeMaxGameChain([])); }); it("returns [] when draftedParticipants is empty", async () => { const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [], DATE_FROM, DATE_TO ); expect(result).toEqual([]); expect(mockDb.selectDistinct).not.toHaveBeenCalled(); }); it("returns one event with one relevant participant (participant1)", async () => { const rows = [{ id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "man-city", participant2Id: "bayern", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City")], DATE_FROM, DATE_TO ); expect(result).toHaveLength(1); expect(result[0].name).toBe("UCL QF"); expect(result[0].relevantParticipants).toEqual([{ id: "man-city", name: "Man City" }]); }); it("returns one event when both participants are drafted (no duplication)", async () => { // Two rows for the same event — the DB query uses selectDistinct on event columns // but we may still get two rows if the WHERE matches on both participants. // The grouping logic must deduplicate the event itself. const rows = [ { id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "man-city", participant2Id: "arsenal", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }, ]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City"), makeParticipant("arsenal", "Arsenal")], DATE_FROM, DATE_TO ); expect(result).toHaveLength(1); expect(result[0].relevantParticipants).toHaveLength(2); const names = result[0].relevantParticipants.map((p) => p.name); expect(names).toContain("Man City"); expect(names).toContain("Arsenal"); }); it("returns multiple events when picks are in different matches", async () => { const rows = [ { id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "man-city", participant2Id: "real-madrid", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }, { id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "arsenal", participant2Id: "psg", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }, ]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City"), makeParticipant("arsenal", "Arsenal")], DATE_FROM, DATE_TO ); // Both matches are in the same event → one result with both picks expect(result).toHaveLength(1); expect(result[0].relevantParticipants).toHaveLength(2); }); it("only includes drafted participants in relevantParticipants, not opponents", async () => { const rows = [{ id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "man-city", participant2Id: "real-madrid", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City")], DATE_FROM, DATE_TO ); expect(result[0].relevantParticipants).toEqual([{ id: "man-city", name: "Man City" }]); // Real Madrid (the opponent) should NOT appear expect(result[0].relevantParticipants.find((p) => p.id === "real-madrid")).toBeUndefined(); }); it("works for ucl_bracket pattern", async () => { mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain([{ id: "e1", name: "UCL SF", eventDate: "2025-04-29", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "man-city", participant2Id: "arsenal", round: "Semifinals", gameNumber: 1, scheduledAt: null, }])); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "ucl_bracket", [makeParticipant("man-city", "Man City")], DATE_FROM, DATE_TO ); expect(result).toHaveLength(1); }); it("includes events with null eventDate when they have games scheduled within the window", async () => { // Real-world case: admin sets game times on individual games (scheduledAt) // but leaves scoringEvent.eventDate null — the event must still appear. const rows = [{ id: "e1", name: "UCL QF", eventDate: null, // ← no date on the event itself eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: null, }]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("barcelona", "Barcelona")], DATE_FROM, DATE_TO ); expect(result).toHaveLength(1); expect(result[0].name).toBe("UCL QF"); expect(result[0].eventDate).toBeNull(); expect(result[0].relevantParticipants).toEqual([{ id: "barcelona", name: "Barcelona" }]); }); it("surfaces earliestGameTime from scheduledAt when eventDate is null", async () => { const gameTime = new Date("2025-04-09T14:00:00.000Z"); const rows = [{ id: "e1", name: "UCL QF", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime, }]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("barcelona", "Barcelona")], DATE_FROM, DATE_TO ); expect(result[0].earliestGameTime).toBe(gameTime.toISOString()); expect(result[0].eventDate).toBeNull(); }); it("series with both games in window produces two entries, each with correct time and label", async () => { const gameTime1 = new Date("2025-04-09T13:00:00.000Z"); const gameTime2 = new Date("2025-04-11T15:00:00.000Z"); const rows = [ { id: "e1", name: "UCL QF", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1, }, { id: "e1", name: "UCL QF", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime2, }, ]; mockDb.selectDistinct .mockReturnValueOnce(makeSelectChain(rows)) .mockReturnValueOnce(makeMaxGameChain([ { eventId: "e1", gameNumber: 1 }, { eventId: "e1", gameNumber: 2 }, ])); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("barcelona", "Barcelona")], DATE_FROM, DATE_TO ); expect(result).toHaveLength(2); const game1 = result.find((r) => r.id === "e1|1"); const game2 = result.find((r) => r.id === "e1|2"); expect(game1?.earliestGameTime).toBe(gameTime1.toISOString()); expect(game1?.matchLabel).toBe("Knockout Stage Game #1"); expect(game2?.earliestGameTime).toBe(gameTime2.toISOString()); expect(game2?.matchLabel).toBe("Knockout Stage Game #2"); }); it("sets matchLabel to round name only (no game number) for single-game matchup", async () => { const rows = [{ id: "e1", name: "PDC World Championship", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "man-city", participant2Id: "arsenal", round: "Semifinals", gameNumber: 1, scheduledAt: null, }]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City")], DATE_FROM, DATE_TO ); expect(result[0].matchLabel).toBe("Semifinals"); }); it("sets matchLabel to null when round name matches event name (single game)", async () => { // e.g. event named "Knockout Stage", round also "Knockout Stage", single game // → show null rather than redundant label const rows = [{ id: "e1", name: "Knockout Stage", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: null, }]; mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("barcelona", "Barcelona")], DATE_FROM, DATE_TO ); expect(result[0].matchLabel).toBeNull(); }); it("shows game number when only game #2 is in the window (game #1 already played)", async () => { const rows = [{ id: "e1", name: "PDC World Championship", eventDate: "2025-04-10", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "man-city", participant2Id: "arsenal", round: "Semifinals", gameNumber: 2, scheduledAt: null, }]; mockDb.selectDistinct .mockReturnValueOnce(makeSelectChain(rows)) .mockReturnValueOnce(makeMaxGameChain([ { eventId: "e1", gameNumber: 1 }, { eventId: "e1", gameNumber: 2 }, ])); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City")], DATE_FROM, DATE_TO ); expect(result[0].matchLabel).toBe("Semifinals Game #2"); }); it("shows Game #1 label when game #1 is upcoming but series has multiple games", async () => { // Game #2 exists in DB but is outside the date window — only Game #1 appears // in the main query. The second (un-date-filtered) query reveals it's a series. const mainRows = [{ id: "e1", name: "NBA Finals", eventDate: "2025-04-10", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, participant1Id: "lakers", participant2Id: "celtics", round: "Finals", gameNumber: 1, scheduledAt: null, }]; const allGameRows = [ { eventId: "e1", gameNumber: 1 }, { eventId: "e1", gameNumber: 2 }, ]; mockDb.selectDistinct .mockReturnValueOnce(makeSelectChain(mainRows)) .mockReturnValueOnce(makeMaxGameChain(allGameRows)); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("lakers", "Lakers")], DATE_FROM, DATE_TO ); expect(result[0].matchLabel).toBe("Finals Game #1"); }); it("all-compete events have null earliestGameTime and matchLabel", async () => { const participants = [makeParticipant("p1", "Verstappen")]; mockDb.query.scoringEvents.findMany.mockResolvedValue([ makeScoringEvent({ id: "e1", name: "Australian GP" }), ]); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO ); expect(result[0].earliestGameTime).toBeNull(); expect(result[0].matchLabel).toBeNull(); }); }); // ── eventStartsAt — all-compete sports ──────────────────────────────────── describe("getUpcomingEventsForDraftedParticipants — eventStartsAt for all-compete sports", () => { beforeEach(() => { vi.clearAllMocks(); mockDb.selectDistinct.mockReturnValue({ from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue([]), }); }); it("populates earliestGameTime from eventStartsAt when set", async () => { const raceStart = new Date("2025-04-06T14:00:00.000Z"); const participants = [makeParticipant("p1", "Verstappen")]; mockDb.query.scoringEvents.findMany.mockResolvedValue([ makeScoringEvent({ id: "e1", name: "Australian GP", eventStartsAt: raceStart }), ]); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO ); expect(result[0].earliestGameTime).toBe(raceStart.toISOString()); }); it("leaves earliestGameTime null when eventStartsAt is not set", async () => { const participants = [makeParticipant("p1", "Verstappen")]; mockDb.query.scoringEvents.findMany.mockResolvedValue([ makeScoringEvent({ id: "e1", name: "Australian GP", eventStartsAt: null }), ]); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO ); expect(result[0].earliestGameTime).toBeNull(); }); it("works for qualifying_points pattern with eventStartsAt", async () => { const majorStart = new Date("2025-04-10T12:00:00.000Z"); const participants = [makeParticipant("g1", "Scheffler")]; mockDb.query.scoringEvents.findMany.mockResolvedValue([ makeScoringEvent({ id: "e1", name: "The Masters", eventStartsAt: majorStart }), ]); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "qualifying_points", participants, DATE_FROM, DATE_TO ); expect(result[0].earliestGameTime).toBe(majorStart.toISOString()); }); it("earliestGameTime from eventStartsAt is used as sort key over eventDate", async () => { // Event on Apr 6, game starts at 14:00 UTC — both should be reflected in earliestGameTime const raceStart = new Date("2025-04-06T14:00:00.000Z"); const participants = [makeParticipant("p1", "Hamilton")]; mockDb.query.scoringEvents.findMany.mockResolvedValue([ makeScoringEvent({ id: "e1", name: "Bahrain GP", eventDate: "2025-04-06", eventStartsAt: raceStart }), ]); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO ); // earliestGameTime carries the full timestamp so the sort key has time precision expect(result[0].earliestGameTime).toBe("2025-04-06T14:00:00.000Z"); expect(result[0].eventDate).toBe("2025-04-06"); }); }); // ── Bracket: misc ────────────────────────────────────────────────────────── describe("getUpcomingEventsForDraftedParticipants — bracket misc", () => { function makeSelectChain(rows: unknown[]) { return { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue(rows), }; } function makeMaxGameChain(rows: unknown[]) { return { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockResolvedValue(rows), }; } beforeEach(() => vi.clearAllMocks()); it("excludes scored (isComplete) matches by passing the filter to the DB query", async () => { // The DB mock returns rows only for matches the WHERE clause matches. // When a match is complete (isComplete=true) the DB should return no rows. // Simulate this by returning an empty result set. // No second mockReturnValueOnce is needed: empty rows means eventIdsFromRows // is empty so the max-game-number lookup is never called. const chain = makeSelectChain([]); mockDb.selectDistinct.mockReturnValueOnce(chain); const result = await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City")], DATE_FROM, DATE_TO ); expect(result).toHaveLength(0); // The WHERE clause includes eq(schema.playoffMatches.isComplete, false). // We verify the query was built without error (accessing the field on the mock). expect(chain.where).toHaveBeenCalled(); }); it("uses leftJoin so events without any games are still considered", async () => { const chain = makeSelectChain([]); mockDb.selectDistinct .mockReturnValueOnce(chain) .mockReturnValueOnce(makeMaxGameChain([])); await getUpcomingEventsForDraftedParticipants( SPORTS_SEASON_ID, "playoff_bracket", [makeParticipant("man-city", "Man City")], DATE_FROM, DATE_TO ); expect(chain.leftJoin).toHaveBeenCalled(); }); });