import { describe, it, expect } from "vitest";
import {
  mlsWinProbability,
  simulateMlsKnockoutGame,
  simulateMlsBestOfThree,
  simulateMlsRegularSeason,
  simulateMlsPlayoffs,
  parseConferenceFromExternalId,
  type MlsConferenceSeed,
  type MlsTeamEntry,
} from "../mls-simulator";
import { SIMULATOR_TYPES } from "../registry";
import { SIMULATOR_MANIFEST } from "../manifest";

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

function makeSeed(
  participantId: string,
  seed: number,
  conference: "Eastern" | "Western" = "Eastern",
  elo = 1500
): MlsConferenceSeed {
  return { participantId, elo, conference, seed };
}

function make15Teams(
  conference: "Eastern" | "Western",
  eloFn: (i: number) => number = () => 1500
): MlsTeamEntry[] {
  return Array.from({ length: 15 }, (_, i) => ({
    participantId: `${conference}-team-${i + 1}`,
    elo: eloFn(i),
    conference,
    currentPoints: 0,
    currentGoalsFor: 0,
    currentGoalDifference: 0,
    remainingGames: 34,
  }));
}

function make9Seeds(conference: "Eastern" | "Western", eloFn: (i: number) => number = () => 1500): MlsConferenceSeed[] {
  return Array.from({ length: 9 }, (_, i) =>
    makeSeed(`${conference}-t${i + 1}`, i + 1, conference, eloFn(i))
  );
}

// ─── mlsWinProbability ────────────────────────────────────────────────────────

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

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

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

// ─── simulateMlsKnockoutGame ──────────────────────────────────────────────────

