import { describe, it, expect } from "vitest";
import {
  americanToImplied,
  getMajorOddsKey,
  resolveSkill,
  simulateMajor,
} from "../golf-simulator";
import type { GolfSkillsRecord } from "~/models/golf-skills";

// ─── americanToImplied ────────────────────────────────────────────────────────

describe("americanToImplied", () => {
  it("converts positive (underdog) American odds correctly", () => {
    // +400 → 100 / (400 + 100) = 0.2
    expect(americanToImplied(400)).toBeCloseTo(0.2, 5);
    // +100 → 100 / 200 = 0.5
    expect(americanToImplied(100)).toBeCloseTo(0.5, 5);
  });

  it("converts negative (favorite) American odds correctly", () => {
    // -200 → 200 / 300 ≈ 0.6667
    expect(americanToImplied(-200)).toBeCloseTo(0.6667, 3);
  });

  it("returns null for odds = 0", () => {
    expect(americanToImplied(0)).toBeNull();
  });

  it("returns probability in (0, 1] for valid odds", () => {
    const p = americanToImplied(200);
    expect(p).not.toBeNull();
    expect(p).toBeGreaterThan(0);
    expect(p).toBeLessThanOrEqual(1);
  });
});

// ─── getMajorOddsKey ──────────────────────────────────────────────────────────

describe("getMajorOddsKey", () => {
  it("maps Masters names to mastersOdds", () => {
    expect(getMajorOddsKey("The Masters")).toBe("mastersOdds");
    expect(getMajorOddsKey("masters tournament")).toBe("mastersOdds");
  });

  it("maps PGA Championship to pgaChampionshipOdds", () => {
    expect(getMajorOddsKey("PGA Championship")).toBe("pgaChampionshipOdds");
    expect(getMajorOddsKey("pga championship 2025")).toBe("pgaChampionshipOdds");
  });

  it("maps US Open to usOpenOdds", () => {
    expect(getMajorOddsKey("US Open")).toBe("usOpenOdds");
    expect(getMajorOddsKey("U.S. Open Golf")).toBe("usOpenOdds");
    expect(getMajorOddsKey("2025 US Open")).toBe("usOpenOdds");
  });

  it("maps Open Championship / British Open to openChampionshipOdds", () => {
    expect(getMajorOddsKey("The Open Championship")).toBe("openChampionshipOdds");
    expect(getMajorOddsKey("British Open")).toBe("openChampionshipOdds");
  });

  it("returns null for unrecognized names", () => {
    expect(getMajorOddsKey("Ryder Cup")).toBeNull();
    expect(getMajorOddsKey("Travelers Championship")).toBeNull();
  });
});

// ─── resolveSkill ─────────────────────────────────────────────────────────────

function makeSkills(overrides: Partial<GolfSkillsRecord> = {}): GolfSkillsRecord {
  return {
    id: "skill-1",
    participantId: "p1",
    sportsSeasonId: "s1",
    sgTotal: null,
    datagolfRank: null,
    mastersOdds: null,
    usOpenOdds: null,
    openChampionshipOdds: null,
    pgaChampionshipOdds: null,
    updatedAt: new Date(),
    ...overrides,
  };
}

describe("resolveSkill", () => {
  it("returns sgTotal when set, ignoring odds", () => {
    const skills = makeSkills({ sgTotal: 2.5, mastersOdds: 400 });
    expect(resolveSkill(skills, "mastersOdds")).toBe(2.5);
  });

  it("falls back to odds-derived skill when sgTotal is null", () => {
    const skills = makeSkills({ sgTotal: null, mastersOdds: 400 });
    const skill = resolveSkill(skills, "mastersOdds");
    // +400 = 20% win prob; skill > 0 because 20% > 1/156 baseline
    expect(skill).toBeGreaterThan(0);
  });

  it("returns 0 when no skills record", () => {
    expect(resolveSkill(undefined, "mastersOdds")).toBe(0);
    expect(resolveSkill(undefined, null)).toBe(0);
  });

  it("returns 0 when sgTotal is null and no matching odds key", () => {
    const skills = makeSkills({ sgTotal: null, mastersOdds: 400 });
    // oddsKey = null → no odds available for this major
    expect(resolveSkill(skills, null)).toBe(0);
  });

  it("returns 0 when sgTotal is null and the odds column is null", () => {
    const skills = makeSkills({ sgTotal: null, mastersOdds: null });
    expect(resolveSkill(skills, "mastersOdds")).toBe(0);
  });

  it("better American odds → higher resolved skill", () => {
    const skillGood = makeSkills({ sgTotal: null, mastersOdds: 200 }); // +200 = 33% win prob
    const skillPoor = makeSkills({ sgTotal: null, mastersOdds: 2000 }); // +2000 = 4.8% win prob
    const sg1 = resolveSkill(skillGood, "mastersOdds");
    const sg2 = resolveSkill(skillPoor, "mastersOdds");
    expect(sg1).toBeGreaterThan(sg2);
  });
});

// ─── simulateMajor ────────────────────────────────────────────────────────────

function makeQPConfig(maxPlacement = 16): Map<number, number> {
  // Default QP: 20, 14, 10, 8, 5, 5, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1
  const values = [20, 14, 10, 8, 5, 5, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1];
  const map = new Map<number, number>();
  for (let i = 0; i < maxPlacement; i++) {
    map.set(i + 1, values[i] ?? 0);
  }
  return map;
}

