import { describe, it, expect } from "vitest";
import {
  nllGameWinProbability,
  simulateNllGame,
  simulateBestOfThree,
  buildNllBracketFromSeeds,
  simulateNllPlayoffs,
  simulateRegularSeasonSeeds,
  resolveQfMatch,
  resolveBo3Match,
  type SeedEntry,
  type SimTeam,
  type MatchSnapshot,
  type GameSnapshot,
} from "../nll-simulator";
import { SIMULATOR_TYPES } from "../registry";
import { SIMULATOR_MANIFEST } from "../manifest";

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

function makeSeed(participantId: string, seed: number, elo = 1500): SeedEntry {
  return { participantId, elo, seed };
}

function make14Teams(eloFn = (_i: number) => 1500) {
  return Array.from({ length: 14 }, (_, i) => ({
    participantId: `team-${i + 1}`,
    elo: eloFn(i),
    currentWins: 0,
    gamesPlayed: 0,
    projectedWins: null as number | null,
  }));
}

// ─── nllGameWinProbability ─────────────────────────────────────────────────────

describe("nllGameWinProbability", () => {
  it("returns 0.5 for equal Elo", () => {
    expect(nllGameWinProbability(1500, 1500)).toBeCloseTo(0.5);
  });

  it("favours the higher-Elo team", () => {
    expect(nllGameWinProbability(1600, 1400)).toBeGreaterThan(0.5);
  });

  it("returns a probability in [0, 1]", () => {
    const p = nllGameWinProbability(2000, 1000);
    expect(p).toBeGreaterThan(0);
    expect(p).toBeLessThan(1);
  });

  it("uses the supplied parityFactor", () => {
    const pLoose = nllGameWinProbability(1600, 1400, 1000);
    const pTight = nllGameWinProbability(1600, 1400, 200);
    expect(pLoose).toBeLessThan(pTight);
  });
});

// ─── simulateNllGame ──────────────────────────────────────────────────────────

describe("simulateNllGame", () => {
  it("returns one winner and one loser from the two teams", () => {
    const a: SimTeam = { participantId: "A", elo: 1500 };
    const b: SimTeam = { participantId: "B", elo: 1500 };
    const result = simulateNllGame(a, b);
    const ids = [result.winner.participantId, result.loser.participantId].toSorted();
    expect(ids).toEqual(["A", "B"]);
  });

  it("winner and loser are different", () => {
    const a: SimTeam = { participantId: "A", elo: 1500 };
    const b: SimTeam = { participantId: "B", elo: 1500 };
    const result = simulateNllGame(a, b);
    expect(result.winner.participantId).not.toBe(result.loser.participantId);
  });

  it("over many trials, higher-Elo team wins more often", () => {
    const strong: SimTeam = { participantId: "strong", elo: 1700 };
    const weak: SimTeam = { participantId: "weak", elo: 1300 };
    let wins = 0;
    for (let i = 0; i < 10_000; i++) {
      if (simulateNllGame(strong, weak).winner.participantId === "strong") wins++;
    }
    expect(wins).toBeGreaterThan(7000);
  });
});

// ─── simulateBestOfThree ──────────────────────────────────────────────────────

