const { normalizeProps, MARKET_MAP, ALLOWED_BOOKS } = require('../../src/utils/oddsNormalizer');

function makeEvent(overrides = {}) {
  return {
    id: 'event-1',
    sport_key: 'basketball_nba',
    home_team: 'Denver Nuggets',
    away_team: 'Los Angeles Lakers',
    commence_time: '2026-03-21T19:00:00Z',
    bookmakers: [],
    ...overrides,
  };
}

function makeBookmaker(key, markets) {
  return { key, title: key, markets };
}

function makeMarket(marketKey, outcomes, lastUpdate = '2026-03-21T14:28:00Z') {
  return { key: marketKey, last_update: lastUpdate, outcomes };
}

function makeOutcome(name, player, price, point) {
  return { name, description: player, price, point };
}

describe('oddsNormalizer', () => {
  describe('normalizeProps', () => {
    it('normalizes a raw response with multiple books and markets', () => {
      const event = makeEvent({
        bookmakers: [
          makeBookmaker('draftkings', [
            makeMarket('player_points', [
              makeOutcome('Over', 'Nikola Jokic', -110, 26.5),
              makeOutcome('Under', 'Nikola Jokic', -110, 26.5),
            ]),
          ]),
          makeBookmaker('fanduel', [
            makeMarket('player_points', [
              makeOutcome('Over', 'Nikola Jokic', -105, 27.0),
              makeOutcome('Under', 'Nikola Jokic', -115, 27.0),
            ]),
          ]),
        ],
      });

      const result = normalizeProps([event]);

      expect(result).toHaveLength(2);
      expect(result[0]).toMatchObject({
        player: 'Nikola Jokic',
        home_team: 'DEN',
        away_team: 'LAL',
        stat_type: 'points',
        book: 'draftkings',
        line: 26.5,
        over_odds: -110,
        under_odds: -110,
      });
      expect(result[1]).toMatchObject({
        player: 'Nikola Jokic',
        book: 'fanduel',
        line: 27.0,
        over_odds: -105,
        under_odds: -115,
      });
    });

    it('filters out books not in the allowed set', () => {
      const event = makeEvent({
        bookmakers: [
          makeBookmaker('bovada', [
            makeMarket('player_points', [
              makeOutcome('Over', 'Jokic', -110, 26.5),
              makeOutcome('Under', 'Jokic', -110, 26.5),
            ]),
          ]),
          makeBookmaker('draftkings', [
            makeMarket('player_points', [
              makeOutcome('Over', 'Jokic', -110, 26.5),
              makeOutcome('Under', 'Jokic', -110, 26.5),
            ]),
          ]),
        ],
      });

      const result = normalizeProps([event]);
      expect(result).toHaveLength(1);
      expect(result[0].book).toBe('draftkings');
    });

    it('maps every market key to its internal stat_type (NBA + soccer)', () => {
      const markets = Object.entries(MARKET_MAP);
      const bookmaker = makeBookmaker(
        'draftkings',
        markets.map(([key]) =>
          makeMarket(key, [
            makeOutcome('Over', 'Test Player', -110, 10.5),
            makeOutcome('Under', 'Test Player', -110, 10.5),
          ])
        )
      );

      const event = makeEvent({ bookmakers: [bookmaker] });
      const result = normalizeProps([event]);

      const statTypes = result.map((p) => p.stat_type);
      const expected = Object.values(MARKET_MAP);
      expect(statTypes).toEqual(expected);
    });

    it('exposes the soccer market keys added in Session 7j', () => {
      // Sanity: soccer odds flow through the same normalizer as NBA. If a
      // future refactor splits MARKET_MAP per-sport, this test makes the
      // surface visible.
      const soccerStatTypes = ['goals', 'shots_on_target', 'shots', 'tackles',
        'cards', 'corners', 'saves', 'goals_conceded', 'passes', 'clean_sheet'];
      const values = Object.values(MARKET_MAP);
      for (const t of soccerStatTypes) {
        expect(values).toContain(t);
      }
    });

    it('Session 56 audit — batter_rbis normalizes to rbi (singular), matching the grade chain', () => {
      // The whole grade/feature/outcome chain keys on 'rbi'. If this reverts to
      // 'rbis', PropLine RBI props silently stop grading + settling again.
      expect(MARKET_MAP.batter_rbis).toBe('rbi');
      expect(MARKET_MAP.batter_doubles).toBe('doubles');
      expect(MARKET_MAP.pitcher_outs).toBe('outs');
    });

    it('exposes the NFL market keys added in the Session 31 audit', () => {
      // Defensive mapping landed before NFL is fully wired so it can't
      // repeat the MLB silent-zero bug. Both odds-api `_yds` and the
      // `_yards` spellings must resolve, and internal names align with
      // config/statFilters.js (passing/rushing/receiving_yards, interceptions).
      expect(MARKET_MAP.player_pass_yds).toBe('passing_yards');
      expect(MARKET_MAP.player_pass_yards).toBe('passing_yards');
      expect(MARKET_MAP.player_rush_yds).toBe('rushing_yards');
      expect(MARKET_MAP.player_reception_yds).toBe('receiving_yards');
      expect(MARKET_MAP.player_receiving_yards).toBe('receiving_yards');
      expect(MARKET_MAP.player_receptions).toBe('receptions');
      expect(MARKET_MAP.player_pass_interceptions).toBe('interceptions');
      expect(MARKET_MAP.player_anytime_td).toBe('anytime_td');

      // End-to-end: an NFL market normalizes to a real prop, not zero.
      const event = makeEvent({
        bookmakers: [
          makeBookmaker('draftkings', [
            makeMarket('player_pass_yds', [
              makeOutcome('Over', 'Patrick Mahomes', -110, 275.5),
              makeOutcome('Under', 'Patrick Mahomes', -110, 275.5),
            ]),
          ]),
        ],
      });
      const result = normalizeProps([event]);
      expect(result).toHaveLength(1);
      expect(result[0].stat_type).toBe('passing_yards');
      expect(result[0].player).toBe('Patrick Mahomes');
    });

    it('handles missing/null odds gracefully (skips incomplete outcomes)', () => {
      const event = makeEvent({
        bookmakers: [
          makeBookmaker('draftkings', [
            makeMarket('player_points', [
              // Missing description
              { name: 'Over', description: null, price: -110, point: 26.5 },
              // Missing point
              { name: 'Over', description: 'Jokic', price: -110, point: null },
              // Valid pair
              makeOutcome('Over', 'LeBron James', -110, 25.5),
              makeOutcome('Under', 'LeBron James', -110, 25.5),
            ]),
          ]),
        ],
      });

      const result = normalizeProps([event]);
      expect(result).toHaveLength(1);
      expect(result[0].player).toBe('LeBron James');
    });

    it('returns empty array for empty input', () => {
      expect(normalizeProps([])).toEqual([]);
    });

    it('returns empty array for events with no bookmakers', () => {
      const event = makeEvent({ bookmakers: undefined });
      expect(normalizeProps([event])).toEqual([]);
    });

    it('handles an outcome with only Over (no Under pair)', () => {
      const event = makeEvent({
        bookmakers: [
          makeBookmaker('fanduel', [
            makeMarket('player_points', [
              makeOutcome('Over', 'Solo Player', -110, 20.5),
            ]),
          ]),
        ],
      });

      const result = normalizeProps([event]);
      expect(result).toHaveLength(1);
      expect(result[0].over_odds).toBe(-110);
      expect(result[0].under_odds).toBeNull();
    });

    it('uses UTC timestamps from the API as fetched_at', () => {
      const ts = '2026-03-21T18:00:00Z';
      const event = makeEvent({
        bookmakers: [
          makeBookmaker('betmgm', [
            makeMarket('player_points', [
              makeOutcome('Over', 'Player A', -110, 10.5),
              makeOutcome('Under', 'Player A', -110, 10.5),
            ], ts),
          ]),
        ],
      });

      const result = normalizeProps([event]);
      expect(result[0].fetched_at).toBe(ts);
    });

    it('maps team names to 3-letter abbreviations', () => {
      const event = makeEvent({
        home_team: 'Golden State Warriors',
        away_team: 'Phoenix Suns',
        bookmakers: [
          makeBookmaker('draftkings', [
            makeMarket('player_points', [
              makeOutcome('Over', 'Steph Curry', -110, 28.5),
              makeOutcome('Under', 'Steph Curry', -110, 28.5),
            ]),
          ]),
        ],
      });

      const result = normalizeProps([event]);
      expect(result[0].home_team).toBe('GSW');
      expect(result[0].away_team).toBe('PHX');
    });
  });

  // Wave 6 — combat (MMA) game-level markets. Without these MARKET_MAP keys,
  // combat moneyline/round-total odds would silently normalize to zero (same
  // silent-failure class as the MLB/NHL gaps closed earlier).
  describe('MMA / combat market keys (Wave 6)', () => {
    it('maps h2h → moneyline and totals → round_total', () => {
      expect(MARKET_MAP.h2h).toBe('moneyline');
      expect(MARKET_MAP.totals).toBe('round_total');
    });
  });
});
