import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
import { EPLSimulator, eplWinProbability, simEplMatch } from "../epl-simulator";

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

vi.mock("~/models/regular-season-standings", () => ({
  getRegularSeasonStandings: vi.fn(),
}));

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

const EPL_TEAMS = [
  "Arsenal", "Manchester City", "Liverpool", "Chelsea", "Tottenham Hotspur",
  "Manchester United", "Newcastle United", "Aston Villa", "Brighton & Hove Albion", "West Ham United",
  "Crystal Palace", "Fulham", "Everton", "Bournemouth", "Brentford",
  "Wolverhampton Wanderers", "Nottingham Forest", "Leeds United", "Burnley", "Sunderland",
];

const PARTICIPANT_ROWS = EPL_TEAMS.map((name, i) => ({ id: `team-${i + 1}`, name }));
const EV_ROWS = PARTICIPANT_ROWS.map((team, i) => ({
  participantId: team.id,
  sourceElo: 1760 - i * 25,
  sourceOdds: null,
}));

function mockSelects(participantRows = PARTICIPANT_ROWS, evRows = EV_ROWS) {
  let selectCallCount = 0;
  return vi.fn().mockImplementation(() => {
    selectCallCount++;
    if (selectCallCount === 1) {
      return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(participantRows) }) };
    }
    return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(evRows) }) };
  });
}

describe("EPL match helpers", () => {
  it("returns 0.5 win probability for equal Elo ratings", () => {
    expect(eplWinProbability(1500, 1500, 400)).toBeCloseTo(0.5, 6);
  });

  it("supports a configurable match parity factor", () => {
    const standard = eplWinProbability(1700, 1500, 400);
    const higherParity = eplWinProbability(1700, 1500, 1000);

    expect(standard).toBeGreaterThan(higherParity);
    expect(higherParity).toBeGreaterThan(0.5);
  });

  it("equal Elo teams draw at a realistic rate", () => {
    let draws = 0;
    const N = 5000;
    for (let i = 0; i < N; i++) {
      if (simEplMatch(1500, 1500, { matchParityFactor: 400 }) === "draw") draws++;
    }
    const drawRate = draws / N;
    expect(drawRate).toBeGreaterThan(0.21);
    expect(drawRate).toBeLessThan(0.31);
  });
});

describe("EPLSimulator.simulate()", () => {
  let mockDb: { select: MockInstance };

  beforeEach(async () => {
    const { database } = await import("~/database/context");
    const { getRegularSeasonStandings } = await import("~/models/regular-season-standings");
    const { getSportsSeasonSimulatorConfig } = await import("~/models/simulator");

    mockDb = { select: mockSelects() };
    (database as unknown as MockInstance).mockReturnValue(mockDb);
    (getRegularSeasonStandings as unknown as MockInstance).mockResolvedValue([]);
    (getSportsSeasonSimulatorConfig as unknown as MockInstance).mockResolvedValue({
      config: { iterations: 10_000, matchParityFactor: 400 },
    });
  });

  it("throws if no participants found", async () => {
    mockDb.select = mockSelects([], []);
    await expect(new EPLSimulator().simulate("season-1")).rejects.toThrow(/No participants found/);
  });

  it("returns one result per EPL club and normalizes top-eight columns", async () => {
    const results = await new EPLSimulator().simulate("season-1");
    expect(results).toHaveLength(20);

    const keys = [
      "probFirst", "probSecond", "probThird", "probFourth",
      "probFifth", "probSixth", "probSeventh", "probEighth",
    ] as const;

    for (const key of keys) {
      const colSum = results.reduce((sum, result) => sum + result.probabilities[key], 0);
      expect(colSum, `${key} column sum`).toBeCloseTo(1.0, 8);
    }

    for (const result of results) {
      expect(result.source).toBe("epl_standings_monte_carlo");
      for (const value of Object.values(result.probabilities)) {
        expect(value).toBeGreaterThanOrEqual(0);
        expect(value).toBeLessThanOrEqual(1);
      }
    }
  });

  it("higher Elo teams have stronger title probability", async () => {
    const results = await new EPLSimulator().simulate("season-1");
    const top = results.find((result) => result.participantId === "team-1");
    const bottom = results.find((result) => result.participantId === "team-20");
    if (!top || !bottom) throw new Error("Expected test teams not found");

    expect(top.probabilities.probFirst).toBeGreaterThan(bottom.probabilities.probFirst);
  });

  it("uses current EPL standings when present", async () => {
    const { getRegularSeasonStandings } = await import("~/models/regular-season-standings");
    (getRegularSeasonStandings as unknown as MockInstance).mockResolvedValue([
      {
        participantId: "team-20",
        wins: 25,
        ties: 5,
        losses: 0,
        gamesPlayed: 30,
        tablePoints: 80,
        goalsFor: 80,
        goalsAgainst: 20,
        goalDifference: 60,
        leagueRank: 1,
      },
      ...PARTICIPANT_ROWS.slice(0, 19).map((team) => ({
        participantId: team.id,
        wins: 8,
        ties: 5,
        losses: 17,
        gamesPlayed: 30,
        tablePoints: 29,
        goalsFor: 35,
        goalsAgainst: 55,
        goalDifference: -20,
        leagueRank: 10,
      })),
    ]);

    const results = await new EPLSimulator().simulate("season-1");
    const leader = results.find((result) => result.participantId === "team-20");
    if (!leader) throw new Error("Expected standings leader not found");

    expect(leader.probabilities.probFirst).toBeGreaterThan(0.95);
  });

  it("throws when Elo/projected points are missing", async () => {
    mockDb.select = mockSelects(PARTICIPANT_ROWS, []);
    await expect(new EPLSimulator().simulate("season-1")).rejects.toThrow(/Missing Elo\/projected points/);
  });
});
