/**
 * Session 64 — DIRECTIONAL CLV (per-read signal only).
 *
 * User-facing: a wrong badge is a credibility kill, so the polarity trap is
 * proven BEFORE the logic exists. The same line move must produce OPPOSITE
 * signs for an over-grade and an under-grade — a move that helps an over hurts
 * an under on the very same prop.
 *
 * NOT an aggregate CLV stat. Aggregate stays held until backtest-proven.
 */
const dclv = require('../../src/services/directionalClv');

// Lock: over -110 / under -110  → fair over ≈ 0.500
// Close: over -150 / under +130 → fair over ≈ 0.598  (market moved toward OVER)
const LOCK = { over: -110, under: -110 };
const CLOSE_TOWARD_OVER = { over: -150, under: 130 };

describe('POLARITY — the badge-inverting trap', () => {
  test('a move toward OVER is POSITIVE for an over-graded read', () => {
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: LOCK.over, lockUnderOdds: LOCK.under,
      closeOverOdds: CLOSE_TOWARD_OVER.over, closeUnderOdds: CLOSE_TOWARD_OVER.under,
    });
    expect(r.state).toBe('positive');
    expect(r.clv).toBeGreaterThan(0);
  });

  test('the SAME move is NEGATIVE for an under-graded read', () => {
    const r = dclv.computeDirectionalClv({
      side: 'under', lockOverOdds: LOCK.over, lockUnderOdds: LOCK.under,
      closeOverOdds: CLOSE_TOWARD_OVER.over, closeUnderOdds: CLOSE_TOWARD_OVER.under,
    });
    expect(r.state).toBe('negative');
    expect(r.clv).toBeLessThan(0);
  });

  test('the two sides are exact mirrors — sign is SIDE-BOUND, not line-direction', () => {
    const args = {
      lockOverOdds: LOCK.over, lockUnderOdds: LOCK.under,
      closeOverOdds: CLOSE_TOWARD_OVER.over, closeUnderOdds: CLOSE_TOWARD_OVER.under,
    };
    const o = dclv.computeDirectionalClv({ ...args, side: 'over' });
    const u = dclv.computeDirectionalClv({ ...args, side: 'under' });
    expect(o.clv).toBeCloseTo(-u.clv, 6);
  });
});

describe('FLAT — threshold is PROBABILITY space, never price space', () => {
  test('a sub-threshold fair-prob move is FLAT, not a badge', () => {
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -110, lockUnderOdds: -110,
      closeOverOdds: -112, closeUnderOdds: -108,   // tiny move
      flatThreshold: 0.02,
    });
    expect(r.state).toBe('flat');
  });

  test('a big PRICE move that is a small PROBABILITY move still respects the prob threshold', () => {
    // Deep favourite: a 40-cent price move is a small probability move.
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -1000, lockUnderOdds: 700,
      closeOverOdds: -1040, closeUnderOdds: 740,
      flatThreshold: 0.02,
    });
    expect(r.state).toBe('flat');
    expect(Math.abs(r.clv)).toBeLessThan(0.02);
  });
});

describe('UNKNOWN — never 0, never a fabricated badge', () => {
  test('a missed close is UNKNOWN', () => {
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -110, lockUnderOdds: -110, missedReason: 'missed_window',
    });
    expect(r.state).toBe('unknown');
    expect(r.clv).toBeNull();
  });

  test('no close at all is UNKNOWN, not flat', () => {
    const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110 });
    expect(r.state).toBe('unknown');
    expect(r.clv).toBeNull();
  });

  test('a doubleheader-ambiguous close is UNKNOWN', () => {
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -110, lockUnderOdds: -110,
      closeOverOdds: -150, closeUnderOdds: 130, missedReason: 'doubleheader_ambiguous',
    });
    expect(r.state).toBe('unknown');
  });

  test('a ONE-SIDED close cannot be de-vigged → UNKNOWN, never half-computed', () => {
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -110, lockUnderOdds: -110,
      closeOverOdds: -150, closeUnderOdds: null,
    });
    expect(r.state).toBe('unknown');
    expect(r.clv).toBeNull();
  });

  test('a one-sided LOCK is UNKNOWN too — both ends must be de-viggable', () => {
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -110, lockUnderOdds: null,
      closeOverOdds: -150, closeUnderOdds: 130,
    });
    expect(r.state).toBe('unknown');
  });
});

describe('SAME de-vig method at both ends', () => {
  test('an unchanged market yields exactly 0 and reads FLAT', () => {
    const r = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -110, lockUnderOdds: -110,
      closeOverOdds: -110, closeUnderOdds: -110,
    });
    expect(r.clv).toBeCloseTo(0, 9);
    expect(r.state).toBe('flat');
  });

  test('a pre-de-vigged lock fair prob may be supplied and must agree', () => {
    const a = dclv.computeDirectionalClv({
      side: 'over', lockOverOdds: -110, lockUnderOdds: -110,
      closeOverOdds: -150, closeUnderOdds: 130,
    });
    const b = dclv.computeDirectionalClv({
      side: 'over', lockFairProb: a.fair_lock,
      closeOverOdds: -150, closeUnderOdds: 130,
    });
    expect(b.clv).toBeCloseTo(a.clv, 9);
  });
});
