import { beforeEach, describe, expect, it, vi } from 'vitest';

import {
  buildTeamPropMarketCandidatesFromRows,
  formatTeamPropMarketCandidatesForPrompt,
  validateTeamPropsAgainstMarketCandidates,
} from '../team-prop-market-candidates';

const mocked = vi.hoisted(() => ({
  getTeamRoster: vi.fn(),
  resolveCanonicalName: vi.fn((name: string) => name),
  poolQuery: vi.fn(),
  fetchMlbPropCandidates: vi.fn(),
  hasTheOddsApiConfigured: vi.fn(() => false),
  getTheOddsSportKey: vi.fn(() => 'basketball_nba'),
  getTheOddsPlayerPropMarketKey: vi.fn((league: string, stat: string) => {
    if (league === 'nba' && stat === 'points') return 'player_points';
    if (league === 'nba' && stat === 'threes') return 'player_threes';
    return null;
  }),
  fetchCurrentEventOdds: vi.fn(),
  fetchCurrentOdds: vi.fn(),
  matchCurrentEvent: vi.fn(),
  selectBestPlayerPropOutcome: vi.fn(),
}));

vi.mock('../canonical-names', () => ({
  getTeamRoster: mocked.getTeamRoster,
  resolveCanonicalName: mocked.resolveCanonicalName,
  getTeamName: vi.fn((teamShort: string) => {
    if (teamShort === 'LAL') return 'Los Angeles Lakers';
    if (teamShort === 'BOS') return 'Boston Celtics';
    return '';
  }),
}));
vi.mock('../../db', () => ({ default: { query: mocked.poolQuery } }));
vi.mock('../mlb-prop-markets', () => ({
  fetchMlbPropCandidates: mocked.fetchMlbPropCandidates,
}));
vi.mock('../the-odds', () => ({
  fetchCurrentEventOdds: mocked.fetchCurrentEventOdds,
  fetchCurrentOdds: mocked.fetchCurrentOdds,
  getTheOddsPlayerPropMarketKey: mocked.getTheOddsPlayerPropMarketKey,
  getTheOddsSportKey: mocked.getTheOddsSportKey,
  hasTheOddsApiConfigured: mocked.hasTheOddsApiConfigured,
  matchCurrentEvent: mocked.matchCurrentEvent,
  selectBestPlayerPropOutcome: mocked.selectBestPlayerPropOutcome,
}));

