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 SPORT_ID = "sport-nfl";

const mockSeason = {
  id: SEASON_ID,
  leagueId: "league-1",
  status: "draft",
  draftRounds: 3,
  draftInitialTime: 120,
  draftIncrementTime: 30,
  draftTimerMode: "chess_clock",
  currentPickNumber: 1,
  draftPaused: false,
};

const mockTeams = [
  { id: TEAM_ID, name: "Team 1", seasonId: SEASON_ID, ownerId: "owner-1" },
  { id: NEXT_TEAM_ID, name: "Team 2", seasonId: SEASON_ID, ownerId: "owner-2" },
];

const mockDraftSlots = [
  { id: "slot-1", seasonId: SEASON_ID, teamId: TEAM_ID, draftOrder: 1, team: mockTeams[0] },
  { id: "slot-2", seasonId: SEASON_ID, teamId: NEXT_TEAM_ID, draftOrder: 2, team: mockTeams[1] },
];

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",
};

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

function makeRequest(data: Record<string, string>) {
  const formData = new FormData();
  for (const [key, value] of Object.entries(data)) {
    formData.append(key, value);
  }
  return new Request("http://localhost/api/draft/force-manual-pick", {
    method: "POST",
    body: formData,
  });
}

function defaultPickRequest() {
  return makeRequest({
    seasonId: SEASON_ID,
    teamId: TEAM_ID,
    participantId: PARTICIPANT_ID,
    pickNumber: "1",
  });
}

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

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

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

    // Auth: default to authenticated commissioner
    const { getAuth } = await import("@clerk/react-router/server");
    vi.mocked(getAuth).mockResolvedValue({ userId: COMMISSIONER_ID } as any);

    // User model: default to non-admin
    const { isUserAdminByClerkId } = await import("~/models/user");
    vi.mocked(isUserAdminByClerkId).mockResolvedValue(false);

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

    // Eligibility model functions
    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);

    // Database — happy-path defaults
    mockDb = {
      query: {
        seasons: { findFirst: vi.fn() },
        commissioners: { findFirst: vi.fn() },
        draftPicks: { findFirst: vi.fn() },
        participants: { findFirst: vi.fn() },
        draftSlots: { findMany: vi.fn() },
        draftTimers: { findFirst: vi.fn() },
      },
      insert: vi.fn().mockReturnThis(),
      values: vi.fn().mockReturnThis(),
      returning: vi.fn(),
      update: vi.fn().mockReturnThis(),
      set: vi.fn().mockReturnThis(),
      where: vi.fn().mockReturnThis(),
      delete: vi.fn().mockReturnThis(),
    };

    mockDb.query.seasons.findFirst.mockResolvedValue(mockSeason);
    mockDb.query.commissioners.findFirst.mockResolvedValue({
      id: "c-1",
      userId: COMMISSIONER_ID,
    });
    mockDb.query.draftPicks.findFirst.mockResolvedValue(null);
    mockDb.query.participants.findFirst.mockResolvedValue(mockParticipant);
    mockDb.query.draftSlots.findMany.mockResolvedValue(mockDraftSlots);
    mockDb.query.draftTimers.findFirst.mockResolvedValue({
      id: "timer-1",
      seasonId: SEASON_ID,
      teamId: TEAM_ID,
      timeRemaining: 75,
    });
    mockDb.returning.mockResolvedValue([mockDraftPick]);

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

  // ── Authorization ──────────────────────────────────────────────────────────

  describe("authorization", () => {
    it("returns 401 when user is not authenticated", async () => {
      const { getAuth } = await import("@clerk/react-router/server");
      vi.mocked(getAuth).mockResolvedValue({ userId: null } as any);

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(401);
    });

    it("returns 403 when authenticated user is not a commissioner", async () => {
      mockDb.query.commissioners.findFirst.mockResolvedValue(null);

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(403);
      const data = await response.json();
      expect(data.error).toMatch(/commissioner/i);
    });
  });

  // ── Input validation ───────────────────────────────────────────────────────

  describe("input validation", () => {
    it("returns 400 when required fields are missing", async () => {
      const response = await action({
        request: makeRequest({ seasonId: SEASON_ID }), // missing teamId, participantId, pickNumber
        params: {},
        context: ctx,
      });

      expect(response.status).toBe(400);
    });

    it("returns 404 when the season does not exist", async () => {
      mockDb.query.seasons.findFirst.mockResolvedValue(null);

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(404);
    });

    it("returns 400 when the draft is not active", async () => {
      mockDb.query.seasons.findFirst.mockResolvedValue({ ...mockSeason, status: "pre_draft" });

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(400);
      const data = await response.json();
      expect(data.error).toMatch(/not currently active/i);
    });

    it("returns 400 when a pick already exists at the requested slot", async () => {
      mockDb.query.draftPicks.findFirst.mockResolvedValueOnce({
        id: "existing-pick",
        participantId: "other-participant",
      });

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(400);
      const data = await response.json();
      expect(data.error).toMatch(/already exists at this slot/i);
    });

    it("returns 400 when the participant is already drafted", async () => {
      // First call: slot check (no pick at this slot number)
      // Second call: participant check (participant already drafted elsewhere)
      mockDb.query.draftPicks.findFirst
        .mockResolvedValueOnce(null)
        .mockResolvedValueOnce({
          id: "existing-pick",
          participantId: PARTICIPANT_ID,
        });

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(400);
      const data = await response.json();
      expect(data.error).toMatch(/already drafted/i);
    });

    it("returns 404 when the participant does not exist", async () => {
      mockDb.query.participants.findFirst.mockResolvedValue(null);

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(404);
    });

    it("returns 400 when the participant's sport is ineligible for the team", async () => {
      const { calculateDraftEligibility } = await import("~/lib/draft-eligibility");
      vi.mocked(calculateDraftEligibility).mockReturnValue({
        eligibleSportIds: new Set(),
        ineligibleReasons: { [SPORT_ID]: "Sport limit reached" },
      } as any);

      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(400);
      const data = await response.json();
      expect(data.error).toMatch(/sport limit reached/i);
    });
  });

  // ── Successful pick ────────────────────────────────────────────────────────

  describe("successful pick", () => {
    it("returns 200 with pick data and next pick number", async () => {
      const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(response.status).toBe(200);
      const data = await response.json();
      expect(data.success).toBe(true);
      expect(data.pick).toBeDefined();
      expect(data.nextPickNumber).toBe(2);
      expect(data.isDraftComplete).toBe(false);
    });

    it("marks the draft as complete when the final pick is made", async () => {
      // 2 teams × 3 rounds = 6 total picks; pick 6 is the last
      const response = await action({
        request: makeRequest({
          seasonId: SEASON_ID,
          teamId: TEAM_ID,
          participantId: PARTICIPANT_ID,
          pickNumber: "6",
        }),
        params: {},
        context: ctx,
      });

      const data = await response.json();
      expect(data.isDraftComplete).toBe(true);
    });

    it("emits pick-made to the correct draft room", async () => {
      await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${SEASON_ID}`);
      expect(mockSocketIO.emit).toHaveBeenCalledWith(
        "pick-made",
        expect.objectContaining({ nextPickNumber: 2, isDraftComplete: false })
      );
    });

    it("removes the participant from all team queues in the season", async () => {
      await action({ request: defaultPickRequest(), params: {}, context: ctx });

      expect(mockDb.delete).toHaveBeenCalled();
      expect(mockSocketIO.emit).toHaveBeenCalledWith("participant-removed-from-queues", {
        participantId: PARTICIPANT_ID,
      });
    });
  });

  // ── Timer behavior ─────────────────────────────────────────────────────────
  //
  // Force manual picks earn the increment the same as any other pick.
  //
  // chess_clock: bank += draftIncrementTime (atomic add).
  // standard:    bank resets to draftIncrementTime for the next turn.
  // Next team's bank is always left completely untouched.

  describe("timer behavior", () => {
    describe("chess_clock mode", () => {
      it("adds the increment to the picking team's bank", async () => {
        // Timer has 75s (from beforeEach mock); 75 + 30 = 105s after increment
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 105 }]);

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

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

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

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

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

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

      it("resets the picking team's timer to draftIncrementTime", async () => {
        mockDb.returning
          .mockResolvedValueOnce([mockDraftPick])
          .mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 30 }]);

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

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

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

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

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

    // ── REGRESSION: Next team's timer must never be touched ──────────────────

    it("REGRESSION: does not emit a timer-update for the next team", async () => {
      await action({ request: defaultPickRequest(), params: {}, context: ctx });

      const nextTeamTimerEmits = mockSocketIO.emit.mock.calls.filter(
        ([event, payload]: [string, any]) =>
          event === "timer-update" && payload?.teamId === NEXT_TEAM_ID
      );
      expect(nextTeamTimerEmits).toHaveLength(0);
    });
  });
});
