/**
 * Tests for getEventsForDates — the admin dashboard "Games to Score" query.
 */
import { describe, it, expect, vi, beforeEach } from "vitest";

// ── DB mock ──────────────────────────────────────────────────────────────────
// select returns a fluent chain; findMany is used in step 2.
const mockFindMany = vi.fn();

function makeSelectChain(rows: { id: string; eventDate?: string | null; gameDate?: string | null }[]) {
  const chain = {
    from: vi.fn().mockReturnThis(),
    leftJoin: vi.fn().mockReturnThis(),
    where: vi.fn().mockResolvedValue(rows),
  };
  return chain;
}

const mockSelect = vi.fn();

const mockDb = {
  query: {
    scoringEvents: { findMany: mockFindMany },
  },
  select: mockSelect,
};

vi.mock("~/database/context", () => ({
  database: () => mockDb,
}));

vi.mock("~/database/schema", () => ({
  scoringEvents: {
    id: "se.id",
    eventDate: "se.event_date",
    eventType: "se.event_type",
    eventStartsAt: "se.event_starts_at",
    createdAt: "se.created_at",
  },
  playoffMatches: {
    scoringEventId: "pm.scoring_event_id",
    id: "pm.id",
  },
  playoffMatchGames: {
    playoffMatchId: "pmg.playoff_match_id",
    scheduledAt: "pmg.scheduled_at",
  },
}));

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 }),
  sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ type: "sql", strings, values }),
}));

import { getEventsForDates } from "../scoring-event";

// ── Helpers ──────────────────────────────────────────────────────────────────

