// Wave 0 — ESPN per-athlete gamelog parser + resolver (the NBA/WNBA grade
// unlock's free source). Hermetic: fetch is injected, no network.

// Redis is a no-op here so the cache read/write paths don't touch a live server.
jest.mock('../../src/utils/redis', () => ({
  cacheGet: async () => null,
  cacheSet: async () => {},
  cacheDel: async () => {},
}));

const espn = require('../../src/services/adapters/espnStatsAdapter');

// Real ESPN NBA gamelog shape (verified live 2026-07-13): a per-response
// `names[]` array + seasonTypes[].categories[].events[{eventId, stats[]}], with
// dates/opponents in a top-level `events` map keyed by eventId. Column order is
// indexed by name token — NBA and WNBA differ, so this is not positional.
const NBA_NAMES = [
  'minutes', 'fieldGoalsMade-fieldGoalsAttempted', 'fieldGoalPct',
  'threePointFieldGoalsMade-threePointFieldGoalsAttempted', 'threePointPct',
  'freeThrowsMade-freeThrowsAttempted', 'freeThrowPct', 'totalRebounds',
  'assists', 'blocks', 'steals', 'fouls', 'turnovers', 'points',
];
//                min   FG     FG%    3PT   3P%   FT    FT%   REB AST BLK STL PF TO PTS
const G_OLD = ['32', '9-19', '47.4', '1-4', '25.0', '4-4', '100', '8', '5', '0', '2', '3', '2', '23'];
const G_MID = ['36', '10-20', '50.0', '3-7', '42.9', '5-6', '83.3', '10', '7', '1', '1', '2', '4', '28'];
const G_NEW = ['40', '8-18', '44.4', '2-6', '33.3', '6-8', '75.0', '12', '3', '1', '0', '1', '4', '24'];

const NBA_FIXTURE = {
  names: NBA_NAMES,
  labels: ['MIN', 'FG', 'FG%', '3PT', '3P%', 'FT', 'FT%', 'REB', 'AST', 'BLK', 'STL', 'PF', 'TO', 'PTS'],
  seasonTypes: [
    {
      displayName: '2025-26 Regular Season',
      categories: [
        {
          displayName: 'January',
          events: [
            { eventId: 'E_OLD', stats: G_OLD },
            { eventId: 'E_MID', stats: G_MID },
            { eventId: 'E_NEW', stats: G_NEW },
          ],
        },
      ],
    },
  ],
  events: {
    E_OLD: { id: 'E_OLD', gameDate: '2026-01-01T00:30:00.000+00:00', atVs: '@', opponent: { abbreviation: 'BOS' } },
    E_MID: { id: 'E_MID', gameDate: '2026-01-03T00:30:00.000+00:00', atVs: 'vs', opponent: { abbreviation: 'GSW' } },
    E_NEW: { id: 'E_NEW', gameDate: '2026-01-05T00:30:00.000+00:00', atVs: 'vs', opponent: { abbreviation: 'HOU' } },
  },
};

const V2_SEARCH = {
  results: [
    {
      type: 'player',
      contents: [
        { uid: 's:40~l:46~a:1966', id: 'guid-abc', displayName: 'LeBron James', sport: 'basketball', defaultLeagueSlug: 'nba' },
      ],
    },
  ],
};

function fetchImplFor(searchPayload, gamelogPayload) {
  return async (url) => {
    if (url.includes('/search/v2')) return searchPayload;
    if (url.includes('/gamelog')) return gamelogPayload;
    return null;
  };
}

beforeEach(() => espn.__internals.gameLogMem.clear());

