// Mock axios and the Redis cache surface BEFORE requiring the adapter so
// jest's module-mock hoisting captures the calls.
const mockAxiosGet = jest.fn();
jest.mock('axios', () => ({ get: (...args) => mockAxiosGet(...args) }));

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 adapter = require('../../src/services/adapters/footballDataAdapter');

beforeEach(() => {
  mockAxiosGet.mockReset();
  mockCacheStore.clear();
  adapter.__internals.resetBucketForTests();
});

describe('footballDataAdapter', () => {
  describe('graceful degradation when API key is missing', () => {
    const original = process.env.FOOTBALL_DATA_API_KEY;
    beforeAll(() => { delete process.env.FOOTBALL_DATA_API_KEY; });
    afterAll(() => { if (original !== undefined) process.env.FOOTBALL_DATA_API_KEY = original; });

    test('hasApiKey reports false', () => {
      expect(adapter.hasApiKey()).toBe(false);
    });

    test('getWorldCupFixtures returns null (does NOT hit axios)', async () => {
      const result = await adapter.getWorldCupFixtures();
      expect(result).toBeNull();
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });

    test('getTeamSquad returns null (does NOT hit axios)', async () => {
      const result = await adapter.getTeamSquad(42);
      expect(result).toBeNull();
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });
  });

  describe('happy path with API key configured', () => {
    beforeAll(() => { process.env.FOOTBALL_DATA_API_KEY = 'test-key-123'; });

    test('getLeagueFixtures projects API response to stable shape', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: {
          matches: [
            {
              id: 1, homeTeam: { name: 'England' }, awayTeam: { name: 'Brazil' },
              utcDate: '2026-06-15T20:00:00Z', status: 'SCHEDULED',
              score: { winner: null }, matchday: 1, venue: 'MetLife Stadium',
            },
          ],
        },
      });

      const fixtures = await adapter.getLeagueFixtures('WC');
      expect(Array.isArray(fixtures)).toBe(true);
      expect(fixtures).toHaveLength(1);
      expect(fixtures[0]).toMatchObject({
        id: 1, homeTeam: 'England', awayTeam: 'Brazil', status: 'SCHEDULED',
        matchday: 1, venue: 'MetLife Stadium', competition: 'WC',
      });
      expect(mockAxiosGet).toHaveBeenCalledTimes(1);
      // Auth header carries the API key — never logged elsewhere.
      const [, opts] = mockAxiosGet.mock.calls[0];
      expect(opts.headers['X-Auth-Token']).toBe('test-key-123');
    });

    test('second identical call serves from cache (axios not re-invoked)', async () => {
      mockAxiosGet.mockResolvedValueOnce({ data: { matches: [{ id: 7 }] } });
      await adapter.getLeagueFixtures('PL');
      await adapter.getLeagueFixtures('PL');
      expect(mockAxiosGet).toHaveBeenCalledTimes(1);
    });

    test('different competition codes use separate cache keys', async () => {
      mockAxiosGet
        .mockResolvedValueOnce({ data: { matches: [{ id: 1 }] } })
        .mockResolvedValueOnce({ data: { matches: [{ id: 2 }] } });
      await adapter.getLeagueFixtures('PL');
      await adapter.getLeagueFixtures('PD');
      expect(mockAxiosGet).toHaveBeenCalledTimes(2);
    });

    test('getLeagueScorers projects to flat shape with goals + assists', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: {
          scorers: [
            {
              player: { name: 'Harry Kane', position: 'Striker', nationality: 'England' },
              team: { name: 'England' },
              goals: 5, assists: 1, playedMatches: 4, minutesPlayed: 360,
            },
          ],
        },
      });
      const scorers = await adapter.getLeagueScorers('WC');
      expect(scorers[0]).toMatchObject({
        name: 'Harry Kane', team: 'England', goals: 5, assists: 1,
        playedMatches: 4, minutesPlayed: 360,
      });
    });

    test('getTeamSquad projects squad rows with position and shirt', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: { squad: [{ id: 9, name: 'Pulisic', position: 'Forward', shirtNumber: 10 }] },
      });
      const squad = await adapter.getTeamSquad(101);
      expect(squad[0]).toMatchObject({ id: 9, name: 'Pulisic', position: 'Forward', shirtNumber: 10 });
    });

    test('empty/missing arrays in upstream → empty list (not null)', async () => {
      mockAxiosGet.mockResolvedValueOnce({ data: {} });
      const fixtures = await adapter.getLeagueFixtures('WC');
      expect(fixtures).toEqual([]);
    });

    test('axios throw → returns null (graceful degradation)', async () => {
      mockAxiosGet.mockRejectedValueOnce(new Error('network down'));
      const fixtures = await adapter.getLeagueFixtures('WC');
      expect(fixtures).toBeNull();
    });

    test('axios throw + prior :stale value → stale-while-revalidate', async () => {
      // Prime the stale cache.
      mockCacheStore.set('soccer:wc:fixtures:stale', { matches: [{ id: 999 }] });
      mockAxiosGet.mockRejectedValueOnce(new Error('upstream 500'));
      const fixtures = await adapter.getLeagueFixtures('WC');
      // The stale value goes through the same projection.
      expect(Array.isArray(fixtures)).toBe(true);
      expect(fixtures).toHaveLength(1);
      expect(fixtures[0].id).toBe(999);
    });
  });

  describe('token bucket rate limiting', () => {
    beforeAll(() => { process.env.FOOTBALL_DATA_API_KEY = 'test-key-123'; });

    test('refuses network call when bucket is drained, falls to stale', async () => {
      // Drain the bucket by consuming all tokens.
      for (let i = 0; i < adapter.__internals.BUCKET_MAX; i += 1) {
        expect(adapter.__internals.tryConsumeToken()).toBe(true);
      }
      // Next consume should fail.
      expect(adapter.__internals.tryConsumeToken()).toBe(false);

      // Prime a stale value.
      mockCacheStore.set('soccer:wc:fixtures:stale', { matches: [{ id: 42 }] });
      const fixtures = await adapter.getLeagueFixtures('WC');
      expect(fixtures[0].id).toBe(42);
      // Critically: axios was NOT called — the bucket short-circuited.
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });

    test('returns null when bucket drained AND no stale value', async () => {
      for (let i = 0; i < adapter.__internals.BUCKET_MAX; i += 1) {
        adapter.__internals.tryConsumeToken();
      }
      const fixtures = await adapter.getLeagueFixtures('UNKNOWN_LEAGUE');
      expect(fixtures).toBeNull();
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });
  });

  describe('input guards', () => {
    test('getLeagueFixtures(null) returns null without touching network', async () => {
      const r = await adapter.getLeagueFixtures(null);
      expect(r).toBeNull();
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });
    test('getTeamSquad(null) returns null without touching network', async () => {
      const r = await adapter.getTeamSquad(null);
      expect(r).toBeNull();
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });
  });
});
