import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
import { LLWSSimulator } from "../llws-simulator";

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

vi.mock("~/services/probability-engine", async (importOriginal) => {
  const actual = await importOriginal() as Record<string, unknown>;
  return { ...actual };
});

// ─── Fixtures ─────────────────────────────────────────────────────────────────

const US_IDS = Array.from({ length: 10 }, (_, i) => `us-${i + 1}`);
const INTL_IDS = Array.from({ length: 10 }, (_, i) => `intl-${i + 1}`);
const ALL_IDS = [...US_IDS, ...INTL_IDS];

/**
 * Build EV rows with descending odds favouring the first team per side.
 * ids[0] is the strongest (best odds → lowest American number for favorites).
 */
function makeEvRows(ids: string[], opts: { includeOdds?: boolean } = {}) {
  return ids.map((participantId, i) => ({
    participantId,
    sourceOdds: opts.includeOdds ? (i === 0 ? -300 : 200 + i * 100) : null,
  }));
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe("LLWSSimulator", () => {
  let mockDb: { select: MockInstance };
  let selectCallCount: number;

  beforeEach(async () => {
    selectCallCount = 0;
    const { database } = await import("~/database/context");
    mockDb = { select: vi.fn() };
    (database as unknown as MockInstance).mockReturnValue(mockDb);
  });

  function setupMockDb(
    participants: { id: string; name?: string; externalId: string | null }[],
    evRows: { participantId: string; sourceOdds: number | null }[]
  ) {
    mockDb.select.mockImplementation(() => {
      const callIndex = selectCallCount++;
      const data = callIndex === 0 ? participants : evRows;
      return { from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(data) }) };
    });
  }

  function defaultParticipants(mode: "randomized" | "fixed" = "randomized") {
    if (mode === "fixed") {
      const usA = US_IDS.slice(0, 5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:A" }));
      const usB = US_IDS.slice(5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:B" }));
      const intlA = INTL_IDS.slice(0, 5).map((id) => ({ id, name: `Team ${id}`, externalId: "Intl:A" }));
      const intlB = INTL_IDS.slice(5).map((id) => ({ id, name: `Team ${id}`, externalId: "Intl:B" }));
      return [...usA, ...usB, ...intlA, ...intlB];
    }
    return [
      ...US_IDS.map((id) => ({ id, name: `US Team ${id}`, externalId: "US" })),
      ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })),
    ];
  }

  // ── Core output structure ─────────────────────────────────────────────────

  describe("output structure", () => {
    it("returns one result per participant (20 total)", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      expect(results).toHaveLength(20);
    });

    it("every result has source 'llws_monte_carlo'", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      for (const r of results) {
        expect(r.source).toBe("llws_monte_carlo");
      }
    });

    it("all probability values are between 0 and 1", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      for (const r of results) {
        const p = r.probabilities;
        for (const v of Object.values(p)) {
          expect(v).toBeGreaterThanOrEqual(0);
          expect(v).toBeLessThanOrEqual(1);
        }
      }
    });
  });

  // ── Probability conservation ──────────────────────────────────────────────

  describe("probability conservation (one winner per sim)", () => {
    it("probFirst sums to ~1.0 across all participants", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("probSecond sums to ~1.0 across all participants", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      const total = results.reduce((s, r) => s + r.probabilities.probSecond, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("probThird sums to ~1.0 across all participants", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      const total = results.reduce((s, r) => s + r.probabilities.probThird, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("probFourth sums to ~1.0 across all participants", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      const total = results.reduce((s, r) => s + r.probabilities.probFourth, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("sum of probFifth across participants equals ~1.0 (4 bracket losers, split evenly)", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      // 4 bracket losers per sim, each assigned bracketLoser/(4*N) → sum = 1.0
      const total = results.reduce((s, r) => s + r.probabilities.probFifth, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("probFifth through probEighth are equal for every participant (even bracket-loser split)", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      for (const r of results) {
        const p = r.probabilities;
        expect(p.probFifth).toBeCloseTo(p.probSixth, 10);
        expect(p.probSixth).toBeCloseTo(p.probSeventh, 10);
        expect(p.probSeventh).toBeCloseTo(p.probEighth, 10);
      }
    });
  });

  // ── Odds-driven probability ───────────────────────────────────────────────

  describe("odds-driven win probability", () => {
    it("strong favourite (us-1) has higher probFirst than a weak team", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS, { includeOdds: true }));
      const results = await new LLWSSimulator().simulate("season-1");
      const byId = new Map(results.map((r) => [r.participantId, r]));
      const us1prob = byId.get("us-1")?.probabilities.probFirst ?? 0;
      const us10prob = byId.get("us-10")?.probabilities.probFirst ?? 0;
      expect(us1prob).toBeGreaterThan(us10prob);
    });

    it("works when no odds are entered (all 50/50 fallback)", async () => {
      setupMockDb(defaultParticipants(), makeEvRows(ALL_IDS)); // no odds
      const results = await new LLWSSimulator().simulate("season-1");
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("with equal odds, each team wins the championship roughly equally", async () => {
      // Equal positive odds (+5000 for every team) → vig-removed prob ≈ 1/20 each.
      const eqOddsRows = ALL_IDS.map((id) => ({ participantId: id, sourceOdds: 5000 }));
      setupMockDb(defaultParticipants(), eqOddsRows);
      const results = await new LLWSSimulator().simulate("season-1");
      for (const r of results) {
        // With equal odds and random pools, each team should win ~5% of the time.
        // Allow a generous band given Monte Carlo variance.
        expect(r.probabilities.probFirst).toBeGreaterThan(0.01);
        expect(r.probabilities.probFirst).toBeLessThan(0.15);
      }
    });
  });

  // ── Pool assignment modes ─────────────────────────────────────────────────

  describe("pool assignment modes", () => {
    it("fixed pools (US:A / US:B / Intl:A / Intl:B) produce valid results", async () => {
      setupMockDb(defaultParticipants("fixed"), makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      expect(results).toHaveLength(20);
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("mixed mode: US fixed pools, Intl randomized", async () => {
      const participants = [
        ...US_IDS.slice(0, 5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:A" })),
        ...US_IDS.slice(5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:B" })),
        ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })),
      ];
      setupMockDb(participants, makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      expect(results).toHaveLength(20);
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });
  });

  // ── Error cases ───────────────────────────────────────────────────────────

  describe("error cases", () => {
    it("throws when participant count is not 20", async () => {
      const nineteen = [...US_IDS, ...INTL_IDS.slice(0, 9)];
      const participants = nineteen.map((id, i) => ({
        id,
        name: i < 10 ? `US Team ${id}` : `Team ${id}`,
        externalId: i < 10 ? "US" : "Intl",
      }));
      setupMockDb(participants, makeEvRows(nineteen));
      await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/exactly 20/);
    });

    it("infers US side from name prefix when externalId is null", async () => {
      const participants = [
        ...US_IDS.map((id) => ({ id, name: `US Team ${id}`, externalId: null })),
        ...INTL_IDS.map((id) => ({ id, name: `Japan ${id}`, externalId: null })),
      ];
      setupMockDb(participants, makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      expect(results).toHaveLength(20);
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("infers US side from exact name 'US' when externalId is null", async () => {
      const participants = [
        ...US_IDS.map((id) => ({ id, name: "US", externalId: null })),
        ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: null })),
      ];
      setupMockDb(participants, makeEvRows(ALL_IDS));
      const results = await new LLWSSimulator().simulate("season-1");
      expect(results).toHaveLength(20);
      const total = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
      expect(total).toBeCloseTo(1.0, 1);
    });

    it("throws when a participant has an unrecognized non-null externalId", async () => {
      const participants = [
        ...US_IDS.map((id) => ({ id, name: `US Team ${id}`, externalId: "US" })),
        ...INTL_IDS.slice(0, 9).map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })),
        { id: "intl-10", name: "Team intl-10", externalId: "CANADA" }, // unrecognized
      ];
      setupMockDb(participants, makeEvRows(ALL_IDS));
      await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/invalid externalId/);
    });

    it("throws when US team count is not 10", async () => {
      // 11 US teams, 9 International
      const participants = [
        ...Array.from({ length: 11 }, (_, i) => ({ id: `us-${i + 1}`, name: `US Team ${i + 1}`, externalId: "US" })),
        ...Array.from({ length: 9 }, (_, i) => ({ id: `intl-${i + 1}`, name: `Team ${i + 1}`, externalId: "Intl" })),
      ];
      setupMockDb(participants, makeEvRows(ALL_IDS));
      await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/10 US teams/);
    });

    it("throws when fixed pools have unequal A/B split", async () => {
      const participants = [
        // 6 in Pool A, 4 in Pool B
        ...US_IDS.slice(0, 6).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:A" })),
        ...US_IDS.slice(6).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:B" })),
        ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })),
      ];
      setupMockDb(participants, makeEvRows(ALL_IDS));
      await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/exactly 5 teams each/);
    });

    it("throws when US externalIds mix pool suffixes and bare side", async () => {
      const participants = [
        // Some US:A, some "US" (no pool suffix) → mixed
        ...US_IDS.slice(0, 5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US:A" })),
        ...US_IDS.slice(5).map((id) => ({ id, name: `US Team ${id}`, externalId: "US" })), // no pool
        ...INTL_IDS.map((id) => ({ id, name: `Team ${id}`, externalId: "Intl" })),
      ];
      setupMockDb(participants, makeEvRows(ALL_IDS));
      await expect(new LLWSSimulator().simulate("season-1")).rejects.toThrow(/mixed externalId formats/);
    });
  });
});
