// Unit: rosterLogs loader (Session 23). Redis-only; no network.

const store = {};
const mockScan = jest.fn();
jest.mock('../../src/utils/redis', () => ({
  cacheGet: jest.fn(async (k) => (k in store ? store[k] : null)),
  getRedisClient: () => ({ scan: mockScan }),
  isDegraded: () => false,
}));

const { loadRosterLogs, __internals } = require('../../src/services/rosterLogs');

beforeEach(() => {
  for (const k of Object.keys(store)) delete store[k];
  mockScan.mockReset();
});

describe('rosterLogs', () => {
  test('fast path: returns a prefetched roster blob without scanning', async () => {
    store['rosterlogs:nba'] = [{ name: 'Wemby', games: [{ points: 30 }] }];
    const roster = await loadRosterLogs('nba');
    expect(roster).toHaveLength(1);
    expect(mockScan).not.toHaveBeenCalled();
  });

  test('scan path: assembles roster from per-player gamelogs keys', async () => {
    store['gamelogs:nba:Wembanyama:20'] = [{ points: 30, team: 'SA', playerId: 'W1' }];
    store['gamelogs:nba:Brunson:20'] = [{ points: 24, team: 'NYK' }];
    mockScan
      .mockResolvedValueOnce(['0', ['gamelogs:nba:Wembanyama:20', 'gamelogs:nba:Brunson:20']]);
    const roster = await loadRosterLogs('nba');
    expect(roster.map((p) => p.name).sort()).toEqual(['Brunson', 'Wembanyama']);
    const wemby = roster.find((p) => p.name === 'Wembanyama');
    expect(wemby.team).toBe('SA');
    expect(wemby.playerId).toBe('W1');
  });

  test('dedupes by highest game-count variant', async () => {
    store['gamelogs:nba:Star:10'] = [{ points: 1 }, { points: 2 }];
    store['gamelogs:nba:Star:20'] = [{ points: 1 }, { points: 2 }, { points: 3 }];
    mockScan.mockResolvedValueOnce(['0', ['gamelogs:nba:Star:10', 'gamelogs:nba:Star:20']]);
    const roster = await loadRosterLogs('nba');
    expect(roster).toHaveLength(1);
    expect(roster[0].games).toHaveLength(3); // the :20 variant wins
  });

  test('empty cache → empty roster, never throws', async () => {
    mockScan.mockResolvedValueOnce(['0', []]);
    expect(await loadRosterLogs('nba')).toEqual([]);
  });

  test('scan failure → returns what it had, no throw', async () => {
    mockScan.mockRejectedValueOnce(new Error('redis down'));
    expect(await loadRosterLogs('nba')).toEqual([]);
  });

  test('playerFromKey parses the player name out of the key', () => {
    expect(__internals.playerFromKey('gamelogs:nba:LeBron James:20', 'nba')).toBe('LeBron James');
  });
});

// Session 25 — box-score aggregation bridge (prefetch key alignment).
describe('rosterLogs — Tank01 box-score aggregation', () => {
  // Route scans by their MATCH pattern (3rd arg) so gamelogs + boxscore
  // scans can return different key sets.
  function routeScan(map) {
    mockScan.mockImplementation(async (_cursor, _m, match) => ['0', map[match] || []]);
  }

  test('aggregates MLB box scores into per-player multi-game logs', async () => {
    routeScan({
      'gamelogs:mlb:*': [],
      'tank01:mlb:boxscore:*': [
        'tank01:mlb:boxscore:20260612_ARI@CIN',
        'tank01:mlb:boxscore:20260611_ARI@LAD',
      ],
    });
    // Newer game (0612) and older game (0611) for the same batter.
    store['tank01:mlb:boxscore:20260612_ARI@CIN'] = [
      { role: 'batter', name: 'Acuna', playerId: 'B1', team: 'ARI', _raw: { H: 2, HR: 1 }, _final: true },
    ];
    store['tank01:mlb:boxscore:20260611_ARI@LAD'] = [
      { role: 'batter', name: 'Acuna', playerId: 'B1', team: 'ARI', _raw: { H: 1, HR: 0 }, _final: true },
    ];
    const roster = await loadRosterLogs('mlb');
    expect(roster).toHaveLength(1);
    const acuna = roster[0];
    expect(acuna.name).toBe('Acuna');
    expect(acuna.team).toBe('ARI');
    expect(acuna.games).toHaveLength(2);
    // Most-recent first → the 2-hit game leads (flattened from _raw).
    expect(acuna.games[0].H).toBe(2);
    expect(acuna.games[1].H).toBe(1);
  });

  test('aggregated MLB logs feed the streaks engine (hit streak)', async () => {
    routeScan({
      'gamelogs:mlb:*': [],
      'tank01:mlb:boxscore:*': ['tank01:mlb:boxscore:20260612_A@B', 'tank01:mlb:boxscore:20260611_A@B'],
    });
    store['tank01:mlb:boxscore:20260612_A@B'] = [{ role: 'batter', name: 'X', team: 'A', _raw: { H: 2 }, _final: true }];
    store['tank01:mlb:boxscore:20260611_A@B'] = [{ role: 'batter', name: 'X', team: 'A', _raw: { H: 1 }, _final: true }];
    const roster = await loadRosterLogs('mlb');
    const { computeStreaks } = require('../../src/services/streaksService');
    const streaks = computeStreaks(roster, 'mlb');
    const hit = streaks.find((s) => s.type === 'hit_streak');
    expect(hit).toBeDefined();
    expect(hit.currentStreak).toBe(2);
  });

  test('NBA box-score rows are consumed without _raw flattening', async () => {
    routeScan({
      'gamelogs:nba:*': [],
      'tank01:nba:boxscore:*': ['tank01:nba:boxscore:20260612_NYK@SA'],
    });
    store['tank01:nba:boxscore:20260612_NYK@SA'] = [
      { playerId: 'W1', name: 'Wemby', team: 'SA', pts: 30, reb: 12, ast: 3, threes: 2, blk: 4, stl: 1 },
    ];
    const roster = await loadRosterLogs('nba');
    expect(roster[0].games[0].pts).toBe(30);
  });

  test('gamelogs path still wins over box scores when present', async () => {
    routeScan({ 'gamelogs:mlb:*': ['gamelogs:mlb:Star:20'] });
    store['gamelogs:mlb:Star:20'] = [{ hits: 3 }, { hits: 2 }];
    const roster = await loadRosterLogs('mlb');
    expect(roster[0].name).toBe('Star');
    expect(roster[0].games).toHaveLength(2);
  });

  test('boxScoreKeyDate extracts the date for recency ordering', () => {
    expect(__internals.boxScoreKeyDate('tank01:mlb:boxscore:20260612_ARI@CIN')).toBe('20260612');
    expect(__internals.boxScoreKeyDate('tank01:mlb:boxscore:weird')).toBe('0');
  });
});
