jest.mock('../../src/services/intelligence/gameLogService', () => ({
  getGameLogs: async () => null,
}));

const cs = require('../../src/services/intelligence/consistencyScore');

describe('consistencyScore.classify', () => {
  test('cv < 0.15 → elite', () => {
    expect(cs.classify(0.10)).toEqual({ consistency: 'elite', score: 1.0 });
  });
  test('cv 0.15-0.30 → reliable', () => {
    expect(cs.classify(0.20)).toEqual({ consistency: 'reliable', score: 0.7 });
  });
  test('cv 0.30-0.50 → volatile', () => {
    expect(cs.classify(0.40)).toEqual({ consistency: 'volatile', score: 0.4 });
  });
  test('cv >= 0.50 → boom_bust', () => {
    expect(cs.classify(0.80)).toEqual({ consistency: 'boom_bust', score: 0.1 });
  });
});

describe('consistencyScore.statsFor', () => {
  test('null for fewer than 2 samples', () => {
    expect(cs.statsFor([])).toBeNull();
    expect(cs.statsFor([25])).toBeNull();
  });

  test('null when mean is zero (can\'t divide)', () => {
    expect(cs.statsFor([0, 0, 0])).toBeNull();
  });

  test('computes mean / stddev / cv for tight values', () => {
    const s = cs.statsFor([25, 24, 26, 25, 24]);
    expect(s.mean).toBeCloseTo(24.8, 1);
    expect(s.cv).toBeLessThan(0.05);
  });

  test('computes wide cv for volatile sample', () => {
    const s = cs.statsFor([5, 30, 35, 8, 28, 12]);
    expect(s.cv).toBeGreaterThan(0.4);
  });
});

describe('consistencyScore.getConsistency', () => {
  test('classifies an elite scorer', async () => {
    const logs = [
      { points: 25 }, { points: 24 }, { points: 26 }, { points: 25 }, { points: 24 },
      { points: 25 }, { points: 26 }, { points: 24 }, { points: 25 }, { points: 25 },
    ];
    const out = await cs.getConsistency({ playerName: 'Elite', sport: 'nba', statType: 'points', gameLogs: logs });
    expect(out.consistency).toBe('elite');
    expect(out.games).toBe(10);
  });

  test('classifies boom/bust', async () => {
    const logs = [
      { points: 5 }, { points: 32 }, { points: 8 }, { points: 35 }, { points: 6 },
      { points: 28 }, { points: 4 }, { points: 30 }, { points: 9 }, { points: 26 },
    ];
    const out = await cs.getConsistency({ playerName: 'Wild', sport: 'nba', statType: 'points', gameLogs: logs });
    expect(out.consistency).toBe('boom_bust');
  });

  test('returns unknown when game logs empty', async () => {
    const out = await cs.getConsistency({ playerName: 'NoData', sport: 'nba', statType: 'points', gameLogs: [] });
    expect(out.consistency).toBe('unknown');
    expect(out.score).toBeNull();
  });

  test('combo stat (pts_reb_ast) summed before measuring', async () => {
    const logs = [
      { points: 20, rebounds: 5, assists: 5 },
      { points: 22, rebounds: 5, assists: 4 },
      { points: 18, rebounds: 6, assists: 5 },
      { points: 21, rebounds: 5, assists: 5 },
    ];
    const out = await cs.getConsistency({ playerName: 'Combo', sport: 'nba', statType: 'pts_reb_ast', gameLogs: logs });
    expect(out.consistency).toBe('elite');
  });
});

