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

vi.mock('../../db', () => ({ default: { query: vi.fn() } }));

import { americanOddsToImpliedProbability, computeSgoRetryDelayMs, deriveEventLifecycleStatus, fetchEvents, parseOdds, parseOpeningOdds, sanitizeGameOddsForLeague, sanitizeMoneylinePair } from '../sgo';

describe('sgo moneyline sanitization', () => {
  it('converts american odds into implied probability', () => {
    expect(americanOddsToImpliedProbability(-150)).toBeCloseTo(0.6, 4);
    expect(americanOddsToImpliedProbability(125)).toBeCloseTo(100 / 225, 4);
  });

  it('keeps sane opposite-sign moneyline pairs intact', () => {
    expect(sanitizeMoneylinePair({ home: -120, away: 110 })).toEqual({ home: -120, away: 110 });
  });

  it('keeps sane pickem-style negative pairs intact', () => {
    expect(sanitizeMoneylinePair({ home: -104, away: -117 })).toEqual({ home: -104, away: -117 });
  });

  it('nulls corrupt same-sign moneyline pairs', () => {
    expect(sanitizeMoneylinePair({ home: -213, away: -257 })).toEqual({ home: null, away: null });
    expect(sanitizeMoneylinePair({ home: 115, away: 102 })).toEqual({ home: null, away: null });
  });

  it('nulls identical moneylines when the spread clearly names a favorite', () => {
    const parsed = parseOdds({
      eventID: 'evt-identical-ml',
      teams: {
        home: { names: { long: 'St. Louis Cardinals', short: 'STL' } },
        away: { names: { long: 'Tampa Bay Rays', short: 'TB' } },
      },
      status: {
        startsAt: '2026-03-29T19:00:00.000Z',
        started: false,
        ended: false,
        displayShort: '7:00 PM',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-257' },
        'points-away-game-ml-away': { bookOdds: '-257' },
        'points-home-game-sp-home': { bookSpread: '-1.5', bookOdds: '140' },
        'points-away-game-sp-away': { bookSpread: '1.5', bookOdds: '-167' },
      },
    });

    expect(parsed.moneyline).toEqual({ home: null, away: null });
  });

  it('keeps identical pickem moneylines when the spread does not name a favorite', () => {
    const parsed = parseOdds({
      eventID: 'evt-pickem-ml',
      teams: {
        home: { names: { long: 'Brooklyn Nets', short: 'BKN' } },
        away: { names: { long: 'Sacramento Kings', short: 'SAC' } },
      },
      status: {
        startsAt: '2026-03-29T19:00:00.000Z',
        started: false,
        ended: false,
        displayShort: '7:00 PM',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-110' },
        'points-away-game-ml-away': { bookOdds: '-110' },
        'points-home-game-sp-home': { bookSpread: '0', bookOdds: '-110' },
        'points-away-game-sp-away': { bookSpread: '0', bookOdds: '-110' },
      },
    });

    expect(parsed.moneyline).toEqual({ home: -110, away: -110 });
  });

  it('keeps parseOdds anchored to current book fields instead of corrupt openBookOdds', () => {
    const parsed = parseOdds({
      eventID: 'evt-1',
      teams: {
        home: { names: { long: 'Atlanta Braves', short: 'ATL' } },
        away: { names: { long: 'Boston Red Sox', short: 'BOS' } },
      },
      status: {
        startsAt: '2026-03-29T19:00:00.000Z',
        started: true,
        ended: false,
        displayShort: 'BOT 3',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-145', openBookOdds: '-669' },
        'points-away-game-ml-away': { bookOdds: '125', openBookOdds: '-127' },
      },
    });

    expect(parsed.moneyline).toEqual({ home: -145, away: 125 });
  });

  it('nulls absurd MLB main markets instead of preserving corrupt feed data', () => {
    const parsed = parseOdds({
      eventID: 'evt-mlb-bad-main',
      leagueID: 'MLB',
      teams: {
        home: { names: { long: 'Colorado Rockies', short: 'COL' } },
        away: { names: { long: 'Houston Astros', short: 'HOU' } },
      },
      status: {
        startsAt: '2026-04-08T19:00:00.000Z',
        started: false,
        ended: false,
        displayShort: '3:00 PM',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-10000' },
        'points-away-game-ml-away': { bookOdds: '3300' },
        'points-home-game-sp-home': { bookSpread: '-6.5', bookOdds: '-110' },
        'points-away-game-sp-away': { bookSpread: '6.5', bookOdds: '-110' },
      },
    } as any);

    expect(parsed.moneyline).toEqual({ home: null, away: null });
    expect(parsed.spread).toEqual({ home: null, away: null });
  });

  it('nulls absurd MLB totals that look like live or alternate markets', () => {
    const parsed = parseOdds({
      eventID: 'evt-mlb-bad-total',
      leagueID: 'MLB',
      teams: {
        home: { names: { long: 'Houston Astros', short: 'HOU' } },
        away: { names: { long: 'Oakland Athletics', short: 'OAK' } },
      },
      status: {
        startsAt: '2026-04-05T20:06:00.000Z',
        started: false,
        ended: false,
        displayShort: '4:06 PM',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-152' },
        'points-away-game-ml-away': { bookOdds: '128' },
        'points-home-game-sp-home': { bookSpread: '-1.5', bookOdds: '140' },
        'points-away-game-sp-away': { bookSpread: '1.5', bookOdds: '-167' },
        'points-all-game-ou-over': { bookOverUnder: '19.5', bookOdds: '105' },
        'points-all-game-ou-under': { bookOverUnder: '19.5', bookOdds: '-143' },
      },
    } as any);

    expect(parsed.total).toEqual({ over: null, under: null });
  });

  it('keeps sane MLB runline markets intact', () => {
    const parsed = parseOdds({
      eventID: 'evt-mlb-good-main',
      leagueID: 'MLB',
      teams: {
        home: { names: { long: 'Seattle Mariners', short: 'SEA' } },
        away: { names: { long: 'Texas Rangers', short: 'TEX' } },
      },
      status: {
        startsAt: '2026-04-08T23:00:00.000Z',
        started: false,
        ended: false,
        displayShort: '7:00 PM',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-135' },
        'points-away-game-ml-away': { bookOdds: '115' },
        'points-home-game-sp-home': { bookSpread: '-1.5', bookOdds: '145' },
        'points-away-game-sp-away': { bookSpread: '1.5', bookOdds: '-170' },
      },
    } as any);

    expect(parsed.moneyline).toEqual({ home: -135, away: 115 });
    expect(parsed.spread).toEqual({
      home: { line: -1.5, odds: 145 },
      away: { line: 1.5, odds: -170 },
    });
  });

  it('nulls MLB runline pairs with absurd price ladders even when the line is 1.5', () => {
    const parsed = parseOdds({
      eventID: 'evt-mlb-bad-runline-odds',
      leagueID: 'MLB',
      teams: {
        home: { names: { long: 'New York Yankees', short: 'NYY' } },
        away: { names: { long: 'Athletics', short: 'OAK' } },
      },
      status: {
        startsAt: '2026-04-08T23:00:00.000Z',
        started: false,
        ended: false,
        displayShort: '7:00 PM',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-526' },
        'points-away-game-ml-away': { bookOdds: '348' },
        'points-home-game-sp-home': { bookSpread: '-1.5', bookOdds: '1400' },
        'points-away-game-sp-away': { bookSpread: '1.5', bookOdds: '-10000' },
      },
    } as any);

    expect(parsed.spread).toEqual({ home: null, away: null });
  });

  it('sanitizes merged MLB payloads even when corrupt odds come from stored fallback data', () => {
    const sanitized = sanitizeGameOddsForLeague('mlb', {
      moneyline: { home: -4555, away: 1900 },
      spread: {
        home: { line: -7.5, odds: -110 },
        away: { line: 7.5, odds: -110 },
      },
      total: { over: null, under: null },
    });

    expect(sanitized.moneyline).toEqual({ home: null, away: null });
    expect(sanitized.spread).toEqual({ home: null, away: null });
  });

  it('sanitizes merged MLB totals when fallback data carries corrupt live numbers', () => {
    const sanitized = sanitizeGameOddsForLeague('mlb', {
      moneyline: { home: -152, away: 128 },
      spread: {
        home: { line: -1.5, odds: 140 },
        away: { line: 1.5, odds: -167 },
      },
      total: {
        over: { line: 12.5, odds: 190 },
        under: { line: 12.5, odds: -315 },
      },
    });

    expect(sanitized.total).toEqual({ over: null, under: null });
  });

  it('does not capture fake opening moneylines from live/current fields after a game has started', () => {
    const parsed = parseOpeningOdds({
      eventID: 'evt-2',
      teams: {
        home: { names: { long: 'Atlanta Braves', short: 'ATL' } },
        away: { names: { long: 'Boston Red Sox', short: 'BOS' } },
      },
      status: {
        startsAt: '2026-03-29T19:00:00.000Z',
        started: true,
        ended: false,
        displayShort: 'BOT 3',
        oddsPresent: true,
      },
      odds: {
        'points-home-game-ml-home': { bookOdds: '-145', openBookOdds: '-669' },
        'points-away-game-ml-away': { bookOdds: '125', openBookOdds: '-127' },
      },
    });

    expect(parsed.moneyline).toEqual({ home: null, away: null });
  });

  it('derives event lifecycle status from feed flags and start time', () => {
    expect(deriveEventLifecycleStatus({
      status: { startsAt: '2026-03-29T23:00:00.000Z', started: false, ended: false, displayShort: '7:00 PM', oddsPresent: true },
    } as any, new Date('2026-03-29T20:00:00.000Z'))).toBe('scheduled');

    expect(deriveEventLifecycleStatus({
      status: { startsAt: '2026-03-29T19:00:00.000Z', started: false, ended: false, displayShort: '7:00 PM', oddsPresent: true },
    } as any, new Date('2026-03-29T20:00:00.000Z'))).toBe('started');

    expect(deriveEventLifecycleStatus({
      status: { startsAt: '2026-03-29T19:00:00.000Z', started: true, ended: false, displayShort: 'BOT 3', oddsPresent: true },
    } as any)).toBe('started');

    expect(deriveEventLifecycleStatus({
      status: { startsAt: '2026-03-29T19:00:00.000Z', started: true, ended: true, displayShort: 'FINAL', oddsPresent: true },
    } as any)).toBe('ended');
  });

  it('backs off exponentially between SGO retries', () => {
    expect(computeSgoRetryDelayMs(0)).toBe(1000);
    expect(computeSgoRetryDelayMs(1)).toBe(2000);
    expect(computeSgoRetryDelayMs(2)).toBe(4000);
  });

  it('retries timeouts before succeeding', async () => {
    const fetchMock = vi
      .fn()
      .mockRejectedValueOnce(Object.assign(new Error('The operation was aborted due to timeout'), { name: 'TimeoutError' }))
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ data: [{ eventID: 'evt-1' }] }),
      });

    vi.stubGlobal('fetch', fetchMock as any);

    const events = await fetchEvents('nba');

    expect(fetchMock).toHaveBeenCalledTimes(2);
    expect(events).toEqual([{ eventID: 'evt-1' }]);
    vi.unstubAllGlobals();
  });

  it('retries retryable HTTP statuses before succeeding', async () => {
    const fetchMock = vi
      .fn()
      .mockResolvedValueOnce({
        ok: false,
        status: 503,
        text: async () => 'upstream unavailable',
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ data: [{ eventID: 'evt-2' }] }),
      });

    vi.stubGlobal('fetch', fetchMock as any);

    const events = await fetchEvents('mlb');

    expect(fetchMock).toHaveBeenCalledTimes(2);
    expect(events).toEqual([{ eventID: 'evt-2' }]);
    vi.unstubAllGlobals();
  });
});
