import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
import { normalizeTeamName } from "~/lib/normalize-team-name";
import { getTeamData, eloWinProbability, AFLSimulator } from "../afl-simulator";

// ─── normalizeTeamName ────────────────────────────────────────────────────────

describe("normalizeTeamName", () => {
  it("lowercases and trims", () => {
    expect(normalizeTeamName("  Western Bulldogs  ")).toBe("western bulldogs");
  });

  it("collapses internal whitespace", () => {
    expect(normalizeTeamName("Greater  Western  Sydney")).toBe("greater western sydney");
  });

  it("is already-normalized identity", () => {
    expect(normalizeTeamName("gold coast")).toBe("gold coast");
  });
});

// ─── getTeamData ──────────────────────────────────────────────────────────────

describe("getTeamData", () => {
  it("returns data for an exact match", () => {
    const d = getTeamData("Western Bulldogs");
    expect(d).toBeDefined();
    expect(d?.elo).toBe(1646);
  });

  it("is case-insensitive", () => {
    expect(getTeamData("western bulldogs")).toEqual(getTeamData("Western Bulldogs"));
  });

  it("returns undefined for an unknown team", () => {
    expect(getTeamData("Springfield Koalas")).toBeUndefined();
  });

  it("all 18 AFL clubs are present", () => {
    const allTeams = [
      "Western Bulldogs", "Gold Coast", "Hawthorn", "Geelong",
      "Adelaide", "Sydney", "Fremantle", "Collingwood",
      "Brisbane Lions", "Greater Western Sydney", "Carlton", "Port Adelaide",
      "St Kilda", "North Melbourne", "Melbourne", "Essendon",
      "Richmond", "West Coast",
    ];
    for (const name of allTeams) {
      expect(getTeamData(name), `missing team: ${name}`).toBeDefined();
    }
  });

  it("Western Bulldogs has the highest Elo", () => {
    const bulldogs = getTeamData("Western Bulldogs")?.elo ?? 0;
    const westCoast = getTeamData("West Coast")?.elo ?? 0;
    expect(bulldogs).toBeGreaterThan(westCoast);
  });

  it("Elo ratings are in the expected range (1250–1750)", () => {
    const allTeams = [
      "Western Bulldogs", "Gold Coast", "Hawthorn", "Geelong",
      "Adelaide", "Sydney", "Fremantle", "Collingwood",
      "Brisbane Lions", "Greater Western Sydney", "Carlton", "Port Adelaide",
      "St Kilda", "North Melbourne", "Melbourne", "Essendon",
      "Richmond", "West Coast",
    ];
    for (const name of allTeams) {
      const elo = getTeamData(name)?.elo ?? 0;
      expect(elo, `${name} elo out of range`).toBeGreaterThanOrEqual(1250);
      expect(elo, `${name} elo out of range`).toBeLessThanOrEqual(1750);
    }
  });
});

// ─── eloWinProbability ────────────────────────────────────────────────────────

describe("eloWinProbability (PARITY_FACTOR = 450)", () => {
  it("returns 0.5 for equal Elo ratings", () => {
    expect(eloWinProbability(1500, 1500)).toBeCloseTo(0.5, 6);
  });

  it("favors the higher-rated team", () => {
    expect(eloWinProbability(1706, 1500)).toBeGreaterThan(0.5);
    expect(eloWinProbability(1295, 1500)).toBeLessThan(0.5);
  });

  it("is anti-symmetric: P(A>B) + P(B>A) = 1", () => {
    const p = eloWinProbability(1706, 1295);
    expect(p + eloWinProbability(1295, 1706)).toBeCloseTo(1.0, 10);
  });

  it("a 450-pt gap gives ~90.9% win probability", () => {
    // P = 1 / (1 + 10^(-450/450)) = 1 / (1 + 10^-1) = 1/1.1 ≈ 0.909
    const p = eloWinProbability(1950, 1500);
    expect(p).toBeCloseTo(1 / 1.1, 5);
  });

  it("Bulldogs (1646) vs Essendon (1342): strongly favors Bulldogs", () => {
    // 304-pt gap at parity 450: P = 1/(1+10^(-304/450)) ≈ 0.826
    const p = eloWinProbability(1646, 1342);
    expect(p).toBeGreaterThan(0.80);
  });
});

// ─── AFLSimulator.simulate() integration tests ───────────────────────────────

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

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

const AFL_TEAMS = [
  "Western Bulldogs", "Gold Coast", "Hawthorn", "Geelong",
  "Adelaide", "Sydney", "Fremantle", "Collingwood",
  "Brisbane Lions", "Greater Western Sydney", "Carlton", "Port Adelaide",
  "St Kilda", "North Melbourne", "Melbourne", "Essendon",
  "Richmond", "West Coast",
];

const PARTICIPANT_ROWS = AFL_TEAMS.map((name, i) => ({
  id: `team-${i + 1}`,
  name,
}));

