import { describe, it, expect, vi, beforeEach } from "vitest";

/**
 * Tests for calculateTeamProjectedScore
 *
 * Key behaviours under test:
 *  - Finalized bracket participants use calculateBracketPoints (averaged), not raw points
 *  - Partial-score participants (still alive) contribute floor points to actualPoints
 *    and their incremental EV (EV - floor) to projectedPoints
 *  - EV < floor is clamped to 0 extra EV (Math.max guard)
 *  - Non-bracket finalized participants use calculateFantasyPoints (raw)
 *  - Pending participants (no result) contribute full EV to projectedPoints
 */

vi.mock("~/services/ev-calculator", () => ({
  calculateEV: vi.fn(),
}));

vi.mock("../participant-expected-value", () => ({
  getParticipantEV: vi.fn(),
}));

import { calculateTeamProjectedScore, calculateTeamScore } from "../scoring-calculator";
import { calculateEV } from "~/services/ev-calculator";
import { getParticipantEV } from "../participant-expected-value";

// Standard scoring rules used across tests
const SCORING = {
  pointsFor1st: 100,
  pointsFor2nd: 70,
  pointsFor3rd: 50,
  pointsFor4th: 40,
  pointsFor5th: 25,
  pointsFor6th: 25,
  pointsFor7th: 15,
  pointsFor8th: 15,
};
// Derived bracket averages for assertions
const AVG_5_TO_8 = (25 + 25 + 15 + 15) / 4; // 20
const AVG_3_TO_4 = (50 + 40) / 2; // 45

/** Build a minimal pick object matching what the query returns */
function makePick(
  participantId: string,
  sportsSeasonId: string,
  scoringPattern: string,
  result: { finalPosition: number | null; isPartialScore: boolean } | null
) {
  return {
    pickNumber: 1,
    round: 1,
    participant: {
      id: participantId,
      sportsSeasonId,
      results: result ? [result] : [],
      sportsSeason: { scoringPattern },
    },
  };
}

/** Build a mock DB that returns the given picks and no bracketTemplateId */
function makeDb(picks: ReturnType<typeof makePick>[]) {
  return {
    query: {
      seasons: {
        findFirst: vi.fn().mockResolvedValue(SCORING),
      },
      draftPicks: {
        findMany: vi.fn().mockResolvedValue(picks),
      },
      scoringEvents: {
        findFirst: vi.fn().mockResolvedValue({ bracketTemplateId: null }),
      },
    },
  } as any;
}

const DUMMY_EV_ROW = {
  probFirst: "0.1",
  probSecond: "0.1",
  probThird: "0.1",
  probFourth: "0.1",
  probFifth: "0.1",
  probSixth: "0.1",
  probSeventh: "0.2",
  probEighth: "0.2",
} as any;

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

describe("calculateTeamProjectedScore", () => {
  describe("finalized bracket participant", () => {
    it("uses bracket-averaged points, not raw position value", async () => {
      // QF loser: finalPosition=5, isPartialScore=false
      // calculateBracketPoints(5) = avg(5,6,7,8) = 20, NOT raw pointsFor5th = 25
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: false }),
      ]);
      vi.mocked(getParticipantEV).mockResolvedValue(null);

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(AVG_5_TO_8); // 20, not 25
      expect(result.projectedPoints).toBe(AVG_5_TO_8);
      expect(result.participantsFinished).toBe(1);
      expect(result.participantsRemaining).toBe(0);
    });

    it("averages 3rd/4th for bracket semi-final losers", async () => {
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", { finalPosition: 3, isPartialScore: false }),
      ]);

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(AVG_3_TO_4); // 45
      expect(result.projectedPoints).toBe(AVG_3_TO_4);
    });

    it("uses raw points for non-bracket (e.g. season_standings) finalized participant", async () => {
      // Non-bracket 3rd place should use pointsFor3rd = 50, not averaged
      const db = makeDb([
        makePick("p1", "ss1", "season_standings", { finalPosition: 3, isPartialScore: false }),
      ]);

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(50); // raw pointsFor3rd
    });
  });

  describe("partial-score bracket participant (still alive)", () => {
    it("floor counts as actual; incremental EV above floor counts toward projected", async () => {
      // Won QF: finalPosition=3 (floor = avg[3,4] = 45), isPartialScore=true
      // EV = 60 → incremental = 60 - 45 = 15 → projected = 45 + 15 = 60
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", { finalPosition: 3, isPartialScore: true }),
      ]);
      vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW);
      vi.mocked(calculateEV).mockReturnValue(60);

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(AVG_3_TO_4); // 45 floor
      expect(result.projectedPoints).toBe(60); // 45 + (60 - 45)
      // Partial participants are NOT counted as finished — they're still competing
      expect(result.participantsFinished).toBe(0);
      expect(result.participantsRemaining).toBe(1);
    });

    it("clamps EV contribution to 0 when EV drops below floor (Math.max guard)", async () => {
      // Won SF: finalPosition=2 (floor = 70), isPartialScore=true
      // EV = 50 (below floor) → incremental = max(0, 50-70) = 0 → projected = 70
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", { finalPosition: 2, isPartialScore: true }),
      ]);
      vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW);
      vi.mocked(calculateEV).mockReturnValue(50); // EV < floor

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(70); // floor = 2nd place = 70
      expect(result.projectedPoints).toBe(70); // no negative EV contribution
    });

    it("partial-score floor for non-scoring round (pos 5) contributes 20 pts", async () => {
      // Won Round of 16 (non-scoring): finalPosition=5, isPartialScore=true → floor=20
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }),
      ]);
      vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW);
      vi.mocked(calculateEV).mockReturnValue(35);

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(AVG_5_TO_8); // 20
      expect(result.projectedPoints).toBe(35); // 20 + (35 - 20)
    });
  });

  describe("pending participant (no result)", () => {
    it("contributes 0 to actualPoints and full EV to projectedPoints", async () => {
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", null),
      ]);
      vi.mocked(getParticipantEV).mockResolvedValue(DUMMY_EV_ROW);
      vi.mocked(calculateEV).mockReturnValue(30);

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(0);
      expect(result.projectedPoints).toBe(30);
      expect(result.participantsFinished).toBe(0);
      expect(result.participantsRemaining).toBe(1);
    });

    it("contributes 0 projected when no EV data exists", async () => {
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", null),
      ]);
      vi.mocked(getParticipantEV).mockResolvedValue(null);

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(0);
      expect(result.projectedPoints).toBe(0);
    });
  });

  describe("mixed team (calculateTeamProjectedScore)", () => {
    it("correctly combines finalized, partial, and pending participants", async () => {
      // Pick 1: bracket finalized 1st place → actual=100, no EV
      // Pick 2: bracket partial pos 5 (floor=20) → actual=20, EV=35 → +15 projected
      // Pick 3: pending → actual=0, EV=25 → +25 projected
      // Total: actual=120, projected=120+15+25=160
      const db = makeDb([
        makePick("p1", "ss1", "playoff_bracket", { finalPosition: 1, isPartialScore: false }),
        makePick("p2", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }),
        makePick("p3", "ss1", "playoff_bracket", null),
      ]);
      vi.mocked(getParticipantEV)
        .mockResolvedValueOnce(DUMMY_EV_ROW) // p2 — partial
        .mockResolvedValueOnce(DUMMY_EV_ROW); // p3 — pending
      vi.mocked(calculateEV)
        .mockReturnValueOnce(35) // p2 EV
        .mockReturnValueOnce(25); // p3 EV

      const result = await calculateTeamProjectedScore("team1", "season1", db);

      expect(result.actualPoints).toBe(120); // 100 + 20
      expect(result.projectedPoints).toBe(160); // 120 + 15 + 25
      expect(result.participantsFinished).toBe(1);
      expect(result.participantsRemaining).toBe(2); // partial + pending
    });
  });
});