function makeEventRow(overrides: Partial<{
  id: string;
  name: string;
  eventDate: string | null;
  eventStartsAt: Date | null;
  eventType: string;
  isComplete: boolean;
  completedAt: Date | null;
  sportsSeasonId: string;
  sportsSeason: { name: string; sport: { name: string; slug: string | null } };
}> = {}) {
  return {
    id: overrides.id ?? "event-1",
    name: overrides.name ?? "Test Event",
    eventDate: overrides.eventDate ?? "2026-03-20",
    eventStartsAt: overrides.eventStartsAt ?? null,
    eventType: overrides.eventType ?? "playoff_game",
    isComplete: overrides.isComplete ?? false,
    completedAt: overrides.completedAt ?? null,
    sportsSeasonId: overrides.sportsSeasonId ?? "season-1",
    sportsSeason: overrides.sportsSeason ?? {
      name: "2026 NFL Playoffs",
      sport: { name: "NFL", slug: "nfl" },
    },
  };
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe("getEventsForDates", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockSelect.mockReturnValue(makeSelectChain([]));
  });

  it("returns empty array and skips DB when no dates given", async () => {
    const result = await getEventsForDates([]);
    expect(result).toEqual([]);
    expect(mockSelect).not.toHaveBeenCalled();
    expect(mockFindMany).not.toHaveBeenCalled();
  });

  it("returns empty array when select finds no matching event IDs", async () => {
    mockSelect.mockReturnValue(makeSelectChain([]));

    const result = await getEventsForDates(["2026-03-20"]);

    expect(result).toEqual([]);
    expect(mockFindMany).not.toHaveBeenCalled();
  });

  it("maps database rows to DashboardScoringEvent shape", async () => {
    mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
    mockFindMany.mockResolvedValueOnce([
      makeEventRow({
        id: "e1",
        name: "Conference Finals",
        eventDate: "2026-03-20",
        eventType: "playoff_game",
        isComplete: false,
        sportsSeasonId: "ss-1",
        sportsSeason: {
          name: "2026 NBA Playoffs",
          sport: { name: "NBA", slug: "nba" },
        },
      }),
    ]);

    const result = await getEventsForDates(["2026-03-20"]);

    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      id: "e1",
      name: "Conference Finals",
      eventDate: "2026-03-20",
      eventType: "playoff_game",
      isComplete: false,
      sportsSeasonId: "ss-1",
      sportsSeasonName: "2026 NBA Playoffs",
      sportName: "NBA",
      sportSlug: "nba",
    });
  });

  it("returns events for multiple dates", async () => {
    mockSelect.mockReturnValue(
      makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }, { id: "e2", eventDate: "2026-03-21", gameDate: null }])
    );
    mockFindMany.mockResolvedValueOnce([
      makeEventRow({ id: "e1", eventDate: "2026-03-20" }),
      makeEventRow({ id: "e2", eventDate: "2026-03-21" }),
    ]);

    const result = await getEventsForDates(["2026-03-20", "2026-03-21"]);

    expect(result).toHaveLength(2);
    expect(result.map((e) => e.id)).toEqual(["e1", "e2"]);
  });

  it("includes completed events with isComplete: true", async () => {
    mockSelect.mockReturnValue(makeSelectChain([{ id: "e-done", eventDate: "2026-03-20", gameDate: null }]));
    mockFindMany.mockResolvedValueOnce([
      makeEventRow({
        id: "e-done",
        isComplete: true,
        completedAt: new Date("2026-03-20T18:00:00Z"),
      }),
    ]);

    const result = await getEventsForDates(["2026-03-20"]);

    expect(result[0].isComplete).toBe(true);
    expect(result[0].completedAt).toBeInstanceOf(Date);
  });

  it("includes eventStartsAt when present", async () => {
    const starts = new Date("2026-03-20T19:30:00Z");
    mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
    mockFindMany.mockResolvedValueOnce([
      makeEventRow({ id: "e1", eventStartsAt: starts }),
    ]);

    const result = await getEventsForDates(["2026-03-20"]);

    expect(result[0].eventStartsAt).toEqual(starts);
  });

  it("uses two DB calls: select then findMany", async () => {
    mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
    mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1" })]);

    await getEventsForDates(["2026-03-20"]);

    expect(mockSelect).toHaveBeenCalledOnce();
    expect(mockFindMany).toHaveBeenCalledOnce();
  });

  it("passes event IDs from step 1 into findMany in step 2", async () => {
    mockSelect.mockReturnValue(
      makeSelectChain([{ id: "abc", eventDate: "2026-03-20", gameDate: null }, { id: "def", eventDate: "2026-03-20", gameDate: null }])
    );
    mockFindMany.mockResolvedValueOnce([]);

    await getEventsForDates(["2026-03-20"]);

    const findManyArgs = mockFindMany.mock.calls[0][0];
    const whereStr = JSON.stringify(findManyArgs.where);
    expect(whereStr).toContain("abc");
    expect(whereStr).toContain("def");
  });

  it("uses leftJoin so bracket events without playoffMatchGames still qualify via eventDate", async () => {
    const chain = makeSelectChain([]);
    mockSelect.mockReturnValue(chain);

    await getEventsForDates(["2026-03-20"]);

    expect(chain.leftJoin).toHaveBeenCalled();
  });

  it("includes both eventDate and scheduledAt timestamp bounds in the where clause", async () => {
    const chain = makeSelectChain([]);
    mockSelect.mockReturnValue(chain);

    await getEventsForDates(["2026-03-20", "2026-03-21"]);

    // The where clause is the first (and only) arg to chain.where
    const whereArg = JSON.stringify((chain.where as ReturnType<typeof vi.fn>).mock.calls[0][0]);
    // eventDate inArray check
    expect(whereArg).toContain("2026-03-20");
    expect(whereArg).toContain("2026-03-21");
    // timestamp bounds for scheduledAt
    expect(whereArg).toContain("T00:00:00.000Z");
    expect(whereArg).toContain("T23:59:59.999Z");
  });

  it("excludes schedule_event type via the where clause", async () => {
    const chain = makeSelectChain([]);
    mockSelect.mockReturnValue(chain);

    await getEventsForDates(["2026-03-20"]);

    const whereArg = JSON.stringify((chain.where as ReturnType<typeof vi.fn>).mock.calls[0][0]);
    expect(whereArg).toContain("playoff_game");
    expect(whereArg).toContain("major_tournament");
    expect(whereArg).toContain("final_standings");
    expect(whereArg).not.toContain("schedule_event");
  });

  it("sets displayDate from eventDate when present", async () => {
    mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
    mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: "2026-03-20" })]);

    const result = await getEventsForDates(["2026-03-20"]);

    expect(result[0].displayDate).toBe("2026-03-20");
  });

  it("sets displayDate from gameDate when eventDate is null", async () => {
    mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: null, gameDate: "2026-03-20" }]));
    mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: null })]);

    const result = await getEventsForDates(["2026-03-20"]);

    expect(result[0].displayDate).toBe("2026-03-20");
  });

  it("uses gameDate over eventDate when eventDate is outside requested dates", async () => {
    // Bracket event: eventDate is the round's overall date (not in range),
    // but an individual game is scheduled on the requested date.
    mockSelect.mockReturnValue(
      makeSelectChain([{ id: "e1", eventDate: "2026-04-05", gameDate: "2026-03-20" }])
    );
    mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: "2026-04-05" })]);

    const result = await getEventsForDates(["2026-03-20"]);

    expect(result).toHaveLength(1);
    expect(result[0].displayDate).toBe("2026-03-20");
  });

  it("returns two entries for an event with games on both requested dates", async () => {
    // Same event has a game today AND a game tomorrow — should appear in both tabs.
    mockSelect.mockReturnValue(
      makeSelectChain([
        { id: "e1", eventDate: null, gameDate: "2026-03-20" },
        { id: "e1", eventDate: null, gameDate: "2026-03-21" },
      ])
    );
    mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: null })]);

    const result = await getEventsForDates(["2026-03-20", "2026-03-21"]);

    expect(result).toHaveLength(2);
    const displayDates = result.map((e) => e.displayDate).toSorted();
    expect(displayDates).toEqual(["2026-03-20", "2026-03-21"]);
    // Both entries share the same event id
    expect(result[0].id).toBe("e1");
    expect(result[1].id).toBe("e1");
  });

  it("deduplicates when multiple matches in same bracket have games on the same day", async () => {
    // Two rows for the same event on the same gameDate (e.g. two matches in today's bracket)
    mockSelect.mockReturnValue(
      makeSelectChain([
        { id: "e1", eventDate: null, gameDate: "2026-03-20" },
        { id: "e1", eventDate: null, gameDate: "2026-03-20" },
      ])
    );
    mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: null })]);

    const result = await getEventsForDates(["2026-03-20"]);

    // Should appear only once, not twice
    expect(result).toHaveLength(1);
    expect(result[0].displayDate).toBe("2026-03-20");
  });
});