describe("simulateMajor", () => {
  const qpConfig = makeQPConfig();

  it("returns an entry for every tracked player", () => {
    const players = [
      { id: "p1", strength: 2.0 },
      { id: "p2", strength: 1.0 },
    ];
    const result = simulateMajor(players, 154, 1.0, qpConfig);
    expect(result.has("p1")).toBe(true);
    expect(result.has("p2")).toBe(true);
  });

  it("total QP awarded does not exceed sum of top-16 QP config", () => {
    const players = Array.from({ length: 10 }, (_, i) => ({ id: `p${i}`, strength: 1.0 }));
    const result = simulateMajor(players, 146, 1.0, qpConfig);
    const totalQP = [...result.values()].reduce((s, v) => s + v, 0);
    const maxQP = [...qpConfig.values()].reduce((s, v) => s + v, 0);
    expect(totalQP).toBeLessThanOrEqual(maxQP);
  });

  it("all QP values are valid (>= 0 and in the config set or 0)", () => {
    const players = [{ id: "p1", strength: 8.0 }, { id: "p2", strength: 1.0 }];
    const result = simulateMajor(players, 154, 1.0, qpConfig);
    const validQP = new Set([0, ...qpConfig.values()]);
    for (const qp of result.values()) {
      expect(validQP.has(qp)).toBe(true);
    }
  });

  it("a stronger player wins more often than a weaker one (statistical)", () => {
    const TRIALS = 5_000;
    let eliteWins = 0;
    const players = [
      { id: "elite", strength: 50.0 }, // very high strength
      { id: "average", strength: 1.0 },
    ];
    const singleQP = new Map([[1, 20], [2, 14]]);

    for (let i = 0; i < TRIALS; i++) {
      const result = simulateMajor(players, 0, 1.0, singleQP);
      if (result.get("elite") === 20) eliteWins++;
    }

    // Elite player should win at least 80% of the time with strength 50x average
    expect(eliteWins / TRIALS).toBeGreaterThan(0.8);
  });

  it("works when tracked players outnumber field size (restCount = 0)", () => {
    const players = Array.from({ length: 200 }, (_, i) => ({ id: `p${i}`, strength: 1.0 }));
    const result = simulateMajor(players, 0, 1.0, qpConfig);
    // All players should have an entry
    expect(result.size).toBe(200);
    // Only 16 can get QP
    const scorers = [...result.values()].filter((v) => v > 0);
    expect(scorers.length).toBeLessThanOrEqual(16);
  });

  it("when all players have equal strength, each wins approximately equally (statistical)", () => {
    const TRIALS = 10_000;
    const N = 3;
    const wins: Record<string, number> = {};
    const players = Array.from({ length: N }, (_, i) => {
      const id = `p${i}`;
      wins[id] = 0;
      return { id, strength: 1.0 };
    });
    const singleQP = new Map([[1, 20]]);

    for (let i = 0; i < TRIALS; i++) {
      const result = simulateMajor(players, 0, 1.0, singleQP);
      for (const [id, qp] of result) {
        if (qp === 20) wins[id]++;
      }
    }

    // Each of 3 equal players should win ~33% ± 5%
    for (const id of Object.keys(wins)) {
      expect(wins[id] / TRIALS).toBeGreaterThan(0.27);
      expect(wins[id] / TRIALS).toBeLessThan(0.39);
    }
  });
});

// ─── Monte Carlo property test ─────────────────────────────────────────────────

describe("simulateMajor Monte Carlo properties", () => {
  it("better SG player wins the head-to-head more often (no rest-of-field)", () => {
    // With no rest-of-field, the win rate is purely determined by strength ratio:
    //   P(elite wins) = exp(0.7 * 3.0) / (exp(0.7 * 3.0) + exp(0.7 * 1.5))
    //                 = 8.17 / (8.17 + 2.86) ≈ 74%
    const TRIALS = 3_000;
    const qpConfig = new Map([[1, 20], [2, 14]]);
    let eliteFirst = 0;

    for (let i = 0; i < TRIALS; i++) {
      const players = [
        { id: "elite", strength: Math.exp(0.7 * 3.0) }, // SG = 3.0
        { id: "good",  strength: Math.exp(0.7 * 1.5) }, // SG = 1.5
      ];
      const result = simulateMajor(players, 0, 1.0, qpConfig);
      if (result.get("elite") === 20) eliteFirst++;
    }

    const eliteWinRate = eliteFirst / TRIALS;
    // Elite player wins ~74% in a head-to-head; allow generous margin for randomness
    expect(eliteWinRate).toBeGreaterThan(0.65);
  });

  it("in a full 156-player field, a player with strength 8 wins ~5% of the time", () => {
    // Calibration check: strength 8 vs 155 opponents at strength 1 → 8 / (8 + 155) ≈ 4.9%
    const TRIALS = 5_000;
    const qpConfig = new Map([[1, 20]]);
    let eliteWins = 0;

    for (let i = 0; i < TRIALS; i++) {
      const players = [{ id: "elite", strength: 8 }];
      const result = simulateMajor(players, 155, 1.0, qpConfig);
      if (result.get("elite") === 20) eliteWins++;
    }

    const winRate = eliteWins / TRIALS;
    // Should be approximately 4.9%; allow ±3% tolerance
    expect(winRate).toBeGreaterThan(0.02);
    expect(winRate).toBeLessThan(0.09);
  });
});
