import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
import {
  buildCollegeHockeyTeams,
  CollegeHockeySimulator,
  npiRankSelectionWeight,
  npiRankToElo,
} from "../college-hockey-simulator";

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

const IDS = Array.from({ length: 16 }, (_, i) => `team-${i + 1}`);
const POOL_IDS = Array.from({ length: 20 }, (_, i) => `team-${i + 1}`);

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

function makeEvRows(ids: string[]) {
  return ids.map((participantId, i) => ({
    participantId,
    sourceElo: 1700 - i * 20,
    sourceOdds: null,
    worldRanking: i + 1,
  }));
}

function makeMatch(round: string, matchNumber: number, p1?: string, p2?: string, winner?: string, loser?: string) {
  return {
    id: `${round}-${matchNumber}`,
    scoringEventId: "event-1",
    round,
    templateRound: null,
    matchNumber,
    participant1Id: p1 ?? null,
    participant2Id: p2 ?? null,
    winnerId: winner ?? null,
    loserId: loser ?? null,
    isComplete: Boolean(winner),
    participant1Score: null,
    participant2Score: null,
    isScoring: round !== "Regional Semifinals",
    seedInfo: null,
    createdAt: new Date(),
    updatedAt: new Date(),
  };
}

function makeCompletedBracket() {
  const r16Winners = IDS.slice(0, 8);
  const qfWinners = IDS.slice(0, 4);
  const sfWinners = IDS.slice(0, 2);

  return [
    ...Array.from({ length: 8 }, (_, i) =>
      makeMatch("Regional Semifinals", i + 1, IDS[i], IDS[15 - i], r16Winners[i], IDS[15 - i])
    ),
    ...Array.from({ length: 4 }, (_, i) =>
      makeMatch("Regional Finals", i + 1, r16Winners[i * 2], r16Winners[i * 2 + 1], qfWinners[i], r16Winners[i * 2 + 1])
    ),
    ...Array.from({ length: 2 }, (_, i) =>
      makeMatch("Frozen Four Semifinals", i + 1, qfWinners[i * 2], qfWinners[i * 2 + 1], sfWinners[i], qfWinners[i * 2 + 1])
    ),
    makeMatch("National Championship", 1, sfWinners[0], sfWinners[1], sfWinners[0], sfWinners[1]),
  ];
}

function makeBracketWithStaleFutureSlots() {
  return [
    ...Array.from({ length: 8 }, (_, i) =>
      makeMatch("Regional Semifinals", i + 1, IDS[i], IDS[15 - i])
    ),
    // These incomplete downstream slots intentionally contain stale participants.
    // The simulator should ignore them and use the simulated/completed upstream winners.
    makeMatch("Regional Finals", 1, "team-9", "team-10"),
    makeMatch("Regional Finals", 2, "team-11", "team-12"),
    makeMatch("Regional Finals", 3, "team-13", "team-14"),
    makeMatch("Regional Finals", 4, "team-15", "team-16"),
    makeMatch("Frozen Four Semifinals", 1),
    makeMatch("Frozen Four Semifinals", 2),
    makeMatch("National Championship", 1),
  ];
}

