/**
 * Tests for draft.make-pick timer behavior across chess_clock and standard modes.
 *
 * chess_clock: after ANY pick (owner, commissioner, admin), bank += increment
 * standard:    after ANY pick, bank resets to exactly draftIncrementTime
 */
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { RouterContextProvider } from "react-router";
import { action } from "~/routes/api/draft.make-pick";

const ctx = {} as unknown as RouterContextProvider;

vi.mock("~/database/context");
vi.mock("~/server/socket", () => ({
  getSocketIO: vi.fn(),
}));
vi.mock("@clerk/react-router/server", () => ({
  getAuth: vi.fn(),
}));
vi.mock("~/models/draft-pick", () => ({
  getDraftPicksWithSports: vi.fn(),
  getTeamDraftPicksWithSports: vi.fn(),
}));
vi.mock("~/models/participant", () => ({
  getParticipantsForSeasonWithSports: vi.fn(),
}));
vi.mock("~/models/season-sport", () => ({
  getSeasonSportsSimple: vi.fn(),
}));
vi.mock("~/lib/draft-eligibility", () => ({
  calculateDraftEligibility: vi.fn(),
}));
vi.mock("~/models/draft-utils", () => ({
  checkAndTriggerNextAutodraft: vi.fn(),
  calculatePickInfo: vi.fn().mockReturnValue({ round: 1, pickInRound: 1, teamIndex: 0 }),
  pruneIneligibleQueueItems: vi.fn().mockResolvedValue([]),
}));
vi.mock("~/models/user", () => ({
  isUserAdminByClerkId: vi.fn(),
}));

// ── Fixtures ──────────────────────────────────────────────────────────────────

const SEASON_ID = "season-1";
const TEAM_ID = "team-1";
const NEXT_TEAM_ID = "team-2";
const PARTICIPANT_ID = "participant-1";
const OWNER_ID = "owner-user-1";
const COMMISSIONER_ID = "commissioner-user-1";
const ADMIN_ID = "admin-user-1";
const SPORT_ID = "sport-nfl";

const mockParticipant = {
  id: PARTICIPANT_ID,
  name: "Patrick Mahomes",
  sportsSeason: {
    id: "sports-season-1",
    sport: { id: SPORT_ID, name: "NFL" },
  },
};

const mockDraftPick = {
  id: "pick-1",
  seasonId: SEASON_ID,
  teamId: TEAM_ID,
  participantId: PARTICIPANT_ID,
  pickNumber: 1,
  round: 1,
  pickInRound: 1,
  pickedByUserId: OWNER_ID,
  pickedByType: "owner",
};

// Two-team draft; team-1 picks first.
const mockDraftSlots = [
  {
    id: "slot-1",
    seasonId: SEASON_ID,
    teamId: TEAM_ID,
    draftOrder: 1,
    team: { id: TEAM_ID, name: "Team 1", seasonId: SEASON_ID, ownerId: OWNER_ID },
  },
  {
    id: "slot-2",
    seasonId: SEASON_ID,
    teamId: NEXT_TEAM_ID,
    draftOrder: 2,
    team: { id: NEXT_TEAM_ID, name: "Team 2", seasonId: SEASON_ID, ownerId: "owner-2" },
  },
];

function makeSeason(overrides: Record<string, unknown> = {}) {
  return {
    id: SEASON_ID,
    leagueId: "league-1",
    status: "draft",
    draftRounds: 3,
    draftInitialTime: 120,
    draftIncrementTime: 30,
    draftTimerMode: "chess_clock",
    currentPickNumber: 1,
    draftPaused: false,
    ...overrides,
  };
}

function makeRequest() {
  const formData = new FormData();
  formData.append("seasonId", SEASON_ID);
  formData.append("participantId", PARTICIPANT_ID);
  return new Request("http://localhost/api/draft/make-pick", {
    method: "POST",
    body: formData,
  });
}

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

