/* ============================================================
   Session 77 — WIRING the dormant adjusters. Pure input-wiring; the adjusters'
   internal logic is not touched. Proves environment + matchup reach the grade.
   ============================================================ */

const ec = require('../../src/services/environmentContext');
const ch = require('../../src/services/challengerProjection');

const SCHEDULE = { games: [
  { homeTeam: { abbreviation: 'COL' }, awayTeam: { abbreviation: 'LAD' }, gameTime: '2026-07-21T20:10Z', venue: 'Coors Field' },
  { homeTeam: { abbreviation: 'TB' }, awayTeam: { abbreviation: 'NYY' }, gameTime: '2026-07-21T23:05Z', venue: 'Tropicana Field' },
] };
const PITCHERS = { games: [
  { home: { team: 'Colorado Rockies', pitcherId: 111 }, away: { team: 'Los Angeles Dodgers', pitcherId: 222 } },
  { home: { team: 'Tampa Bay Rays', pitcherId: 333 }, away: { team: 'New York Yankees', pitcherId: 444 } },
] };
const PEOPLE = { people: [
  { id: 111, pitchHand: { code: 'R' } }, { id: 222, pitchHand: { code: 'L' } },
  { id: 333, pitchHand: { code: 'R' } }, { id: 444, pitchHand: { code: 'R' } },
] };
// A Coors hitter (LAD) facing the Rockies' RHP; deep vs-RHP power split.
const SPLITS_LAD = { stats: [{ splits: [
  { split: { code: 'vl' }, stat: { plateAppearances: 120, avg: '.250', slg: '.420' } },
  { split: { code: 'vr' }, stat: { plateAppearances: 420, avg: '.250', slg: '.560' } },
] }] };

// Route each URL to its fixture.
const fetchJson = async (url) => {
  if (url.includes('/pitchers')) return PITCHERS;
  if (url.includes('/api/schedule/mlb')) return SCHEDULE;
  if (url.includes('statsapi') && url.includes('/schedule')) return SCHEDULE;
  if (url.includes('personIds=')) return PEOPLE;
  if (url.includes('open-meteo')) return { hourly: {
    time: ['2026-07-21T20:00'], temperature_2m: [88], wind_speed_10m: [14], wind_direction_10m: [185], precipitation_probability: [0],
  } };
  if (url.includes('statSplits')) return SPLITS_LAD;
  return {};
};

async function build() {
  return ec.buildContext('mlb', { fetchJson,
    schedule: SCHEDULE, pitchers: PITCHERS, people: PEOPLE });
}

describe('team resolution', () => {
  it('maps full names and abbreviations to the coord key', () => {
    expect(ec.abbrOf('Los Angeles Dodgers')).toBe('LAD');
    expect(ec.abbrOf('COL')).toBe('COL');
    expect(ec.abbrOf('Nobody')).toBeNull();
    expect(ec.abbrOf(null)).toBeNull();
  });
  it('has coordinates for all 30 parks', () => {
    expect(Object.keys(ec.PARK_COORDS)).toHaveLength(30);
  });
});

describe('environment reaches the grade', () => {
  it('a Coors prop gets env_multiplier > 1 (park × weather)', async () => {
    const ctx = await build();
    const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: 'R' });
    expect(c.environment).not.toBeNull();
    expect(c.environment.multiplier).toBeGreaterThan(1);
    expect(c.environment.park_base).toBeGreaterThan(1);   // Coors
    expect(c.environment.weather_mod).toBeGreaterThan(1);  // wind out-ish, warm
  });

  it('a DOME prop keeps its park factor but weather stands down', async () => {
    const ctx = await build();
    const c = await ctx.contextFor({ team: 'NYY', stat_type: 'home_runs', direction: 'over', playerId: 8, bats: 'R' });
    // Tampa's park factor may compose to != 1, but the weather mod is neutral.
    if (c.environment) {
      expect(c.environment.weather_mod).toBe(1);
      expect(c.environment.weather_state).toBe('dome_na');
    }
  });

  it('a prop for a team not on the slate gets NO environment', async () => {
    const ctx = await build();
    const c = await ctx.contextFor({ team: 'SEA', stat_type: 'home_runs', direction: 'over', playerId: 7, bats: 'R' });
    expect(c.environment).toBeNull();
  });

  it('never fabricates a venue — a grade with no team is absent', async () => {
    const ctx = await build();
    const c = await ctx.contextFor({ team: null, stat_type: 'home_runs', direction: 'over', playerId: 6, bats: 'R' });
    expect(c.environment).toBeNull();
  });
});

