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

import type { NormalizedPlayerPropMarket } from '../../services/mlb-market-normalizer';
import { runMlbMarketCompletionCron } from '../mlb-market-completion';

function makeMarket(overrides: Partial<NormalizedPlayerPropMarket> = {}): NormalizedPlayerPropMarket {
  return {
    eventId: 'evt-1',
    startsAt: '2026-03-28T19:00:00.000Z',
    homeTeam: 'LAD',
    awayTeam: 'SEA',
    playerId: 'SHOHEI_OHTANI_1_MLB',
    playerName: 'Shohei Ohtani',
    teamShort: 'LAD',
    statType: 'batting_hits',
    normalizedStatType: 'batting_hits',
    line: 1.5,
    marketName: 'Shohei Ohtani Hits Over/Under',
    over: {
      odds: -110,
      impliedProb: 0.5238,
      source: 'fanduel',
      provider: 'sportsgameodds',
      sportsbookName: 'fanduel',
      sportsbookId: 'fanduel',
      timestamp: '2026-03-28T18:58:00.000Z',
      rawMarketId: 'over-1',
    },
    under: {
      odds: -105,
      impliedProb: 0.5122,
      source: 'draftkings',
      provider: 'sportsgameodds',
      sportsbookName: 'draftkings',
      sportsbookId: 'draftkings',
      timestamp: '2026-03-28T18:58:00.000Z',
      rawMarketId: 'under-1',
    },
    primarySource: 'fanduel',
    completionMethod: 'multi_source',
    completenessStatus: 'multi_source_complete',
    sourceMap: [],
    isGapFilled: true,
    availableSides: ['over', 'under'],
    rawMarketCount: 2,
    updatedAt: '2026-03-28T18:58:00.000Z',
    ...overrides,
  };
}