describe("college hockey input helpers", () => {
  it("maps better NPI ranks to higher Elo ratings", () => {
    expect(npiRankToElo(1, 64)).toBeGreaterThan(npiRankToElo(16, 64));
  });

  it("maps better NPI ranks to higher Zipf selection weights", () => {
    expect(npiRankSelectionWeight(1)).toBeGreaterThan(npiRankSelectionWeight(10));
  });

  it("builds team strength from admin-editable NPI rank when Elo is absent", () => {
    const teams = buildCollegeHockeyTeams(["one", "two"], [
      { participantId: "one", sourceElo: null, sourceOdds: null, worldRanking: 1 },
      { participantId: "two", sourceElo: null, sourceOdds: null, worldRanking: 20 },
    ]);

    expect(teams[0].elo).toBeGreaterThan(teams[1].elo);
    expect(teams[0].selectionWeight).toBeGreaterThan(teams[1].selectionWeight);
  });

  it("uses NPI rank before Elo for seeding when both are present", () => {
    const teams = buildCollegeHockeyTeams(["strong-elo", "better-npi"], [
      { participantId: "strong-elo", sourceElo: 1800, sourceOdds: null, worldRanking: 12 },
      { participantId: "better-npi", sourceElo: 1500, sourceOdds: null, worldRanking: 2 },
    ]);

    const byId = new Map(teams.map((team) => [team.participantId, team]));
    expect(byId.get("better-npi")?.seedScore).toBeGreaterThan(byId.get("strong-elo")?.seedScore ?? 0);
  });

  it("gives no-odds teams a fallback field-selection weight when odds are partial", () => {
    const teams = buildCollegeHockeyTeams(["with-odds", "no-odds"], [
      { participantId: "with-odds", sourceElo: null, sourceOdds: 200, worldRanking: 1 },
      { participantId: "no-odds", sourceElo: null, sourceOdds: null, worldRanking: 2 },
    ]);

    const noOdds = teams.find((team) => team.participantId === "no-odds");
    expect(noOdds?.selectionWeight).toBeGreaterThan(0);
  });
});

describe("CollegeHockeySimulator", () => {
  let mockDb: {
    query: {
      scoringEvents: { findFirst: MockInstance };
      playoffMatches: { findMany: MockInstance };
    };
    select: MockInstance;
  };
  let selectCallCount: number;

  beforeEach(async () => {
    selectCallCount = 0;
    const { database } = await import("~/database/context");
    mockDb = {
      query: {
        scoringEvents: { findFirst: vi.fn() },
        playoffMatches: { findMany: vi.fn() },
      },
      select: vi.fn(),
    };
    (database as unknown as MockInstance).mockReturnValue(mockDb);
  });

  function setupPreBracket(ids: string[]) {
    mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
    mockDb.select.mockImplementation(() => {
      const data = selectCallCount++ === 0 ? makeParticipants(ids) : makeEvRows(ids);
      return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) };
    });
  }

  function setupExistingBracket(matches = makeCompletedBracket()) {
    mockDb.query.scoringEvents.findFirst.mockResolvedValue({ id: "event-1" });
    mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);
    mockDb.select.mockImplementation(() => ({
      from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(makeEvRows(IDS)) }),
    }));
  }

  it("simulates a pre-bracket 20-team pool and returns all participants", async () => {
    setupPreBracket(POOL_IDS);
    const results = await new CollegeHockeySimulator().simulate("season-1");

    expect(results).toHaveLength(20);
    expect(results.every((r) => r.source === "college_hockey_monte_carlo")).toBe(true);
  });

  it("normalizes champion and finalist probability buckets", async () => {
    setupPreBracket(IDS);
    const results = await new CollegeHockeySimulator().simulate("season-1");

    expect(results.reduce((sum, r) => sum + r.probabilities.probFirst, 0)).toBeCloseTo(1, 1);
    expect(results.reduce((sum, r) => sum + r.probabilities.probSecond, 0)).toBeCloseTo(1, 1);
  });

  it("honors completed results in an existing Frozen Four bracket", async () => {
    setupExistingBracket();
    const results = await new CollegeHockeySimulator().simulate("season-1");
    const byId = new Map(results.map((result) => [result.participantId, result]));

    expect(byId.get("team-1")?.probabilities.probFirst).toBe(1);
    expect(byId.get("team-2")?.probabilities.probSecond).toBe(1);
  });

  it("ignores stale future-round slots when upstream rounds are unresolved", async () => {
    setupExistingBracket(makeBracketWithStaleFutureSlots());
    const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0);

    try {
      const results = await new CollegeHockeySimulator().simulate("season-1");
      const byId = new Map(results.map((result) => [result.participantId, result]));

      expect(byId.get("team-1")?.probabilities.probFirst).toBe(1);
      expect(byId.get("team-9")?.probabilities.probFirst).toBe(0);
    } finally {
      randomSpy.mockRestore();
    }
  });
});
