import { describe, it, expect } from "vitest";
import {
  gameWinProb,
  seriesWinProb,
  sampleField,
  simulateSwiss,
  simulateChampionsStage,
} from "../cs-major-simulator";

// Helper to build a pool of teams
function makeTeams(n: number, baseElo = 1800, baseRank = 1) {
  return Array.from({ length: n }, (_, i) => ({
    id: `team-${i}`,
    elo: baseElo - i * 10,
    rank: baseRank + i,
  }));
}

// ─── gameWinProb ──────────────────────────────────────────────────────────────

describe("gameWinProb", () => {
  it("returns 0.5 for equal Elo", () => {
    expect(gameWinProb(1800, 1800)).toBeCloseTo(0.5, 5);
    expect(gameWinProb(2000, 2000)).toBeCloseTo(0.5, 5);
  });

  it("returns > 0.5 for positive advantage, < 0.5 for negative", () => {
    expect(gameWinProb(2000, 1700)).toBeGreaterThan(0.5);
    expect(gameWinProb(1700, 2000)).toBeLessThan(0.5);
  });

  it("is symmetric: p(a,b) + p(b,a) = 1", () => {
    expect(gameWinProb(2000, 1700) + gameWinProb(1700, 2000)).toBeCloseTo(1.0, 5);
  });

  it("stays strictly in (0, 1)", () => {
    const p = gameWinProb(3000, 1000);
    expect(p).toBeLessThan(1);
    expect(p).toBeGreaterThan(0);
  });
});

// ─── seriesWinProb ────────────────────────────────────────────────────────────

describe("seriesWinProb", () => {
  it("returns 0.5 for equal players (p=0.5) in any format", () => {
    expect(seriesWinProb(0.5, 2)).toBeCloseTo(0.5, 5);  // Bo3
    expect(seriesWinProb(0.5, 3)).toBeCloseTo(0.5, 5);  // Bo5
    expect(seriesWinProb(0.5, 4)).toBeCloseTo(0.5, 5);  // Bo7
  });

  it("amplifies better team advantage in longer formats", () => {
    const bo3 = seriesWinProb(0.6, 2);
    const bo5 = seriesWinProb(0.6, 3);
    expect(bo5).toBeGreaterThan(bo3);
  });

  it("approaches 1.0 as p → 1.0", () => {
    expect(seriesWinProb(0.9999, 2)).toBeCloseTo(1.0, 3);
    expect(seriesWinProb(0.9999, 3)).toBeCloseTo(1.0, 3);
  });

  it("sums to 1 with complement", () => {
    const p = 0.63;
    expect(seriesWinProb(p, 2) + seriesWinProb(1 - p, 2)).toBeCloseTo(1.0, 5);
    expect(seriesWinProb(p, 3) + seriesWinProb(1 - p, 3)).toBeCloseTo(1.0, 5);
  });

  it("Bo3 win prob with p=0.6 is in expected range", () => {
    // P(2-0) + P(2-1) = 0.36 + 2*0.36*0.4 = 0.36 + 0.288 = 0.648
    expect(seriesWinProb(0.6, 2)).toBeCloseTo(0.648, 3);
  });
});

// ─── sampleField ──────────────────────────────────────────────────────────────

describe("sampleField", () => {
  it("returns all teams when pool <= fieldSize", () => {
    const pool = makeTeams(20);
    const field = sampleField(pool, 32);
    expect(field).toHaveLength(20);
  });

  it("returns fieldSize teams when pool > fieldSize", () => {
    const pool = makeTeams(50);
    const field = sampleField(pool, 32);
    expect(field).toHaveLength(32);
  });

  it("always includes the top 12 ranked teams", () => {
    const pool = makeTeams(50);
    const top12Ids = new Set(pool.slice(0, 12).map((t) => t.id));

    for (let i = 0; i < 20; i++) {
      const field = sampleField(pool, 32);
      const fieldIds = new Set(field.map((t) => t.id));
      for (const id of top12Ids) {
        expect(fieldIds.has(id)).toBe(true);
      }
    }
  });

  it("never includes more teams than fieldSize", () => {
    const pool = makeTeams(100);
    for (let i = 0; i < 10; i++) {
      expect(sampleField(pool, 32)).toHaveLength(32);
    }
  });

  it("returns unique teams", () => {
    const pool = makeTeams(50);
    const field = sampleField(pool, 32);
    const ids = field.map((t) => t.id);
    expect(new Set(ids).size).toBe(ids.length);
  });
});

// ─── simulateSwiss ────────────────────────────────────────────────────────────