describe('parseGameLog (pure)', () => {
  it('normalizes per-game rows keyed by VYNDR stat names, most-recent first', () => {
    const rows = espn.parseGameLog(NBA_FIXTURE);
    expect(Array.isArray(rows)).toBe(true);
    expect(rows).toHaveLength(3);
    // most-recent first → E_NEW leads
    expect(rows[0].date).toBe('2026-01-05T00:30:00.000+00:00');
    expect(rows[0].opponent).toBe('HOU');
    expect(rows[0].isHome).toBe(true);
    expect(rows[0].stat).toMatchObject({
      points: 24, rebounds: 12, assists: 3, threes: 2, steals: 0, blocks: 1, turnovers: 4, minutes: 40,
    });
    // pra computed, not a raw column
    expect(rows[0].stat.pra).toBe(24 + 12 + 3);
    // oldest last, away game
    expect(rows[2].date).toBe('2026-01-01T00:30:00.000+00:00');
    expect(rows[2].isHome).toBe(false);
  });

  it('indexes columns by the response names[] (WNBA order differs from NBA)', () => {
    const wnbaNames = [
      'minutes', 'points', 'totalRebounds', 'assists', 'steals', 'blocks', 'turnovers',
      'fieldGoalsMade-fieldGoalsAttempted', 'fieldGoalPct',
      'threePointFieldGoalsMade-threePointFieldGoalsAttempted', 'threePointPct',
      'freeThrowsMade-freeThrowsAttempted', 'freeThrowPct', 'fouls',
    ];
    //         min  PTS  REB  AST STL BLK TO   FG      FG%   3PT   3P%  FT    FT%  PF
    const row = ['31', '20', '12', '2', '1', '2', '3', '9-23', '39.1', '2-5', '40.0', '2-2', '100.0', '2'];
    const wnbaFixture = {
      names: wnbaNames,
      seasonTypes: [{ categories: [{ events: [{ eventId: 'W1', stats: row }] }] }],
      events: { W1: { gameDate: '2026-07-13T01:00:00.000+00:00', atVs: 'vs', opponent: { abbreviation: 'IND' } } },
    };
    const rows = espn.parseGameLog(wnbaFixture);
    expect(rows).toHaveLength(1);
    expect(rows[0].stat).toMatchObject({ points: 20, rebounds: 12, assists: 2, steals: 1, blocks: 2, turnovers: 3, threes: 2 });
  });

  it('omits a stat ESPN did not report (absent, never 0)', () => {
    // Drop the points column entirely.
    const noPtsNames = NBA_NAMES.slice(0, -1); // remove 'points'
    const fixture = {
      names: noPtsNames,
      seasonTypes: [{ categories: [{ events: [{ eventId: 'X', stats: G_NEW.slice(0, -1) }] }] }],
      events: { X: { gameDate: '2026-01-05T00:30:00.000+00:00' } },
    };
    const rows = espn.parseGameLog(fixture);
    expect(rows[0].stat).not.toHaveProperty('points');
    expect(rows[0].stat).not.toHaveProperty('pra'); // pra needs all three → absent
    expect(rows[0].stat.rebounds).toBe(12);
  });

  it('returns null on an unrecognized shape and never throws', () => {
    expect(espn.parseGameLog(null)).toBeNull();
    expect(espn.parseGameLog({})).toBeNull();
    expect(espn.parseGameLog({ names: [], seasonTypes: [] })).toBeNull();
    expect(espn.parseGameLog({ names: NBA_NAMES, seasonTypes: [] })).toBeNull();
    expect(() => espn.parseGameLog({ names: NBA_NAMES, seasonTypes: 'bad', events: 5 })).not.toThrow();
  });
});

describe('resolveAthleteId', () => {
  it('extracts the numeric athlete id from the v2 uid', async () => {
    const id = await espn.resolveAthleteId('LeBron James', 'nba', { fetchImpl: fetchImplFor(V2_SEARCH, NBA_FIXTURE) });
    expect(id).toBe('1966');
  });

  it('disambiguates by league (a WNBA query never returns the NCAA namesake)', async () => {
    const dual = {
      results: [{
        type: 'player',
        contents: [
          { uid: 's:40~l:59~a:3149391', displayName: "A'ja Wilson", defaultLeagueSlug: 'wnba' },
          { uid: 's:40~l:54~a:4412077', displayName: "A'Ja Wilson", defaultLeagueSlug: 'womens-college-basketball' },
        ],
      }],
    };
    const id = await espn.resolveAthleteId("A'ja Wilson", 'wnba', { fetchImpl: fetchImplFor(dual, NBA_FIXTURE) });
    expect(id).toBe('3149391');
  });

  it('returns null when no player section matches', async () => {
    const id = await espn.resolveAthleteId('Nobody', 'nba', { fetchImpl: fetchImplFor({ results: [] }, NBA_FIXTURE) });
    expect(id).toBeNull();
  });
});

describe('getPlayerGameLog', () => {
  it('resolves → fetches → normalizes into { found, id, last10 }', async () => {
    const res = await espn.getPlayerGameLog('LeBron James', 'nba', { fetchImpl: fetchImplFor(V2_SEARCH, NBA_FIXTURE) });
    expect(res.found).toBe(true);
    expect(res.id).toBe('1966');
    expect(res.last10).toHaveLength(3);
    expect(res.last10[0].stat.points).toBe(24);
  });

  it('returns { found:false } for an unknown sport or empty name (no throw)', async () => {
    await expect(espn.getPlayerGameLog('X', 'nfl')).resolves.toEqual({ found: false });
    await expect(espn.getPlayerGameLog('', 'nba')).resolves.toEqual({ found: false });
  });

  it('returns { found:false } when the gamelog shape is unrecognized', async () => {
    const res = await espn.getPlayerGameLog('LeBron James', 'nba', { fetchImpl: fetchImplFor(V2_SEARCH, { garbage: true }) });
    expect(res.found).toBe(false);
  });
});