const PARTICIPANT_IDS = PARTICIPANT_ROWS.map((r) => r.id);

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

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

    mockDb = {
      select: vi.fn().mockReturnValue({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockResolvedValue(PARTICIPANT_ROWS),
        }),
      }),
    };

    (database as unknown as MockInstance).mockReturnValue(mockDb);
    // Default: no standings (pre-season)
    (getRegularSeasonStandings as unknown as MockInstance).mockResolvedValue([]);
  });

  it("throws if no participants found", async () => {
    mockDb.select.mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([]),
      }),
    });
    const sim = new AFLSimulator();
    await expect(sim.simulate("season-1")).rejects.toThrow(/No participants found/);
  });

  it("returns 18 results — one per AFL club", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");
    expect(results).toHaveLength(18);
  });

  it("all probability values are non-negative", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");
    for (const r of results) {
      for (const val of Object.values(r.probabilities)) {
        expect(val).toBeGreaterThanOrEqual(0);
      }
    }
  });

  it("each column (probFirst through probEighth) sums to 1.0 across all participants", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");

    const keys = [
      "probFirst", "probSecond", "probThird", "probFourth",
      "probFifth", "probSixth", "probSeventh", "probEighth",
    ] as const;
    for (const key of keys) {
      const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
      expect(colSum, `${key} column sum`).toBeCloseTo(1.0, 2);
    }
  });

  it("P5/P6 and P7/P8 are distinct tiers (separate column sums, not a combined 5–8 pool)", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");

    // probFifth should sum to 1.0 (SF losers only) — NOT 2.0 (which would happen if EF losers were mixed in)
    const fifthSum  = results.reduce((s, r) => s + r.probabilities.probFifth, 0);
    const seventhSum = results.reduce((s, r) => s + r.probabilities.probSeventh, 0);
    expect(fifthSum).toBeCloseTo(1.0, 2);
    expect(seventhSum).toBeCloseTo(1.0, 2);
  });

  it("probThird === probFourth for every participant (3rd/4th share same points in AFL)", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");
    for (const r of results) {
      expect(r.probabilities.probThird).toBeCloseTo(r.probabilities.probFourth, 10);
    }
  });

  it("probFifth === probSixth for every participant (5th/6th share same points in AFL)", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");
    for (const r of results) {
      expect(r.probabilities.probFifth).toBeCloseTo(r.probabilities.probSixth, 10);
    }
  });

  it("probSeventh === probEighth for every participant (7th/8th share same points in AFL)", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");
    for (const r of results) {
      expect(r.probabilities.probSeventh).toBeCloseTo(r.probabilities.probEighth, 10);
    }
  });

  it("uses source: 'afl_bracket_monte_carlo' on all results", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");
    for (const r of results) {
      expect(r.source).toBe("afl_bracket_monte_carlo");
    }
  });

  it("all result participant IDs match input participant IDs", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");
    const resultIds = new Set(results.map((r) => r.participantId));
    for (const id of PARTICIPANT_IDS) {
      expect(resultIds.has(id), `missing participant: ${id}`).toBe(true);
    }
  });

  it("Western Bulldogs (highest Elo) has the highest championship probability", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");

    const bulldogsResult = results.find((r) => r.participantId === "team-1"); // Western Bulldogs (highest Elo)
    const westCoastResult = results.find((r) => r.participantId === "team-18"); // West Coast (near-lowest Elo)
    if (!bulldogsResult || !westCoastResult) throw new Error("Expected results not found");

    // The #1 Elo team should win the championship more often than the last-ranked team
    expect(bulldogsResult.probabilities.probFirst).toBeGreaterThan(westCoastResult.probabilities.probFirst);
  });

  it("bottom-ranked teams rarely make finals (low combined probability)", async () => {
    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");

    // West Coast and Richmond (16th/17th Elo) should have very low combined finals probability
    const westCoast = results.find((r) => r.participantId === "team-18");
    const richmond  = results.find((r) => r.participantId === "team-17");
    if (!westCoast || !richmond) throw new Error("Expected results not found");

    const wcTotal = Object.values(westCoast.probabilities).reduce((a, b) => a + b, 0);
    const ricTotal = Object.values(richmond.probabilities).reduce((a, b) => a + b, 0);

    // Combined probability for a bottom team should be well below 1.0
    expect(wcTotal).toBeLessThan(0.5);
    expect(ricTotal).toBeLessThan(0.5);
  });

  it("mid-season standings: team with most wins has elevated finals probability", async () => {
    const { getRegularSeasonStandings } = await import("~/models/regular-season-standings");

    // Give Western Bulldogs (team-1) 15 wins from 18 games — top of ladder
    (getRegularSeasonStandings as unknown as MockInstance).mockResolvedValue([
      { participantId: "team-1", wins: 15, gamesPlayed: 18, losses: 3 },
      // All other teams have 5 wins
      ...PARTICIPANT_IDS.slice(1).map((id) => ({ participantId: id, wins: 5, gamesPlayed: 18, losses: 13 })),
    ]);

    const sim = new AFLSimulator();
    const results = await sim.simulate("season-1");

    const leader = results.find((r) => r.participantId === "team-1");
    const bottom = results.find((r) => r.participantId === "team-18");
    if (!leader || !bottom) throw new Error("Expected results not found");

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