import { describe, it, expect } from "vitest";
import {
  calculateEV,
  validateProbabilities,
  normalizeProbabilities,
  calculateProjectedTotal,
  type ScoringRules,
  type ProbabilityDistribution,
} from "../ev-calculator";

describe("calculateEV", () => {
  const defaultScoring: ScoringRules = {
    pointsFor1st: 100,
    pointsFor2nd: 70,
    pointsFor3rd: 50,
    pointsFor4th: 40,
    pointsFor5th: 25,
    pointsFor6th: 25,
    pointsFor7th: 15,
    pointsFor8th: 15,
  };

  it("should calculate EV for equal probabilities", () => {
    const probabilities: ProbabilityDistribution = {
      probFirst: 0.125,
      probSecond: 0.125,
      probThird: 0.125,
      probFourth: 0.125,
      probFifth: 0.125,
      probSixth: 0.125,
      probSeventh: 0.125,
      probEighth: 0.125,
    };

    const ev = calculateEV(probabilities, defaultScoring);

    // EV = 0.125 × (100+70+50+40+25+25+15+15) = 0.125 × 340 = 42.5
    expect(ev).toBe(42.5);
  });

  it("should calculate EV for favorite (high probability of 1st)", () => {
    const probabilities: ProbabilityDistribution = {
      probFirst: 0.50,
      probSecond: 0.30,
      probThird: 0.10,
      probFourth: 0.05,
      probFifth: 0.03,
      probSixth: 0.01,
      probSeventh: 0.005,
      probEighth: 0.005,
    };

    const ev = calculateEV(probabilities, defaultScoring);

    // EV = 0.50×100 + 0.30×70 + 0.10×50 + 0.05×40 + 0.03×25 + 0.01×25 + 0.005×15 + 0.005×15
    // = 50 + 21 + 5 + 2 + 0.75 + 0.25 + 0.075 + 0.075 = 79.15
    expect(ev).toBe(79.15);
  });

  it("should calculate EV for underdog (low probability of 1st)", () => {
    const probabilities: ProbabilityDistribution = {
      probFirst: 0.02,
      probSecond: 0.05,
      probThird: 0.08,
      probFourth: 0.10,
      probFifth: 0.15,
      probSixth: 0.20,
      probSeventh: 0.20,
      probEighth: 0.20,
    };

    const ev = calculateEV(probabilities, defaultScoring);

    // EV = 0.02×100 + 0.05×70 + 0.08×50 + 0.10×40 + 0.15×25 + 0.20×25 + 0.20×15 + 0.20×15
    // = 2 + 3.5 + 4 + 4 + 3.75 + 5 + 3 + 3 = 28.25
    expect(ev).toBe(28.25);
  });

  it("should calculate EV with custom scoring rules", () => {
    const customScoring: ScoringRules = {
      pointsFor1st: 200,
      pointsFor2nd: 150,
      pointsFor3rd: 100,
      pointsFor4th: 80,
      pointsFor5th: 50,
      pointsFor6th: 50,
      pointsFor7th: 30,
      pointsFor8th: 30,
    };

    const probabilities: ProbabilityDistribution = {
      probFirst: 0.20,
      probSecond: 0.20,
      probThird: 0.15,
      probFourth: 0.15,
      probFifth: 0.10,
      probSixth: 0.10,
      probSeventh: 0.05,
      probEighth: 0.05,
    };

    const ev = calculateEV(probabilities, customScoring);

    // EV = 0.20×200 + 0.20×150 + 0.15×100 + 0.15×80 + 0.10×50 + 0.10×50 + 0.05×30 + 0.05×30
    // = 40 + 30 + 15 + 12 + 5 + 5 + 1.5 + 1.5 = 110
    expect(ev).toBe(110);
  });

  it("should handle 100% probability of one placement (finished participant)", () => {
    const probabilities: ProbabilityDistribution = {
      probFirst: 1.0,
      probSecond: 0,
      probThird: 0,
      probFourth: 0,
      probFifth: 0,
      probSixth: 0,
      probSeventh: 0,
      probEighth: 0,
    };

    const ev = calculateEV(probabilities, defaultScoring);
    expect(ev).toBe(100); // 1.0 × 100 = 100
  });

  it("should handle probabilities with decimal places", () => {
    const probabilities: ProbabilityDistribution = {
      probFirst: 0.1575,
      probSecond: 0.1425,
      probThird: 0.1350,
      probFourth: 0.1275,
      probFifth: 0.1100,
      probSixth: 0.1050,
      probSeventh: 0.1125,
      probEighth: 0.1100,
    };

    const ev = calculateEV(probabilities, defaultScoring);

    // EV = 0.1575×100 + 0.1425×70 + ... = 46.29
    expect(ev).toBeCloseTo(46.29, 2);
  });

  it("should return 0 when all probabilities are 0", () => {
    const probabilities: ProbabilityDistribution = {
      probFirst: 0,
      probSecond: 0,
      probThird: 0,
      probFourth: 0,
      probFifth: 0,
      probSixth: 0,
      probSeventh: 0,
      probEighth: 0,
    };

    const ev = calculateEV(probabilities, defaultScoring);
    expect(ev).toBe(0);
  });
});

