import { describe, it, expect, vi, beforeEach } from "vitest";
import { autoPickForTeam, pruneIneligibleQueueItems } from "../draft-utils";

// Mock all external model dependencies so we can control their return values
vi.mock("~/models/draft-pick", () => ({
  getDraftPicksWithSports: vi.fn(),
  getTeamDraftPicksWithSports: vi.fn(),
  isParticipantDrafted: vi.fn(),
}));

vi.mock("~/models/participant", () => ({
  getParticipantsForSeasonWithSports: vi.fn(),
}));

vi.mock("~/models/season-sport", () => ({
  getSeasonSportsSimple: vi.fn(),
}));

vi.mock("~/models/draft-queue", () => ({
  getTeamQueue: vi.fn(),
  getAllQueuesForSeason: vi.fn(),
}));

vi.mock("~/lib/draft-eligibility", () => ({
  calculateDraftEligibility: vi.fn(),
}));

// Prevent actual socket/db usage
vi.mock("~/server/socket");
vi.mock("~/database/context");

import {
  getDraftPicksWithSports,
  getTeamDraftPicksWithSports,
  isParticipantDrafted,
} from "~/models/draft-pick";
import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport";
import { getTeamQueue, getAllQueuesForSeason } from "~/models/draft-queue";
import { calculateDraftEligibility } from "~/lib/draft-eligibility";

const SEASON_ID = "season-1";
const TEAM_ID = "team-1";
const DRAFT_ROUNDS = 10;
const ALL_TEAM_IDS = [TEAM_ID, "team-2"];

// A minimal mock db for cases that do NOT reach getTopAvailableParticipant.
// `where` resolves to [] so `await db.select()...where()` yields an array.
function makeMockDb(overrides: Record<string, unknown> = {}) {
  const mockDb: Record<string, unknown> = {
    query: {
      participants: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    },
    select: vi.fn().mockReturnThis(),
    from: vi.fn().mockReturnThis(),
    innerJoin: vi.fn().mockReturnThis(),
    where: vi.fn().mockResolvedValue([]),
    orderBy: vi.fn().mockResolvedValue([]),
    delete: vi.fn().mockReturnThis(),
    ...overrides,
  };
  return mockDb;
}

// Build a mock db whose select chain returns a real EV participant.
// getTopAvailableParticipant makes three sequential awaitable calls:
//   1. select().from().where()            → drafted picks      → []
//   2. select().from().innerJoin()×2.where() → season sports   → [{sportsSeasonId, sportId}]
//   3. select().from().where().orderBy()  → top participant    → [{id, ...}]
function makeMockDbWithEvParticipant(participantId: string) {
  return {
    query: { participants: { findMany: vi.fn().mockResolvedValue([]) } },
    select: vi.fn().mockReturnThis(),
    from: vi.fn().mockReturnThis(),
    innerJoin: vi.fn().mockReturnThis(),
    where: vi.fn()
      .mockResolvedValueOnce([])                                          // call 1: drafted picks
      .mockResolvedValueOnce([{ sportsSeasonId: "ss-1", sportId: "sport-1" }]) // call 2: season sports
      .mockReturnThis(),                                                  // call 3: chain to orderBy
    orderBy: vi.fn().mockResolvedValue([
      { id: participantId, name: "EV Player", expectedValue: "100.00" },
    ]),
    delete: vi.fn().mockReturnThis(),
  };
}

// Minimal eligibility stub — makes every sport eligible.
function stubEligibility() {
  vi.mocked(calculateDraftEligibility).mockReturnValue({
    eligibleSportIds: new Set(["sport-1"]),
  } as ReturnType<typeof calculateDraftEligibility>);
}

beforeEach(() => {
  vi.clearAllMocks();
  vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
  vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
  vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
  vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
  stubEligibility();
});

describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
  describe("empty queue", () => {
    it("returns null when queueOnly=true (no EV fallback)", async () => {
      vi.mocked(getTeamQueue).mockResolvedValue([]);

      const result = await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        makeMockDb() as any,
        true // queueOnly
      );

      expect(result).toBeNull();
    });

    it("returns the top EV participant when queueOnly=false (falls back to EV)", async () => {
      vi.mocked(getTeamQueue).mockResolvedValue([]);

      const mockDb = makeMockDbWithEvParticipant("p-ev");

      const result = await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        mockDb as any,
        false // queueOnly off
      );

      // Must return the EV participant — proves the EV path ran, not the queueOnly guard.
      expect(result).toBe("p-ev");
    });
  });

  describe("all queue items already drafted", () => {
    it("returns null when queueOnly=true and every queued participant is taken", async () => {
      const queue = [
        { id: "q-1", participantId: "p-1", queuePosition: 1 },
        { id: "q-2", participantId: "p-2", queuePosition: 2 },
      ];
      vi.mocked(getTeamQueue).mockResolvedValue(queue as any);

      const mockDb = makeMockDb({
        query: {
          participants: {
            findMany: vi.fn().mockResolvedValue([
              {
                id: "p-1",
                name: "Player One",
                sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
              },
              {
                id: "p-2",
                name: "Player Two",
                sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
              },
            ]),
          },
        },
      });

      // Both participants are already drafted
      vi.mocked(isParticipantDrafted).mockResolvedValue(true);

      const result = await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        mockDb as any,
        true // queueOnly
      );

      expect(result).toBeNull();
    });

    it("removes stale queue entries when all items are drafted", async () => {
      const queue = [{ id: "q-1", participantId: "p-1", queuePosition: 1 }];
      vi.mocked(getTeamQueue).mockResolvedValue(queue as any);

      const mockDelete = vi.fn().mockReturnThis();
      const mockWhere = vi.fn().mockResolvedValue(undefined);
      const mockDb = makeMockDb({
        query: {
          participants: {
            findMany: vi.fn().mockResolvedValue([
              {
                id: "p-1",
                name: "Player One",
                sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
              },
            ]),
          },
        },
        delete: mockDelete,
        where: mockWhere,
      });

      vi.mocked(isParticipantDrafted).mockResolvedValue(true);

      await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        mockDb as any,
        true
      );

      // Stale items should be cleaned from the queue
      expect(mockDelete).toHaveBeenCalled();
    });
  });

  describe("ineligible sport — only queued player's sport is full", () => {
    it("removes the ineligible queue item and returns null when queueOnly=true", async () => {
      // Scenario: team has a snooker player queued but snooker is already full (not in eligibleSportIds)
      const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }];
      vi.mocked(getTeamQueue).mockResolvedValue(queue as any);

      // Snooker is NOT eligible — only "sport-other" is
      vi.mocked(calculateDraftEligibility).mockReturnValue({
        eligibleSportIds: new Set(["sport-other"]),
      } as ReturnType<typeof calculateDraftEligibility>);

      const mockDelete = vi.fn().mockReturnThis();
      const mockWhere = vi.fn().mockResolvedValue(undefined);
      const mockDb = makeMockDb({
        query: {
          participants: {
            findMany: vi.fn().mockResolvedValue([
              {
                id: "p-snooker",
                name: "Ronnie O'Sullivan",
                sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } },
              },
            ]),
          },
        },
        delete: mockDelete,
        where: mockWhere,
      });

      vi.mocked(isParticipantDrafted).mockResolvedValue(false); // not drafted — ineligible due to sport

      const result = await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        mockDb as any,
        true // queueOnly — no EV fallback
      );

      // Queue item should be removed
      expect(mockDelete).toHaveBeenCalled();
      // No valid pick available
      expect(result).toBeNull();
    });

    it("falls back to EV pick from eligible sports when queueOnly=false", async () => {
      // Scenario: same ineligible snooker player, but queueOnly is off so system picks best available
      const queue = [{ id: "q-snooker", participantId: "p-snooker", queuePosition: 1 }];
      vi.mocked(getTeamQueue).mockResolvedValue(queue as any);

      vi.mocked(calculateDraftEligibility).mockReturnValue({
        eligibleSportIds: new Set(["sport-other"]),
      } as ReturnType<typeof calculateDraftEligibility>);

      // where() call order:
      //   1. delete().where()           — remove ineligible snooker item from queue
      //   2. select().from().where()    — getTopAvailableParticipant: drafted picks → []
      //   3. select().from().innerJoin().innerJoin().where() — season sports → [{...}]
      //   4. select().from(participants).where().orderBy() — participant query (chain)
      const mockDb = {
        query: {
          participants: {
            findMany: vi.fn().mockResolvedValue([
              {
                id: "p-snooker",
                name: "Ronnie O'Sullivan",
                sportsSeason: { sport: { id: "sport-snooker", name: "Snooker" } },
              },
            ]),
          },
        },
        select: vi.fn().mockReturnThis(),
        from: vi.fn().mockReturnThis(),
        innerJoin: vi.fn().mockReturnThis(),
        where: vi.fn()
          .mockResolvedValueOnce(undefined)                                              // call 1: delete ineligible queue items
          .mockResolvedValueOnce([])                                                     // call 2: drafted picks
          .mockResolvedValueOnce([{ sportsSeasonId: "ss-other", sportId: "sport-other" }]) // call 3: season sports
          .mockReturnThis(),                                                             // call 4: participant query → chain to orderBy
        orderBy: vi.fn().mockResolvedValue([
          { id: "p-ev", name: "EV Player", expectedValue: "90.00" },
        ]),
        delete: vi.fn().mockReturnThis(),
      };

      vi.mocked(isParticipantDrafted).mockResolvedValue(false);

      const result = await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        mockDb as any,
        false // queueOnly off — should fall back to best available
      );

      // Should pick the best available from eligible sports, not the snooker player
      expect(result).toBe("p-ev");
    });
  });

  describe("partial queue — first item drafted, second available", () => {
    it("skips drafted item and returns the next valid participant", async () => {
      const queue = [
        { id: "q-1", participantId: "p-1", queuePosition: 1 },
        { id: "q-2", participantId: "p-2", queuePosition: 2 },
      ];
      vi.mocked(getTeamQueue).mockResolvedValue(queue as any);

      const mockDb = makeMockDb({
        query: {
          participants: {
            findMany: vi.fn().mockResolvedValue([
              {
                id: "p-1",
                name: "Player One",
                sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
              },
              {
                id: "p-2",
                name: "Player Two",
                sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
              },
            ]),
          },
        },
      });

      // p-1 is already drafted, p-2 is available
      vi.mocked(isParticipantDrafted).mockImplementation(
        async (_seasonId, participantId) => participantId === "p-1"
      );

      const result = await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        mockDb as any,
        true // queueOnly — still uses queue, just no EV fallback
      );

      expect(result).toBe("p-2");
    });

    it("returns the first available participant when none are drafted (happy path)", async () => {
      const queue = [
        { id: "q-1", participantId: "p-1", queuePosition: 1 },
        { id: "q-2", participantId: "p-2", queuePosition: 2 },
      ];
      vi.mocked(getTeamQueue).mockResolvedValue(queue as any);

      const mockDb = makeMockDb({
        query: {
          participants: {
            findMany: vi.fn().mockResolvedValue([
              {
                id: "p-1",
                name: "Player One",
                sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
              },
              {
                id: "p-2",
                name: "Player Two",
                sportsSeason: { sport: { id: "sport-1", name: "NFL" } },
              },
            ]),
          },
        },
      });

      vi.mocked(isParticipantDrafted).mockResolvedValue(false);

      const result = await autoPickForTeam(
        SEASON_ID,
        TEAM_ID,
        DRAFT_ROUNDS,
        ALL_TEAM_IDS,
        mockDb as any,
        true
      );

      expect(result).toBe("p-1");
    });
  });
});

