const mockAxiosGet = jest.fn();
jest.mock('axios', () => ({ get: (...args) => mockAxiosGet(...args) }));

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

const adapter = require('../../src/services/adapters/tank01MlbAdapter');

beforeEach(() => {
  mockAxiosGet.mockReset();
  mockCacheStore.clear();
  mockCacheTtls.clear();
});

describe('tank01MlbAdapter', () => {
  describe('graceful degradation (no RAPID_API_KEY)', () => {
    const original = process.env.RAPID_API_KEY;
    beforeAll(() => { delete process.env.RAPID_API_KEY; });
    afterAll(() => { if (original !== undefined) process.env.RAPID_API_KEY = original; });

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

    test('all endpoints return null without touching axios', async () => {
      expect(await adapter.getMLBBoxScore('20260611_ATL_NYM')).toBeNull();
      expect(await adapter.getMLBBatterVsPitcher('B1', 'P1')).toBeNull();
      expect(await adapter.getMLBDailyScoreboard('20260611')).toBeNull();
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });
  });

  describe('with key configured', () => {
    beforeAll(() => { process.env.RAPID_API_KEY = 'test-rapid-key'; });

    test('RapidAPI host header is the MLB-specific Tank01 host', async () => {
      mockAxiosGet.mockResolvedValueOnce({ data: { body: [] } });
      await adapter.getMLBDailyScoreboard('20260611');
      const [url, opts] = mockAxiosGet.mock.calls[0];
      expect(url).toMatch(/^https:\/\/tank01-mlb-live-in-game-real-time-statistics\.p\.rapidapi\.com/);
      expect(opts.headers['x-rapidapi-host']).toBe('tank01-mlb-live-in-game-real-time-statistics.p.rapidapi.com');
    });

    test('TANK01_MLB_HOST override is honored', async () => {
      const original = process.env.TANK01_MLB_HOST;
      process.env.TANK01_MLB_HOST = 'alt-mlb.rapidapi.com';
      mockAxiosGet.mockResolvedValueOnce({ data: { body: [] } });
      await adapter.getMLBDailyScoreboard('20260611');
      const [url] = mockAxiosGet.mock.calls[0];
      expect(url).toMatch(/^https:\/\/alt-mlb\.rapidapi\.com/);
      if (original !== undefined) process.env.TANK01_MLB_HOST = original;
      else delete process.env.TANK01_MLB_HOST;
    });

    test('getMLBBoxScore tags batters and pitchers with role', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: {
          body: {
            gameStatus: 'Final',
            playerStats: {
              batting:  { 'B1': { longName: 'Ronald Acuña',  teamAbv: 'ATL' } },
              pitching: { 'P1': { longName: 'Spencer Strider', teamAbv: 'ATL' } },
            },
          },
        },
      });
      const stats = await adapter.getMLBBoxScore('GAME-1');
      expect(stats.find((s) => s.role === 'batter').name).toBe('Ronald Acuña');
      expect(stats.find((s) => s.role === 'pitcher').name).toBe('Spencer Strider');
      // Final → 24h TTL.
      expect(mockCacheTtls.get('tank01:mlb:boxscore:GAME-1')).toBe(adapter.__internals.TTL.boxScoreFinal);
    });

    test('In-progress game keeps 5-min TTL on the box score', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: { body: { gameStatus: 'InProgress', playerStats: { batting: {}, pitching: {} } } },
      });
      await adapter.getMLBBoxScore('GAME-2');
      expect(mockCacheTtls.get('tank01:mlb:boxscore:GAME-2')).toBe(adapter.__internals.TTL.boxScoreLive);
    });

    test('getMLBBatterVsPitcher projects single-object payload', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: {
          body: { batterID: 'B1', pitcherID: 'P1', PA: 18, AB: 16, H: 5, HR: 1, RBI: 3, SO: 4, AVG: '.313', OPS: '.857' },
        },
      });
      const bvp = await adapter.getMLBBatterVsPitcher('B1', 'P1');
      expect(bvp).toMatchObject({
        batterId: 'B1', pitcherId: 'P1',
        plateAppearances: 18, atBats: 16, hits: 5, homeRuns: 1, rbi: 3, strikeouts: 4,
        avg: '.313', ops: '.857',
      });
    });

    test('getMLBBatterVsPitcher handles array of matchups', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: {
          body: [
            { batterID: 'B1', pitcherID: 'P1', PA: 12, H: 3, SO: 5 },
            { batterID: 'B1', pitcherID: 'P1', PA: 6,  H: 2, SO: 1 },
          ],
        },
      });
      const bvp = await adapter.getMLBBatterVsPitcher('B1', 'P1');
      expect(Array.isArray(bvp)).toBe(true);
      expect(bvp).toHaveLength(2);
      expect(bvp[0].plateAppearances).toBe(12);
    });

    test('null IDs return null without touching axios', async () => {
      expect(await adapter.getMLBBoxScore(null)).toBeNull();
      expect(await adapter.getMLBBatterVsPitcher(null, 'P1')).toBeNull();
      expect(await adapter.getMLBBatterVsPitcher('B1', null)).toBeNull();
      expect(await adapter.getMLBDailyScoreboard(null)).toBeNull();
      expect(mockAxiosGet).not.toHaveBeenCalled();
    });

    test('getMLBDailyScoreboard projects both array + map shapes', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: { body: { 'GAME-X': { gameID: 'GAME-X', home: 'NYY', away: 'BOS', homePts: 5, awayPts: 4, gameStatus: 'Final' } } },
      });
      const games = await adapter.getMLBDailyScoreboard('20260611');
      expect(games).toHaveLength(1);
      expect(games[0]).toMatchObject({ gameId: 'GAME-X', homeTeam: 'NYY', awayTeam: 'BOS', homeScore: 5, awayScore: 4 });
    });

    test('cache hit on repeat call', async () => {
      mockAxiosGet.mockResolvedValueOnce({ data: { body: { batterID: 'B', PA: 1 } } });
      await adapter.getMLBBatterVsPitcher('B', 'P');
      await adapter.getMLBBatterVsPitcher('B', 'P');
      expect(mockAxiosGet).toHaveBeenCalledTimes(1);
    });

    test('axios throw → stale fallback', async () => {
      mockCacheStore.set('tank01:mlb:scoreboard:20260611:stale', { body: [{ gameID: 'STALE' }] });
      mockAxiosGet.mockRejectedValueOnce(new Error('upstream 500'));
      const games = await adapter.getMLBDailyScoreboard('20260611');
      expect(games[0].gameId).toBe('STALE');
    });

    // Session 23 — game-level book-by-book betting odds.
    test('getMLBBettingOdds returns the raw body and caches at the odds TTL', async () => {
      mockAxiosGet.mockResolvedValueOnce({
        data: { body: { '20260612_ARI@CIN': { sportsBooks: [{ sportsBook: 'bet365', odds: {} }] } } },
      });
      const body = await adapter.getMLBBettingOdds('2026-06-12');
      expect(body['20260612_ARI@CIN']).toBeDefined();
      const [url] = mockAxiosGet.mock.calls[0];
      expect(url).toMatch(/getMLBBettingOdds\?gameDate=20260612/);
      expect(mockCacheTtls.get('tank01:mlb:odds:20260612')).toBe(adapter.__internals.TTL.odds);
    });

    test('getMLBBettingOdds second call within TTL does not hit Tank01', async () => {
      mockAxiosGet.mockResolvedValueOnce({ data: { body: { g: {} } } });
      await adapter.getMLBBettingOdds('2026-06-12');
      await adapter.getMLBBettingOdds('2026-06-12');
      expect(mockAxiosGet).toHaveBeenCalledTimes(1);
    });

    test('getMLBBettingOdds null date returns null without axios', async () => {
      expect(await adapter.getMLBBettingOdds(null)).toBeNull();
    });
  });
});