describe("draft.make-pick action — timer mode behavior", () => {
  let mockDb: any;
  let mockSocketIO: any;

  beforeEach(async () => {
    vi.clearAllMocks();

    const { getAuth } = await import("@clerk/react-router/server");
    vi.mocked(getAuth).mockResolvedValue({ userId: OWNER_ID } as any);

    const { isUserAdminByClerkId } = await import("~/models/user");
    vi.mocked(isUserAdminByClerkId).mockResolvedValue(false);

    mockSocketIO = { to: vi.fn().mockReturnThis(), emit: vi.fn() };
    const socketModule = await import("~/server/socket");
    vi.mocked(socketModule.getSocketIO).mockReturnValue(mockSocketIO);

    const { getDraftPicksWithSports, getTeamDraftPicksWithSports } = await import("~/models/draft-pick");
    vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
    vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);

    const { getParticipantsForSeasonWithSports } = await import("~/models/participant");
    vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);

    const { getSeasonSportsSimple } = await import("~/models/season-sport");
    vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);

    const { calculateDraftEligibility } = await import("~/lib/draft-eligibility");
    vi.mocked(calculateDraftEligibility).mockReturnValue({
      eligibleSportIds: new Set([SPORT_ID]),
      ineligibleReasons: {},
    } as any);

    const { checkAndTriggerNextAutodraft } = await import("~/models/draft-utils");
    vi.mocked(checkAndTriggerNextAutodraft).mockResolvedValue(undefined);

    mockDb = {
      query: {
        seasons: { findFirst: vi.fn() },
        commissioners: { findFirst: vi.fn().mockResolvedValue(null) },
        draftPicks: { findFirst: vi.fn().mockResolvedValue(null) },
        participants: { findFirst: vi.fn().mockResolvedValue(mockParticipant) },
        draftSlots: { findMany: vi.fn().mockResolvedValue(mockDraftSlots) },
        draftTimers: { findFirst: vi.fn().mockResolvedValue({ id: "timer-1", timeRemaining: 75 }) },
        draftQueue: { findMany: vi.fn().mockResolvedValue([]) },
      },
      insert: vi.fn().mockReturnThis(),
      values: vi.fn().mockReturnThis(),
      returning: vi.fn().mockResolvedValue([mockDraftPick]),
      update: vi.fn().mockReturnThis(),
      set: vi.fn().mockReturnThis(),
      where: vi.fn().mockReturnThis(),
      delete: vi.fn().mockReturnThis(),
    };

    const { database } = await import("~/database/context");
    vi.mocked(database).mockReturnValue(mockDb);
  });

  // ── chess_clock mode ────────────────────────────────────────────────────────

  describe("chess_clock mode", () => {
    beforeEach(() => {
      mockDb.query.seasons.findFirst.mockResolvedValue(
        makeSeason({ draftTimerMode: "chess_clock", draftInitialTime: 120, draftIncrementTime: 30 })
      );
    });

    describe("owner pick", () => {
      // Auth default is OWNER_ID (team owner), no extra setup needed.

      it("emits timer-update with bank + increment", async () => {
        // DB returns 75 + 30 = 105 after the atomic add
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]);

        await action({ request: makeRequest(), params: {}, context: ctx });

        expect(mockSocketIO.emit).toHaveBeenCalledWith(
          "timer-update",
          expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 })
        );
      });

      it("accumulates a larger bank when more time was remaining", async () => {
        mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 100 });
        // 100 + 30 = 130
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 130 }]);

        await action({ request: makeRequest(), params: {}, context: ctx });

        expect(mockSocketIO.emit).toHaveBeenCalledWith(
          "timer-update",
          expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 130 })
        );
      });

      it("writes the timer update to the DB", async () => {
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]);

        await action({ request: makeRequest(), params: {}, context: ctx });

        expect(mockDb.set).toHaveBeenCalledWith(
          expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) })
        );
      });
    });

    describe("commissioner pick", () => {
      beforeEach(async () => {
        const { getAuth } = await import("@clerk/react-router/server");
        vi.mocked(getAuth).mockResolvedValue({ userId: COMMISSIONER_ID } as any);

        mockDb.query.commissioners.findFirst.mockResolvedValue({ id: "c-1", userId: COMMISSIONER_ID });
        // Commissioner does not own the team on the clock
        mockDb.query.draftSlots.findMany.mockResolvedValue([
          {
            ...mockDraftSlots[0],
            team: { ...mockDraftSlots[0].team, ownerId: "someone-else" },
          },
          mockDraftSlots[1],
        ]);
      });

      it("emits timer-update with bank + increment", async () => {
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]);

        await action({ request: makeRequest(), params: {}, context: ctx });

        expect(mockSocketIO.emit).toHaveBeenCalledWith(
          "timer-update",
          expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 })
        );
      });

      it("writes the timer update to the DB", async () => {
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]);

        await action({ request: makeRequest(), params: {}, context: ctx });

        expect(mockDb.set).toHaveBeenCalledWith(
          expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) })
        );
      });
    });

    describe("admin pick", () => {
      beforeEach(async () => {
        const { getAuth } = await import("@clerk/react-router/server");
        vi.mocked(getAuth).mockResolvedValue({ userId: ADMIN_ID } as any);

        const { isUserAdminByClerkId } = await import("~/models/user");
        vi.mocked(isUserAdminByClerkId).mockResolvedValue(true);

        // Admin does not own the team on the clock
        mockDb.query.draftSlots.findMany.mockResolvedValue([
          {
            ...mockDraftSlots[0],
            team: { ...mockDraftSlots[0].team, ownerId: "someone-else" },
          },
          mockDraftSlots[1],
        ]);
      });

      it("emits timer-update with bank + increment", async () => {
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]);

        await action({ request: makeRequest(), params: {}, context: ctx });

        expect(mockSocketIO.emit).toHaveBeenCalledWith(
          "timer-update",
          expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 })
        );
      });

      it("writes the timer update to the DB", async () => {
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]);

        await action({ request: makeRequest(), params: {}, context: ctx });

        expect(mockDb.set).toHaveBeenCalledWith(
          expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) })
        );
      });
    });
  });

  // ── standard mode ───────────────────────────────────────────────────────────

  describe("standard mode", () => {
    beforeEach(() => {
      mockDb.query.seasons.findFirst.mockResolvedValue(
        makeSeason({ draftTimerMode: "standard", draftInitialTime: 30, draftIncrementTime: 30 })
      );
    });

    it("owner pick — resets bank to exactly draftIncrementTime", async () => {
      mockDb.returning
        .mockResolvedValueOnce([mockDraftPick])
        .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]);

      await action({ request: makeRequest(), params: {}, context: ctx });

      expect(mockSocketIO.emit).toHaveBeenCalledWith(
        "timer-update",
        expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 })
      );
    });

    it("owner pick — fast picker does NOT accumulate time (bank never exceeds increment)", async () => {
      // Team had 25s left (picked quickly); standard mode ignores prior balance
      mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 25 });
      mockDb.returning
        .mockResolvedValueOnce([mockDraftPick])
        .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]);

      await action({ request: makeRequest(), params: {}, context: ctx });

      expect(mockSocketIO.emit).toHaveBeenCalledWith(
        "timer-update",
        expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 })
      );
    });

    it("commissioner pick — resets bank to exactly draftIncrementTime", async () => {
      const { getAuth } = await import("@clerk/react-router/server");
      vi.mocked(getAuth).mockResolvedValue({ userId: COMMISSIONER_ID } as any);
      mockDb.query.commissioners.findFirst.mockResolvedValue({ id: "c-1", userId: COMMISSIONER_ID });
      mockDb.query.draftSlots.findMany.mockResolvedValue([
        { ...mockDraftSlots[0], team: { ...mockDraftSlots[0].team, ownerId: "someone-else" } },
        mockDraftSlots[1],
      ]);

      mockDb.returning
        .mockResolvedValueOnce([mockDraftPick])
        .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]);

      await action({ request: makeRequest(), params: {}, context: ctx });

      expect(mockSocketIO.emit).toHaveBeenCalledWith(
        "timer-update",
        expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 })
      );
    });

    it("admin pick — resets bank to exactly draftIncrementTime", async () => {
      const { getAuth } = await import("@clerk/react-router/server");
      vi.mocked(getAuth).mockResolvedValue({ userId: ADMIN_ID } as any);
      const { isUserAdminByClerkId } = await import("~/models/user");
      vi.mocked(isUserAdminByClerkId).mockResolvedValue(true);
      mockDb.query.draftSlots.findMany.mockResolvedValue([
        { ...mockDraftSlots[0], team: { ...mockDraftSlots[0].team, ownerId: "someone-else" } },
        mockDraftSlots[1],
      ]);

      mockDb.returning
        .mockResolvedValueOnce([mockDraftPick])
        .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]);

      await action({ request: makeRequest(), params: {}, context: ctx });

      expect(mockSocketIO.emit).toHaveBeenCalledWith(
        "timer-update",
        expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 })
      );
    });

    it("uses custom draftIncrementTime when configured", async () => {
      mockDb.query.seasons.findFirst.mockResolvedValue(
        makeSeason({ draftTimerMode: "standard", draftInitialTime: 90, draftIncrementTime: 90 })
      );
      mockDb.returning
        .mockResolvedValueOnce([mockDraftPick])
        .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 90 }]);

      await action({ request: makeRequest(), params: {}, context: ctx });

      expect(mockSocketIO.emit).toHaveBeenCalledWith(
        "timer-update",
        expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 90 })
      );
    });
  });
});
