'use strict';

// MLB grade-degradation regression locks (2026-07-17). Three bugs the phone
// audit surfaced, fixed in engine1 + analyzeViaEngine1. See
// specs/audit-data/mlb-grade-degradation.md.

const engine1 = require('../../src/services/intelligence/engine1');
const { __internals } = require('../../src/services/intelligence/analyzeViaEngine1');
const { projectionFor, edgePctFor, insufficientDataResult } = __internals;
const gradeAdapter = require('../../src/utils/gradeAdapter');
const { fourLetterGrade, legacyConfidence } = gradeAdapter.__internals;
const BANDS = require('../../src/services/python/data/grade_thresholds.json').grade_scale;

// The canonical threshold table: displayed 0-100 confidence -> 11-step letter.
function thresholdTable(conf0to100) {
  const p = conf0to100 / 100;
  for (const [letter, band] of Object.entries(BANDS)) {
    if (p >= band.low && p <= band.high) return letter;
  }
  return null;
}

describe('FIX #1 — projection <= 0 REFUSES (a grade is never emitted with no projection)', () => {
  test('a zero l5_avg is not a projection -> null (falls through to nothing)', () => {
    expect(projectionFor({ l5_avg: 0 }, { stat_type: 'home_runs' })).toBeNull();
  });
  test('a negative reference is not a projection -> null', () => {
    expect(projectionFor({ l5_avg: -1.2 }, { stat_type: 'hits' })).toBeNull();
  });
  test('a zero PRIMARY reference falls through to the next POSITIVE one', () => {
    // l5 degenerate 0, but l20 is real -> use l20 (player still has a projection)
    expect(projectionFor({ l5_avg: 0, l20_avg: 1.2 }, { stat_type: 'total_bases' })).toBe(1.2);
  });
  test('all references zero/absent -> null -> the read refuses', () => {
    expect(projectionFor({ l5_avg: 0, l20_avg: 0 }, { stat_type: 'hits' })).toBeNull();
    expect(projectionFor({}, { stat_type: 'hits' })).toBeNull();
  });
  test('a real positive projection is preserved', () => {
    expect(projectionFor({ l5_avg: 1.4 }, { stat_type: 'total_bases' })).toBe(1.4);
  });
  test('the refusal shape carries grade=null, projection=null, insufficient_data', () => {
    const r = insufficientDataResult({ player: 'X', stat_type: 'home_runs', line: 0.5 }, []);
    expect(r.grade).toBeNull();
    expect(r.projection).toBeNull();
    expect(r.insufficient_data).toBe(true);
  });
});

describe('FIX #2 — edge_pct is (model - line) / line, never the projection=0 degeneracy', () => {
  test('a proj=0 under no longer yields a fabricated +100% (ref nulled upstream)', () => {
    // projectionFor now nulls a 0 reference, so edgePctFor sees ref=null -> 0
    expect(edgePctFor({ l5_avg: 0 }, { line: 0.5, direction: 'under', stat_type: 'home_runs' })).toBe(0);
  });
  test('a real over edge = (proj - line) / line * 100', () => {
    // proj 0.6 vs line 0.5 over -> (0.6-0.5)/0.5 = 20%
    expect(edgePctFor({ l5_avg: 0.6 }, { line: 0.5, direction: 'over', stat_type: 'home_runs' })).toBe(20);
  });
  test('a real under edge flips sign', () => {
    // proj 6 vs line 6.5 under -> (6.5-6)/6.5 ~ 7.7%
    expect(edgePctFor({ l5_avg: 6 }, { line: 6.5, direction: 'under', stat_type: 'strikeouts' })).toBeCloseTo(7.7, 1);
  });
  test('a passed ref is used verbatim (edge and persisted projection cannot diverge)', () => {
    expect(edgePctFor({ l5_avg: 999 }, { line: 0.5, direction: 'over' }, 0.6)).toBe(20);
  });
  test('edges over a plausible slate are NOT quantized to a degenerate cluster', () => {
    // varied real projections + lines -> a spread of values, none the fake 100
    const slate = [
      { f: { l5_avg: 1.4 }, line: 1.5, dir: 'under' },
      { f: { l5_avg: 6.9 }, line: 6.5, dir: 'over' },
      { f: { l5_avg: 2.1 }, line: 2.5, dir: 'under' },
      { f: { l5_avg: 0.7 }, line: 0.5, dir: 'over' },
    ];
    const edges = slate.map((s) => edgePctFor(s.f, { line: s.line, direction: s.dir }));
    expect(edges).not.toContain(100);
    expect(new Set(edges).size).toBeGreaterThan(1); // not one repeated value
  });
});

describe('FIX #3 — letter == threshold_table(displayed_confidence) for EVERY grade', () => {
  test('each 11-step grade’s confidence resolves back to the same letter', () => {
    for (const g of engine1.GRADE_SCALE) {
      const displayedConf = legacyConfidence(engine1.GRADE_TO_CONFIDENCE[g]); // 0-100
      const resolved = thresholdTable(displayedConf);                        // 11-step
      expect(resolved).toBe(g); // the midpoint lands squarely back in the grade's own band
    }
  });
  test('the 4-letter collapse also agrees (what the leaderboard shows)', () => {
    for (const g of engine1.GRADE_SCALE) {
      const displayedConf = legacyConfidence(engine1.GRADE_TO_CONFIDENCE[g]);
      const resolvedLetter = fourLetterGrade(thresholdTable(displayedConf));
      expect(resolvedLetter).toBe(fourLetterGrade(g));
    }
  });
  test('confidence values stay strictly ascending', () => {
    const conf = engine1.GRADE_SCALE.map((g) => engine1.GRADE_TO_CONFIDENCE[g]);
    for (let i = 1; i < conf.length; i += 1) expect(conf[i]).toBeGreaterThan(conf[i - 1]);
  });
});
