import { describe, it, expect } from 'vitest';
import { expectedHome, eloDelta, goalDiffMultiplier } from './elo';
import { poissonPmf, scoreMatrix, outcomeProbs, lambdasFromElo, type ModelParams } from './poisson';
import { predictMatch, knockoutAdvanceProb, type RatingsModel } from './predict';

const PARAMS: ModelParams = { goalsPerElo: 0.0057, avgGoals: 2.73, rho: -0.05, homeAdvElo: 70 };

describe('elo', () => {
  it('gives the stronger team a higher expected score', () => {
    expect(expectedHome(1900, 1700, 0)).toBeGreaterThan(0.5);
    expect(expectedHome(1700, 1900, 0)).toBeLessThan(0.5);
  });

  it('is symmetric around 0.5 at equal ratings with no home edge', () => {
    expect(expectedHome(1800, 1800, 0)).toBeCloseTo(0.5, 6);
  });

  it('moves the winner up and the loser down by equal amounts', () => {
    const d = eloDelta(2, 0, 1800, 1800, 40, 0);
    expect(d).toBeGreaterThan(0); // home won → positive delta
    // a bigger win moves more
    expect(Math.abs(eloDelta(5, 0, 1800, 1800, 40, 0))).toBeGreaterThan(Math.abs(d));
  });

  it('weights blowouts more', () => {
    expect(goalDiffMultiplier(1)).toBe(1);
    expect(goalDiffMultiplier(3)).toBeGreaterThan(goalDiffMultiplier(2));
  });
});

describe('poisson / dixon-coles', () => {
  it('pmf over a team sums to ~1', () => {
    let s = 0;
    for (let k = 0; k <= 15; k++) s += poissonPmf(1.4, k);
    expect(s).toBeCloseTo(1, 4);
  });

  it('score matrix is a normalized distribution', () => {
    const m = scoreMatrix(1.6, 1.1, -0.05);
    let s = 0;
    for (const row of m) for (const p of row) s += p;
    expect(s).toBeCloseTo(1, 6);
  });

  it('outcome probabilities sum to 1', () => {
    const p = outcomeProbs(scoreMatrix(1.6, 1.1, -0.05));
    expect(p.home + p.draw + p.away).toBeCloseTo(1, 6);
    expect(p.home).toBeGreaterThan(p.away); // higher lambda favored
  });

  it('maps a bigger Elo edge to a bigger goal expectation', () => {
    const a = lambdasFromElo(1900, 1700, PARAMS, 0);
    const b = lambdasFromElo(1800, 1800, PARAMS, 0);
    expect(a.lambdaHome).toBeGreaterThan(b.lambdaHome);
    expect(a.lambdaAway).toBeLessThan(b.lambdaAway);
  });
});

describe('predictMatch', () => {
  const model: RatingsModel = {
    asOf: '2026-06-10',
    params: PARAMS,
    ratings: { Strong: 2050, Weak: 1650, Even: 1850 },
  };

  it('favours the stronger team and sums to 1', () => {
    const p = predictMatch('Strong', 'Weak', model).probs;
    expect(p.home + p.draw + p.away).toBeCloseTo(1, 6);
    expect(p.home).toBeGreaterThan(0.6);
  });

  it('applies host home advantage', () => {
    const neutral = predictMatch('Even', 'Even', model, 0).probs.home;
    const atHome = predictMatch('Even', 'Even', model, 70).probs.home;
    expect(atHome).toBeGreaterThan(neutral);
  });

  it('knockout advance prob is in (0,1) and favours the stronger team', () => {
    const p = predictMatch('Strong', 'Weak', model);
    const adv = knockoutAdvanceProb(p.probs, p.eloHome, p.eloAway);
    expect(adv).toBeGreaterThan(0.5);
    expect(adv).toBeLessThan(1);
  });
});