describe('mlb-market-completion cron', () => {
  const release = vi.fn();
  const query = vi.fn();
  const connect = vi.fn(async () => ({ query, release }));
  const end = vi.fn(async () => {});

  const pool = { connect, end };
  const ensureCronRegistration = vi.fn(async () => {});
  const createRun = vi.fn(async () => 'run-1');
  const finalizeRun = vi.fn(async () => {});
  const refreshCronMetrics = vi.fn(async () => {});
  const fetchEvents = vi.fn(async () => [{
    eventId: 'evt-1',
    startsAt: '2026-03-28T19:00:00.000Z',
    homeTeam: 'LAD',
    awayTeam: 'SEA',
  }]);
  const buildMarkets = vi.fn(async () => [makeMarket()]);
  const storeMarkets = vi.fn(async () => {});
  const summarizeMarkets = vi.fn(() => ({
    totalMarkets: 1,
    sourceComplete: 0,
    multiSourceComplete: 1,
    incomplete: 0,
    gapFilled: 1,
    underOnlyMarkets: 0,
    overOnlyMarkets: 0,
    twoWayMarkets: 1,
    zeroGapFillEvents: 0,
    completeRate: 1,
    gapFillAttempts: 1,
    gapFillSuccesses: 1,
    gapFillSuccessRate: 1,
    breakdownByPropType: { batting_hits: 1 },
    sourceDistribution: { fanduel: 1, draftkings: 1 },
    eventBreakdown: [{
      eventId: 'evt-1',
      startsAt: '2026-03-28T19:00:00.000Z',
      homeTeam: 'LAD',
      awayTeam: 'SEA',
      totalMarkets: 1,
      sourceComplete: 0,
      multiSourceComplete: 1,
      incomplete: 0,
      gapFilledMarkets: 1,
      underOnlyMarkets: 0,
      overOnlyMarkets: 0,
      twoWayMarkets: 1,
      zeroGapFillEvent: false,
      completionRate: 1,
    }],
  }));

  beforeEach(() => {
    vi.clearAllMocks();
    query.mockImplementation(async (sql: string) => {
      if (sql.includes('pg_try_advisory_lock')) return { rows: [{ locked: true }] };
      return { rows: [] };
    });
  });

  it('persists a successful run and returns metrics consistent with processed markets', async () => {
    const result = await runMlbMarketCompletionCron({
      pool,
      now: () => 1000,
      dateET: () => '2026-03-28',
      fetchEvents,
      buildMarkets,
      storeMarkets,
      ensureCronRegistration,
      createRun,
      finalizeRun,
      refreshCronMetrics,
      summarizeMarkets,
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.metrics).toMatchObject({
      total_events: 1,
      processed_events: 1,
      failed_events: 0,
      total_markets: 1,
      source_complete: 0,
      multi_source_complete: 1,
      incomplete: 0,
      gap_filled_markets: 1,
      under_only_markets: 0,
      over_only_markets: 0,
      two_way_markets: 1,
      zero_gap_fill_events: 0,
      gap_fill_attempts: 1,
      gap_fill_successes: 1,
      failed_gap_fill_attempts: 0,
      breakdown_by_prop_type: { batting_hits: 1 },
      source_distribution: { fanduel: 1, draftkings: 1 },
      event_breakdown: [expect.objectContaining({
        event_id: 'evt-1',
        gap_filled_markets: 1,
        two_way_markets: 1,
        zero_gap_fill_event: false,
      })],
    });
    expect(ensureCronRegistration).toHaveBeenCalledTimes(1);
    expect(createRun).toHaveBeenCalledTimes(1);
    expect(finalizeRun).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
      runId: 'run-1',
      status: 'SUCCESS',
      metrics: expect.objectContaining({
        total_markets: 1,
        multi_source_complete: 1,
      }),
    }));
    expect(refreshCronMetrics).toHaveBeenCalledTimes(1);
    expect(storeMarkets).toHaveBeenCalledTimes(1);
    expect(query).toHaveBeenCalledWith('BEGIN');
    expect(query).toHaveBeenCalledWith('COMMIT');
    expect(query).toHaveBeenCalledWith('SELECT pg_advisory_unlock($1)', [90328011]);
    expect(release).toHaveBeenCalledTimes(1);
  });

  it('marks the run as skipped when the advisory lock is unavailable', async () => {
    query.mockImplementation(async (sql: string) => {
      if (sql.includes('pg_try_advisory_lock')) return { rows: [{ locked: false }] };
      return { rows: [] };
    });

    const result = await runMlbMarketCompletionCron({
      pool,
      now: () => 1000,
      dateET: () => '2026-03-28',
      fetchEvents,
      buildMarkets,
      storeMarkets,
      ensureCronRegistration,
      createRun,
      finalizeRun,
      refreshCronMetrics,
      summarizeMarkets,
    });

    expect(result.status).toBe('SKIPPED');
    expect(result.metrics).toMatchObject({
      cron: 'mlb_market_completion',
      date_et: '2026-03-28',
      reason: 'lock_not_acquired',
    });
    expect(fetchEvents).not.toHaveBeenCalled();
    expect(finalizeRun).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
      runId: 'run-1',
      status: 'SKIPPED',
      metrics: expect.objectContaining({ reason: 'lock_not_acquired' }),
    }));
    expect(refreshCronMetrics).toHaveBeenCalledTimes(1);
    expect(query).not.toHaveBeenCalledWith('SELECT pg_advisory_unlock($1)', [90328011]);
    expect(release).toHaveBeenCalledTimes(1);
  });

  it('finalizes the run as failed and releases the lock on fatal errors', async () => {
    fetchEvents.mockRejectedValueOnce(new Error('db blew up'));

    await expect(runMlbMarketCompletionCron({
      pool,
      now: () => 1000,
      dateET: () => '2026-03-28',
      fetchEvents,
      buildMarkets,
      storeMarkets,
      ensureCronRegistration,
      createRun,
      finalizeRun,
      refreshCronMetrics,
      summarizeMarkets,
    })).rejects.toThrow('db blew up');

    expect(finalizeRun).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
      runId: 'run-1',
      status: 'FAILED',
      metrics: expect.objectContaining({
        cron: 'mlb_market_completion',
        date_et: '2026-03-28',
      }),
      errorMessage: 'db blew up',
    }));
    expect(refreshCronMetrics).toHaveBeenCalledTimes(1);
    expect(query).toHaveBeenCalledWith('SELECT pg_advisory_unlock($1)', [90328011]);
    expect(release).toHaveBeenCalledTimes(1);
  });
});