describe("simulateBestOfThree", () => {
  it("returns one winner and one loser", () => {
    const a: SimTeam = { participantId: "A", elo: 1500 };
    const b: SimTeam = { participantId: "B", elo: 1500 };
    const result = simulateBestOfThree(a, b);
    expect(result.winner.participantId).not.toBe(result.loser.participantId);
    const ids = [result.winner.participantId, result.loser.participantId].toSorted();
    expect(ids).toEqual(["A", "B"]);
  });

  it("gamesPlayed is between 2 and 3", () => {
    const a: SimTeam = { participantId: "A", elo: 1500 };
    const b: SimTeam = { participantId: "B", elo: 1500 };
    for (let i = 0; i < 1000; i++) {
      const { gamesPlayed } = simulateBestOfThree(a, b);
      expect(gamesPlayed).toBeGreaterThanOrEqual(2);
      expect(gamesPlayed).toBeLessThanOrEqual(3);
    }
  });

  it("from a 1-1 state always plays exactly 1 more game", () => {
    const a: SimTeam = { participantId: "A", elo: 1500 };
    const b: SimTeam = { participantId: "B", elo: 1500 };
    for (let i = 0; i < 1000; i++) {
      const { gamesPlayed } = simulateBestOfThree(a, b, 400, { winsA: 1, winsB: 1 });
      expect(gamesPlayed).toBe(1);
    }
  });

  it("respects existing series state: team with 2 wins is declared winner immediately", () => {
    const a: SimTeam = { participantId: "A", elo: 1500 };
    const b: SimTeam = { participantId: "B", elo: 1500 };
    const result = simulateBestOfThree(a, b, 400, { winsA: 2, winsB: 0 });
    expect(result.winner.participantId).toBe("A");
  });

  it("simulates from a 1-0 state correctly", () => {
    const a: SimTeam = { participantId: "A", elo: 1900 };
    const b: SimTeam = { participantId: "B", elo: 1100 };
    let aWins = 0;
    for (let i = 0; i < 5_000; i++) {
      const r = simulateBestOfThree(a, b, 400, { winsA: 1, winsB: 0 });
      if (r.winner.participantId === "A") aWins++;
    }
    // A is heavily favoured and already leads 1-0
    expect(aWins).toBeGreaterThan(4000);
  });
});

// ─── buildNllBracketFromSeeds ─────────────────────────────────────────────────

describe("buildNllBracketFromSeeds", () => {
  const seeds = Array.from({ length: 8 }, (_, i) => makeSeed(`t${i + 1}`, i + 1));

  it("produces 4 matchups", () => {
    const bracket = buildNllBracketFromSeeds(seeds);
    expect(bracket).toHaveLength(4);
  });

  it("QF1 is seed 1 vs seed 8", () => {
    const [[a, b]] = buildNllBracketFromSeeds(seeds);
    expect(a.seed).toBe(1);
    expect(b.seed).toBe(8);
  });

  it("QF2 is seed 4 vs seed 5", () => {
    const [, [a, b]] = buildNllBracketFromSeeds(seeds);
    expect(a.seed).toBe(4);
    expect(b.seed).toBe(5);
  });

  it("QF3 is seed 2 vs seed 7", () => {
    const [, , [a, b]] = buildNllBracketFromSeeds(seeds);
    expect(a.seed).toBe(2);
    expect(b.seed).toBe(7);
  });

  it("QF4 is seed 3 vs seed 6", () => {
    const [, , , [a, b]] = buildNllBracketFromSeeds(seeds);
    expect(a.seed).toBe(3);
    expect(b.seed).toBe(6);
  });

  it("throws if a required seed is missing", () => {
    const incomplete = seeds.slice(0, 7); // missing seed 8
    expect(() => buildNllBracketFromSeeds(incomplete)).toThrow();
  });
});

// ─── simulateNllPlayoffs ──────────────────────────────────────────────────────