describe("simulateMlsKnockoutGame", () => {
  it("returns one winner and one loser from the two teams", () => {
    const a = makeSeed("A", 1);
    const b = makeSeed("B", 2);
    const result = simulateMlsKnockoutGame(a, b);
    const ids = [result.winner.participantId, result.loser.participantId].toSorted();
    expect(ids).toEqual(["A", "B"]);
  });

  it("winner and loser are always different", () => {
    const a = makeSeed("A", 1);
    const b = makeSeed("B", 2);
    for (let i = 0; i < 200; i++) {
      const result = simulateMlsKnockoutGame(a, b);
      expect(result.winner.participantId).not.toBe(result.loser.participantId);
    }
  });

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

// ─── simulateMlsBestOfThree ────────────────────────────────────────────────────

describe("simulateMlsBestOfThree", () => {
  it("returns one winner and one loser", () => {
    const a = makeSeed("A", 1);
    const b = makeSeed("B", 2);
    const result = simulateMlsBestOfThree(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 = makeSeed("A", 1);
    const b = makeSeed("B", 2);
    for (let i = 0; i < 500; i++) {
      const { gamesPlayed } = simulateMlsBestOfThree(a, b);
      expect(gamesPlayed).toBeGreaterThanOrEqual(2);
      expect(gamesPlayed).toBeLessThanOrEqual(3);
    }
  });

  it("from a 1-1 state always plays exactly 1 more game", () => {
    const a = makeSeed("A", 1);
    const b = makeSeed("B", 2);
    for (let i = 0; i < 500; i++) {
      const { gamesPlayed } = simulateMlsBestOfThree(a, b, undefined, { winsHigher: 1, winsLower: 1 });
      expect(gamesPlayed).toBe(1);
    }
  });

  it("respects existing state: team with 2 wins is declared winner immediately", () => {
    const a = makeSeed("A", 1);
    const b = makeSeed("B", 2);
    const result = simulateMlsBestOfThree(a, b, undefined, { winsHigher: 2, winsLower: 0 });
    expect(result.winner.participantId).toBe("A");
    expect(result.gamesPlayed).toBe(0);
  });

  it("higher-Elo team wins series more often", () => {
    const strong = makeSeed("strong", 1, "Eastern", 1800);
    const weak = makeSeed("weak", 2, "Eastern", 1200);
    let wins = 0;
    for (let i = 0; i < 5_000; i++) {
      if (simulateMlsBestOfThree(strong, weak).winner.participantId === "strong") wins++;
    }
    expect(wins).toBeGreaterThan(4000);
  });
});

// ─── simulateMlsRegularSeason ─────────────────────────────────────────────────

describe("simulateMlsRegularSeason", () => {
  it("returns exactly 9 seeds per conference from a 30-team input", () => {
    const teams = [...make15Teams("Eastern"), ...make15Teams("Western")];
    const { east, west } = simulateMlsRegularSeason(teams);
    expect(east).toHaveLength(9);
    expect(west).toHaveLength(9);
  });

  it("seeds are numbered 1–9 with no duplicates within each conference", () => {
    const teams = [...make15Teams("Eastern"), ...make15Teams("Western")];
    const { east, west } = simulateMlsRegularSeason(teams);
    const eastNums = east.map((s) => s.seed).toSorted((a, b) => a - b);
    const westNums = west.map((s) => s.seed).toSorted((a, b) => a - b);
    expect(eastNums).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
    expect(westNums).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
  });

  it("conference separation holds — no Eastern team appears in Western seeds", () => {
    const teams = [...make15Teams("Eastern"), ...make15Teams("Western")];
    const { east, west } = simulateMlsRegularSeason(teams);
    for (const s of east) expect(s.participantId).toMatch(/^Eastern-/);
    for (const s of west) expect(s.participantId).toMatch(/^Western-/);
  });

  it("all returned participant IDs are from the input teams", () => {
    const teams = [...make15Teams("Eastern"), ...make15Teams("Western")];
    const ids = new Set(teams.map((t) => t.participantId));
    const { east, west } = simulateMlsRegularSeason(teams);
    for (const s of [...east, ...west]) {
      expect(ids.has(s.participantId)).toBe(true);
    }
  });

  it("higher-Elo Eastern team qualifies more often", () => {
    const teams = [
      ...make15Teams("Eastern", (i) => i === 0 ? 1900 : 1400),
      ...make15Teams("Western"),
    ];
    let qualified = 0;
    for (let i = 0; i < 500; i++) {
      const { east } = simulateMlsRegularSeason(teams);
      if (east.some((s) => s.participantId === "Eastern-team-1")) qualified++;
    }
    expect(qualified).toBeGreaterThan(450);
  });

  it("works with a full preseason 0-0 state", () => {
    const teams = [...make15Teams("Eastern"), ...make15Teams("Western")];
    expect(() => simulateMlsRegularSeason(teams)).not.toThrow();
  });

  it("respects current points as a floor", () => {
    const teams = [
      ...make15Teams("Eastern").map((t, i) =>
        i === 0 ? { ...t, currentPoints: 90, remainingGames: 0 } : t
      ),
      ...make15Teams("Western"),
    ];
    // A team with 90 points and 0 remaining games always tops the East
    for (let i = 0; i < 50; i++) {
      const { east } = simulateMlsRegularSeason(teams);
      expect(east[0].participantId).toBe("Eastern-team-1");
    }
  });
});

// ─── simulateMlsPlayoffs ──────────────────────────────────────────────────────

describe("simulateMlsPlayoffs", () => {
  const east = make9Seeds("Eastern");
  const west = make9Seeds("Western");

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

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

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

  it("confFinalsLosers has exactly 2 unique entries", () => {
    const result = simulateMlsPlayoffs(east, west);
    expect(result.confFinalsLosers).toHaveLength(2);
    expect(new Set(result.confFinalsLosers).size).toBe(2);
  });

  it("confSemiLosers has exactly 4 unique entries", () => {
    const result = simulateMlsPlayoffs(east, west);
    expect(result.confSemiLosers).toHaveLength(4);
    expect(new Set(result.confSemiLosers).size).toBe(4);
  });

  it("champion comes from the playoff field", () => {
    const allIds = new Set([...east, ...west].map((s) => s.participantId));
    const result = simulateMlsPlayoffs(east, west);
    expect(allIds.has(result.champion)).toBe(true);
  });

  it("MLS Cup never pits two teams from the same conference", () => {
    const eastIds = new Set(east.map((s) => s.participantId));
    const westIds = new Set(west.map((s) => s.participantId));
    for (let i = 0; i < 100; i++) {
      const { champion, finalist } = simulateMlsPlayoffs(east, west);
      const champIsEast = eastIds.has(champion);
      const champIsWest = westIds.has(champion);
      const finalIsEast = eastIds.has(finalist);
      const finalIsWest = westIds.has(finalist);
      // Champion and finalist must be from opposite conferences
      expect(champIsEast !== finalIsEast || champIsWest !== finalIsWest).toBe(true);
    }
  });

  it("all 8 placed participants are unique", () => {
    const result = simulateMlsPlayoffs(east, west);
    const all = [
      result.champion,
      result.finalist,
      ...result.confFinalsLosers,
      ...result.confSemiLosers,
    ];
    expect(new Set(all).size).toBe(8);
  });

  it("higher-Elo seed-1 East team wins MLS Cup more often than lower-Elo seed-9 East team", () => {
    const strongEast = make9Seeds("Eastern", (i) => 1800 - i * 30);
    let seed1Wins = 0;
    for (let i = 0; i < 3_000; i++) {
      if (simulateMlsPlayoffs(strongEast, west).champion === "Eastern-t1") seed1Wins++;
    }
    expect(seed1Wins).toBeGreaterThan(500);
  });
});

// ─── Probability column sums ───────────────────────────────────────────────────

describe("simulateMlsPlayoffs probability column sums", () => {
  const east = make9Seeds("Eastern");
  const west = make9Seeds("Western");
  const N = 1000;

  it("probFirst sums to exactly N across all participants", () => {
    const counts = new Map([...east, ...west].map((s) => [s.participantId, 0]));
    for (let i = 0; i < N; i++) {
      const r = simulateMlsPlayoffs(east, west);
      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("confFinalsLosers: 2 per sim → total = 2*N", () => {
    let total = 0;
    for (let i = 0; i < N; i++) {
      total += simulateMlsPlayoffs(east, west).confFinalsLosers.length;
    }
    expect(total).toBe(2 * N);
  });

  it("confSemiLosers: 4 per sim → total = 4*N", () => {
    let total = 0;
    for (let i = 0; i < N; i++) {
      total += simulateMlsPlayoffs(east, west).confSemiLosers.length;
    }
    expect(total).toBe(4 * N);
  });
});

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

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

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

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

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

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

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

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

  it("manifest defaultConfig has soccer-appropriate parameters", () => {
    expect(SIMULATOR_MANIFEST["mls_bracket"].defaultConfig).toMatchObject({
      seasonGames: 34,
      parityFactor: 400,
      baseDrawRate: 0.26,
    });
  });
});

// ─── parseConferenceFromExternalId ────────────────────────────────────────────

describe("parseConferenceFromExternalId", () => {
  it('returns "Eastern" for exact match', () => {
    expect(parseConferenceFromExternalId("Eastern")).toBe("Eastern");
  });

  it('returns "Western" for exact match', () => {
    expect(parseConferenceFromExternalId("Western")).toBe("Western");
  });

  it("is case-insensitive", () => {
    expect(parseConferenceFromExternalId("eastern")).toBe("Eastern");
    expect(parseConferenceFromExternalId("WESTERN")).toBe("Western");
  });

  it("returns null for a numeric ESPN team id", () => {
    expect(parseConferenceFromExternalId("339")).toBeNull();
  });

  it("returns null for null/undefined", () => {
    expect(parseConferenceFromExternalId(null)).toBeNull();
    expect(parseConferenceFromExternalId(undefined)).toBeNull();
  });

  it("returns null for an unrecognized string", () => {
    expect(parseConferenceFromExternalId("Northeast")).toBeNull();
  });
});
