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

// ─── Helpers ──────────────────────────────────────────────────────────────────

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

function makeAdvancedTeams(n: number, losses = 0, baseElo = 1800, baseRank = 1): AdvancedTeam[] {
  return Array.from({ length: n }, (_, i) => ({
    id: `team-${i}`,
    elo: baseElo - i * 10,
    rank: baseRank + i,
    losses,
  }));
}

function makeQPConfig(): Map<number, number> {
  const config = new Map<number, number>();
  config.set(1, 500);
  config.set(2, 400);
  config.set(3, 325);
  config.set(4, 250);
  config.set(5, 175);
  config.set(6, 125);
  config.set(7, 75);
  config.set(8, 50);
  for (let i = 9; i <= 16; i++) config.set(i, (17 - i) * 10);
  return config;
}

// ─── 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("advanced teams have a losses field with value 0, 1, or 2", () => {
    const teams = makeTeams(16);
    const result = simulateSwiss(teams, false);
    for (const t of result.advanced) {
      expect(typeof t.losses).toBe("number");
      expect(t.losses).toBeGreaterThanOrEqual(0);
      expect(t.losses).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("works with decisiveMatchesBo3 enabled", () => {
    const teams = makeTeams(16);
    const result = simulateSwiss(teams, false, true);
    expect(result.advanced).toHaveLength(8);
    expect(result.eliminated).toHaveLength(8);
  });

  it("returns empty arrays for 0 teams", () => {
    const result = simulateSwiss([], false);
    expect(result.advanced).toHaveLength(0);
    expect(result.eliminated).toHaveLength(0);
  });

  it("throws for odd team count", () => {
    expect(() => simulateSwiss(makeTeams(15), false)).toThrow(/even number of teams/);
    expect(() => simulateSwiss(makeTeams(1), false)).toThrow(/even number of teams/);
  });

  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(makeAdvancedTeams(7))).toThrow();
    expect(() => simulateChampionsStage(makeAdvancedTeams(9))).toThrow();
  });

  it("assigns placements to all 8 teams", () => {
    const teams = makeAdvancedTeams(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 placement-3s, four placement-5s", () => {
    const teams = makeAdvancedTeams(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)).toHaveLength(2);  // both SF losers
    expect(placements.filter((p) => p === 5)).toHaveLength(4);  // all QF losers
  });

  it("seeds by losses ascending, then rank as tiebreaker", () => {
    // Team with 0 losses gets seeded 1st regardless of world rank.
    // Make all teams equal Elo so seeding determines matchups purely.
    // team-0 has 0 losses (seed 1) vs team-7 has 2 losses (seed 8).
    const teams: AdvancedTeam[] = [
      { id: "a", elo: 1800, rank: 1, losses: 0 },
      { id: "b", elo: 1800, rank: 2, losses: 0 },
      { id: "c", elo: 1800, rank: 3, losses: 1 },
      { id: "d", elo: 1800, rank: 4, losses: 1 },
      { id: "e", elo: 1800, rank: 5, losses: 2 },
      { id: "f", elo: 1800, rank: 6, losses: 2 },
      { id: "g", elo: 1800, rank: 7, losses: 2 },
      { id: "h", elo: 1800, rank: 8, losses: 2 },
    ];
    // With equal Elo, seeding by losses produces consistent bracket structure.
    // Just verify it runs correctly and produces valid placements.
    const result = simulateChampionsStage(teams);
    expect(result.placements.size).toBe(8);
    const values = [...result.placements.values()];
    expect(values).toContain(1);
    expect(values).toContain(2);
  });

  it("rank used as tiebreaker when losses are equal", () => {
    const teams = makeAdvancedTeams(8, 1);  // all have losses = 1
    const result = simulateChampionsStage(teams);
    expect(result.placements.size).toBe(8);
  });

  it("top Elo team wins more often than last (stochastic)", () => {
    const teams = Array.from({ length: 8 }, (_, i): AdvancedTeam => ({
      id: `team-${i}`,
      elo: 1800 - i * 100,
      rank: i + 1,
      losses: 0,
    }));
    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);
  });
});

// ─── calcStage3ExitQP ─────────────────────────────────────────────────────────

// Placements 9–16 with descending QP: 9→80, 10→70, ..., 16→10
function makeStage3QPConfig(): Map<number, number> {
  const config = new Map<number, number>();
  for (let i = 9; i <= 16; i++) {
    config.set(i, (17 - i) * 10);
  }
  return config;
}

