import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
import {
  setWinProb,
  matchWinProb,
  getSeededMatchOrder,
  buildR1Bracket,
  DartsSimulator,
} from "../darts-simulator";

// ─── Pure math: setWinProb ────────────────────────────────────────────────────

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

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

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

  it("larger Elo gap → higher win probability (monotonic)", () => {
    const p100 = setWinProb(1900, 1800);
    const p300 = setWinProb(2100, 1800);
    const p500 = setWinProb(2300, 1800);
    expect(p300).toBeGreaterThan(p100);
    expect(p500).toBeGreaterThan(p300);
  });

  it("returns value strictly in (0, 1) for any finite Elo", () => {
    expect(setWinProb(3000, 1000)).toBeLessThan(1);
    expect(setWinProb(3000, 1000)).toBeGreaterThan(0);
    expect(setWinProb(1000, 3000)).toBeLessThan(1);
    expect(setWinProb(1000, 3000)).toBeGreaterThan(0);
  });

  it("calibration: 100-pt gap → ~62% per set (ELO_DIVISOR=200)", () => {
    // 1 / (1 + e^(-100/200)) ≈ 0.6225
    expect(setWinProb(1900, 1800)).toBeCloseTo(0.6225, 2);
  });
});

// ─── Pure math: matchWinProb ──────────────────────────────────────────────────

describe("matchWinProb", () => {
  it("returns 0.5 for equal players (p=0.5) at any match length", () => {
    // PDC formats: bo3 (S=2), bo5 (S=3), bo7 (S=4), bo11 (S=6), bo13 (S=7)
    expect(matchWinProb(0.5, 2)).toBeCloseTo(0.5, 3);
    expect(matchWinProb(0.5, 3)).toBeCloseTo(0.5, 3);
    expect(matchWinProb(0.5, 4)).toBeCloseTo(0.5, 3);
    expect(matchWinProb(0.5, 6)).toBeCloseTo(0.5, 3);
    expect(matchWinProb(0.5, 7)).toBeCloseTo(0.5, 3);
  });

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

  it("approaches 0.0 as p → 0.0", () => {
    expect(matchWinProb(0.0001, 2)).toBeCloseTo(0.0, 3);
    expect(matchWinProb(0.0001, 7)).toBeCloseTo(0.0, 3);
  });

  it("longer matches amplify stronger player's advantage", () => {
    // p=0.6: win prob should be higher in best-of-13 than best-of-3
    const bo3  = matchWinProb(0.6, 2);
    const bo13 = matchWinProb(0.6, 7);
    expect(bo13).toBeGreaterThan(bo3);
  });

  it("match win prob + opponent win prob sums to 1.0", () => {
    const p = 0.63;
    expect(matchWinProb(p, 3) + matchWinProb(1 - p, 3)).toBeCloseTo(1.0, 5);
    expect(matchWinProb(p, 6) + matchWinProb(1 - p, 6)).toBeCloseTo(1.0, 5);
  });

  it("best-of-3 (S=2): reference check for p=0.6 → ~0.648", () => {
    // P(2-0) = 0.6^2 = 0.36; P(2-1) = 2 * 0.6^2 * 0.4 = 0.288; total = 0.648
    expect(matchWinProb(0.6, 2)).toBeCloseTo(0.648, 3);
  });
});

// ─── Bracket structure: getSeededMatchOrder ──────────────────────────────────

describe("getSeededMatchOrder", () => {
  it("for n=2, returns [1, 2]", () => {
    expect(getSeededMatchOrder(2)).toEqual([1, 2]);
  });

  it("for n=4, seed 1 vs 4 in top half, seed 2 vs 3 in bottom half", () => {
    // Algorithm: start [1,2] → expand to [1,4,2,3]
    expect(getSeededMatchOrder(4)).toEqual([1, 4, 2, 3]);
  });

  it("for n=8, seed 1 and seed 2 are in opposite halves", () => {
    const order = getSeededMatchOrder(8);
    expect(order).toHaveLength(8);
    const idx1 = order.indexOf(1);
    const idx2 = order.indexOf(2);
    // Seeds 1 and 2 should be in different halves (0-3 vs 4-7)
    expect(Math.floor(idx1 / 4)).not.toBe(Math.floor(idx2 / 4));
  });

  it("for n=32, contains all seeds 1–32 exactly once", () => {
    const order = getSeededMatchOrder(32);
    expect(order).toHaveLength(32);
    const sorted = [...order].toSorted((a, b) => a - b);
    expect(sorted).toEqual(Array.from({ length: 32 }, (_, i) => i + 1));
  });

  it("for n=32, seed 1 and seed 2 are in opposite halves", () => {
    const order = getSeededMatchOrder(32);
    const idx1 = order.indexOf(1);
    const idx2 = order.indexOf(2);
    // Opposite halves: one in [0,15], other in [16,31]
    expect(Math.floor(idx1 / 16)).not.toBe(Math.floor(idx2 / 16));
  });
});

