/**
 * Tests for draft.force-manual-pick timer behavior across chess_clock and standard modes.
 *
 * chess_clock: force picks earn the increment just like any other pick (bank += increment)
 * standard:    force picks always reset the bank to exactly draftIncrementTime
 */
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { RouterContextProvider } from "react-router";
import { action } from "~/routes/api/draft.force-manual-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 }),
}));
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 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: COMMISSIONER_ID,
  pickedByType: "commissioner",
};

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-1" },
  },
  {
    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("teamId", TEAM_ID);
  formData.append("participantId", PARTICIPANT_ID);
  formData.append("pickNumber", "1");
  return new Request("http://localhost/api/draft/force-manual-pick", {
    method: "POST",
    body: formData,
  });
}

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

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

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

    // Default: authenticated as commissioner
    const { getAuth } = await import("@clerk/react-router/server");
    vi.mocked(getAuth).mockResolvedValue({ userId: COMMISSIONER_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({ id: "c-1", userId: COMMISSIONER_ID }),
        },
        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", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 75 }),
        },
      },
      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("commissioner force pick", () => {
      it("emits timer-update with bank + increment", async () => {
        // Pre-pick bank: 75s. Expected after increment: 75 + 30 = 105s.
        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 });

        // Two DB updates: timer row + season.currentPickNumber
        expect(mockDb.set).toHaveBeenCalledWith(
          expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) })
        );
      });

      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 })
        );
      });
    });

    describe("admin force 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 is not a commissioner
        mockDb.query.commissioners.findFirst.mockResolvedValue(null);
      });

      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 })
      );
    });

    describe("commissioner force pick", () => {
      it("resets bank to exactly draftIncrementTime", async () => {
        // Pre-pick bank doesn't matter — standard always resets
        mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 5 });
        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("does not accumulate time even when bank was large", async () => {
        // Team had 25s left; standard mode resets to increment, never adds to 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 })
        );
      });
    });

    describe("admin force 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);

        mockDb.query.commissioners.findFirst.mockResolvedValue(null);
      });

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

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

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