// Mocked deps for the feature cache. Every sub-service is replaced so we
// can verify the cache OMITS features whose source returns null.

const mockGameLogs = { logs: null, careerPlayoff: null };
jest.mock('../../src/services/intelligence/gameLogService', () => ({
  getGameLogs: async () => mockGameLogs.logs,
  getCareerPlayoffGames: async () => mockGameLogs.careerPlayoff,
  getWithWithoutStats: async () => null,
}));

const mockTeam = { stats: null, rank: null };
jest.mock('../../src/services/intelligence/teamStatsCache', () => ({
  getTeamStats: async () => mockTeam.stats,
  getOpponentRank: async () => mockTeam.rank,
  refreshTeamStats: async () => ({ captured: 0 }),
}));

const mockRef = { impact: null };
jest.mock('../../src/services/intelligence/refSignals', () => ({
  getRefImpact: async () => mockRef.impact,
  setRefAssignment: async () => ({ ok: true }),
}));

const mockCoach = { impact: null };
jest.mock('../../src/services/intelligence/coachSignals', () => ({
  getCoachImpact: async () => mockCoach.impact,
  getCoachProfile: async () => null,
  tenureAdjustment: () => 0.5,
  loadSeed: () => ({ coaches: [] }),
}));

const mockLineup = { roleValue: 1.0 };
jest.mock('../../src/services/intelligence/lineupSignals', () => ({
  roleValue: () => mockLineup.roleValue,
  classifyByUsage: () => 'primary_handler',
  getProjectedStarters: async () => ({ home: [], away: [] }),
}));

const mockInjuries = { list: [] };
jest.mock('../../src/services/intelligence/injuryParser', () => ({
  getTeamInjuries: async () => mockInjuries.list,
  isPlayerOut: async () => false,
  getMissingStarters: async () => [],
}));

const mockLineMovement = { lm: null };
jest.mock('../../src/services/intelligence/lineMovement', () => ({
  getLineMovement: async () => mockLineMovement.lm,
}));

const mockCache = { current: new Map() };
jest.mock('../../src/utils/redis', () => ({
  cacheGet: async (k) => mockCache.current.get(k) ?? null,
  cacheSet: async (k, v) => { mockCache.current.set(k, v); return true; },
  cacheDel: async (k) => { mockCache.current.delete(k); return true; },
}));

const fc = require('../../src/services/intelligence/featureCache');

beforeEach(() => {
  mockGameLogs.logs = null;
  mockGameLogs.careerPlayoff = null;
  mockTeam.stats = null;
  mockTeam.rank = null;
  mockRef.impact = null;
  mockCoach.impact = null;
  mockLineup.roleValue = 1.0;
  mockInjuries.list = [];
  mockLineMovement.lm = null;
  mockCache.current.clear();
});

describe('feature cache — missing data omits features', () => {
  test('with no upstream data, only context + injury_severity_score (=0) populate', async () => {
    const payload = await fc.getFeatures({
      playerId: 'p1',
      playerName: 'P',
      statType: 'points',
      sport: 'nba',
      gameId: 'g1',
      gameContext: { home_away: 'home', rest_days: 1 },
      teamId: 18,
    });
    expect(payload.features.home_away).toBe(1.0);
    expect(payload.features.rest_days).toBe(1);
    expect(payload.features.injury_severity_score).toBe(0);
    // Missing because upstreams returned null:
    expect(payload.features.l5_avg).toBeUndefined();
    expect(payload.features.opp_rank_stat).toBeUndefined();
    expect(payload.features.ref_pace_adjustment).toBeUndefined();
    expect(payload.meta.features_missing).toContain('l5_avg');
    expect(payload.meta.features_missing).toContain('ref_pace_adjustment');
    expect(payload.meta.features_available).toContain('home_away');
  });
});

describe('feature cache — game log features', () => {
  test('computes l5_avg, l20_avg, l10_stddev from a 10-game log', async () => {
    mockGameLogs.logs = [
      { points: 30 }, { points: 25 }, { points: 28 }, { points: 22 }, { points: 35 },
      { points: 20 }, { points: 18 }, { points: 26 }, { points: 31 }, { points: 24 },
    ];
    const payload = await fc.getFeatures({
      playerId: 'p2', playerName: 'P', statType: 'points',
      sport: 'nba', gameId: 'g2', gameContext: {},
    });
    expect(payload.features.l5_avg).toBeCloseTo(28.0, 1);
    expect(payload.features.l20_avg).toBeCloseTo(25.9, 1);
    expect(payload.features.l10_stddev).toBeGreaterThan(0);
  });

  test('combo stats (pts_reb_ast) sum components per game', async () => {
    mockGameLogs.logs = [
      { points: 25, rebounds: 5, assists: 7 },
      { points: 22, rebounds: 6, assists: 8 },
      { points: 30, rebounds: 4, assists: 5 },
    ];
    const payload = await fc.getFeatures({
      playerId: 'p3', playerName: 'P', statType: 'pts_reb_ast',
      sport: 'nba', gameId: 'g3', gameContext: {},
    });
    // (37 + 36 + 39) / 3 = 37.33
    expect(payload.features.l5_avg).toBeCloseTo(37.33, 1);
  });
});