// ─── Bracket structure: buildR1Bracket ───────────────────────────────────────

describe("buildR1Bracket", () => {
  const seeded   = Array.from({ length: 32 }, (_, i) => `seed-${i + 1}`);
  const unseeded = Array.from({ length: 96 }, (_, i) => `unseed-${i}`);

  it("returns exactly 64 pairs", () => {
    const pairs = buildR1Bracket(seeded, unseeded);
    expect(pairs).toHaveLength(64);
  });

  it("covers all 128 players (32 seeded + 96 unseeded)", () => {
    const pairs = buildR1Bracket(seeded, unseeded);
    const allPlayers = pairs.flat();
    expect(allPlayers).toHaveLength(128);

    for (const [a, b] of pairs) {
      expect(a).toBeTruthy();
      expect(b).toBeTruthy();
    }

    // All seeded players appear exactly once
    for (const s of seeded) {
      expect(allPlayers.filter(p => p === s)).toHaveLength(1);
    }

    // All unseeded players appear exactly once
    for (const u of unseeded) {
      expect(allPlayers.filter(p => p === u)).toHaveLength(1);
    }
  });

  it("each of the 32 seeded players faces an unseeded opponent in their R1 match", () => {
    const pairs = buildR1Bracket(seeded, unseeded);
    const seededSet = new Set(seeded);

    // Even-indexed pairs (0, 2, 4, ...) are seeded vs unseeded
    for (let i = 0; i < 64; i += 2) {
      const [a, b] = pairs[i];
      const aSeeded = seededSet.has(a);
      const bSeeded = seededSet.has(b);
      expect(aSeeded !== bSeeded).toBe(true);
    }
  });

  it("odd-indexed pairs are all unseeded vs unseeded (interleaved bracket structure)", () => {
    const pairs = buildR1Bracket(seeded, unseeded);
    const seededSet = new Set(seeded);

    // Odd-indexed pairs (1, 3, 5, ...) are unseeded vs unseeded
    for (let i = 1; i < 64; i += 2) {
      const [a, b] = pairs[i];
      expect(seededSet.has(a)).toBe(false);
      expect(seededSet.has(b)).toBe(false);
    }
  });
});

// ─── Integration tests with mocked DB ────────────────────────────────────────

const PARTICIPANT_IDS = Array.from({ length: 128 }, (_, i) => `player-${i + 1}`);
const ELO_BASE = 1750;

function makeMatch(
  round: string,
  matchNumber: number,
  p1Index: number,
  p2Index: number,
  opts: { winnerId?: string; loserId?: string; isComplete?: boolean } = {}
) {
  return {
    id: `${round}-match-${matchNumber}`,
    scoringEventId: "event-1",
    round,
    matchNumber,
    participant1Id: PARTICIPANT_IDS[p1Index],
    participant2Id: PARTICIPANT_IDS[p2Index],
    winnerId: opts.winnerId ?? null,
    loserId: opts.loserId ?? null,
    isComplete: opts.isComplete ?? false,
    isScoring: true,
    templateRound: round,
    seedInfo: null,
    participant1Score: null,
    participant2Score: null,
    createdAt: new Date(),
    updatedAt: new Date(),
  };
}

/** Build a full 7-round bracket (128 players, 127 matches). */
function makeFullBracket(): ReturnType<typeof makeMatch>[] {
  const matches: ReturnType<typeof makeMatch>[] = [];
  for (let i = 0; i < 64; i++) matches.push(makeMatch("Round 1", i + 1, i * 2, i * 2 + 1));
  for (let i = 0; i < 32; i++) matches.push(makeMatch("Round 2", i + 1, 0, 1));
  for (let i = 0; i < 16; i++) matches.push(makeMatch("Round 3", i + 1, 0, 1));
  for (let i = 0; i < 8;  i++) matches.push(makeMatch("Round 4", i + 1, 0, 1));
  for (let i = 0; i < 4;  i++) matches.push(makeMatch("Quarter-Finals", i + 1, 0, 1));
  for (let i = 0; i < 2;  i++) matches.push(makeMatch("Semi-Finals", i + 1, 0, 1));
  matches.push(makeMatch("Final", 1, 0, 1));
  return matches;
}