describe('team prop market candidates', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mocked.hasTheOddsApiConfigured.mockReturnValue(false);
    mocked.getTheOddsSportKey.mockReturnValue('basketball_nba');
    mocked.getTheOddsPlayerPropMarketKey.mockImplementation((league: string, stat: string) => {
      if (league === 'nba' && stat === 'points') return 'player_points';
      if (league === 'nba' && stat === 'threes') return 'player_threes';
      return null;
    });
    mocked.fetchCurrentOdds.mockResolvedValue([]);
    mocked.fetchMlbPropCandidates.mockResolvedValue([]);
    mocked.fetchCurrentEventOdds.mockResolvedValue({
      timestamp: '2026-03-29T23:00:00.000Z',
      eventId: 'evt-1',
      sportKey: 'basketball_nba',
      commenceTime: '2026-03-29T23:30:00.000Z',
      homeTeam: 'Phoenix Suns',
      awayTeam: 'Boston Celtics',
      bookmakers: [],
    });
    mocked.matchCurrentEvent.mockReturnValue(null);
    mocked.selectBestPlayerPropOutcome.mockReturnValue(null);
  });

  it('builds roster-filtered grouped candidates from raw PlayerPropLine rows', () => {
    mocked.getTeamRoster.mockReturnValue(['Devin Booker', 'Kevin Durant']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -115,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -105,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
        {
          playerExternalId: 'Jalen_Green_999_NBA',
          propType: 'points',
          lineValue: 21.5,
          oddsAmerican: -110,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Kevin_Durant_321_NBA',
          propType: 'points+rebounds+assists',
          lineValue: 40.5,
          oddsAmerican: -110,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
      ],
      { league: 'nba', teamShort: 'PHX' },
    );

    expect(candidates).toEqual(expect.arrayContaining([
      expect.objectContaining({
        player: 'Devin Booker',
        statType: 'points',
        marketLineValue: 27.5,
        overOdds: -115,
        underOdds: -105,
        availableSides: ['over', 'under'],
      }),
      expect.objectContaining({
        player: 'Kevin Durant',
        statType: 'pointsreboundsassists',
        marketLineValue: 40.5,
      }),
    ]));

    expect(formatTeamPropMarketCandidatesForPrompt(candidates, 'Phoenix Suns')).toContain('Devin Booker');
  });

  it('uses raw bookmaker metadata and game_ou market directions for SGO-carried FanDuel rows', () => {
    mocked.getTeamRoster.mockReturnValue(['Brandon Miller']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'BRANDON_MILLER_1_NBA',
          propType: 'points',
          lineValue: 19.5,
          oddsAmerican: -128,
          vendor: 'sportsgameodds',
          market: 'game_ou_under',
          marketScope: null,
          raw: { bookmaker: 'fanduel', side: 'under', period: 'game' },
        },
      ],
      { league: 'nba', teamShort: 'CHA' },
    );

    expect(candidates).toEqual(expect.arrayContaining([
      expect.objectContaining({
        player: 'Brandon Miller',
        statType: 'points',
        marketLineValue: 19.5,
        underOdds: -128,
        availableSides: ['under'],
        sourceMap: [expect.objectContaining({ side: 'under', source: 'fanduel' })],
      }),
    ]));
  });

  it('supports expanded WNBA stat markets through the shared registry', () => {
    mocked.getTeamRoster.mockReturnValue(['Caitlin Clark']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'CAITLIN_CLARK_22_WNBA',
          propType: 'three_pointers_made',
          lineValue: 3.5,
          oddsAmerican: -102,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'CAITLIN_CLARK_22_WNBA',
          propType: 'three_pointers_made',
          lineValue: 3.5,
          oddsAmerican: -118,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
      ],
      { league: 'wnba', teamShort: 'IND' },
    );

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Caitlin Clark',
        statType: 'threes',
        normalizedStatType: 'threes',
        marketLineValue: 3.5,
        overOdds: -102,
        underOdds: -118,
        availableSides: ['over', 'under'],
      }),
    ]);
  });

  it('suppresses garbage one-sided low-line markets and keeps the best line per player stat', () => {
    mocked.getTeamRoster.mockReturnValue(['Chet Holmgren', 'Shai Gilgeous-Alexander']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'Chet_Holmgren_123_NBA',
          propType: 'points',
          lineValue: 3.5,
          oddsAmerican: -700,
          vendor: 'sportsgameodds',
          raw: { side: 'under' },
        },
        {
          playerExternalId: 'Shai_Gilgeous-Alexander_456_NBA',
          propType: 'points',
          lineValue: 30.5,
          oddsAmerican: -115,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Shai_Gilgeous-Alexander_456_NBA',
          propType: 'points',
          lineValue: 30.5,
          oddsAmerican: -105,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
        {
          playerExternalId: 'Shai_Gilgeous-Alexander_456_NBA',
          propType: 'points',
          lineValue: 32.5,
          oddsAmerican: -110,
          vendor: 'sportsgameodds',
          raw: { side: 'under' },
        },
      ],
      { league: 'nba', teamShort: 'OKC' },
    );

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Shai Gilgeous-Alexander',
        statType: 'points',
        marketLineValue: 30.5,
        overOdds: -115,
        underOdds: -105,
      }),
    ]);
  });

  it('suppresses absurd nhl ladder markets that only offer one side', () => {
    mocked.getTeamRoster.mockReturnValue(['Alex Newhook', 'Cole Caufield']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'Alex_Newhook_123_NHL',
          propType: 'points',
          lineValue: 0.5,
          oddsAmerican: -750,
          vendor: 'sportsgameodds',
          raw: { side: 'under' },
        },
        {
          playerExternalId: 'Alex_Newhook_123_NHL',
          propType: 'points',
          lineValue: 1.5,
          oddsAmerican: 4000,
          vendor: 'sportsgameodds',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Cole_Caufield_456_NHL',
          propType: 'shots_onGoal',
          lineValue: 2.5,
          oddsAmerican: -125,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Cole_Caufield_456_NHL',
          propType: 'shots_onGoal',
          lineValue: 2.5,
          oddsAmerican: 100,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
      ],
      { league: 'nhl', teamShort: 'MTL' },
    );

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Cole Caufield',
        statType: 'shots_onGoal',
        marketLineValue: 2.5,
        overOdds: -125,
        underOdds: 100,
      }),
    ]);
  });

  it('pairs missing sides across books and marks the market as multi-source complete', () => {
    mocked.getTeamRoster.mockReturnValue(['Jayson Tatum']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'Jayson_Tatum_1_NBA',
          propType: 'points',
          lineValue: 29.5,
          oddsAmerican: -118,
          vendor: 'consensus',
          updatedAt: '2026-03-29T20:00:00.000Z',
          oddId: 'consensus-over',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Jayson_Tatum_1_NBA',
          propType: 'points',
          lineValue: 29.5,
          oddsAmerican: -102,
          vendor: 'draftkings',
          updatedAt: '2026-03-29T20:01:00.000Z',
          oddId: 'dk-under',
          raw: { side: 'under' },
        },
      ],
      { league: 'nba', teamShort: 'BOS' },
    );

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Jayson Tatum',
        statType: 'points',
        marketLineValue: 29.5,
        overOdds: -118,
        underOdds: -102,
        completenessStatus: 'multi_source_complete',
        completionMethod: 'multi_source',
        isGapFilled: true,
        source: 'draftkings',
        sourceMap: [
          expect.objectContaining({ side: 'over', source: 'consensus', rawMarketId: 'consensus-over' }),
          expect.objectContaining({ side: 'under', source: 'draftkings', rawMarketId: 'dk-under' }),
        ],
      }),
    ]);
  });

  it('supports expanded registry-backed nba markets like threes', () => {
    mocked.getTeamRoster.mockReturnValue(['Desmond Bane']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'Desmond_Bane_1_NBA',
          propType: 'three_pointers_made',
          lineValue: 3.5,
          oddsAmerican: -105,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Desmond_Bane_1_NBA',
          propType: 'threes',
          lineValue: 3.5,
          oddsAmerican: -125,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
      ],
      { league: 'nba', teamShort: 'MEM' },
    );

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Desmond Bane',
        statType: 'threes',
        normalizedStatType: 'threes',
        propLabel: '3PT Made 3.5',
        overOdds: -105,
        underOdds: -125,
      }),
    ]);
  });

  it('suppresses one-sided basketball micro markets that create fake surface area', () => {
    mocked.getTeamRoster.mockReturnValue(['Scottie Barnes', 'Immanuel Quickley', 'RJ Barrett']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'Scottie_Barnes_1_NBA',
          propType: 'steals',
          lineValue: 1.5,
          oddsAmerican: -110,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
        {
          playerExternalId: 'Immanuel_Quickley_2_NBA',
          propType: 'blocks',
          lineValue: 0.5,
          oddsAmerican: 120,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'RJ_Barrett_3_NBA',
          propType: 'turnovers',
          lineValue: 2.5,
          oddsAmerican: -125,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
        {
          playerExternalId: 'Scottie_Barnes_1_NBA',
          propType: 'points',
          lineValue: 19.5,
          oddsAmerican: -112,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Scottie_Barnes_1_NBA',
          propType: 'points',
          lineValue: 19.5,
          oddsAmerican: -108,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
      ],
      { league: 'nba', teamShort: 'TOR' },
    );

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Scottie Barnes',
        statType: 'points',
        marketLineValue: 19.5,
        availableSides: ['over', 'under'],
      }),
    ]);
  });

  it('matches source-backed rows when upstream uses full team names instead of short codes', async () => {
    mocked.getTeamRoster.mockReturnValue(['LeBron James']);
    mocked.poolQuery.mockResolvedValueOnce({
      rows: [
        {
          playerExternalId: 'LeBron_James_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -115,
          vendor: 'fanduel',
          marketName: 'LeBron James Points Over/Under',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'LeBron_James_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -105,
          vendor: 'draftkings',
          marketName: 'LeBron James Points Over/Under',
          raw: { side: 'under' },
        },
      ],
    });

    const { fetchTeamPropMarketCandidates } = await import('../team-prop-market-candidates');
    const candidates = await fetchTeamPropMarketCandidates({
      league: 'nba',
      teamShort: 'LAL',
      opponentShort: 'BOS',
      teamName: 'Los Angeles Lakers',
      opponentName: 'Boston Celtics',
      homeTeam: 'Los Angeles Lakers',
      awayTeam: 'Boston Celtics',
      startsAt: '2026-03-29T23:30:00.000Z',
    });

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Lebron James',
        statType: 'points',
        marketLineValue: 27.5,
        overOdds: -115,
        underOdds: -105,
      }),
    ]);
    expect(mocked.poolQuery).toHaveBeenCalledTimes(1);
    expect(mocked.poolQuery.mock.calls[0][1][2]).toEqual(expect.arrayContaining(['LAL', 'LOS ANGELES LAKERS']));
    expect(mocked.poolQuery.mock.calls[0][1][3]).toEqual(expect.arrayContaining(['BOS', 'BOSTON CELTICS']));
  });

  it('fetches alias prop types so canonical normalization can recover valid rows', async () => {
    mocked.getTeamRoster.mockReturnValue(['Devin Booker', 'Kevin Durant']);
    mocked.poolQuery.mockResolvedValueOnce({
      rows: [
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'threePointersMade',
          lineValue: 3.5,
          oddsAmerican: -102,
          vendor: 'fanduel',
          marketName: 'Devin Booker 3PT Made Over/Under',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'threePointersMade',
          lineValue: 3.5,
          oddsAmerican: -118,
          vendor: 'draftkings',
          marketName: 'Devin Booker 3PT Made Over/Under',
          raw: { side: 'under' },
        },
        {
          playerExternalId: 'Kevin_Durant_321_NBA',
          propType: 'points+rebounds+assists',
          lineValue: 41.5,
          oddsAmerican: -110,
          vendor: 'fanduel',
          marketName: 'Kevin Durant Pts + Reb + Ast Over/Under',
          raw: { side: 'over' },
        },
      ],
    });

    const { fetchTeamPropMarketCandidates } = await import('../team-prop-market-candidates');
    const candidates = await fetchTeamPropMarketCandidates({
      league: 'nba',
      teamShort: 'PHX',
      opponentShort: 'BOS',
      teamName: 'Phoenix Suns',
      opponentName: 'Boston Celtics',
      homeTeam: 'Phoenix Suns',
      awayTeam: 'Boston Celtics',
      startsAt: '2026-03-29T23:30:00.000Z',
      skipTheOddsVerification: true,
    });

    expect(candidates).toEqual(expect.arrayContaining([
      expect.objectContaining({
        player: 'Devin Booker',
        statType: 'threes',
        marketLineValue: 3.5,
        overOdds: -102,
        underOdds: -118,
      }),
      expect.objectContaining({
        player: 'Kevin Durant',
        statType: 'pointsreboundsassists',
        marketLineValue: 41.5,
      }),
    ]));
    expect(mocked.poolQuery.mock.calls[0][1][4]).toEqual(expect.arrayContaining([
      'threes',
      'threepointersmade',
      'pointsreboundsassists',
      'points+rebounds+assists',
    ]));
  });

  it('keeps valid candidates when TheOdds does not return a FanDuel overlay', async () => {
    mocked.getTeamRoster.mockReturnValue(['Devin Booker']);
    mocked.poolQuery.mockResolvedValueOnce({
      rows: [
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -112,
          vendor: 'draftkings',
          updatedAt: '2026-03-29T20:00:00.000Z',
          raw: { side: 'over' },
        },
      ],
    });
    mocked.hasTheOddsApiConfigured.mockReturnValue(true);
    mocked.fetchCurrentOdds.mockResolvedValue([
      {
        id: 'evt-1',
        sport_key: 'basketball_nba',
        sport_title: 'NBA',
        commence_time: '2026-03-29T23:30:00.000Z',
        home_team: 'Phoenix Suns',
        away_team: 'Boston Celtics',
        bookmakers: [],
      },
    ]);
    mocked.matchCurrentEvent.mockReturnValue({
      id: 'evt-1',
      sport_key: 'basketball_nba',
      sport_title: 'NBA',
      commence_time: '2026-03-29T23:30:00.000Z',
      home_team: 'Phoenix Suns',
      away_team: 'Boston Celtics',
      bookmakers: [],
    });
    mocked.fetchCurrentEventOdds.mockResolvedValue({
      timestamp: '2026-03-29T23:00:00.000Z',
      eventId: 'evt-1',
      sportKey: 'basketball_nba',
      commenceTime: '2026-03-29T23:30:00.000Z',
      homeTeam: 'Phoenix Suns',
      awayTeam: 'Boston Celtics',
      bookmakers: [],
    });

    const { fetchTeamPropMarketCandidates } = await import('../team-prop-market-candidates');
    const candidates = await fetchTeamPropMarketCandidates({
      league: 'nba',
      teamShort: 'PHX',
      opponentShort: 'BOS',
      teamName: 'Phoenix Suns',
      opponentName: 'Boston Celtics',
      homeTeam: 'Phoenix Suns',
      awayTeam: 'Boston Celtics',
      startsAt: '2026-03-29T23:30:00.000Z',
    });

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Devin Booker',
        statType: 'points',
        marketLineValue: 27.5,
        overOdds: -112,
        underOdds: null,
        source: 'draftkings',
      }),
    ]);
  });

  it('falls back to per-market event odds fetch when a batched The Odds player-prop request is rejected', async () => {
    mocked.getTeamRoster.mockReturnValue(['Devin Booker']);
    mocked.poolQuery.mockResolvedValueOnce({
      rows: [
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -112,
          vendor: 'draftkings',
          updatedAt: '2026-03-29T20:00:00.000Z',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'threes',
          lineValue: 3.5,
          oddsAmerican: -118,
          vendor: 'draftkings',
          updatedAt: '2026-03-29T20:00:00.000Z',
          raw: { side: 'under' },
        },
      ],
    });
    mocked.hasTheOddsApiConfigured.mockReturnValue(true);
    mocked.fetchCurrentOdds.mockResolvedValue([
      {
        id: 'evt-1',
        sport_key: 'basketball_nba',
        sport_title: 'NBA',
        commence_time: '2026-03-29T23:30:00.000Z',
        home_team: 'Phoenix Suns',
        away_team: 'Boston Celtics',
        bookmakers: [],
      },
    ]);
    mocked.matchCurrentEvent.mockReturnValue({
      id: 'evt-1',
      sport_key: 'basketball_nba',
      sport_title: 'NBA',
      commence_time: '2026-03-29T23:30:00.000Z',
      home_team: 'Phoenix Suns',
      away_team: 'Boston Celtics',
      bookmakers: [],
    });
    mocked.fetchCurrentEventOdds
      .mockRejectedValueOnce(new Error('TheOddsAPI 422: {"error_code":"INVALID_MARKET"}'))
      .mockResolvedValueOnce({
        timestamp: '2026-03-29T23:00:00.000Z',
        eventId: 'evt-1',
        sportKey: 'basketball_nba',
        commenceTime: '2026-03-29T23:30:00.000Z',
        homeTeam: 'Phoenix Suns',
        awayTeam: 'Boston Celtics',
        bookmakers: [{
          key: 'fanduel',
          title: 'FanDuel',
          markets: [{
            key: 'player_points',
            outcomes: [
              { name: 'Over', description: 'Devin Booker', price: -105, point: 27.5 },
              { name: 'Under', description: 'Devin Booker', price: -115, point: 27.5 },
            ],
          }],
        }],
      })
      .mockRejectedValueOnce(new Error('TheOddsAPI 422: {"error_code":"INVALID_MARKET"}'));
    mocked.selectBestPlayerPropOutcome.mockImplementation((snapshot: any, params: any) => {
      if (params.marketKey !== 'player_points') return null;
      if (params.direction === 'over') return { bookmakerKey: 'draftkings', bookmakerTitle: 'DraftKings', odds: -105, line: 27.5 };
      if (params.direction === 'under') return { bookmakerKey: 'draftkings', bookmakerTitle: 'DraftKings', odds: -115, line: 27.5 };
      return null;
    });

    const { fetchTeamPropMarketCandidates } = await import('../team-prop-market-candidates');
    const candidates = await fetchTeamPropMarketCandidates({
      league: 'nba',
      teamShort: 'PHX',
      opponentShort: 'BOS',
      teamName: 'Phoenix Suns',
      opponentName: 'Boston Celtics',
      homeTeam: 'Phoenix Suns',
      awayTeam: 'Boston Celtics',
      startsAt: '2026-03-29T23:30:00.000Z',
    });

    expect(mocked.fetchCurrentOdds).toHaveBeenCalledWith(expect.objectContaining({
      sportKey: 'basketball_nba',
      markets: ['h2h'],
    }));
    expect(mocked.fetchCurrentOdds.mock.calls[0]?.[0]?.bookmakers).toBeUndefined();
    expect(mocked.fetchCurrentEventOdds).toHaveBeenCalledTimes(3);
    expect(candidates).toEqual(expect.arrayContaining([
      expect.objectContaining({
        player: 'Devin Booker',
        statType: 'points',
        overOdds: -112,
        underOdds: -115,
        availableSides: ['over', 'under'],
        source: 'draftkings',
        completenessStatus: 'source_complete',
        completionMethod: 'source',
      }),
      expect.objectContaining({
        player: 'Devin Booker',
        statType: 'threes',
        overOdds: null,
        underOdds: -118,
        availableSides: ['under'],
      }),
    ]));
  });

  it('fills missing sides from any supported The Odds bookmaker instead of only FanDuel', async () => {
    mocked.getTeamRoster.mockReturnValue(['Troy Terry']);
    mocked.getTheOddsSportKey.mockReturnValueOnce('icehockey_nhl');
    mocked.getTheOddsPlayerPropMarketKey.mockImplementation(((league: string, stat: string) => {
      if (league === 'nhl' && stat === 'shots_onGoal') return 'player_shots_on_goal';
      if (league === 'nba' && stat === 'points') return 'player_points';
      if (league === 'nba' && stat === 'threes') return 'player_threes';
      return null;
    }) as any);
    mocked.hasTheOddsApiConfigured.mockReturnValue(true);
    mocked.poolQuery.mockResolvedValueOnce({
      rows: [
        {
          playerExternalId: 'Troy_Terry_1_NHL',
          propType: 'shots_onGoal',
          lineValue: 2.5,
          oddsAmerican: -155,
          vendor: 'betmgm',
          updatedAt: '2026-03-29T20:00:00.000Z',
          oddId: 'betmgm-under',
          raw: { side: 'under' },
        },
      ],
    });
    mocked.fetchCurrentOdds.mockResolvedValueOnce([
      {
        id: 'evt-ana-sj',
        sport_key: 'icehockey_nhl',
        sport_title: 'NHL',
        commence_time: '2026-03-29T23:30:00.000Z',
        home_team: 'San Jose Sharks',
        away_team: 'Anaheim Ducks',
        bookmakers: [],
      },
    ]);
    mocked.matchCurrentEvent.mockReturnValueOnce({
      id: 'evt-ana-sj',
      sport_key: 'icehockey_nhl',
      sport_title: 'NHL',
      commence_time: '2026-03-29T23:30:00.000Z',
      home_team: 'San Jose Sharks',
      away_team: 'Anaheim Ducks',
      bookmakers: [],
    });
    mocked.fetchCurrentEventOdds.mockResolvedValueOnce({
      timestamp: '2026-03-29T23:00:00.000Z',
      eventId: 'evt-ana-sj',
      sportKey: 'icehockey_nhl',
      commenceTime: '2026-03-29T23:30:00.000Z',
      homeTeam: 'San Jose Sharks',
      awayTeam: 'Anaheim Ducks',
      bookmakers: [
        {
          key: 'draftkings',
          title: 'DraftKings',
          markets: [{
            key: 'player_shots_on_goal',
            outcomes: [
              { name: 'Over', description: 'Troy Terry', price: 120, point: 2.5 },
              { name: 'Under', description: 'Troy Terry', price: -145, point: 2.5 },
            ],
          }],
        },
        {
          key: 'betmgm',
          title: 'BetMGM',
          markets: [{
            key: 'player_shots_on_goal',
            outcomes: [
              { name: 'Under', description: 'Troy Terry', price: -155, point: 2.5 },
            ],
          }],
        },
      ],
    });
    mocked.selectBestPlayerPropOutcome.mockImplementation((snapshot: any, params: any) => {
      if (params.marketKey !== 'player_shots_on_goal') return null;
      if (params.direction === 'over') {
        return {
          bookmakerKey: 'draftkings',
          bookmakerTitle: 'DraftKings',
          odds: 120,
          line: 2.5,
          marketUpdatedAt: '2026-03-29T23:00:00.000Z',
        };
      }
      if (params.direction === 'under') {
        return {
          bookmakerKey: 'betmgm',
          bookmakerTitle: 'BetMGM',
          odds: -155,
          line: 2.5,
          marketUpdatedAt: '2026-03-29T23:00:00.000Z',
        };
      }
      return null;
    });

    const { fetchTeamPropMarketCandidates } = await import('../team-prop-market-candidates');
    const candidates = await fetchTeamPropMarketCandidates({
      league: 'nhl',
      teamShort: 'ANA',
      opponentShort: 'SJ',
      teamName: 'Anaheim Ducks',
      opponentName: 'San Jose Sharks',
      homeTeam: 'San Jose Sharks',
      awayTeam: 'Anaheim Ducks',
      startsAt: '2026-03-29T23:30:00.000Z',
    });

    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Troy Terry',
        statType: 'shots_onGoal',
        overOdds: 120,
        underOdds: -155,
        availableSides: ['over', 'under'],
        source: 'draftkings',
        completenessStatus: 'multi_source_complete',
        completionMethod: 'multi_source',
        isGapFilled: true,
        sourceMap: [
          expect.objectContaining({ side: 'over', source: 'draftkings', odds: 120 }),
          expect.objectContaining({ side: 'under', source: 'betmgm', odds: -155, rawMarketId: 'betmgm-under' }),
        ],
      }),
    ]);
  });

  it('routes mlb candidate fetches through the normalized mlb market pipeline', async () => {
    mocked.fetchMlbPropCandidates.mockResolvedValueOnce([
      {
        player: 'Shohei Ohtani',
        teamShort: 'LAD',
        statType: 'batting_hits',
        normalizedStatType: 'batting_hits',
        prop: 'Hits 1.5',
        marketLineValue: 1.5,
        overOdds: -110,
        underOdds: -105,
        availableSides: ['over', 'under'],
        gameStart: '2026-03-29T23:30:00.000Z',
        marketName: 'Shohei Ohtani Hits Over/Under',
        source: 'player_prop_line',
        completenessStatus: 'multi_source_complete',
        completionMethod: 'multi_source',
        isGapFilled: true,
        sourceMap: [],
      },
    ]);

    const { fetchTeamPropMarketCandidates } = await import('../team-prop-market-candidates');
    const candidates = await fetchTeamPropMarketCandidates({
      league: 'mlb',
      teamShort: 'LAD',
      opponentShort: 'SEA',
      teamName: 'Los Angeles Dodgers',
      opponentName: 'Seattle Mariners',
      homeTeam: 'Los Angeles Dodgers',
      awayTeam: 'Seattle Mariners',
      startsAt: '2026-03-29T23:30:00.000Z',
    });

    expect(mocked.fetchMlbPropCandidates).toHaveBeenCalledWith({
      teamShort: 'LAD',
      teamName: 'Los Angeles Dodgers',
      opponentShort: 'SEA',
      startsAt: '2026-03-29T23:30:00.000Z',
    });
    expect(mocked.poolQuery).not.toHaveBeenCalled();
    expect(candidates).toEqual([
      expect.objectContaining({
        player: 'Shohei Ohtani',
        statType: 'batting_hits',
        normalizedStatType: 'batting_hits',
        propLabel: 'Hits 1.5',
        marketLineValue: 1.5,
        overOdds: -110,
        underOdds: -105,
        availableSides: ['over', 'under'],
        completenessStatus: 'multi_source_complete',
        completionMethod: 'multi_source',
        isGapFilled: true,
      }),
    ]);
  });

  it('drops generated props that do not match real source-backed candidates', () => {
    mocked.getTeamRoster.mockReturnValue(['Devin Booker']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -115,
          vendor: 'fanduel',
          raw: { side: 'over' },
        },
        {
          playerExternalId: 'Devin_Booker_123_NBA',
          propType: 'points',
          lineValue: 27.5,
          oddsAmerican: -105,
          vendor: 'draftkings',
          raw: { side: 'under' },
        },
      ],
      { league: 'nba', teamShort: 'PHX' },
    );

    const validated = validateTeamPropsAgainstMarketCandidates(
      [
        {
          player: 'Devin Booker',
          prop: 'Points Over 27.5',
          recommendation: 'over',
          reasoning: 'x',
          stat_type: 'points',
          market_line_value: 27.5,
        },
        {
          player: 'Jalen Green',
          prop: 'Points Over 21.5',
          recommendation: 'over',
          reasoning: 'x',
          stat_type: 'points',
          market_line_value: 21.5,
        },
        {
          player: 'Devin Booker',
          prop: 'Assists Over 6.5',
          recommendation: 'over',
          reasoning: 'x',
          stat_type: 'assists',
          market_line_value: 6.5,
        },
      ],
      candidates,
      'nba',
    );

    expect(validated).toEqual([
      expect.objectContaining({
        player: 'Devin Booker',
        stat_type: 'points',
        market_line_value: 27.5,
      }),
    ]);
  });

  it('drops generated props when the requested side is not actually priced', () => {
    mocked.getTeamRoster.mockReturnValue(['Brandon Miller']);

    const candidates = buildTeamPropMarketCandidatesFromRows(
      [
        {
          playerExternalId: 'BRANDON_MILLER_1_NBA',
          propType: 'points',
          lineValue: 19.5,
          oddsAmerican: -128,
          vendor: 'sportsgameodds',
          market: 'game_ou_under',
          marketScope: null,
          raw: { bookmaker: 'fanduel', side: 'under', period: 'game' },
        },
      ],
      { league: 'nba', teamShort: 'CHA' },
    );

    const validated = validateTeamPropsAgainstMarketCandidates(
      [
        {
          player: 'Brandon Miller',
          prop: 'Points Over 19.5',
          recommendation: 'over',
          reasoning: 'x',
          stat_type: 'points',
          market_line_value: 19.5,
        },
      ],
      candidates,
      'nba',
    );

    expect(validated).toEqual([]);
  });
});
