import { describe, expect, it } from 'vitest';
import {
  deriveStoredLifecycleStatus,
  mergeOpeningOdds,
  oddsPayloadChanged,
  openingPayloadNeedsBackfill,
  sourceBackedTeamPropsNeedRefresh,
  shouldRetryLateOpeningTeamProps,
  shouldRefreshPregameOdds,
} from '../odds-refresh';

describe('odds-refresh worker', () => {
  it('ignores equivalent odds payloads with different key order', () => {
    expect(oddsPayloadChanged(
      {
        moneyline: { home: -120, away: 110 },
        spread: { home: { line: -1.5, odds: -110 }, away: { line: 1.5, odds: -110 } },
      },
      {
        spread: { away: { odds: -110, line: 1.5 }, home: { odds: -110, line: -1.5 } },
        moneyline: { away: 110, home: -120 },
      },
    )).toBe(false);
  });

  it('detects a real line move', () => {
    expect(oddsPayloadChanged(
      {
        total: { over: { line: 8.5, odds: -110 }, under: { line: 8.5, odds: -110 } },
      },
      {
        total: { over: { line: 9, odds: -110 }, under: { line: 9, odds: -110 } },
      },
    )).toBe(true);
  });

  it('keeps syncing stored current odds until the event is over', () => {
    expect(shouldRefreshPregameOdds('scheduled')).toBe(true);
    expect(shouldRefreshPregameOdds('started')).toBe(true);
    expect(shouldRefreshPregameOdds('ended')).toBe(false);
  });

  it('retries empty source-backed team props after the cooldown when the game has not started', () => {
    expect(shouldRetryLateOpeningTeamProps({
      league: 'nba',
      propCount: 0,
      generatedAt: '2026-03-31T13:00:00.000Z',
      startsAt: '2026-03-31T23:40:00.000Z',
      now: new Date('2026-03-31T14:00:00.000Z'),
    })).toBe(true);
  });

  it('does not retry non-empty, unsupported, or already-started team props', () => {
    expect(shouldRetryLateOpeningTeamProps({
      league: 'nba',
      propCount: 1,
      generatedAt: '2026-03-31T13:00:00.000Z',
      startsAt: '2026-03-31T23:40:00.000Z',
      now: new Date('2026-03-31T14:00:00.000Z'),
    })).toBe(false);

    expect(shouldRetryLateOpeningTeamProps({
      league: 'mlb',
      propCount: 0,
      generatedAt: '2026-03-31T13:00:00.000Z',
      startsAt: '2026-03-31T23:40:00.000Z',
      now: new Date('2026-03-31T14:00:00.000Z'),
    })).toBe(false);

    expect(shouldRetryLateOpeningTeamProps({
      league: 'nba',
      propCount: 0,
      generatedAt: '2026-03-31T13:45:00.000Z',
      startsAt: '2026-03-31T13:50:00.000Z',
      now: new Date('2026-03-31T14:00:00.000Z'),
    })).toBe(false);
  });

  it('refreshes source-backed team props when an empty bundle gains candidates', () => {
    expect(sourceBackedTeamPropsNeedRefresh({
      league: 'nba',
      props: [],
      candidates: [
        {
          player: 'Quenton Jackson',
          statType: 'points',
          normalizedStatType: 'points',
          marketLineValue: 13.5,
          overOdds: null,
          underOdds: -125,
          availableSides: ['under'],
          source: 'fanduel',
          marketName: 'Quenton Jackson Points Over/Under',
          propLabel: 'Points 13.5',
        },
      ],
    })).toBe(true);
  });

  it('does not refresh source-backed team props when stored props still match current candidates', () => {
    expect(sourceBackedTeamPropsNeedRefresh({
      league: 'nba',
      props: [
        {
          player: 'Quenton Jackson',
          stat_type: 'points',
          market_line_value: 13.5,
          recommendation: 'under',
          odds: -125,
        },
      ],
      candidates: [
        {
          player: 'Quenton Jackson',
          statType: 'points',
          normalizedStatType: 'points',
          marketLineValue: 13.5,
          overOdds: null,
          underOdds: -125,
          availableSides: ['under'],
          source: 'fanduel',
          marketName: 'Quenton Jackson Points Over/Under',
          propLabel: 'Points 13.5',
        },
      ],
    })).toBe(false);
  });

  it('refreshes source-backed team props when the stored price drifts on the same side and line', () => {
    expect(sourceBackedTeamPropsNeedRefresh({
      league: 'nba',
      props: [
        {
          player: 'Collin Sexton',
          stat_type: 'points',
          market_line_value: 16.5,
          recommendation: 'under',
          odds: -102,
        },
      ],
      candidates: [
        {
          player: 'Collin Sexton',
          statType: 'points',
          normalizedStatType: 'points',
          marketLineValue: 16.5,
          overOdds: null,
          underOdds: -130,
          availableSides: ['under'],
          source: 'fanduel',
          marketName: 'Collin Sexton Points Over/Under',
          propLabel: 'Points 16.5',
        },
      ],
    })).toBe(true);
  });

  it('refreshes source-backed team props when live candidates add a new market the bundle does not have yet', () => {
    expect(sourceBackedTeamPropsNeedRefresh({
      league: 'nba',
      props: [
        {
          player: 'Collin Sexton',
          stat_type: 'points',
          market_line_value: 16.5,
          recommendation: 'under',
          odds: -102,
        },
      ],
      candidates: [
        {
          player: 'Collin Sexton',
          statType: 'points',
          normalizedStatType: 'points',
          marketLineValue: 16.5,
          overOdds: null,
          underOdds: -102,
          availableSides: ['under'],
          source: 'fanduel',
          marketName: 'Collin Sexton Points Over/Under',
          propLabel: 'Points 16.5',
        },
        {
          player: 'Collin Sexton',
          statType: 'rebounds',
          normalizedStatType: 'rebounds',
          marketLineValue: 5.5,
          overOdds: -118,
          underOdds: null,
          availableSides: ['over'],
          source: 'fanduel',
          marketName: 'Collin Sexton Rebounds Over/Under',
          propLabel: 'Rebounds 5.5',
        },
      ],
    })).toBe(true);
  });

  it('refreshes source-backed team props when the stored line no longer exists or the market disappears', () => {
    expect(sourceBackedTeamPropsNeedRefresh({
      league: 'nba',
      props: [
        {
          player: 'Collin Sexton',
          stat_type: 'points',
          market_line_value: 17.5,
          recommendation: 'under',
        },
      ],
      candidates: [
        {
          player: 'Collin Sexton',
          statType: 'points',
          normalizedStatType: 'points',
          marketLineValue: 16.5,
          overOdds: null,
          underOdds: -102,
          availableSides: ['under'],
          source: 'fanduel',
          marketName: 'Collin Sexton Points Over/Under',
          propLabel: 'Points 16.5',
        },
      ],
    })).toBe(true);

    expect(sourceBackedTeamPropsNeedRefresh({
      league: 'nba',
      props: [
        {
          player: 'Walter Clayton Jr.',
          stat_type: 'assists',
          market_line_value: 4.5,
          recommendation: 'under',
        },
      ],
      candidates: [],
    })).toBe(true);
  });

  it('does not refresh when stored legacy aliases canonicalize to the current candidate identity', () => {
    expect(sourceBackedTeamPropsNeedRefresh({
      league: 'mlb',
      props: [
        {
          player: 'Aaron Judge',
          stat_type: 'homeruns',
          market_line_value: 0.5,
          recommendation: 'over',
          odds: 175,
        },
      ],
      candidates: [
        {
          player: 'Aaron Judge',
          statType: 'batting_homeRuns',
          normalizedStatType: 'batting_homeRuns',
          marketLineValue: 0.5,
          overOdds: 175,
          underOdds: null,
          availableSides: ['over'],
          source: 'fanduel',
          marketName: 'Aaron Judge Home Runs Over/Under',
          propLabel: 'Home Runs 0.5',
        },
      ],
    })).toBe(false);
  });

  it('advances stored scheduled events to started after first pitch', () => {
    expect(deriveStoredLifecycleStatus({
      currentStatus: 'scheduled',
      startsAt: '2026-03-29T19:00:00.000Z',
      now: new Date('2026-03-29T20:00:00.000Z'),
    })).toBe('started');
  });

  it('advances old unfinished stored events to ended after the grace window', () => {
    expect(deriveStoredLifecycleStatus({
      currentStatus: 'scheduled',
      startsAt: '2026-03-29T12:00:00.000Z',
      now: new Date('2026-03-29T21:00:00.000Z'),
    })).toBe('ended');

    expect(deriveStoredLifecycleStatus({
      currentStatus: 'started',
      startsAt: '2026-03-29T12:00:00.000Z',
      now: new Date('2026-03-29T21:00:00.000Z'),
    })).toBe('ended');
  });

  it('keeps backfilling opening odds until all market families are present', () => {
    expect(openingPayloadNeedsBackfill({
      opening_moneyline: { home: -120, away: 110 },
      opening_spread: null,
      opening_total: null,
    })).toBe(true);

    expect(openingPayloadNeedsBackfill({
      opening_moneyline: { home: -120, away: 110 },
      opening_spread: {
        home: { line: -1.5, odds: -110 },
        away: { line: 1.5, odds: -110 },
      },
      opening_total: {
        over: { line: 8.5, odds: -110 },
        under: { line: 8.5, odds: -110 },
      },
    })).toBe(false);
  });

  it('merges newly discovered opening markets without wiping earlier ones', () => {
    expect(mergeOpeningOdds({
      moneyline: { home: -120, away: 110 },
      spread: null,
      total: null,
    }, {
      moneyline: { home: null, away: null },
      spread: {
        home: { line: -1.5, odds: -110 },
        away: { line: 1.5, odds: -110 },
      },
      total: {
        over: { line: 8.5, odds: -110 },
        under: { line: 8.5, odds: -110 },
      },
    })).toEqual({
      moneyline: { home: -120, away: 110 },
      spread: {
        home: { line: -1.5, odds: -110 },
        away: { line: 1.5, odds: -110 },
      },
      total: {
        over: { line: 8.5, odds: -110 },
        under: { line: 8.5, odds: -110 },
      },
    });
  });
});
