// Integration: /api/gamelines/:sport (Session 23).
//
// The Tank01 adapters are mocked — we assert the route normalizes the
// book-by-book body, parses teams from the gameID, handles the missing
// API-key path gracefully, and never 500s on adapter failure.

const express = require('express');
const request = require('supertest');

jest.mock('../../src/services/adapters/tank01NbaAdapter', () => ({
  getNBABettingOdds: jest.fn(),
  hasApiKey: jest.fn(() => true),
}));
jest.mock('../../src/services/adapters/tank01MlbAdapter', () => ({
  getMLBBettingOdds: jest.fn(),
  hasApiKey: jest.fn(() => true),
}));
jest.mock('../../src/services/scheduleService', () => ({
  todayET: () => '2026-06-12',
}));

const nbaAdapter = require('../../src/services/adapters/tank01NbaAdapter');
const mlbAdapter = require('../../src/services/adapters/tank01MlbAdapter');

function mountApp() {
  delete require.cache[require.resolve('../../src/routes/gameLines')];
  const routes = require('../../src/routes/gameLines');
  const app = express();
  app.use('/api/gamelines', routes);
  return app;
}

// Session 25 — the REAL Tank01 shape (traced): sportsbooks are top-level
// keys on the game object, alongside non-book keys (awayTeam, gameID…).
const MLB_BODY = {
  '20260612_ARI@CIN': {
    gameID: '20260612_ARI@CIN',
    awayTeam: 'ARI',
    homeTeam: 'CIN',
    last_updated_e_time: '1718200000',
    bet365: { homeTeamML: '-110', awayTeamML: '+100', totalOver: '9.5', totalOverOdds: '-105', totalUnderOdds: '-115', homeTeamRunLine: '-1.5' },
    betmgm: { homeTeamML: '-115', awayTeamML: '-105', totalOver: '9' },
    caesars: { homeTeamML: '-112', awayTeamML: '-102', totalUnder: '9.5' },
  },
};

// Legacy array shape — still supported for backward compatibility.
const MLB_BODY_LEGACY = {
  '20260612_ARI@CIN': {
    gameID: '20260612_ARI@CIN',
    sportsBooks: [
      { sportsBook: 'bet365', odds: { homeTeamMLOdds: '-110', awayTeamMLOdds: '+100', totalOver: '9.5' } },
    ],
  },
};

beforeEach(() => {
  jest.clearAllMocks();
  // clearAllMocks wipes call history but not custom implementations set via
  // mockReturnValue in a prior test — restore the configured-key default.
  nbaAdapter.hasApiKey.mockReturnValue(true);
  mlbAdapter.hasApiKey.mockReturnValue(true);
});

describe('GET /api/gamelines/:sport', () => {
  test('mlb returns book-by-book odds from top-level book keys (real shape)', async () => {
    mlbAdapter.getMLBBettingOdds.mockResolvedValue(MLB_BODY);
    const res = await request(mountApp()).get('/api/gamelines/mlb');
    expect(res.status).toBe(200);
    expect(res.body.source).toBe('tank01');
    const game = res.body.games['20260612_ARI@CIN'];
    expect(game.homeTeam).toBe('CIN');
    expect(game.awayTeam).toBe('ARI');
    // Books must be POPULATED (the Session 25 bug: this was {}).
    expect(Object.keys(game.books).sort()).toEqual(['bet365', 'betmgm', 'caesars']);
    expect(game.books.bet365.homeML).toBe('-110');
    expect(game.books.bet365.awayML).toBe('+100');
    expect(game.books.bet365.total).toBe('9.5');
    expect(game.books.bet365.homeSpread).toBe('-1.5');
    expect(game.books.betmgm.homeML).toBe('-115');
    expect(game.books.caesars.total).toBe('9.5'); // totalUnder fallback
  });

  test('non-book keys (awayTeam, homeTeam, gameID) are excluded from books', async () => {
    mlbAdapter.getMLBBettingOdds.mockResolvedValue(MLB_BODY);
    const res = await request(mountApp()).get('/api/gamelines/mlb');
    const books = res.body.games['20260612_ARI@CIN'].books;
    expect(books.awayteam).toBeUndefined();
    expect(books.hometeam).toBeUndefined();
    expect(books.gameid).toBeUndefined();
  });

  test('missing fields normalize to null, never undefined/crash', async () => {
    mlbAdapter.getMLBBettingOdds.mockResolvedValue(MLB_BODY);
    const res = await request(mountApp()).get('/api/gamelines/mlb');
    const betmgm = res.body.games['20260612_ARI@CIN'].books.betmgm;
    expect(betmgm.homeSpread).toBeNull();
    expect(betmgm.overOdds).toBeNull();
    expect(Object.prototype.hasOwnProperty.call(betmgm, 'homeSpread')).toBe(true);
  });

  test('legacy sportsBooks array shape still normalizes', async () => {
    mlbAdapter.getMLBBettingOdds.mockResolvedValue(MLB_BODY_LEGACY);
    const res = await request(mountApp()).get('/api/gamelines/mlb');
    expect(res.body.games['20260612_ARI@CIN'].books.bet365.homeML).toBe('-110');
  });

  test('nba returns empty games object off-season (not an error)', async () => {
    nbaAdapter.getNBABettingOdds.mockResolvedValue({});
    const res = await request(mountApp()).get('/api/gamelines/nba');
    expect(res.status).toBe(200);
    expect(res.body.games).toEqual({});
  });

  test('missing RAPID_API_KEY → graceful, configured:false, not a crash', async () => {
    mlbAdapter.hasApiKey.mockReturnValue(false);
    const res = await request(mountApp()).get('/api/gamelines/mlb');
    expect(res.status).toBe(200);
    expect(res.body.configured).toBe(false);
    expect(mlbAdapter.getMLBBettingOdds).not.toHaveBeenCalled();
  });

  test('adapter throwing → empty games, 200 not 500', async () => {
    mlbAdapter.getMLBBettingOdds.mockRejectedValue(new Error('rapidapi 429'));
    const res = await request(mountApp()).get('/api/gamelines/mlb');
    expect(res.status).toBe(200);
    expect(res.body.games).toEqual({});
  });

  test('unsupported sport → 404', async () => {
    const res = await request(mountApp()).get('/api/gamelines/soccer');
    expect(res.status).toBe(404);
  });

  test('adapter called once per request (adapter owns TTL)', async () => {
    mlbAdapter.getMLBBettingOdds.mockResolvedValue(MLB_BODY);
    const app = mountApp();
    await request(app).get('/api/gamelines/mlb');
    expect(mlbAdapter.getMLBBettingOdds).toHaveBeenCalledTimes(1);
  });
});