describe("calcStage3ExitQP", () => {
  it("returns empty map for empty input", () => {
    const result = calcStage3ExitQP([], new Map());
    expect(result.size).toBe(0);
  });

  it("tie-splits QP within each wins group", () => {
    // 2 teams at 2 wins, 2 teams at 1 win, 4 teams at 0 wins
    const elim = [
      { id: "a", wins: 2 }, { id: "b", wins: 2 },          // slots 9-10: avg (80+70)/2 = 75
      { id: "c", wins: 1 }, { id: "d", wins: 1 },          // slots 11-12: avg (60+50)/2 = 55
      { id: "e", wins: 0 }, { id: "f", wins: 0 },
      { id: "g", wins: 0 }, { id: "h", wins: 0 },          // slots 13-16: avg (40+30+20+10)/4 = 25
    ];
    const config = makeStage3QPConfig();
    const result = calcStage3ExitQP(elim, config);

    expect(result.get("a")).toBeCloseTo(75);
    expect(result.get("b")).toBeCloseTo(75);
    expect(result.get("c")).toBeCloseTo(55);
    expect(result.get("d")).toBeCloseTo(55);
    expect(result.get("e")).toBeCloseTo(25);
    expect(result.get("h")).toBeCloseTo(25);
  });

  it("all teams with the same wins get identical averaged QP", () => {
    const elim = Array.from({ length: 8 }, (_, i) => ({ id: `t-${i}`, wins: 1 }));
    const config = makeStage3QPConfig();
    const result = calcStage3ExitQP(elim, config);

    // Average of slots 9–16: (80+70+60+50+40+30+20+10) / 8 = 45
    const expected = (80 + 70 + 60 + 50 + 40 + 30 + 20 + 10) / 8;
    for (const [, qp] of result) {
      expect(qp).toBeCloseTo(expected);
    }
  });

  it("higher wins groups get higher QP", () => {
    const elim = [
      { id: "high", wins: 2 },
      { id: "mid",  wins: 1 },
      { id: "low",  wins: 0 },
    ];
    // Pad to 8 to use real slots: add 5 more at wins=0
    for (let i = 0; i < 5; i++) elim.push({ id: `pad-${i}`, wins: 0 });
    const config = makeStage3QPConfig();
    const result = calcStage3ExitQP(elim, config);

    const highQP = result.get("high") ?? 0;
    const midQP  = result.get("mid")  ?? 0;
    const lowQP  = result.get("low")  ?? 0;
    expect(highQP).toBeGreaterThan(midQP);
    expect(midQP).toBeGreaterThan(lowQP);
  });
});

// ─── simulateOneMajor ─────────────────────────────────────────────────────────

describe("simulateOneMajor", () => {
  it("returns a QP entry for all pool teams (no stage data)", () => {
    const pool = makeTeams(32);
    const result = simulateOneMajor(pool, undefined, makeQPConfig());
    expect(result.size).toBe(32);
    for (const [, qp] of result) {
      expect(qp).toBeGreaterThanOrEqual(0);
    }
  });

  it("at least one team earns positive QP", () => {
    const pool = makeTeams(32);
    const result = simulateOneMajor(pool, undefined, makeQPConfig());
    const nonZero = [...result.values()].filter((q) => q > 0);
    expect(nonZero.length).toBeGreaterThan(0);
  });

  it("exactly 16 teams earn 0 QP (stage 1 and 2 exits)", () => {
    // Run multiple times to get a stable count — should always be 16 with 32-team pool
    const pool = makeTeams(32);
    const qpConfig = makeQPConfig();
    for (let run = 0; run < 5; run++) {
      const result = simulateOneMajor(pool, undefined, qpConfig);
      const zeroCount = [...result.values()].filter((q) => q === 0).length;
      expect(zeroCount).toBe(16);
    }
  });

  it("stage 1 eliminated teams earn 0 QP when stage is complete", () => {
    const pool = makeTeams(32);
    const qpConfig = makeQPConfig();

    // Build stage results: 16 teams in stage 1, 8 in stage 2, 8 in stage 3
    // Stage 1 eliminations: first 8 pool teams
    const stageResults = new Map<string, {
      stageEntry: number;
      stageEliminated: number | null;
      stageEliminatedWins: number | null;
    }>();
    for (let i = 0; i < 16; i++) {
      stageResults.set(`team-${i}`, {
        stageEntry: 1,
        stageEliminated: i < 8 ? 1 : null,  // first 8 eliminated at stage 1
        stageEliminatedWins: i < 8 ? (i % 3) : null,
      });
    }
    for (let i = 16; i < 24; i++) {
      stageResults.set(`team-${i}`, { stageEntry: 2, stageEliminated: null, stageEliminatedWins: null });
    }
    for (let i = 24; i < 32; i++) {
      stageResults.set(`team-${i}`, { stageEntry: 3, stageEliminated: null, stageEliminatedWins: null });
    }

    const result = simulateOneMajor(pool, stageResults, qpConfig);

    // Stage 1 eliminated teams (team-0 through team-7) must get 0 QP
    for (let i = 0; i < 8; i++) {
      expect(result.get(`team-${i}`)).toBe(0);
    }
  });

  it("works with a pool exactly equal to FIELD_SIZE (32 teams)", () => {
    // sampleField returns all teams when pool.length <= fieldSize
    const pool = makeTeams(32);
    expect(() => simulateOneMajor(pool, undefined, makeQPConfig())).not.toThrow();
  });

  it("QF and SF losers receive averaged QP (not individual slot QP)", () => {
    // With a uniform qpConfig we can verify tie-splitting: if slots 5-8 all have
    // distinct values, QF losers should all get the average, not random individual values.
    const qpConfig = new Map([
      [1, 1000], [2, 800],
      [3, 600],  [4, 400],
      [5, 300],  [6, 200], [7, 100], [8, 50],
      ...Array.from({ length: 8 }, (_, i): [number, number] => [i + 9, 0]),
    ]);
    const expectedQFAvg = (300 + 200 + 100 + 50) / 4;  // 162.5
    const expectedSFAvg = (600 + 400) / 2;             // 500

    const pool = makeTeams(32);

    // Run a few times; every QF and SF loser should get the averaged values
    for (let run = 0; run < 5; run++) {
      const result = simulateOneMajor(pool, undefined, qpConfig);
      const qpValues = [...result.values()];
      // The averaged values should appear in the results
      const hasQFAvg = qpValues.some((q) => Math.abs(q - expectedQFAvg) < 0.01);
      const hasSFAvg = qpValues.some((q) => Math.abs(q - expectedSFAvg) < 0.01);
      expect(hasQFAvg).toBe(true);
      expect(hasSFAvg).toBe(true);
    }
  });
});