describe("simulateNllPlayoffs", () => {
  const seeds = Array.from({ length: 8 }, (_, i) => makeSeed(`t${i + 1}`, i + 1));

  it("returns exactly one champion", () => {
    const result = simulateNllPlayoffs(seeds);
    expect(typeof result.champion).toBe("string");
  });

  it("returns exactly one finalist", () => {
    const result = simulateNllPlayoffs(seeds);
    expect(typeof result.finalist).toBe("string");
  });

  it("champion and finalist are different", () => {
    const result = simulateNllPlayoffs(seeds);
    expect(result.champion).not.toBe(result.finalist);
  });

  it("returns 2 SF losers", () => {
    const result = simulateNllPlayoffs(seeds);
    expect(result.sfLosers).toHaveLength(2);
  });

  it("returns 4 QF losers", () => {
    const result = simulateNllPlayoffs(seeds);
    expect(result.qfLosers).toHaveLength(4);
  });

  it("champion comes from the playoff field (seeds 1–8)", () => {
    const ids = new Set(seeds.map((s) => s.participantId));
    const result = simulateNllPlayoffs(seeds);
    expect(ids.has(result.champion)).toBe(true);
  });

  it("all 8 participants appear exactly once across all placements", () => {
    const result = simulateNllPlayoffs(seeds);
    const all = [
      result.champion,
      result.finalist,
      ...result.sfLosers,
      ...result.qfLosers,
    ];
    expect(all).toHaveLength(8);
    expect(new Set(all).size).toBe(8);
  });

  it("higher-Elo team (seed 1) wins more often than lower-Elo team (seed 8)", () => {
    const strongSeeds = Array.from({ length: 8 }, (_, i) =>
      makeSeed(`t${i + 1}`, i + 1, 1800 - i * 50)
    );
    let seed1Wins = 0;
    for (let i = 0; i < 5_000; i++) {
      if (simulateNllPlayoffs(strongSeeds).champion === "t1") seed1Wins++;
    }
    expect(seed1Wins).toBeGreaterThan(1500);
  });
});

// ─── simulateRegularSeasonSeeds ───────────────────────────────────────────────

describe("simulateRegularSeasonSeeds", () => {
  it("returns exactly 8 seeds from a 14-team field", () => {
    const teams = make14Teams();
    const seeds = simulateRegularSeasonSeeds(teams);
    expect(seeds).toHaveLength(8);
  });

  it("seeds are numbered 1–8 with no duplicates", () => {
    const teams = make14Teams();
    const seeds = simulateRegularSeasonSeeds(teams);
    const seedNums = seeds.map((s) => s.seed).toSorted((a, b) => a - b);
    expect(seedNums).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
  });

  it("all returned participant IDs are from the input teams", () => {
    const teams = make14Teams();
    const ids = new Set(teams.map((t) => t.participantId));
    const seeds = simulateRegularSeasonSeeds(teams);
    for (const s of seeds) {
      expect(ids.has(s.participantId)).toBe(true);
    }
  });

  it("higher-Elo teams qualify for playoffs more often", () => {
    const teams = make14Teams((i) => i === 0 ? 1900 : 1400);
    let qualifiedCount = 0;
    for (let i = 0; i < 1000; i++) {
      const seeds = simulateRegularSeasonSeeds(teams);
      if (seeds.some((s) => s.participantId === "team-1")) qualifiedCount++;
    }
    expect(qualifiedCount).toBeGreaterThan(900);
  });

  it("team with higher projectedWins qualifies more often", () => {
    const base = make14Teams(() => 1500);
    base[0] = { ...base[0], projectedWins: 17 }; // 17/18 projected wins
    base[1] = { ...base[1], projectedWins: 2 };  // 2/18 projected wins

    let t1Qualifies = 0;
    let t2Qualifies = 0;
    for (let i = 0; i < 1000; i++) {
      const seeds = simulateRegularSeasonSeeds(base);
      const ids = seeds.map((s) => s.participantId);
      if (ids.includes("team-1")) t1Qualifies++;
      if (ids.includes("team-2")) t2Qualifies++;
    }
    expect(t1Qualifies).toBeGreaterThan(t2Qualifies);
  });

  it("mid-season: prior adjusts for remaining wins — team with more projected wins still ahead qualifies more", () => {
    // All teams are at the halfway point with 0 currentWins so currentWins don't confound.
    // team-1 has a very high remaining projection; team-2 has a very low one.
    // With equal Elo, the prior blend should make team-1 qualify more.
    const teams = make14Teams(() => 1500).map((t) => ({
      ...t, currentWins: 0, gamesPlayed: 9,
    }));
    teams[0] = { ...teams[0], projectedWins: 17 }; // priorRate = min(1, 17/9) = 1.0
    teams[1] = { ...teams[1], projectedWins: 2 };  // priorRate = 2/9 ≈ 0.22

    let t1Qualifies = 0;
    let t2Qualifies = 0;
    for (let i = 0; i < 1000; i++) {
      const seeds = simulateRegularSeasonSeeds(teams);
      const ids = seeds.map((s) => s.participantId);
      if (ids.includes("team-1")) t1Qualifies++;
      if (ids.includes("team-2")) t2Qualifies++;
    }
    expect(t1Qualifies).toBeGreaterThan(t2Qualifies);
  });

  it("works with a full pre-season 0-0 state", () => {
    const teams = make14Teams(() => 1500);
    expect(() => simulateRegularSeasonSeeds(teams)).not.toThrow();
  });

  it("respects existing wins as a floor (current wins carry over)", () => {
    // Team with 16 wins already can't go below 16.
    const teams = make14Teams(() => 1500);
    teams[0] = { ...teams[0], currentWins: 16, gamesPlayed: 17 };
    for (let i = 0; i < 100; i++) {
      const seeds = simulateRegularSeasonSeeds(teams);
      // team-1 must be seeded (has ≥16 wins, max 18 total)
      expect(seeds.some((s) => s.participantId === "team-1")).toBe(true);
    }
  });
});

