// Unit: line-snapshot service (Session 28). Redis-only; mocked.

const mockStore = {}; // key -> array of JSON strings (list)
const mockScan = jest.fn();
const mockRedis = {
  rpush: jest.fn(async (k, v) => { (mockStore[k] = mockStore[k] || []).push(v); return mockStore[k].length; }),
  ltrim: jest.fn(async (k, start, end) => {
    if (mockStore[k]) mockStore[k] = mockStore[k].slice(start, end === -1 ? undefined : end + 1);
    return 'OK';
  }),
  expire: jest.fn(async () => 1),
  lrange: jest.fn(async (k) => mockStore[k] || []),
  scan: mockScan,
};

jest.mock('../../src/utils/redis', () => ({
  getRedisClient: () => mockRedis,
  isDegraded: () => false,
}));

const svc = require('../../src/services/lineSnapshotService');

beforeEach(() => {
  for (const k of Object.keys(mockStore)) delete mockStore[k];
  jest.clearAllMocks();
});

describe('lineSnapshotService — recording', () => {
  test('records a snapshot per prop into the right key', async () => {
    const n = await svc.recordSnapshots('nba', [
      { gameId: 'g1', player: 'Wemby', stat: 'points', line: 28.5, book: 'dk' },
    ], 1000);
    expect(n).toBe(1);
    const hist = await svc.getLineHistory('nba', 'g1', 'Wemby', 'points');
    expect(hist).toEqual([{ time: 1000, line: 28.5, book: 'dk' }]);
  });

  test('skips props missing required fields', async () => {
    const n = await svc.recordSnapshots('nba', [{ player: 'X' }, { gameId: 'g', player: 'Y', stat: 's', line: 'NaN' }], 1);
    expect(n).toBe(0);
  });
});

describe('lineSnapshotService — classifyMovement', () => {
  test('empty / single → stable, no error', () => {
    expect(svc.classifyMovement([]).movement).toBe('stable');
    expect(svc.classifyMovement([{ line: 5 }]).movement).toBe('stable');
    expect(svc.classifyMovement([{ line: 5 }]).delta).toBe(0);
  });

  test('rising vs dropping', () => {
    expect(svc.classifyMovement([{ line: 26.5 }, { line: 28.5 }]).movement).toBe('rising');
    expect(svc.classifyMovement([{ line: 28.5 }, { line: 26.5 }]).movement).toBe('dropping');
  });

  test('sharp signal at >= 1.5 point move', () => {
    expect(svc.classifyMovement([{ line: 28.5 }, { line: 26.5 }]).sharpSignal).toBe(true);  // 2.0
    expect(svc.classifyMovement([{ line: 28.5 }, { line: 27.5 }]).sharpSignal).toBe(false); // 1.0
  });

  test('< 0.5 move is stable', () => {
    expect(svc.classifyMovement([{ line: 28.5 }, { line: 28.5 }]).movement).toBe('stable');
    expect(svc.classifyMovement([{ line: 28.5 }, { line: 28.2 }]).movement).toBe('stable');
  });
});

describe('lineSnapshotService — biggest movers', () => {
  test('classifies + sorts by absolute delta desc', async () => {
    mockScan.mockResolvedValueOnce(['0', [
      'linehistory:mlb:g1:Acuna:hits',
      'linehistory:mlb:g2:Soto:total_bases',
    ]]);
    mockStore['linehistory:mlb:g1:Acuna:hits'] = [
      JSON.stringify({ time: 1, line: 1.5 }),
      JSON.stringify({ time: 2, line: 2.5 }), // +1.0
    ];
    mockStore['linehistory:mlb:g2:Soto:total_bases'] = [
      JSON.stringify({ time: 1, line: 3.5 }),
      JSON.stringify({ time: 2, line: 1.0 }), // -2.5 (bigger, sharp)
    ];
    const movers = await svc.getBiggestMovers('mlb');
    expect(movers).toHaveLength(2);
    expect(movers[0].player).toBe('Soto');       // bigger |delta| first
    expect(movers[0].delta).toBe(-2.5);
    expect(movers[0].sharpSignal).toBe(true);
    expect(movers[1].player).toBe('Acuna');
  });

  test('filters out sub-threshold (stable) props', async () => {
    mockScan.mockResolvedValueOnce(['0', ['linehistory:mlb:g1:Flat:hits']]);
    mockStore['linehistory:mlb:g1:Flat:hits'] = [
      JSON.stringify({ time: 1, line: 1.5 }),
      JSON.stringify({ time: 2, line: 1.5 }),
    ];
    const movers = await svc.getBiggestMovers('mlb');
    expect(movers).toEqual([]);
  });

  test('parseKey extracts sport/game/player/stat', () => {
    expect(svc.__internals.parseKey('linehistory:nba:g1:LeBron James:points')).toEqual({
      sport: 'nba', gameId: 'g1', player: 'LeBron James', stat: 'points',
    });
  });
});
