import { describe, it, expect, vi, beforeEach } from "vitest"; import { autoPickForTeam, pruneIneligibleQueueItems } from "../draft-utils"; // Mock all external model dependencies so we can control their return values vi.mock("~/models/draft-pick", () => ({ getDraftPicksWithSports: vi.fn(), getTeamDraftPicksWithSports: vi.fn(), isParticipantDrafted: vi.fn(), })); vi.mock("~/models/season-participant", () => ({ getParticipantsForSeasonWithSports: vi.fn(), })); vi.mock("~/models/season-sport", () => ({ getSeasonSportsSimple: vi.fn(), })); vi.mock("~/models/draft-queue", () => ({ getTeamQueue: vi.fn(), getAllQueuesForSeason: vi.fn(), })); vi.mock("~/lib/draft-eligibility", () => ({ calculateDraftEligibility: vi.fn(), })); // Prevent actual socket/db usage vi.mock("~/server/socket"); vi.mock("~/database/context"); import { getDraftPicksWithSports, getTeamDraftPicksWithSports, isParticipantDrafted, } from "~/models/draft-pick"; import { getParticipantsForSeasonWithSports } from "~/models/season-participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; import { getTeamQueue, getAllQueuesForSeason } from "~/models/draft-queue"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; const SEASON_ID = "season-1"; const TEAM_ID = "team-1"; const DRAFT_ROUNDS = 10; const ALL_TEAM_IDS = [TEAM_ID, "team-2"]; // A minimal mock db for cases that do NOT reach getTopAvailableParticipant. // `where` resolves to [] so `await db.select()...where()` yields an array. function makeMockDb(overrides: Record = {}) { const mockDb: Record = { query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([]), }, }, select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockResolvedValue([]), orderBy: vi.fn().mockResolvedValue([]), delete: vi.fn().mockReturnThis(), ...overrides, }; return mockDb; } // Build a mock db whose select chain returns a real EV participant. // getTopAvailableParticipant makes three sequential awaitable calls: // 1. select().from().where() → drafted picks → [] // 2. select().from().innerJoin()×2.where() → season sports → [{sportsSeasonId, sportId}] // 3. select().from().where().orderBy() → top participant → [{id, ...}] function makeMockDbWithEvParticipant(participantId: string) { return { query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([]) } }, select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn() .mockResolvedValueOnce([]) // call 1: drafted picks .mockResolvedValueOnce([{ sportsSeasonId: "ss-1", sportId: "sport-1" }]) // call 2: season sports .mockReturnThis(), // call 3: chain to orderBy orderBy: vi.fn().mockResolvedValue([ { id: participantId, name: "EV Player", expectedValue: "100.00" }, ]), delete: vi.fn().mockReturnThis(), }; } // Minimal eligibility stub — makes every sport eligible. function stubEligibility() { vi.mocked(calculateDraftEligibility).mockReturnValue({ eligibleSportIds: new Set(["sport-1"]), } as ReturnType); } beforeEach(() => { vi.clearAllMocks(); vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); stubEligibility(); }); describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => { describe("empty queue", () => { it("returns null when queueOnly=true (no EV fallback)", async () => { vi.mocked(getTeamQueue).mockResolvedValue([]); const result = await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, makeMockDb() as any, true // queueOnly ); expect(result).toBeNull(); }); it("returns the top EV participant when queueOnly=false (falls back to EV)", async () => { vi.mocked(getTeamQueue).mockResolvedValue([]); const mockDb = makeMockDbWithEvParticipant("p-ev"); const result = await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, mockDb as any, false // queueOnly off ); // Must return the EV participant — proves the EV path ran, not the queueOnly guard. expect(result).toBe("p-ev"); }); }); describe("all queue items already drafted", () => { it("returns null when queueOnly=true and every queued participant is taken", async () => { const queue = [ { id: "q-1", participantId: "p-1", queuePosition: 1 }, { id: "q-2", participantId: "p-2", queuePosition: 2 }, ]; vi.mocked(getTeamQueue).mockResolvedValue(queue as any); const mockDb = makeMockDb({ query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([ { id: "p-1", name: "Player One", sportsSeason: { sport: { id: "sport-1", name: "NFL" } }, }, { id: "p-2", name: "Player Two", sportsSeason: { sport: { id: "sport-1", name: "NFL" } }, }, ]), }, }, }); // Both participants are already drafted vi.mocked(isParticipantDrafted).mockResolvedValue(true); const result = await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, mockDb as any, true // queueOnly ); expect(result).toBeNull(); }); it("removes stale queue entries when all items are drafted", async () => { const queue = [{ id: "q-1", participantId: "p-1", queuePosition: 1 }]; vi.mocked(getTeamQueue).mockResolvedValue(queue as any); const mockDelete = vi.fn().mockReturnThis(); const mockWhere = vi.fn().mockResolvedValue(undefined); const mockDb = makeMockDb({ query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([ { id: "p-1", name: "Player One", sportsSeason: { sport: { id: "sport-1", name: "NFL" } }, }, ]), }, }, delete: mockDelete, where: mockWhere, }); vi.mocked(isParticipantDrafted).mockResolvedValue(true); await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, mockDb as any, true ); // Stale items should be cleaned from the queue expect(mockDelete).toHaveBeenCalled(); }); }); describe("ineligible sport — only queued player's sport is full", () => { it("removes the ineligible queue item and returns null when queueOnly=true", async () => { // Scenario: team has a snooker player queued but snooker is already full (not in eligibleSportIds) const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }]; vi.mocked(getTeamQueue).mockResolvedValue(queue as any); // Snooker is NOT eligible — only "sport-other" is vi.mocked(calculateDraftEligibility).mockReturnValue({ eligibleSportIds: new Set(["sport-other"]), } as ReturnType); const mockDelete = vi.fn().mockReturnThis(); const mockWhere = vi.fn().mockResolvedValue(undefined); const mockDb = makeMockDb({ query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([ { id: "p-snooker", name: "Ronnie O'Sullivan", sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } }, }, ]), }, }, delete: mockDelete, where: mockWhere, }); vi.mocked(isParticipantDrafted).mockResolvedValue(false); // not drafted — ineligible due to sport const result = await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, mockDb as any, true // queueOnly — no EV fallback ); // Queue item should be removed expect(mockDelete).toHaveBeenCalled(); // No valid pick available expect(result).toBeNull(); }); it("falls back to EV pick from eligible sports when queueOnly=false", async () => { // Scenario: same ineligible snooker player, but queueOnly is off so system picks best available const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }]; vi.mocked(getTeamQueue).mockResolvedValue(queue as any); vi.mocked(calculateDraftEligibility).mockReturnValue({ eligibleSportIds: new Set(["sport-other"]), } as ReturnType); // where() call order: // 1. delete().where() — remove ineligible snooker item from queue // 2. select().from().where() — getTopAvailableParticipant: drafted picks → [] // 3. select().from().innerJoin().innerJoin().where() — season sports → [{...}] // 4. select().from(participants).where().orderBy() — participant query (chain) const mockDb = { query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([ { id: "p-snooker", name: "Ronnie O'Sullivan", sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } }, }, ]), }, }, select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn() .mockResolvedValueOnce(undefined) // call 1: delete ineligible queue items .mockResolvedValueOnce([]) // call 2: drafted picks .mockResolvedValueOnce([{ sportsSeasonId: "ss-other", sportId: "sport-other" }]) // call 3: season sports .mockReturnThis(), // call 4: participant query → chain to orderBy orderBy: vi.fn().mockResolvedValue([ { id: "p-ev", name: "EV Player", expectedValue: "90.00" }, ]), delete: vi.fn().mockReturnThis(), }; vi.mocked(isParticipantDrafted).mockResolvedValue(false); const result = await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, mockDb as any, false // queueOnly off — should fall back to best available ); // Should pick the best available from eligible sports, not the snooker player expect(result).toBe("p-ev"); }); }); describe("partial queue — first item drafted, second available", () => { it("skips drafted item and returns the next valid participant", async () => { const queue = [ { id: "q-1", participantId: "p-1", queuePosition: 1 }, { id: "q-2", participantId: "p-2", queuePosition: 2 }, ]; vi.mocked(getTeamQueue).mockResolvedValue(queue as any); const mockDb = makeMockDb({ query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([ { id: "p-1", name: "Player One", sportsSeason: { sport: { id: "sport-1", name: "NFL" } }, }, { id: "p-2", name: "Player Two", sportsSeason: { sport: { id: "sport-1", name: "NFL" } }, }, ]), }, }, }); // p-1 is already drafted, p-2 is available vi.mocked(isParticipantDrafted).mockImplementation( async (_seasonId, participantId) => participantId === "p-1" ); const result = await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, mockDb as any, true // queueOnly — still uses queue, just no EV fallback ); expect(result).toBe("p-2"); }); it("returns the first available participant when none are drafted (happy path)", async () => { const queue = [ { id: "q-1", participantId: "p-1", queuePosition: 1 }, { id: "q-2", participantId: "p-2", queuePosition: 2 }, ]; vi.mocked(getTeamQueue).mockResolvedValue(queue as any); const mockDb = makeMockDb({ query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([ { id: "p-1", name: "Player One", sportsSeason: { sport: { id: "sport-1", name: "NFL" } }, }, { id: "p-2", name: "Player Two", sportsSeason: { sport: { id: "sport-1", name: "NFL" } }, }, ]), }, }, }); vi.mocked(isParticipantDrafted).mockResolvedValue(false); const result = await autoPickForTeam( SEASON_ID, TEAM_ID, DRAFT_ROUNDS, ALL_TEAM_IDS, mockDb as any, true ); expect(result).toBe("p-1"); }); }); }); function makeSeasonQueues(entries: [string, { id: string; participantId: string; queuePosition: number }[]][]) { return new Map(entries.map(([teamId, items]) => [teamId, items as any])); } describe("pruneIneligibleQueueItems", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); }); it("removes queued snooker player when snooker is no longer eligible for a team", async () => { vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([ { id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } }, ] as any); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); // team-1: snooker ineligible; team-2: snooker still eligible vi.mocked(calculateDraftEligibility) .mockReturnValueOnce({ eligibleSportIds: new Set([]) } as any) .mockReturnValueOnce({ eligibleSportIds: new Set(["sport-snooker"]) } as any); vi.mocked(getAllQueuesForSeason).mockResolvedValue( makeSeasonQueues([ [TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]], ["team-2", []], ]) ); const mockDelete = vi.fn().mockReturnThis(); const mockWhere = vi.fn().mockResolvedValue(undefined); const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere }); const result = await pruneIneligibleQueueItems({ seasonId: SEASON_ID, draftRounds: DRAFT_ROUNDS, allTeamIds: ALL_TEAM_IDS, db: mockDb as any, }); expect(result).toHaveLength(1); expect(result[0].teamId).toBe(TEAM_ID); expect(result[0].removedParticipantIds).toEqual(["p-snooker"]); expect(mockDelete).toHaveBeenCalledTimes(1); }); it("returns empty array when all teams still have their sports eligible", async () => { vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([ { id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } }, ] as any); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); vi.mocked(calculateDraftEligibility).mockReturnValue( { eligibleSportIds: new Set(["sport-snooker"]) } as any ); vi.mocked(getAllQueuesForSeason).mockResolvedValue( makeSeasonQueues([ [TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]], ["team-2", [{ id: "q-2", participantId: "p-snooker", queuePosition: 1 }]], ]) ); const mockDelete = vi.fn().mockReturnThis(); const mockDb = makeMockDb({ delete: mockDelete }); const result = await pruneIneligibleQueueItems({ seasonId: SEASON_ID, draftRounds: DRAFT_ROUNDS, allTeamIds: ALL_TEAM_IDS, db: mockDb as any, }); expect(result).toHaveLength(0); expect(mockDelete).not.toHaveBeenCalled(); }); it("skips teams with empty queues and does not call delete", async () => { vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); vi.mocked(calculateDraftEligibility).mockReturnValue( { eligibleSportIds: new Set([]) } as any ); // Both teams return no queue items from the single bulk fetch vi.mocked(getAllQueuesForSeason).mockResolvedValue(new Map()); const mockDelete = vi.fn().mockReturnThis(); const mockDb = makeMockDb({ delete: mockDelete }); const result = await pruneIneligibleQueueItems({ seasonId: SEASON_ID, draftRounds: DRAFT_ROUNDS, allTeamIds: ALL_TEAM_IDS, db: mockDb as any, }); expect(result).toHaveLength(0); expect(mockDelete).not.toHaveBeenCalled(); // The bulk fetch replaces per-team queries — getTeamQueue should never be called expect(vi.mocked(getTeamQueue)).not.toHaveBeenCalled(); }); it("prunes only the ineligible items when a team has a mixed queue", async () => { // Team has two queued players: snooker (ineligible) and NFL (still eligible) vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([ { id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } }, { id: "p-nfl", name: "Patrick Mahomes", sport: { id: "sport-nfl", name: "NFL" } }, ] as any); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); vi.mocked(calculateDraftEligibility).mockReturnValue( { eligibleSportIds: new Set(["sport-nfl"]) } as any // snooker full, NFL still ok ); vi.mocked(getAllQueuesForSeason).mockResolvedValue( makeSeasonQueues([ [TEAM_ID, [ { id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }, { id: "q-nfl", participantId: "p-nfl", queuePosition: 2 }, ]], ]) ); const mockDelete = vi.fn().mockReturnThis(); const mockWhere = vi.fn().mockResolvedValue(undefined); const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere }); const result = await pruneIneligibleQueueItems({ seasonId: SEASON_ID, draftRounds: DRAFT_ROUNDS, allTeamIds: [TEAM_ID], db: mockDb as any, }); expect(result).toHaveLength(1); expect(result[0].removedParticipantIds).toEqual(["p-snooker"]); // The NFL player must NOT be removed expect(result[0].removedParticipantIds).not.toContain("p-nfl"); expect(mockDelete).toHaveBeenCalledTimes(1); }); it("skips a queue item whose participant is not found in season sports and logs a warning", async () => { vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); // allParticipants does NOT include p-mystery vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]); vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); vi.mocked(calculateDraftEligibility).mockReturnValue( { eligibleSportIds: new Set([]) } as any ); vi.mocked(getAllQueuesForSeason).mockResolvedValue( makeSeasonQueues([ [TEAM_ID, [{ id: "q-mystery", participantId: "p-mystery", queuePosition: 1 }]], ]) ); const mockDelete = vi.fn().mockReturnThis(); const mockDb = makeMockDb({ delete: mockDelete }); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const result = await pruneIneligibleQueueItems({ seasonId: SEASON_ID, draftRounds: DRAFT_ROUNDS, allTeamIds: [TEAM_ID], db: mockDb as any, }); // Item is skipped — not deleted, not returned expect(result).toHaveLength(0); expect(mockDelete).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("p-mystery")); warnSpy.mockRestore(); }); });