// Odds-cache prewarmer (Session 22).
//
// Pins the contract that matters:
//   1. ODDS_PREWARM unset → script no-ops (free tier safety)
//   2. --dry-run → no provider calls, summary records the sports
//   3. Happy path → getOdds called per sport, summary captures source/props/quota
//   4. Quota-block mid-run → remaining sports skip with the right reason
//   5. parseArgs handles --sports + --dry-run + defaults

const path = require('path');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'odds-prefetch.js');

// Mock the two modules the script lazy-requires. We don't preload
// the script — jest.isolateModules around each test gives us a
// fresh module cache so the env flag is re-read.
jest.mock('../../src/services/oddsService', () => ({
  getOdds: jest.fn(),
}));
jest.mock('../../src/services/quotaTracker', () => ({
  getQuotaStatus: jest.fn(),
}));
const oddsService = require('../../src/services/oddsService');
const quotaTracker = require('../../src/services/quotaTracker');

const { main, __internals } = require(SCRIPT);

beforeEach(() => {
  oddsService.getOdds.mockReset();
  quotaTracker.getQuotaStatus.mockReset();
  // Default healthy quota — tests override per-case.
  quotaTracker.getQuotaStatus.mockResolvedValue({
    allowed: true, used: 50, limit: 500, pct: 0.1,
  });
  delete process.env.ODDS_PREWARM;
});

describe('parseArgs', () => {
  test('defaults to nba,wnba,mlb non-dry-run', () => {
    const a = __internals.parseArgs(['node', 'odds-prefetch.js']);
    expect(a.sports).toEqual(['nba', 'wnba', 'mlb']);
    expect(a.dryRun).toBe(false);
  });
  test('honors --sports=', () => {
    const a = __internals.parseArgs(['node', 's', '--sports=nba']);
    expect(a.sports).toEqual(['nba']);
  });
  test('honors --dry-run', () => {
    const a = __internals.parseArgs(['node', 's', '--dry-run']);
    expect(a.dryRun).toBe(true);
  });
  test('handles --sports=nba,mlb,wnba in any order with --dry-run', () => {
    const a = __internals.parseArgs(['node', 's', '--dry-run', '--sports=nba,mlb,wnba']);
    expect(a.sports).toEqual(['nba', 'mlb', 'wnba']);
    expect(a.dryRun).toBe(true);
  });
});

describe('main — ODDS_PREWARM gating', () => {
  test('refuses to run when ODDS_PREWARM is unset', async () => {
    const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
    const result = await main(['node', 'odds-prefetch.js']);
    expect(result.enabled).toBe(false);
    expect(result.skipped).toBe('not_enabled');
    expect(oddsService.getOdds).not.toHaveBeenCalled();
    warnSpy.mockRestore();
  });

  test('refuses to run when ODDS_PREWARM is "0" or "false"', async () => {
    const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
    for (const v of ['0', 'false', 'no', '']) {
      process.env.ODDS_PREWARM = v;
      const r = await main(['node', 'odds-prefetch.js']);
      expect(r.skipped).toBe('not_enabled');
    }
    expect(oddsService.getOdds).not.toHaveBeenCalled();
    warnSpy.mockRestore();
  });
});

describe('main — dry-run', () => {
  test('does not call getOdds, records skipped:dry_run per sport', async () => {
    process.env.ODDS_PREWARM = '1';
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    const result = await main(['node', 'odds-prefetch.js', '--sports=nba,mlb', '--dry-run']);
    expect(oddsService.getOdds).not.toHaveBeenCalled();
    expect(result.dryRun).toBe(true);
    expect(result.sports.nba).toEqual({ skipped: 'dry_run' });
    expect(result.sports.mlb).toEqual({ skipped: 'dry_run' });
    logSpy.mockRestore();
  });
});

describe('main — happy path', () => {
  test('calls getOdds per sport, captures source/props/quota', async () => {
    process.env.ODDS_PREWARM = '1';
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    oddsService.getOdds
      .mockResolvedValueOnce({ source: 'live', props: [{ a: 1 }, { a: 2 }, { a: 3 }], quota_remaining: 480 })
      .mockResolvedValueOnce({ source: 'cache', props: [{ a: 1 }], quota_remaining: 480 });

    const result = await main(['node', 'odds-prefetch.js', '--sports=nba,mlb']);
    expect(oddsService.getOdds).toHaveBeenCalledTimes(2);
    expect(result.sports.nba).toMatchObject({ source: 'live', props: 3, quota_remaining: 480 });
    expect(result.sports.mlb).toMatchObject({ source: 'cache', props: 1 });
    logSpy.mockRestore();
  });

  test('captures credits spent as the delta between before/after tracker reads', async () => {
    process.env.ODDS_PREWARM = '1';
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    quotaTracker.getQuotaStatus
      .mockResolvedValueOnce({ allowed: true, used: 50, limit: 500, pct: 0.1 })   // initial
      .mockResolvedValueOnce({ allowed: true, used: 50, limit: 500, pct: 0.1 })   // pre-nba check
      .mockResolvedValueOnce({ allowed: true, used: 57, limit: 500, pct: 0.114 }); // final
    oddsService.getOdds.mockResolvedValueOnce({ source: 'live', props: [], quota_remaining: 443 });

    const result = await main(['node', 'odds-prefetch.js', '--sports=nba']);
    expect(result.creditsSpent).toBe(7); // 57 - 50
    expect(result.quota.after).toBe(57);
    logSpy.mockRestore();
  });
});

describe('main — quota-blocked mid-run', () => {
  test('skips remaining sports when the tracker blocks', async () => {
    process.env.ODDS_PREWARM = '1';
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
    quotaTracker.getQuotaStatus
      .mockResolvedValueOnce({ allowed: true, used: 470, limit: 500, pct: 0.94 })  // initial
      .mockResolvedValueOnce({ allowed: true, used: 470, limit: 500, pct: 0.94 })  // pre-nba
      .mockResolvedValueOnce({ allowed: false, used: 480, limit: 500, pct: 0.96 }) // pre-mlb
      .mockResolvedValueOnce({ allowed: false, used: 480, limit: 500, pct: 0.96 }) // pre-wnba
      .mockResolvedValueOnce({ allowed: false, used: 480, limit: 500, pct: 0.96 }); // final

    oddsService.getOdds.mockResolvedValueOnce({ source: 'live', props: [], quota_remaining: 20 });

    const result = await main(['node', 'odds-prefetch.js', '--sports=nba,mlb,wnba']);
    expect(oddsService.getOdds).toHaveBeenCalledTimes(1);
    expect(result.sports.nba.source).toBe('live');
    expect(result.sports.mlb).toMatchObject({ skipped: 'quota_blocked' });
    expect(result.sports.wnba).toMatchObject({ skipped: 'quota_blocked' });
    logSpy.mockRestore();
    warnSpy.mockRestore();
  });
});

describe('main — error per sport does not break the loop', () => {
  test('records {error} for the failing sport, continues with the next', async () => {
    process.env.ODDS_PREWARM = '1';
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
    oddsService.getOdds
      .mockRejectedValueOnce(new Error('upstream 502'))
      .mockResolvedValueOnce({ source: 'live', props: [{ a: 1 }], quota_remaining: 480 });

    const result = await main(['node', 'odds-prefetch.js', '--sports=nba,mlb']);
    expect(result.sports.nba.error).toMatch(/upstream 502/);
    expect(result.sports.mlb.source).toBe('live');
    logSpy.mockRestore();
    warnSpy.mockRestore();
  });
});
