// Source-cascade tests for soccerFeatureExtractor (Session 9). The
// pre-existing soccerFeatureExtractor.test.js covers the legacy
// football-data path; this suite verifies that:
//   - api-football data wins when the prefetch alias exists
//   - footapi wins when api-football is missing but footapi alias exists
//   - football-data is still served when only the legacy key is set
//   - The `meta.sources` map attributes correctly per lookup
//
// We mock cacheGet to inspect which key the cascade asked for.

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

const { normalizeName } = require('../../src/utils/normalize');
const extractor = require('../../src/services/intelligence/soccerFeatureExtractor');

// Session 39 (QA) — de-flake. When a cache key misses, the extractor falls
// through to live network adapters; under full-suite concurrency that async
// chain occasionally exceeds Jest's 5s default (intermittent timeout on the
// nextMatch/referee cascade tests). Same fix family as Session 32's CPU-bound
// pipeline test — give the suite headroom. Test-only; no service change.
jest.setTimeout(20000);

beforeEach(() => { mockCacheStore.clear(); });

describe('soccerFeatureExtractor — source cascade (Session 9)', () => {
  test('api-football wins when its alias is populated', async () => {
    const n = normalizeName('Lionel Messi');
    // Only the apifootball alias is populated — others should not be consulted.
    mockCacheStore.set(`apifootball:player_by_name:${n}`, {
      team: 'Argentina', goals_per_90: 0.92, minutes_per_game: 88,
    });
    const r = await extractor.extractSoccerFeatures({
      player: 'Lionel Messi', stat_type: 'goals', line: 0.5, direction: 'over',
    });
    expect(r.features.goals_per_90).toBe(0.92);
    expect(r.meta.sources.player).toBe('api-football');
  });

  test('footapi wins when api-football is empty but footapi is populated', async () => {
    const n = normalizeName('Harry Kane');
    mockCacheStore.set(`footapi:player_by_name:${n}`, {
      team: 'England', goals_per_90: 0.81,
    });
    const r = await extractor.extractSoccerFeatures({
      player: 'Harry Kane', stat_type: 'goals', line: 0.5, direction: 'over',
    });
    expect(r.features.goals_per_90).toBe(0.81);
    expect(r.meta.sources.player).toBe('footapi');
  });

  test('football-data legacy key is the final fallback', async () => {
    const n = normalizeName('Bukayo Saka');
    mockCacheStore.set(`soccer:player:${n}`, {
      team: 'England', goals_per_90: 0.4,
    });
    const r = await extractor.extractSoccerFeatures({
      player: 'Bukayo Saka', stat_type: 'goals', line: 0.5, direction: 'over',
    });
    expect(r.features.goals_per_90).toBe(0.4);
    expect(r.meta.sources.player).toBe('football-data');
  });

  test('all-miss case → null source + errors populated', async () => {
    const r = await extractor.extractSoccerFeatures({
      player: 'Unknown', stat_type: 'goals', line: 0.5, direction: 'over',
    });
    expect(r.meta.sources.player).toBeNull();
    expect(r.meta.errors).toContain('player_not_found_in_cache');
  });

  test('nextMatch cascade — api-football preferred', async () => {
    const n = normalizeName('Vinicius Junior');
    mockCacheStore.set(`apifootball:player_by_name:${n}`, { team: 'Brazil' });
    mockCacheStore.set('apifootball:nextmatch:Brazil', {
      opponent: 'Argentina', venue: 'MetLife Stadium', isHome: true, referee: 'A. Taylor',
    });
    mockCacheStore.set('soccer:nextmatch:Brazil', {
      opponent: 'STALE', venue: 'old', isHome: false, referee: 'STALE',
    });
    const r = await extractor.extractSoccerFeatures({
      player: 'Vinicius Junior', stat_type: 'goals', line: 0.5, direction: 'over',
    });
    expect(r.meta.opponentAbbr).toBe('Argentina'); // api-football won
    expect(r.meta.sources.nextMatch).toBe('api-football');
  });

  test('referee cascade falls through to legacy key when richer sources empty', async () => {
    const n = normalizeName('Anyone');
    mockCacheStore.set(`apifootball:player_by_name:${n}`, { team: 'X' });
    mockCacheStore.set('apifootball:nextmatch:X', {
      opponent: 'Y', venue: 'V', isHome: true, referee: 'Bjorn',
    });
    mockCacheStore.set('soccer:referee:Bjorn', {
      cards_per_game: 4.2, penalties_per_game: 0.3,
    });
    const r = await extractor.extractSoccerFeatures({
      player: 'Anyone', stat_type: 'cards', line: 0.5, direction: 'over',
    });
    expect(r.features.referee_cards_per_game).toBe(4.2);
    expect(r.meta.sources.referee).toBe('football-data');
  });

  test('multiple sources active → independent attribution per lookup', async () => {
    const n = normalizeName('Mixed');
    mockCacheStore.set(`apifootball:player_by_name:${n}`, { team: 'France', goals_per_90: 1.1 });
    // Match data only in legacy.
    mockCacheStore.set('soccer:nextmatch:France', {
      opponent: 'Italy', venue: 'AT&T Stadium', isHome: false, referee: 'X',
    });
    // Referee only in footapi.
    mockCacheStore.set('footapi:referee_by_name:X', { cards_per_game: 5.5 });

    const r = await extractor.extractSoccerFeatures({
      player: 'Mixed', stat_type: 'goals', line: 0.5, direction: 'over',
    });
    expect(r.meta.sources).toEqual({
      player: 'api-football',
      nextMatch: 'football-data',
      lastFixture: null,
      referee: 'footapi',
    });
  });
});
