import { describe, it, expect } from "vitest";
import { kenpomWinProbability, getNetRating, normalizeTeamName } from "../ncaam-simulator";

// ─── KenPom win probability formula ──────────────────────────────────────────

describe("kenpomWinProbability", () => {
  it("returns 0.5 for equal teams", () => {
    expect(kenpomWinProbability(25, 25)).toBeCloseTo(0.5, 10);
  });

  it("returns >0.5 for team A with higher netrtg", () => {
    expect(kenpomWinProbability(30, 20)).toBeGreaterThan(0.5);
  });

  it("returns <0.5 for team A with lower netrtg", () => {
    expect(kenpomWinProbability(20, 30)).toBeLessThan(0.5);
  });

  it("is symmetric: P(A>B) + P(B>A) = 1", () => {
    const pAB = kenpomWinProbability(35, 20);
    const pBA = kenpomWinProbability(20, 35);
    expect(pAB + pBA).toBeCloseTo(1.0, 10);
  });

  it("matches the logistic formula exactly for known values", () => {
    // netrtgA=38.90 (Duke 2025-26), netrtgB=0 → 1/(1+exp(-38.90/7.5))
    const expected = 1 / (1 + Math.exp(-38.90 / 7.5));
    expect(kenpomWinProbability(38.90, 0)).toBeCloseTo(expected, 10);
  });

  it("handles negative netrtg (weak team vs average)", () => {
    // negative netrtg is valid (below-average team)
    const p = kenpomWinProbability(-2, 0);
    expect(p).toBeGreaterThan(0);
    expect(p).toBeLessThan(0.5);
  });

  it("large AEM gap pushes probability toward 1", () => {
    // 38 point gap (Duke-caliber vs near-zero) → high win prob
    expect(kenpomWinProbability(38, 0)).toBeGreaterThan(0.99);
  });

  it("scale factor: a 7.5-point gap yields ~73% win probability", () => {
    // 1 / (1 + exp(-7.5/7.5)) = 1/(1+exp(-1)) ≈ 0.7311
    expect(kenpomWinProbability(7.5, 0)).toBeCloseTo(0.7311, 3);
  });
});

// ─── Team name normalization ──────────────────────────────────────────────────

describe("normalizeTeamName", () => {
  it("converts to lowercase", () => {
    expect(normalizeTeamName("Duke")).toBe("duke");
  });

  it("trims leading/trailing whitespace", () => {
    expect(normalizeTeamName("  Duke  ")).toBe("duke");
  });

  it("collapses internal whitespace", () => {
    expect(normalizeTeamName("North  Carolina")).toBe("north carolina");
  });

  it("preserves punctuation", () => {
    expect(normalizeTeamName("St. John's")).toBe("st. john's");
  });
});

// ─── KenPom data lookup ───────────────────────────────────────────────────────

describe("getNetRating", () => {
  it("returns correct netrtg for exact known name (lowercase)", () => {
    expect(getNetRating("duke")).toBeCloseTo(38.90, 2);
  });

  it("is case-insensitive", () => {
    expect(getNetRating("Duke")).toBeCloseTo(38.90, 2);
    expect(getNetRating("DUKE")).toBeCloseTo(38.90, 2);
  });

  it("handles abbreviated name variants", () => {
    // Both 'michigan st.' and 'michigan state' should resolve
    expect(getNetRating("Michigan St.")).toBeCloseTo(28.31, 2);
    expect(getNetRating("Michigan State")).toBeCloseTo(28.31, 2);
  });

  it("handles name with apostrophe", () => {
    expect(getNetRating("St. John's")).toBeCloseTo(25.91, 2);
  });

  it("returns 0.0 for an unknown team name", () => {
    expect(getNetRating("Fictional University")).toBe(0.0);
  });

  it("returns 0.0 for empty string", () => {
    expect(getNetRating("")).toBe(0.0);
  });

  it("handles negative netrtg (below-average teams)", () => {
    expect(getNetRating("Idaho")).toBeCloseTo(1.53, 2);
    expect(getNetRating("Eastern Washington")).toBeCloseTo(0.00, 2);
  });
});

