/**
 * Session 64 Order 1 — date-targeted settlement + terminal states.
 *
 * The bug: a lookup miss did `pending += 1` with no terminal state, so a row
 * that could NEVER settle (DNP, or the rolling window passed the game) was
 * indistinguishable from one settling tomorrow. These lock the distinctions.
 */
const src = require('../../src/services/settleSource');

const matchesDate = (r, d) => r.date === d;
const statValue = (s, st) => (s && s[st] != null ? Number(s[st]) : null);
const base = {
  sport: 'mlb', playerName: 'X', gameDate: '2026-07-17', statType: 'doubles',
  matchesDate, statValue,
};

describe('classifyGameState', () => {
  test('final states', () => {
    expect(src.classifyGameState({ status: 'Final' })).toBe('final');
    expect(src.classifyGameState({ state: 'post' })).toBe('final');
  });
  test('postponed/cancelled are void', () => {
    expect(src.classifyGameState({ detailedState: 'Postponed' })).toBe('void');
    expect(src.classifyGameState({ status: 'Cancelled' })).toBe('void');
  });
  test('SUSPENDED is NOT final — it resumes and must stay pending', () => {
    expect(src.classifyGameState({ detailedState: 'Suspended' })).toBe('not_final');
  });
  test('scheduled / in progress are not final', () => {
    expect(src.classifyGameState({ status: 'Scheduled' })).toBe('not_final');
    expect(src.classifyGameState({ status: 'In Progress' })).toBe('not_final');
  });
  test('unknown when there is nothing to read', () => {
    expect(src.classifyGameState({})).toBe('unknown');
  });
});

describe('absenceMeaning — what a missing player row MEANS', () => {
  test('any unplayed game → pending (never void a bet that may still settle)', () => {
    expect(src.absenceMeaning([{ status: 'Final' }, { status: 'Suspended' }])).toBe('pending');
  });
  test('all games postponed → void', () => {
    expect(src.absenceMeaning([{ status: 'Postponed' }])).toBe('void');
  });
  test('game final but player absent → void (confirmed DNP)', () => {
    expect(src.absenceMeaning([{ status: 'Final' }])).toBe('void');
  });
  test('no schedule → unknown, not a guess', () => {
    expect(src.absenceMeaning([])).toBe('unknown');
  });
});

describe('resolveOutcome', () => {
  test('player has a line on that date → SETTLED with the real value', async () => {
    const r = await src.resolveOutcome({
      ...base,
      getFullLog: async () => [{ date: '2026-07-17', stat: { doubles: 1 } }],
      getSchedule: async () => [{ status: 'Final' }],
    });
    expect(r.state).toBe('settled');
    expect(r.value).toBe(1);
  });

  test('a ZERO value still settles (0 is a real result, not absence)', async () => {
    const r = await src.resolveOutcome({
      ...base,
      getFullLog: async () => [{ date: '2026-07-17', stat: { doubles: 0 } }],
      getSchedule: async () => [{ status: 'Final' }],
    });
    expect(r.state).toBe('settled');
    expect(r.value).toBe(0);
  });

  // Session 64 — this MUST NOT void. Player-absence from a labelled date can
  // equally mean the row's date is wrong (ledger derives game_date from the
  // GRADE time when the feed has no game_time, so late-UTC snapshots label the
  // previous ET day). Verified live: Freddie Freeman's 97-game log has no
  // 2026-07-18 because he played the 17th and 19th. Voiding on absence
  // destroyed real results.
  test('player absent on a FINAL game → VOID (dates proven correct; DNP is real)', async () => {
    const r = await src.resolveOutcome({
      ...base,
      getFullLog: async () => [{ date: '2026-07-16', stat: { doubles: 0 } }],
      getSchedule: async () => [{ status: 'Final' }],
    });
    expect(r.state).toBe('void');
    expect(r.reason).toBe('player_dnp');
  });

  test('postponed game → VOID with the postponed reason', async () => {
    const r = await src.resolveOutcome({
      ...base,
      getFullLog: async () => [],
      getSchedule: async () => [{ status: 'Postponed' }],
    });
    expect(r.state).toBe('void');
    expect(r.reason).toBe('game_postponed_or_cancelled');
  });

  test('SUSPENDED game stays PENDING — never voided', async () => {
    const r = await src.resolveOutcome({
      ...base,
      getFullLog: async () => [],
      getSchedule: async () => [{ status: 'Suspended' }],
    });
    expect(r.state).toBe('pending');
  });

  test('a game OUTSIDE a rolling window still settles from the full log', async () => {
    // The exact live failure: Jul 12 game, window now starts Jul 6.
    const full = Array.from({ length: 40 }, (_, i) => ({
      date: `2026-07-${String(i + 1).padStart(2, '0')}`, stat: { doubles: i % 3 },
    }));
    const r = await src.resolveOutcome({
      ...base, gameDate: '2026-07-12',
      getFullLog: async () => full,
      getSchedule: async () => [{ status: 'Final' }],
    });
    expect(r.state).toBe('settled');
  });

  test('player PLAYED but the stat is missing from the box row → unknown, NEVER void', async () => {
    const r = await src.resolveOutcome({
      ...base,
      getFullLog: async () => [{ date: '2026-07-17', stat: { hits: 2 } }],
      getSchedule: async () => [{ status: 'Final' }],
    });
    expect(r.state).toBe('unknown');
    expect(r.reason).toBe('stat_not_in_box_row');
  });

  test('a throwing source degrades to unknown, never throws', async () => {
    const r = await src.resolveOutcome({
      ...base,
      getFullLog: async () => { throw new Error('net'); },
      getSchedule: async () => { throw new Error('net'); },
    });
    expect(r.state).toBe('unknown');
  });
});
