import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
import { NCAAFootballSimulator } from "../ncaa-football-simulator";

vi.mock("~/database/context", () => ({
  database: vi.fn(),
}));

// Use real implementations of pure math functions to avoid mock call accumulation
// across 50k-iteration simulations. Only mock convertFuturesToElo (complex behavior).
vi.mock("~/services/probability-engine", async (importOriginal) => {
  const actual = await importOriginal() as Record<string, unknown>;
  return {
    ...actual,
    convertFuturesToElo: vi.fn((oddsInput: { participantId: string; odds: number }[]) => {
      return new Map(oddsInput.map((o, i) => [o.participantId, 1600 - i * 50]));
    }),
  };
});

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

const TEAM_IDS = Array.from({ length: 12 }, (_, i) => `team-${i + 1}`);

/** Build EV rows with sourceElo ratings (descending — team-1 is best). */
function makeEvRows(
  ids: string[],
  opts: { includeOdds?: boolean } = {}
) {
  return ids.map((participantId, i) => ({
    participantId,
    sourceElo: 1700 - i * 50, // 1700, 1650, 1600, …
    sourceOdds: opts.includeOdds ? (i === 0 ? -200 : 300 + i * 50) : null,
  }));
}

function makeParticipants(ids: string[]) {
  return ids.map((id) => ({ id }));
}

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

describe("NCAAFootballSimulator", () => {
  let mockDb: { select: MockInstance };
  let selectCallCount: number;

  beforeEach(async () => {
    selectCallCount = 0;
    const { database } = await import("~/database/context");

    mockDb = { select: vi.fn() };
    (database as unknown as MockInstance).mockReturnValue(mockDb);
  });

  function setupMockDb(participantIds: string[], opts: { includeOdds?: boolean } = {}) {
    const participants = makeParticipants(participantIds);
    const evRows = makeEvRows(participantIds, opts);

    mockDb.select.mockImplementation(() => {
      const callIndex = selectCallCount++;
      const data = callIndex === 0 ? participants : evRows;
      return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) };
    });
  }

  // ── Post-bracket mode (exactly 12 teams) ─────────────────────────────────

  describe("post-bracket mode (exactly 12 teams)", () => {
    it("returns one result per participant", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      expect(results).toHaveLength(12);
    });

    it("all probabilities are between 0 and 1", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");

      for (const r of results) {
        const p = r.probabilities;
        expect(p.probFirst).toBeGreaterThanOrEqual(0);
        expect(p.probFirst).toBeLessThanOrEqual(1);
        expect(p.probSecond).toBeGreaterThanOrEqual(0);
        expect(p.probSecond).toBeLessThanOrEqual(1);
      }
    });

    it("top-rated team has highest champion probability", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");

      const byId = new Map(results.map((r) => [r.participantId, r]));
      expect(byId.get("team-1")?.probabilities.probFirst).toBeGreaterThan(
        byId.get("team-12")?.probabilities.probFirst ?? 0
      );
    });

    it("probFirst sums to approximately 1.0", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      const total = results.reduce((sum, r) => sum + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("probSecond sums to approximately 1.0", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      const total = results.reduce((sum, r) => sum + r.probabilities.probSecond, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("probThird equals probFourth for every team (tied SF placement)", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      for (const r of results) {
        expect(r.probabilities.probThird).toBeCloseTo(r.probabilities.probFourth, 10);
      }
    });

    it("probFifth through probEighth are equal for every team (tied QF placement)", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      for (const r of results) {
        const p = r.probabilities;
        expect(p.probFifth).toBeCloseTo(p.probSixth, 10);
        expect(p.probSixth).toBeCloseTo(p.probSeventh, 10);
        expect(p.probSeventh).toBeCloseTo(p.probEighth, 10);
      }
    });

    it("weakest team has very low champion probability", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      const byId = new Map(results.map((r) => [r.participantId, r]));
      // team-12 (Elo 1150) vs team-1 (Elo 1700): 550-point gap → should rarely win
      expect(byId.get("team-12")?.probabilities.probFirst).toBeLessThan(0.02);
    });

    it("source is cfp_monte_carlo", async () => {
      setupMockDb(TEAM_IDS);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      for (const r of results) {
        expect(r.source).toBe("cfp_monte_carlo");
      }
    });

    it("uses futures odds when sourceOdds present", async () => {
      setupMockDb(TEAM_IDS, { includeOdds: true });
      const results = await new NCAAFootballSimulator().simulate("season-1");
      expect(results).toHaveLength(12);
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });
  });

  // ── Pre-bracket mode (>12 teams — probabilistic selection) ───────────────

  describe("pre-bracket mode (>12 teams)", () => {
    it("returns one result per participant when pool is larger than 12", async () => {
      const twentyTeams = Array.from({ length: 20 }, (_, i) => `team-${i + 1}`);
      setupMockDb(twentyTeams);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      expect(results).toHaveLength(20);
    });

    it("probFirst still sums to ~1.0 across all teams (one champion per sim)", async () => {
      const twentyTeams = Array.from({ length: 20 }, (_, i) => `team-${i + 1}`);
      setupMockDb(twentyTeams);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      const total = results.reduce((sum, r) => sum + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("top team has much higher probFirst than bottom team in larger pool", async () => {
      const twentyTeams = Array.from({ length: 20 }, (_, i) => `team-${i + 1}`);
      setupMockDb(twentyTeams);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      const byId = new Map(results.map((r) => [r.participantId, r]));
      // team-1 (Elo 1700) should win far more often than team-20 (Elo 750)
      expect(byId.get("team-1")?.probabilities.probFirst).toBeGreaterThan(
        (byId.get("team-20")?.probabilities.probFirst ?? 0) * 10
      );
    });

    it("bottom-pool teams (large Elo gap) rarely appear in champion slot", async () => {
      const twentyTeams = Array.from({ length: 20 }, (_, i) => `team-${i + 1}`);
      setupMockDb(twentyTeams);
      const results = await new NCAAFootballSimulator().simulate("season-1");
      const byId = new Map(results.map((r) => [r.participantId, r]));
      // team-19 and team-20 have Elos of 800 and 750 — far below the top 12 (~1150+)
      // With softmax T=100, their selection weight is negligible
      expect(byId.get("team-19")?.probabilities.probFirst).toBeLessThan(0.01);
      expect(byId.get("team-20")?.probabilities.probFirst).toBeLessThan(0.01);
    });

    it("uses futures odds as selection weights when provided", async () => {
      const twentyTeams = Array.from({ length: 20 }, (_, i) => `team-${i + 1}`);
      setupMockDb(twentyTeams, { includeOdds: true });
      const results = await new NCAAFootballSimulator().simulate("season-1");
      expect(results).toHaveLength(20);
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });
  });

  // ── Error cases ───────────────────────────────────────────────────────────

  describe("error cases", () => {
    it("throws when fewer than 12 participants provided", async () => {
      const elevenTeams = TEAM_IDS.slice(0, 11);
      let call = 0;
      mockDb.select.mockImplementation(() => {
        const data = call++ === 0 ? makeParticipants(elevenTeams) : makeEvRows(elevenTeams);
        return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) };
      });
      await expect(new NCAAFootballSimulator().simulate("season-1")).rejects.toThrow(/at least 12/);
    });

    it("throws when participant list is empty", async () => {
      let call = 0;
      mockDb.select.mockImplementation(() => {
        const data = call++ === 0 ? [] : [];
        return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) };
      });
      await expect(new NCAAFootballSimulator().simulate("season-1")).rejects.toThrow(/at least 12/);
    });
  });
});