// ─── Bracket path logic ───────────────────────────────────────────────────────

describe("NCAAM bracket advancement path", () => {
  it("R64 matches 1 and 2 feed R32 match 1 (Math.ceil convention)", () => {
    expect(Math.ceil(1 / 2)).toBe(1);
    expect(Math.ceil(2 / 2)).toBe(1);
  });

  it("R64 matches 3 and 4 feed R32 match 2", () => {
    expect(Math.ceil(3 / 2)).toBe(2);
    expect(Math.ceil(4 / 2)).toBe(2);
  });

  it("R64 matches 31 and 32 feed R32 match 16", () => {
    expect(Math.ceil(31 / 2)).toBe(16);
    expect(Math.ceil(32 / 2)).toBe(16);
  });

  it("32 R64 matches produce exactly 16 R32 slots", () => {
    const r32Slots = new Set<number>();
    for (let i = 1; i <= 32; i++) r32Slots.add(Math.ceil(i / 2));
    expect(r32Slots.size).toBe(16);
  });

  it("R32 match i uses 0-indexed winners at (i-1)*2 and (i-1)*2+1", () => {
    for (let i = 1; i <= 16; i++) {
      const p1 = (i - 1) * 2;
      const p2 = (i - 1) * 2 + 1;
      expect(p1).toBeGreaterThanOrEqual(0);
      expect(p2).toBeLessThan(32);
    }
  });

  it("S16 match i uses 0-indexed R32 winners at (i-1)*2 and (i-1)*2+1", () => {
    for (let i = 1; i <= 8; i++) {
      const p1 = (i - 1) * 2;
      const p2 = (i - 1) * 2 + 1;
      expect(p1).toBeGreaterThanOrEqual(0);
      expect(p2).toBeLessThan(16);
    }
  });

  it("E8 match i uses 0-indexed S16 winners at (i-1)*2 and (i-1)*2+1", () => {
    for (let i = 1; i <= 4; i++) {
      const _p1 = (i - 1) * 2;
      const p2 = (i - 1) * 2 + 1;
      expect(p2).toBeLessThan(8);
    }
  });

  it("FF match i uses 0-indexed E8 winners at (i-1)*2 and (i-1)*2+1", () => {
    for (let i = 1; i <= 2; i++) {
      const _p1 = (i - 1) * 2;
      const p2 = (i - 1) * 2 + 1;
      expect(p2).toBeLessThan(4);
    }
  });
});

// ─── Probability bucket math ──────────────────────────────────────────────────

describe("NCAAM placement bucket probability math", () => {
  const N = 50_000;

  it("champion: count/N = 1.0 when a team wins every simulation", () => {
    const c = N;
    expect(c / N).toBeCloseTo(1.0, 10);
  });

  it("finalist: count/N = 1.0 when a team reaches final every simulation", () => {
    expect(N / N).toBeCloseTo(1.0, 10);
  });

  it("FF loser slots: count/(2*N) sums to 1.0 across teams whose counts total 2*N", () => {
    // 2 FF losers per sim → total across all teams = 2*N
    // e.g. 2 teams each appearing N times: (N + N) / (2*N) = 1.0
    const counts = [N, N];
    const sum = counts.reduce((s, c) => s + c / (2 * N), 0);
    expect(sum).toBeCloseTo(1.0, 10);
  });

  it("E8 loser slots: count/(4*N) sums to 1.0 across teams whose counts total 4*N", () => {
    // 4 E8 losers per sim → total across all teams = 4*N
    // e.g. 4 teams each appearing N times: (4*N) / (4*N) = 1.0
    const counts = [N, N, N, N];
    const sum = counts.reduce((s, c) => s + c / (4 * N), 0);
    expect(sum).toBeCloseTo(1.0, 10);
  });

  it("probThird equals probFourth for the same team (same ff count / 2N)", () => {
    const ffCount = 12500;
    const probThird = ffCount / (2 * N);
    const probFourth = ffCount / (2 * N);
    expect(probThird).toBe(probFourth);
  });

  it("probFifth through probEighth are equal for the same team (same e8 count / 4N)", () => {
    const e8Count = 6250;
    const probs = [e8Count / (4 * N), e8Count / (4 * N), e8Count / (4 * N), e8Count / (4 * N)];
    expect(probs[0]).toBe(probs[1]);
    expect(probs[1]).toBe(probs[2]);
    expect(probs[2]).toBe(probs[3]);
  });

  it("a team exiting in R64 has all-zero probabilities", () => {
    // R64 losers are never counted in any bucket → all counts stay 0
    const c = 0, f = 0, ff = 0, e8 = 0;
    expect(c / N).toBe(0);
    expect(f / N).toBe(0);
    expect(ff / (2 * N)).toBe(0);
    expect(e8 / (4 * N)).toBe(0);
  });
});