// ── Index-of-dispersion classifier (low-mean count stats) ───────────────────
// STANDING two-sided pin: the CV→IoD fix must recover a real +1.0 for steady
// low-mean hitters WITHOUT firing on genuine boom-bust, and must abstain on a
// thin sample. If any of these flip, the consistency signal has regressed.
describe('consistencyScore.classifyIoD (Poisson-anchored boundaries)', () => {
  test('iod < 0.60 → elite (clearly under-dispersed)', () => {
    expect(cs.classifyIoD(0.40)).toEqual({ consistency: 'elite', score: 1.0 });
  });
  test('0.60 ≤ iod < 0.85 → reliable (steadier than random)', () => {
    expect(cs.classifyIoD(0.78)).toEqual({ consistency: 'reliable', score: 0.7 });
  });
  test('0.85 ≤ iod ≤ 1.30 → volatile / neutral (Poisson band, no factor)', () => {
    expect(cs.classifyIoD(1.00)).toEqual({ consistency: 'volatile', score: 0.4 });
    expect(cs.classifyIoD(1.25)).toEqual({ consistency: 'volatile', score: 0.4 });
  });
  test('iod > 1.30 → boom_bust (clear spike)', () => {
    expect(cs.classifyIoD(2.17)).toEqual({ consistency: 'boom_bust', score: 0.1 });
  });
});

describe('consistencyScore.getConsistency — low-mean IoD path', () => {
  // A steady low-mean contact hitter (mean 0.8, under-dispersed) — CV would
  // have blanket-classified this 'unknown'; IoD recovers the +1.0 signal.
  test('steady low-mean hitter → consistent (recovers the suppressed +1.0)', async () => {
    const logs = [2, 1, 0, 1, 0, 1, 1, 1, 0, 1].map((hits) => ({ hits }));
    const out = await cs.getConsistency({ playerName: 'Steady', sport: 'mlb', statType: 'hits', gameLogs: logs });
    expect(out.method).toBe('iod');
    expect(['elite', 'reliable']).toContain(out.consistency); // engine1 → +1.0
    expect(out.score).toBeGreaterThan(0);
  });

  // A genuine boom-bust low-mean bat (mostly 0, occasional 3) must NOT be
  // mislabeled consistent — the fix stays two-sided.
  test('boom-bust low-mean hitter → stays boom_bust (no false consistency)', async () => {
    const logs = [0, 0, 3, 0, 0, 2, 0, 0, 3, 0].map((hits) => ({ hits }));
    const out = await cs.getConsistency({ playerName: 'Spiky', sport: 'mlb', statType: 'hits', gameLogs: logs });
    expect(out.method).toBe('iod');
    expect(out.consistency).toBe('boom_bust'); // engine1 → −1.0
  });

  // A near-Poisson low-mean bat sits in the neutral band → NO factor either way.
  test('Poisson-ish low-mean hitter → volatile / neutral (no factor)', async () => {
    const logs = [1, 0, 1, 2, 0, 1, 1, 0, 2, 1].map((hits) => ({ hits })); // mean 0.9, iod ≈ 1
    const out = await cs.getConsistency({ playerName: 'Random', sport: 'mlb', statType: 'hits', gameLogs: logs });
    expect(out.method).toBe('iod');
    expect(['volatile', 'reliable', 'boom_bust']).toContain(out.consistency);
  });

  // Sample floor: too few games for a trustworthy IoD → honest 'unknown'.
  test('thin sample (< floor games) → unknown (no small-sample guess)', async () => {
    const logs = [1, 1, 0, 1, 1].map((hits) => ({ hits })); // 5 games < MIN_GAMES_FOR_IOD (8)
    const out = await cs.getConsistency({ playerName: 'Thin', sport: 'mlb', statType: 'hits', gameLogs: logs });
    expect(out.consistency).toBe('unknown');
    expect(out.score).toBeNull();
    expect(out.reason).toBe('low_mean_thin_sample');
  });

  // The high-mean CV path is untouched by the swap (guards against blast radius).
  test('high-mean stat still uses the CV path (unchanged)', async () => {
    const logs = [
      { points: 25 }, { points: 24 }, { points: 26 }, { points: 25 }, { points: 24 },
      { points: 25 }, { points: 26 }, { points: 24 }, { points: 25 }, { points: 25 },
    ];
    const out = await cs.getConsistency({ playerName: 'Elite', sport: 'nba', statType: 'points', gameLogs: logs });
    expect(out.method).toBe('cv');
    expect(out.consistency).toBe('elite');
  });
});