describe('matchup (platoon) reaches the grade', () => {
  it('fires with a declared opposing pitcher + known hitter hand', async () => {
    const ctx = await build();
    // LAD hitter, opposing pitcher 111 is RHP → uses the deep vs-RHP power split.
    const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: 'R' });
    expect(c.matchup).not.toBeNull();
    expect(c.matchup.pitcher_hand).toBe('R');
    expect(c.matchup.observed_pa).toBe(420);
  });

  it('honest-absents when the hitter hand is unknown', async () => {
    const ctx = await build();
    const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: null });
    expect(c.matchup).toBeNull();
  });

  it('honest-absents when no opposing pitcher is declared', async () => {
    const ctx = await ec.buildContext('mlb', { fetchJson, schedule: SCHEDULE, pitchers: { games: [] }, people: PEOPLE });
    const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: 'R' });
    expect(c.matchup).toBeNull();
  });
});

describe('non-MLB honest-absents entirely', () => {
  it('wnba returns no environment or matchup', async () => {
    const ctx = await ec.buildContext('wnba', { fetchJson });
    const c = await ctx.contextFor({ team: 'LV', stat_type: 'points', direction: 'over' });
    expect(c.environment).toBeNull();
    expect(c.matchup).toBeNull();
  });
});

describe('the combined adjustment stays BOUNDED', () => {
  it('archetype + environment + platoon do not compound into an over-adjust', async () => {
    const axes = require('../../src/services/archetypeAxes');
    const slugger = axes.classifyPlayer({ role: 'batter', sample_pa: 400, barrel_pct: 20, k_pct: 20, chase_pct: 30, avg_launch_angle: 18, sweet_spot_pct: 40 });
    // Coors + wind-out + a favourable platoon, all at once, on a slugger.
    const env = { multiplier: 1.28, label: 'PARK × WEATHER', park_base: 1.15, weather_mod: 1.11 };
    const matchup = { multiplier: 1.14, label: 'PLATOON', pitcher_hand: 'R' };
    const r = ch.adjust({ pWin: 0.5, direction: 'over', statType: 'home_runs', classification: slugger, environment: env, matchup });
    // Every layer is capped, and the total nudge is clamped — the combined move
    // must stay a lean, not a 40% swing.
    expect(Math.abs(r.delta)).toBeLessThan(0.12);
    // and all three are independently recorded for attribution
    const axesSeen = r.adjustments.map((a) => a.axis);
    expect(axesSeen).toContain('environment');
    expect(axesSeen).toContain('matchup');
  });
});

describe('async attachChallenger still preserves the champion', () => {
  it('passes environment + matchup through, p_win untouched', async () => {
    const out = await ch.attachChallenger(
      [{ player: 'X', stat_type: 'home_runs', direction: 'over', p_win: 0.5 }],
      () => null,
      async () => ({ environment: { multiplier: 1.2, park_base: 1.2, weather_mod: 1, weather_state: 'present' }, matchup: null }),
    );
    expect(out[0].p_win).toBe(0.5);                 // champion untouched
    expect(out[0].env_multiplier).toBe(1.2);        // retained attributably
    expect(out[0].p_win_challenger).toBeGreaterThan(0.5);
  });
});