// ─── Not-participating exclusion ──────────────────────────────────────────────
//
// simulateOneMajor uses sampleField to draw a 32-team field from the pool.
// sampleField returns all teams when pool.length <= 32, so pools must be > 32
// for exclusion tests — otherwise the field shrinks below 32 and the Swiss
// bracket gets an odd count. The actual simulator filters the pool before
// calling simulateOneMajor, so the real-world scenario (pool of 20–30 teams,
// 1 withdrawn) is safe: sampleField just uses all remaining teams up to 32.

describe("not-participating exclusion (CS2)", () => {
  const qpConfig = makeQPConfig();

  it("a team not in the pool gets 0 QP from that major", () => {
    // Use 40 teams so sampleField still draws 32 after the exclusion.
    const allTeams = makeTeams(40);
    const excluded = allTeams[0];
    const reducedPool = allTeams.slice(1); // 39 teams — sampleField draws 32 from these

    const result = simulateOneMajor(reducedPool, undefined, qpConfig);
    expect(result.has(excluded.id)).toBe(false);
  });

  it("excluding a dominant team from the pool means they never win", () => {
    const TRIALS = 200;
    // 40 teams: one dominant + 39 average. sampleField always draws 32.
    const dominantTeam = { id: "dominant", elo: 5000, rank: 1 };
    const otherTeams = makeTeams(39, 1500, 2);
    const allPool = [dominantTeam, ...otherTeams];
    const reducedPool = otherTeams; // dominant excluded

    let dominantWinsWithFull = 0;
    let dominantWinsWithReduced = 0;
    const firstPlaceQP = qpConfig.get(1) ?? 0;

    for (let i = 0; i < TRIALS; i++) {
      const fullResult = simulateOneMajor(allPool, undefined, qpConfig);
      if ((fullResult.get(dominantTeam.id) ?? 0) === firstPlaceQP) dominantWinsWithFull++;

      const reducedResult = simulateOneMajor(reducedPool, undefined, qpConfig);
      if ((reducedResult.get(dominantTeam.id) ?? 0) === firstPlaceQP) dominantWinsWithReduced++;
    }

    // Dominant team (Elo 5000 vs 1500) wins nearly every sampled major
    expect(dominantWinsWithFull).toBeGreaterThan(TRIALS * 0.8);
    // Excluded team never appears → 0 wins
    expect(dominantWinsWithReduced).toBe(0);
  });

  it("per-event pool filtering is independent — exclusion in one event does not affect another", () => {
    // Verify that two separate filtered pools produce independent results.
    const allTeams = makeTeams(40);
    const excluded = allTeams[0];
    const fullPool = allTeams;
    const reducedPool = allTeams.slice(1); // exclude team[0] from event 2

    const resultEvent1 = simulateOneMajor(fullPool, undefined, qpConfig);
    const resultEvent2 = simulateOneMajor(reducedPool, undefined, qpConfig);

    expect(resultEvent1.has(excluded.id)).toBe(true);  // present in event 1
    expect(resultEvent2.has(excluded.id)).toBe(false); // excluded from event 2
  });
});