// ─── Output probability column sums ──────────────────────────────────────────

describe("simulateNllPlayoffs probability column sums", () => {
  it("probFirst sums to 1 across all participants", () => {
    const N = 1000;
    const seeds = Array.from({ length: 8 }, (_, i) =>
      makeSeed(`t${i + 1}`, i + 1, 1500)
    );
    const counts = new Map(seeds.map((s) => [s.participantId, 0]));
    for (let i = 0; i < N; i++) {
      const r = simulateNllPlayoffs(seeds);
      counts.set(r.champion, (counts.get(r.champion) ?? 0) + 1);
    }
    const total = [...counts.values()].reduce((s, c) => s + c, 0);
    expect(total).toBe(N);
  });

  it("probThird+Fourth sums: 2 SF losers per sim → total = 2*N", () => {
    const N = 1000;
    const seeds = Array.from({ length: 8 }, (_, i) =>
      makeSeed(`t${i + 1}`, i + 1, 1500)
    );
    let total = 0;
    for (let i = 0; i < N; i++) {
      total += simulateNllPlayoffs(seeds).sfLosers.length;
    }
    expect(total).toBe(2 * N);
  });

  it("probFifth–Eighth sums: 4 QF losers per sim → total = 4*N", () => {
    const N = 1000;
    const seeds = Array.from({ length: 8 }, (_, i) =>
      makeSeed(`t${i + 1}`, i + 1, 1500)
    );
    let total = 0;
    for (let i = 0; i < N; i++) {
      total += simulateNllPlayoffs(seeds).qfLosers.length;
    }
    expect(total).toBe(4 * N);
  });
});

// ─── resolveQfMatch ───────────────────────────────────────────────────────────

function makeMatch(overrides: Partial<MatchSnapshot> = {}): MatchSnapshot {
  return {
    id: "match-1",
    isComplete: false,
    winnerId: null,
    loserId: null,
    participant1Id: "A",
    participant2Id: "B",
    ...overrides,
  };
}

describe("resolveQfMatch", () => {
  it("returns stored result deterministically when match is complete", () => {
    const match = makeMatch({ isComplete: true, winnerId: "A", loserId: "B" });
    const eloMap = new Map([["A", 1500], ["B", 1500]]);
    for (let i = 0; i < 100; i++) {
      const result = resolveQfMatch(match, eloMap);
      expect(result.winner).toBe("A");
      expect(result.loser).toBe("B");
    }
  });

  it("simulates a game when match is incomplete", () => {
    const match = makeMatch();
    const eloMap = new Map([["A", 1500], ["B", 1500]]);
    const result = resolveQfMatch(match, eloMap);
    const ids = [result.winner, result.loser].toSorted();
    expect(ids).toEqual(["A", "B"]);
  });

  it("throws when participant slots are missing", () => {
    const match = makeMatch({ participant1Id: null });
    const eloMap = new Map<string, number>();
    expect(() => resolveQfMatch(match, eloMap)).toThrow();
  });

  it("throws when match is undefined", () => {
    const eloMap = new Map<string, number>();
    expect(() => resolveQfMatch(undefined, eloMap)).toThrow();
  });
});

