/**
 * 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" },
  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 = new Date("2025-04-01");
const DATE_TO = new Date("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("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();
  });
});