describe("simulateSwiss", () => {
  it("returns exactly 8 advanced and 8 eliminated from 16 teams", () => {
    const teams = makeTeams(16);
    const result = simulateSwiss(teams, false);
    expect(result.advanced).toHaveLength(8);
    expect(result.eliminated).toHaveLength(8);
  });

  it("all advanced teams have 3 wins (implicit by advancement threshold)", () => {
    const teams = makeTeams(16);
    const result = simulateSwiss(teams, false);
    // Advanced teams reached 3 wins — we can't directly check wins here,
    // but we verify each participant appears in exactly one group
    const allIds = new Set([
      ...result.advanced.map((t) => t.id),
      ...result.eliminated.map((t) => t.id),
    ]);
    expect(allIds.size).toBe(16);
    for (const team of teams) {
      expect(allIds.has(team.id)).toBe(true);
    }
  });

  it("eliminated teams have wins 0, 1, or 2", () => {
    const teams = makeTeams(16);
    const result = simulateSwiss(teams, false);
    for (const t of result.eliminated) {
      expect(t.wins).toBeGreaterThanOrEqual(0);
      expect(t.wins).toBeLessThanOrEqual(2);
    }
  });

  it("total wins + losses = total matches played (conservation check)", () => {
    const teams = makeTeams(16);
    const result = simulateSwiss(teams, false);
    // Eliminated teams have exactly 3 losses
    // Total losses = 8 * 3 = 24
    // Total wins = sum of wins for all eliminated + 8 * 3 for advanced = ?
    // Each win by advanced team = 1 loss for an eliminated team
    const totalLossesEliminated = result.eliminated.length * 3; // 24
    expect(totalLossesEliminated).toBe(24);
  });

  it("works with Bo3 format (Swiss all Bo3)", () => {
    const teams = makeTeams(16);
    const result = simulateSwiss(teams, true);
    expect(result.advanced).toHaveLength(8);
    expect(result.eliminated).toHaveLength(8);
  });

  it("teams with much higher Elo advance more often (stochastic check)", () => {
    // Top 8 teams have Elo 2000+, bottom 8 have Elo 1000
    const teams = [
      ...Array.from({ length: 8 }, (_, i) => ({ id: `top-${i}`, elo: 2000, rank: i + 1 })),
      ...Array.from({ length: 8 }, (_, i) => ({ id: `bot-${i}`, elo: 1000, rank: i + 9 })),
    ];

    let topAdvances = 0;
    const runs = 100;
    for (let i = 0; i < runs; i++) {
      const result = simulateSwiss(teams, false);
      for (const t of result.advanced) {
        if (t.id.startsWith("top-")) topAdvances++;
      }
    }

    // Top teams should advance much more than half the time across runs
    // Expected: ~7-8 top teams advance per run → topAdvances should be >> 400 (50%)
    expect(topAdvances / runs).toBeGreaterThan(6);
  });
});

// ─── simulateChampionsStage ───────────────────────────────────────────────────

describe("simulateChampionsStage", () => {
  it("throws if not exactly 8 teams", () => {
    expect(() => simulateChampionsStage(makeTeams(7))).toThrow();
    expect(() => simulateChampionsStage(makeTeams(9))).toThrow();
  });

  it("assigns placements to all 8 teams", () => {
    const teams = makeTeams(8);
    const result = simulateChampionsStage(teams);
    expect(result.placements.size).toBe(8);
    for (const team of teams) {
      expect(result.placements.has(team.id)).toBe(true);
    }
  });

  it("has exactly one 1st, one 2nd, two 3rd/4th, four 5th-8th", () => {
    const teams = makeTeams(8);
    const result = simulateChampionsStage(teams);
    const placements = [...result.placements.values()];
    expect(placements.filter(p => p === 1)).toHaveLength(1);
    expect(placements.filter(p => p === 2)).toHaveLength(1);
    expect(placements.filter(p => p >= 3 && p <= 4)).toHaveLength(2);
    expect(placements.filter(p => p >= 5 && p <= 8)).toHaveLength(4);
  });

  it("top Elo team wins more often than last (stochastic)", () => {
    // Use a clear Elo spread so team-0 has a decisive advantage.
    // makeTeams(8) only gives a 70-pt gap (1800→1730) which puts the
    // true win rate right at the 0.2 threshold — extremely flaky.
    // 100-pt steps (1800→1100) give team-0 a ~40% win rate, well clear of 0.25.
    const teams = Array.from({ length: 8 }, (_, i) => ({
      id: `team-${i}`,
      elo: 1800 - i * 100,
      rank: i + 1,
    }));
    let wins = 0;
    for (let i = 0; i < 1000; i++) {
      const result = simulateChampionsStage(teams);
      if (result.placements.get("team-0") === 1) wins++;
    }
    // Should win well above 12.5% (1/8 random baseline)
    expect(wins / 1000).toBeGreaterThan(0.25);
  });
});