describe('feature cache — team features', () => {
  test('opp_rank_stat + pace_factor + team_pace come from team cache', async () => {
    mockTeam.stats = { pace: 102.4 };
    mockTeam.rank = 7;
    const payload = await fc.getFeatures({
      playerId: 'p4', playerName: 'P', statType: 'points',
      sport: 'nba', gameId: 'g4', opponentAbbr: 'BOS', gameContext: {},
    });
    expect(payload.features.opp_rank_stat).toBe(7);
    expect(payload.features.pace_factor).toBe(102.4);
    expect(payload.features.team_pace).toBe(102.4);
  });
});

describe('feature cache — injury features', () => {
  test('counts missing starters', async () => {
    mockInjuries.list = [
      { playerId: '1', playerName: 'A', status: 'OUT' },
      { playerId: '2', playerName: 'B', status: 'OUT' },
      { playerId: '3', playerName: 'C', status: 'QUESTIONABLE' },
    ];
    const payload = await fc.getFeatures({
      playerId: 'p5', playerName: 'P', statType: 'points',
      sport: 'nba', gameId: 'g5', teamId: 18,
      knownStarterIds: ['1', '2', '3', '4'],
      gameContext: {},
    });
    // Only OUT counts as missing starter (status=QUESTIONABLE skipped).
    expect(payload.features.injury_severity_score).toBe(2);
    expect(payload.features.teammate_absence_bump).toBeCloseTo(0.10, 5);
  });
});

describe('feature cache — context features cover all knobs', () => {
  test('every documented context field round-trips', async () => {
    const payload = await fc.getFeatures({
      playerId: 'p6', playerName: 'P', statType: 'points',
      sport: 'nba', gameId: 'g6',
      gameContext: {
        home_away: 'away',
        rest_days: 0,
        game_count_in_7d: 4,
        season_type: 2,
        game_in_series: 1,
        season_phase: 0.95,
      },
    });
    const f = payload.features;
    expect(f.home_away).toBe(0.0);
    expect(f.rest_days).toBe(0);
    expect(f.game_count_in_7d).toBe(4);
    expect(f.season_type).toBe(2);
    expect(f.game_in_series).toBe(1);
    expect(f.season_phase).toBe(0.95);
  });
});

describe('feature cache — cache key reuse', () => {
  test('second call returns the cached vector', async () => {
    const args = { playerId: 'p7', playerName: 'P', statType: 'points', sport: 'nba', gameId: 'g7', gameContext: {} };
    const first = await fc.getFeatures(args);
    mockGameLogs.logs = [{ points: 99 }]; // upstream changes
    const second = await fc.getFeatures(args);
    expect(second).toEqual(first);
  });
});

describe('feature cache — coach + ref + lineup features', () => {
  test('coach delta + ref pace + lineup role pass through', async () => {
    mockCoach.impact = { adjusted_pace_delta: 1.4, without_primary_pace_shift: 0.5 };
    mockRef.impact = { pace_impact: 0.6, foul_adjustment: 22 };
    const payload = await fc.getFeatures({
      playerId: 'p8', playerName: 'P', statType: 'points', sport: 'nba',
      gameId: 'g8', teamAbbr: 'NYK',
      role: 'primary_handler',
      gameContext: {},
    });
    expect(payload.features.coach_pace_delta).toBe(1.4);
    expect(payload.features.coach_player_interaction).toBe(0.5);
    expect(payload.features.ref_pace_adjustment).toBe(0.6);
    expect(payload.features.ref_foul_adjustment).toBe(22);
    expect(payload.features.lineup_ball_handler_role).toBe(1.0);
  });
});

describe('feature cache — line movement', () => {
  test('line_delta surfaces the movement value', async () => {
    mockLineMovement.lm = { movement: -0.5 };
    const payload = await fc.getFeatures({
      playerId: 'p9', playerName: 'P', statType: 'points',
      sport: 'nba', gameId: 'g9', gameContext: {},
    });
    expect(payload.features.line_delta).toBe(-0.5);
  });
});
