const mockSnaps = { current: [] }; jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => ({ from() { const proxy = { select() { return proxy; }, eq() { return proxy; }, order() { return Promise.resolve({ data: mockSnaps.current, error: null }); }, }; return proxy; }, }), })); const lm = require('../../src/services/intelligence/lineMovement'); beforeEach(() => { mockSnaps.current = []; }); describe('getLineMovement', () => { test('returns null when fewer than two snapshots', async () => { mockSnaps.current = [{ line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }]; expect(await lm.getLineMovement('g', 'P', 'points')).toBeNull(); }); test('reports opening and current line correctly', async () => { mockSnaps.current = [ { line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }, { line: 25.5, over_odds: -120, under_odds: +100, snapshot_at: 't2' }, { line: 26.0, over_odds: -130, under_odds: +110, snapshot_at: 't3' }, ]; const result = await lm.getLineMovement('g', 'P', 'points'); expect(result).toMatchObject({ opening_line: 25.5, current_line: 26.0, movement: 0.5, direction: 'up', snapshots_count: 3, }); }); }); describe('reverseLineMovement', () => { test('detects RLM when public is on over but line moves to under', async () => { mockSnaps.current = [ { line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }, { line: 24.5, over_odds: -130, under_odds: +110, snapshot_at: 't2' }, ]; const r = await lm.reverseLineMovement('g', 'P', 'points'); expect(r.isReverse).toBe(true); expect(r.score).toBeGreaterThan(0); expect(r.publicSide).toBe('over'); expect(r.lineDirection).toBe('under'); }); test('returns isReverse=false when line moved with public', async () => { mockSnaps.current = [ { line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }, { line: 26.5, over_odds: -130, under_odds: +110, snapshot_at: 't2' }, ]; const r = await lm.reverseLineMovement('g', 'P', 'points'); expect(r.isReverse).toBe(false); expect(r.score).toBe(0); }); test('returns null on flat movement', async () => { mockSnaps.current = [ { line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }, { line: 25.5, over_odds: -115, under_odds: -105, snapshot_at: 't2' }, ]; expect(await lm.reverseLineMovement('g', 'P', 'points')).toBeNull(); }); test('uses provided publicBetPct over odds-direction heuristic', async () => { mockSnaps.current = [ { line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }, { line: 26.5, over_odds: -110, under_odds: -110, snapshot_at: 't2' }, ]; const r = await lm.reverseLineMovement('g', 'P', 'points', 30); expect(r.isReverse).toBe(true); expect(r.publicSide).toBe('under'); expect(r.lineDirection).toBe('over'); }); }); describe('juiceDegradation', () => { test('flags juice change when the line itself stayed put', async () => { mockSnaps.current = [ { line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }, { line: 25.5, over_odds: -130, under_odds: +110, snapshot_at: 't2' }, ]; const r = await lm.juiceDegradation('g', 'P', 'points'); expect(r.applicable).toBe(true); expect(r.score).toBeGreaterThan(0); }); test('not applicable when line moved significantly', async () => { mockSnaps.current = [ { line: 25.5, over_odds: -110, under_odds: -110, snapshot_at: 't1' }, { line: 28.5, over_odds: -120, under_odds: +100, snapshot_at: 't2' }, ]; const r = await lm.juiceDegradation('g', 'P', 'points'); expect(r.applicable).toBe(false); }); });