describe("calculateTeamScore", () => {
  // calculateTeamScore writes totalPoints, placementCounts, and participantsCompleted
  // to the teamStandings table. Partial-score participants (still alive in bracket)
  // must NOT be counted as completed or appear in placement tiebreakers.

  it("partial-score participant contributes floor to totalPoints but not to placementCounts or participantsCompleted", async () => {
    // QF survivor: finalPosition=5 (floor=20), isPartialScore=true
    const db = makeDb([
      makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }),
    ]);

    const result = await calculateTeamScore("team1", "season1", db);

    expect(result.totalPoints).toBe(AVG_5_TO_8); // 20 floor in score
    expect(result.participantsCompleted).toBe(0); // still alive → not completed
    expect(result.placementCounts[5]).toBe(0); // no placement badge until finalized
    expect(result.participantsTotal).toBe(1);
  });

  it("finalized bracket participant counts toward placementCounts and participantsCompleted", async () => {
    // QF loser: finalPosition=5, isPartialScore=false
    const db = makeDb([
      makePick("p1", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: false }),
    ]);

    const result = await calculateTeamScore("team1", "season1", db);

    expect(result.totalPoints).toBe(AVG_5_TO_8); // 20 bracket-averaged
    expect(result.participantsCompleted).toBe(1);
    expect(result.placementCounts[5]).toBe(1);
  });

  it("eliminated in non-scoring round (finalPosition=0) counts as completed", async () => {
    // Round of 16 loser: 0 pts, fully eliminated (isPartialScore=false)
    const db = makeDb([
      makePick("p1", "ss1", "playoff_bracket", { finalPosition: 0, isPartialScore: false }),
    ]);

    const result = await calculateTeamScore("team1", "season1", db);

    expect(result.totalPoints).toBe(0); // no points for pre-bracket elimination
    expect(result.participantsCompleted).toBe(1); // done — should not count as remaining
    expect(result.participantsTotal).toBe(1);
    expect(result.placementCounts[1]).toBe(0); // no placement badge
  });

  it("mixed team: partial counts in totalPoints only; finalized counts everywhere", async () => {
    // p1: finalized 1st → totalPoints +100, placementCounts[1]++, completed++
    // p2: partial pos 5 (floor 20) → totalPoints +20, no placement, not completed
    const db = makeDb([
      makePick("p1", "ss1", "playoff_bracket", { finalPosition: 1, isPartialScore: false }),
      makePick("p2", "ss1", "playoff_bracket", { finalPosition: 5, isPartialScore: true }),
    ]);

    const result = await calculateTeamScore("team1", "season1", db);

    expect(result.totalPoints).toBe(100 + AVG_5_TO_8); // 120
    expect(result.participantsCompleted).toBe(1);
    expect(result.participantsTotal).toBe(2);
    expect(result.placementCounts[1]).toBe(1);
    expect(result.placementCounts[5]).toBe(0);
  });
});