// ─── EV alignment with scoring rules ─────────────────────────────────────────

describe("NCAAM EV calculation alignment with scoring rules", () => {
  // DEFAULT_SCORING_RULES: Champion=100, Finalist=70, FF loser=45, E8 loser=20, rest=0
  // (sum = 340; must match DEFAULT_SCORING_RULES in admin.sports-seasons.$id.simulate.tsx)
  const SCORING = {
    first: 100,
    second: 70,
    thirdFourth: 45,
    fifthToEighth: 20,
  };

  function computeEV(probs: {
    probFirst: number; probSecond: number;
    probThird: number; probFourth: number;
    probFifth: number; probSixth: number; probSeventh: number; probEighth: number;
  }): number {
    return (
      probs.probFirst   * SCORING.first +
      probs.probSecond  * SCORING.second +
      probs.probThird   * SCORING.thirdFourth +
      probs.probFourth  * SCORING.thirdFourth +
      probs.probFifth   * SCORING.fifthToEighth +
      probs.probSixth   * SCORING.fifthToEighth +
      probs.probSeventh * SCORING.fifthToEighth +
      probs.probEighth  * SCORING.fifthToEighth
    );
  }

  it("champion with probFirst=1 earns 100 EV", () => {
    const probs = {
      probFirst: 1, probSecond: 0, probThird: 0, probFourth: 0,
      probFifth: 0, probSixth: 0, probSeventh: 0, probEighth: 0,
    };
    expect(computeEV(probs)).toBeCloseTo(100, 10);
  });

  it("finalist with probSecond=1 earns 70 EV", () => {
    const probs = {
      probFirst: 0, probSecond: 1, probThird: 0, probFourth: 0,
      probFifth: 0, probSixth: 0, probSeventh: 0, probEighth: 0,
    };
    expect(computeEV(probs)).toBeCloseTo(70, 10);
  });

  it("confirmed FF loser (probThird=probFourth=0.5 each) earns 45 EV", () => {
    const probs = {
      probFirst: 0, probSecond: 0, probThird: 0.5, probFourth: 0.5,
      probFifth: 0, probSixth: 0, probSeventh: 0, probEighth: 0,
    };
    expect(computeEV(probs)).toBeCloseTo(45, 10);
  });

  it("confirmed E8 loser (probFifth–Eighth=0.25 each) earns 20 EV", () => {
    const probs = {
      probFirst: 0, probSecond: 0, probThird: 0, probFourth: 0,
      probFifth: 0.25, probSixth: 0.25, probSeventh: 0.25, probEighth: 0.25,
    };
    expect(computeEV(probs)).toBeCloseTo(20, 10);
  });

  it("R64 loser (all zeros) earns 0 EV", () => {
    const probs = {
      probFirst: 0, probSecond: 0, probThird: 0, probFourth: 0,
      probFifth: 0, probSixth: 0, probSeventh: 0, probEighth: 0,
    };
    expect(computeEV(probs)).toBe(0);
  });
});