function makeEVRows(eloOverride?: Map<string, number>) {
  return PARTICIPANT_IDS.map((participantId) => ({
    participantId,
    sourceElo: eloOverride?.get(participantId) ?? ELO_BASE,
    worldRanking: null,
  }));
}

vi.mock("~/database/context", () => ({
  database: vi.fn(),
}));

describe("DartsSimulator.simulate() — Path A (bracket populated)", () => {
  let mockDb: {
    query: {
      scoringEvents: { findFirst: MockInstance };
      playoffMatches: { findMany: MockInstance };
    };
    select: MockInstance;
  };

  beforeEach(async () => {
    const { database } = await import("~/database/context");

    mockDb = {
      query: {
        scoringEvents: { findFirst: vi.fn() },
        playoffMatches: { findMany: vi.fn() },
      },
      select: vi.fn().mockReturnValue({
        from: vi.fn().mockReturnValue({
          where: vi.fn().mockResolvedValue(makeEVRows()),
        }),
      }),
    };

    (database as unknown as MockInstance).mockReturnValue(mockDb);

    mockDb.query.scoringEvents.findFirst.mockResolvedValue({
      id: "event-1",
      sportsSeasonId: "season-1",
      eventType: "playoff_game",
    });
  });

  it("throws if bracket does not have exactly 7 rounds", async () => {
    // Only 6 rounds (missing Final)
    const matches = [
      ...Array.from({ length: 64 }, (_, i) => makeMatch("Round 1", i + 1, i * 2, i * 2 + 1)),
      ...Array.from({ length: 32 }, (_, i) => makeMatch("Round 2", i + 1, 0, 1)),
      ...Array.from({ length: 16 }, (_, i) => makeMatch("Round 3", i + 1, 0, 1)),
      ...Array.from({ length: 8  }, (_, i) => makeMatch("Round 4", i + 1, 0, 1)),
      ...Array.from({ length: 4  }, (_, i) => makeMatch("Quarter-Finals", i + 1, 0, 1)),
      ...Array.from({ length: 2  }, (_, i) => makeMatch("Semi-Finals", i + 1, 0, 1)),
    ];
    mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);

    const sim = new DartsSimulator();
    await expect(sim.simulate("season-1")).rejects.toThrow(/Expected 7 rounds/);
  });

  it("throws if R1 does not have 64 matches", async () => {
    const matches = [
      ...Array.from({ length: 32 }, (_, i) => makeMatch("Round 1", i + 1, i * 2, i * 2 + 1)),
      ...Array.from({ length: 32 }, (_, i) => makeMatch("Round 2", i + 1, 0, 1)),
      ...Array.from({ length: 16 }, (_, i) => makeMatch("Round 3", i + 1, 0, 1)),
      ...Array.from({ length: 8  }, (_, i) => makeMatch("Round 4", i + 1, 0, 1)),
      ...Array.from({ length: 4  }, (_, i) => makeMatch("Quarter-Finals", i + 1, 0, 1)),
      ...Array.from({ length: 2  }, (_, i) => makeMatch("Semi-Finals", i + 1, 0, 1)),
      makeMatch("Final", 1, 0, 1),
    ];
    mockDb.query.playoffMatches.findMany.mockResolvedValue(matches);

    const sim = new DartsSimulator();
    await expect(sim.simulate("season-1")).rejects.toThrow(/Expected 64 R1 matches/);
  });

  it("returns 128 results with valid probability distributions", async () => {
    mockDb.query.playoffMatches.findMany.mockResolvedValue(makeFullBracket());

    const sim = new DartsSimulator();
    const results = await sim.simulate("season-1");

    expect(results).toHaveLength(128);
    for (const r of results) {
      const p = r.probabilities;
      expect(p.probFirst).toBeGreaterThanOrEqual(0);
      expect(p.probFirst).toBeLessThanOrEqual(1);
      const sum = p.probFirst + p.probSecond + p.probThird + p.probFourth +
        p.probFifth + p.probSixth + p.probSeventh + p.probEighth;
      expect(sum).toBeGreaterThanOrEqual(0);
      expect(sum).toBeLessThanOrEqual(1.001);
    }
  });

  it("each probability column sums to 1.0 across all 128 participants", async () => {
    mockDb.query.playoffMatches.findMany.mockResolvedValue(makeFullBracket());

    const sim = new DartsSimulator();
    const results = await sim.simulate("season-1");

    const keys = [
      "probFirst", "probSecond", "probThird", "probFourth",
      "probFifth", "probSixth", "probSeventh", "probEighth",
    ] as const;
    for (const key of keys) {
      const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
      expect(colSum).toBeCloseTo(1.0, 2);
    }
  });

  it("labels results with darts_world_championship_monte_carlo source", async () => {
    mockDb.query.playoffMatches.findMany.mockResolvedValue(makeFullBracket());

    const sim = new DartsSimulator();
    const results = await sim.simulate("season-1");

    expect(results.every(r => r.source === "darts_world_championship_monte_carlo")).toBe(true);
  });
});