// ─── resolveBo3Match ──────────────────────────────────────────────────────────

function noGames(): GameSnapshot[] {
  return [];
}

describe("resolveBo3Match", () => {
  it("returns stored result deterministically when match is complete", () => {
    const match = makeMatch({ isComplete: true, winnerId: "B", loserId: "A" });
    const eloMap = new Map([["A", 1500], ["B", 1500]]);
    for (let i = 0; i < 100; i++) {
      const result = resolveBo3Match(match, noGames(), eloMap);
      expect(result.winner).toBe("B");
      expect(result.loser).toBe("A");
    }
  });

  it("simulates from scratch when no games played", () => {
    const match = makeMatch();
    const eloMap = new Map([["A", 1500], ["B", 1500]]);
    const result = resolveBo3Match(match, noGames(), eloMap);
    const ids = [result.winner, result.loser].toSorted();
    expect(ids).toEqual(["A", "B"]);
  });

  it("returns winner deterministically when game rows show 2 wins for p1", () => {
    const match = makeMatch();
    const games: GameSnapshot[] = [{ winnerId: "A" }, { winnerId: "A" }];
    const eloMap = new Map([["A", 1500], ["B", 1500]]);
    for (let i = 0; i < 100; i++) {
      const result = resolveBo3Match(match, games, eloMap);
      expect(result.winner).toBe("A");
      expect(result.loser).toBe("B");
    }
  });

  it("simulates correctly from a 1-1 game state", () => {
    const match = makeMatch();
    const games: GameSnapshot[] = [{ winnerId: "A" }, { winnerId: "B" }];
    const eloMap = new Map([["A", 1900], ["B", 1100]]);
    let aWins = 0;
    for (let i = 0; i < 5_000; i++) {
      if (resolveBo3Match(match, games, eloMap).winner === "A") aWins++;
    }
    expect(aWins).toBeGreaterThan(4000);
  });

  it("accepts p1/p2 overrides when match slots are null", () => {
    const match = makeMatch({ participant1Id: null, participant2Id: null });
    const eloMap = new Map([["X", 1500], ["Y", 1500]]);
    const result = resolveBo3Match(match, noGames(), eloMap, "X", "Y");
    const ids = [result.winner, result.loser].toSorted();
    expect(ids).toEqual(["X", "Y"]);
  });

  it("throws when participant slots are missing and no overrides given", () => {
    const match = makeMatch({ participant1Id: null });
    const eloMap = new Map<string, number>();
    expect(() => resolveBo3Match(match, noGames(), eloMap)).toThrow();
  });
});

// ─── Manifest, registry, and schema enum sync ────────────────────────────────

describe("nll_bracket simulator sync", () => {
  it("nll_bracket is in SIMULATOR_TYPES", () => {
    expect(SIMULATOR_TYPES).toContain("nll_bracket");
  });

  it("nll_bracket has a manifest profile", () => {
    expect(SIMULATOR_MANIFEST["nll_bracket"]).toBeDefined();
  });

  it("manifest profile requires sourceElo", () => {
    expect(SIMULATOR_MANIFEST["nll_bracket"].requiredInputs).toContain("sourceElo");
  });

  it("manifest profile has projectedWins as optional input", () => {
    expect(SIMULATOR_MANIFEST["nll_bracket"].optionalInputs).toContain("projectedWins");
  });

  it("manifest profile lists regularStandings and bracket setup sections", () => {
    const sections = SIMULATOR_MANIFEST["nll_bracket"].setupSections;
    expect(sections).toContain("regularStandings");
    expect(sections).toContain("bracket");
  });

  it("manifest profile has sourceElo derivable from projectedWins", () => {
    const derivable = SIMULATOR_MANIFEST["nll_bracket"].derivableInputs;
    expect(derivable?.sourceElo).toContain("projectedWins");
  });
});