describe("validateProbabilities", () => {
  it("should validate probabilities that sum to 1.0", () => {
    const valid: ProbabilityDistribution = {
      probFirst: 0.20,
      probSecond: 0.20,
      probThird: 0.15,
      probFourth: 0.15,
      probFifth: 0.10,
      probSixth: 0.10,
      probSeventh: 0.05,
      probEighth: 0.05,
    };

    expect(validateProbabilities(valid)).toBe(true);
  });

  it("should accept probabilities within tolerance (default 0.01)", () => {
    const nearlyValid: ProbabilityDistribution = {
      probFirst: 0.2005,
      probSecond: 0.20,
      probThird: 0.15,
      probFourth: 0.15,
      probFifth: 0.10,
      probSixth: 0.10,
      probSeventh: 0.05,
      probEighth: 0.0495,
    };

    expect(validateProbabilities(nearlyValid)).toBe(true);
  });

  it("should reject probabilities that sum too high", () => {
    const tooHigh: ProbabilityDistribution = {
      probFirst: 0.20,
      probSecond: 0.20,
      probThird: 0.20,
      probFourth: 0.20,
      probFifth: 0.10,
      probSixth: 0.10,
      probSeventh: 0.05,
      probEighth: 0.05,
    };

    expect(validateProbabilities(tooHigh)).toBe(false);
  });

  it("should reject probabilities that sum too low", () => {
    const tooLow: ProbabilityDistribution = {
      probFirst: 0.10,
      probSecond: 0.10,
      probThird: 0.10,
      probFourth: 0.10,
      probFifth: 0.10,
      probSixth: 0.10,
      probSeventh: 0.05,
      probEighth: 0.05,
    };

    expect(validateProbabilities(tooLow)).toBe(false);
  });

  it("should allow custom tolerance", () => {
    const probabilities: ProbabilityDistribution = {
      probFirst: 0.21,
      probSecond: 0.20,
      probThird: 0.15,
      probFourth: 0.15,
      probFifth: 0.10,
      probSixth: 0.10,
      probSeventh: 0.05,
      probEighth: 0.04,
    };

    // Sum is 1.0, within any tolerance
    expect(validateProbabilities(probabilities, 0.01)).toBe(true);
    expect(validateProbabilities(probabilities, 0.001)).toBe(true);
  });
});

describe("normalizeProbabilities", () => {
  it("should normalize probabilities that sum to more than 1.0", () => {
    const input: ProbabilityDistribution = {
      probFirst: 0.22,
      probSecond: 0.22,
      probThird: 0.17,
      probFourth: 0.17,
      probFifth: 0.11,
      probSixth: 0.11,
      probSeventh: 0.055,
      probEighth: 0.055,
    };
    // Sum = 1.11

    const normalized = normalizeProbabilities(input);

    // Each should be scaled down by 1/1.11
    expect(normalized.probFirst).toBeCloseTo(0.1982, 3);
    expect(normalized.probSecond).toBeCloseTo(0.1982, 3);

    // Sum should be exactly 1.0 (within rounding)
    const sum = Object.values(normalized).reduce((a, b) => a + b, 0);
    expect(sum).toBeCloseTo(1.0, 3);
  });

  it("should normalize probabilities that sum to less than 1.0", () => {
    const input: ProbabilityDistribution = {
      probFirst: 0.18,
      probSecond: 0.18,
      probThird: 0.135,
      probFourth: 0.135,
      probFifth: 0.09,
      probSixth: 0.09,
      probSeventh: 0.045,
      probEighth: 0.045,
    };
    // Sum = 0.90

    const normalized = normalizeProbabilities(input);

    // Each should be scaled up by 1/0.90
    expect(normalized.probFirst).toBeCloseTo(0.20, 2);
    expect(normalized.probSecond).toBeCloseTo(0.20, 2);

    const sum = Object.values(normalized).reduce((a, b) => a + b, 0);
    expect(sum).toBeCloseTo(1.0, 3);
  });

  it("should handle probabilities that already sum to 1.0", () => {
    const input: ProbabilityDistribution = {
      probFirst: 0.20,
      probSecond: 0.20,
      probThird: 0.15,
      probFourth: 0.15,
      probFifth: 0.10,
      probSixth: 0.10,
      probSeventh: 0.05,
      probEighth: 0.05,
    };

    const normalized = normalizeProbabilities(input);

    // Should remain essentially unchanged
    expect(normalized.probFirst).toBeCloseTo(0.20, 3);
    expect(normalized.probSecond).toBeCloseTo(0.20, 3);
  });

  it("should handle all zeros by returning equal probabilities", () => {
    const input: ProbabilityDistribution = {
      probFirst: 0,
      probSecond: 0,
      probThird: 0,
      probFourth: 0,
      probFifth: 0,
      probSixth: 0,
      probSeventh: 0,
      probEighth: 0,
    };

    const normalized = normalizeProbabilities(input);

    // Should return 0.125 for each (equal distribution)
    expect(normalized.probFirst).toBe(0.125);
    expect(normalized.probSecond).toBe(0.125);
    expect(normalized.probEighth).toBe(0.125);
  });
});

describe("calculateProjectedTotal", () => {
  it("should calculate projected total with multiple unfinished participants", () => {
    const result = calculateProjectedTotal(
      150, // actual points from finished
      [45.5, 30.2, 25.8, 20.1] // EVs of unfinished participants
    );

    expect(result.actualPoints).toBe(150);
    expect(result.projectedPoints).toBe(271.6); // 150 + 121.6
    expect(result.participantsRemaining).toBe(4);
  });

  it("should handle no remaining participants", () => {
    const result = calculateProjectedTotal(250, []);

    expect(result.actualPoints).toBe(250);
    expect(result.projectedPoints).toBe(250); // Same as actual
    expect(result.participantsRemaining).toBe(0);
  });

  it("should handle zero actual points", () => {
    const result = calculateProjectedTotal(0, [50, 40, 30]);

    expect(result.actualPoints).toBe(0);
    expect(result.projectedPoints).toBe(120);
    expect(result.participantsRemaining).toBe(3);
  });

  it("should round to 2 decimal places", () => {
    const result = calculateProjectedTotal(100.123, [25.456, 30.789]);

    expect(result.actualPoints).toBe(100.12);
    expect(result.projectedPoints).toBe(156.37); // Rounded
  });
});