// ─── Path B: pre-bracket simulation ──────────────────────────────────────────

describe("DartsSimulator.simulate() — Path B (pre-bracket)", () => {
  let mockDb: {
    query: {
      scoringEvents: { findFirst: MockInstance };
      playoffMatches: { findMany: MockInstance };
    };
    select: MockInstance;
  };

  // 128 participants with world rankings
  const SEASON_PARTICIPANTS = Array.from({ length: 128 }, (_, i) => ({
    id: `player-${i + 1}`,
    name: `Player ${i + 1}`,
  }));

  function makeEVRowsWithRankings() {
    return SEASON_PARTICIPANTS.map((p, i) => ({
      participantId: p.id,
      sourceElo: ELO_BASE - i * 100, // higher seed = higher Elo; large gap needed for reliable MC ordering
      worldRanking: i + 1,
    }));
  }

  beforeEach(async () => {
    const { database } = await import("~/database/context");

    const eloRows = makeEVRowsWithRankings();
    const participantRows = SEASON_PARTICIPANTS;

    mockDb = {
      query: {
        scoringEvents: { findFirst: vi.fn() },
        playoffMatches: { findMany: vi.fn() },
      },
      select: vi.fn()
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(eloRows) }),
        })
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(participantRows) }),
        }),
    };

    (database as unknown as MockInstance).mockReturnValue(mockDb);

    mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
    mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
  });

  it("returns 128 results when bracket is not yet populated", async () => {
    const sim = new DartsSimulator();
    const results = await sim.simulate("season-1");

    expect(results).toHaveLength(128);
  }, 15_000);

  it("each probability column sums to 1.0 across all 128 participants", async () => {
    const sim = new DartsSimulator();
    const results = await sim.simulate("season-1");

    const keys = [
      "probFirst", "probSecond", "probThird", "probFourth",
      "probFifth", "probSixth", "probSeventh", "probEighth",
    ] as const;
    for (const key of keys) {
      const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
      expect(colSum).toBeCloseTo(1.0, 2);
    }
  }, 15_000);

  it("world #1 (highest Elo) has the highest probFirst", async () => {
    const sim = new DartsSimulator();
    const results = await sim.simulate("season-1");

    const sorted = [...results].toSorted((a, b) => b.probabilities.probFirst - a.probabilities.probFirst);
    // player-1 is world #1 with the highest Elo — should have highest win probability
    expect(sorted[0].participantId).toBe("player-1");
  }, 15_000);

  it("throws if fewer than 2 participants exist", async () => {
    const { database } = await import("~/database/context");
    const tooFew = [{ id: "player-1", name: "Solo" }];
    const eloRowsFew = [{ participantId: "player-1", sourceElo: ELO_BASE, worldRanking: 1 }];

    (database as unknown as MockInstance).mockReturnValue({
      ...mockDb,
      query: {
        scoringEvents: { findFirst: vi.fn().mockResolvedValue(null) },
        playoffMatches: { findMany: vi.fn().mockResolvedValue([]) },
      },
      select: vi.fn()
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(eloRowsFew) }),
        })
        .mockReturnValueOnce({
          from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(tooFew) }),
        }),
    });

    const sim = new DartsSimulator();
    await expect(sim.simulate("season-1")).rejects.toThrow(/at least 2 participants/);
  });
});