function makeSeasonQueues(entries: [string, { id: string; participantId: string; queuePosition: number }[]][]) {
  return new Map(entries.map(([teamId, items]) => [teamId, items as any]));
}

describe("pruneIneligibleQueueItems", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
    vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
    vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
    vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
  });

  it("removes queued snooker player when snooker is no longer eligible for a team", async () => {
    vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
    vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
      { id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
    ] as any);
    vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);

    // team-1: snooker ineligible; team-2: snooker still eligible
    vi.mocked(calculateDraftEligibility)
      .mockReturnValueOnce({ eligibleSportIds: new Set([]) } as any)
      .mockReturnValueOnce({ eligibleSportIds: new Set(["sport-snooker"]) } as any);

    vi.mocked(getAllQueuesForSeason).mockResolvedValue(
      makeSeasonQueues([
        [TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]],
        ["team-2", []],
      ])
    );

    const mockDelete = vi.fn().mockReturnThis();
    const mockWhere = vi.fn().mockResolvedValue(undefined);
    const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere });

    const result = await pruneIneligibleQueueItems({
      seasonId: SEASON_ID,
      draftRounds: DRAFT_ROUNDS,
      allTeamIds: ALL_TEAM_IDS,
      db: mockDb as any,
    });

    expect(result).toHaveLength(1);
    expect(result[0].teamId).toBe(TEAM_ID);
    expect(result[0].removedParticipantIds).toEqual(["p-snooker"]);
    expect(mockDelete).toHaveBeenCalledTimes(1);
  });

  it("returns empty array when all teams still have their sports eligible", async () => {
    vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
    vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
      { id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
    ] as any);
    vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);

    vi.mocked(calculateDraftEligibility).mockReturnValue(
      { eligibleSportIds: new Set(["sport-snooker"]) } as any
    );

    vi.mocked(getAllQueuesForSeason).mockResolvedValue(
      makeSeasonQueues([
        [TEAM_ID, [{ id: "q-1", participantId: "p-snooker", queuePosition: 1 }]],
        ["team-2", [{ id: "q-2", participantId: "p-snooker", queuePosition: 1 }]],
      ])
    );

    const mockDelete = vi.fn().mockReturnThis();
    const mockDb = makeMockDb({ delete: mockDelete });

    const result = await pruneIneligibleQueueItems({
      seasonId: SEASON_ID,
      draftRounds: DRAFT_ROUNDS,
      allTeamIds: ALL_TEAM_IDS,
      db: mockDb as any,
    });

    expect(result).toHaveLength(0);
    expect(mockDelete).not.toHaveBeenCalled();
  });

  it("skips teams with empty queues and does not call delete", async () => {
    vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
    vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
    vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
    vi.mocked(calculateDraftEligibility).mockReturnValue(
      { eligibleSportIds: new Set([]) } as any
    );

    // Both teams return no queue items from the single bulk fetch
    vi.mocked(getAllQueuesForSeason).mockResolvedValue(new Map());

    const mockDelete = vi.fn().mockReturnThis();
    const mockDb = makeMockDb({ delete: mockDelete });

    const result = await pruneIneligibleQueueItems({
      seasonId: SEASON_ID,
      draftRounds: DRAFT_ROUNDS,
      allTeamIds: ALL_TEAM_IDS,
      db: mockDb as any,
    });

    expect(result).toHaveLength(0);
    expect(mockDelete).not.toHaveBeenCalled();
    // The bulk fetch replaces per-team queries — getTeamQueue should never be called
    expect(vi.mocked(getTeamQueue)).not.toHaveBeenCalled();
  });

  it("prunes only the ineligible items when a team has a mixed queue", async () => {
    // Team has two queued players: snooker (ineligible) and NFL (still eligible)
    vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
    vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([
      { id: "p-snooker", name: "Ronnie O'Sullivan", sport: { id: "sport-snooker", name: "Snooker" } },
      { id: "p-nfl", name: "Patrick Mahomes", sport: { id: "sport-nfl", name: "NFL" } },
    ] as any);
    vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);

    vi.mocked(calculateDraftEligibility).mockReturnValue(
      { eligibleSportIds: new Set(["sport-nfl"]) } as any  // snooker full, NFL still ok
    );

    vi.mocked(getAllQueuesForSeason).mockResolvedValue(
      makeSeasonQueues([
        [TEAM_ID, [
          { id: "q-snooker", participantId: "p-snooker", queuePosition: 1 },
          { id: "q-nfl",    participantId: "p-nfl",    queuePosition: 2 },
        ]],
      ])
    );

    const mockDelete = vi.fn().mockReturnThis();
    const mockWhere = vi.fn().mockResolvedValue(undefined);
    const mockDb = makeMockDb({ delete: mockDelete, where: mockWhere });

    const result = await pruneIneligibleQueueItems({
      seasonId: SEASON_ID,
      draftRounds: DRAFT_ROUNDS,
      allTeamIds: [TEAM_ID],
      db: mockDb as any,
    });

    expect(result).toHaveLength(1);
    expect(result[0].removedParticipantIds).toEqual(["p-snooker"]);
    // The NFL player must NOT be removed
    expect(result[0].removedParticipantIds).not.toContain("p-nfl");
    expect(mockDelete).toHaveBeenCalledTimes(1);
  });

  it("skips a queue item whose participant is not found in season sports and logs a warning", async () => {
    vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
    // allParticipants does NOT include p-mystery
    vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
    vi.mocked(getSeasonSportsSimple).mockResolvedValue([]);
    vi.mocked(calculateDraftEligibility).mockReturnValue(
      { eligibleSportIds: new Set([]) } as any
    );

    vi.mocked(getAllQueuesForSeason).mockResolvedValue(
      makeSeasonQueues([
        [TEAM_ID, [{ id: "q-mystery", participantId: "p-mystery", queuePosition: 1 }]],
      ])
    );

    const mockDelete = vi.fn().mockReturnThis();
    const mockDb = makeMockDb({ delete: mockDelete });
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

    const result = await pruneIneligibleQueueItems({
      seasonId: SEASON_ID,
      draftRounds: DRAFT_ROUNDS,
      allTeamIds: [TEAM_ID],
      db: mockDb as any,
    });

    // Item is skipped — not deleted, not returned
    expect(result).toHaveLength(0);
    expect(mockDelete).not.toHaveBeenCalled();
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("p-mystery"));

    warnSpy.mockRestore();
  });
});
