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

const { optionalAuth, poolQuery } = vi.hoisted(() => ({
  optionalAuth: vi.fn((_req: any, _res: any, next: any) => next()),
  poolQuery: vi.fn(),
}));

vi.mock('../../middleware/auth', () => ({ optionalAuth }));
vi.mock('../../db', () => ({ default: { query: poolQuery } }));

describe('/events contract', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.resetModules();
    delete process.env.EVENTS_MARKET_BREAKDOWN;
    delete process.env.FEATURE_STEAM_UNLOCK;
    delete process.env.FEATURE_SHARP_UNLOCK;
  });

  it('returns grouped events with forecastMeta and no marketBreakdown when flag is off', async () => {
    process.env.EVENTS_MARKET_BREAKDOWN = 'false';
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          odds_updated_at: '2026-03-27T18:41:00.000Z',
          source: 'sgo-auto+theodds',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          prop_count: 12,
          player_props_ready_count: 3,
          team_props_count: 2,
          fc_confidence: '0.74',
          fc_data: { value_rating: 8, forecast_side: 'Lakers', sharp_money_indicator: 'yes', line_movement_analysis: 'yes', spread_edge: 3.5, total_edge: 2.1 },
          fc_model_signals: { modelSignals: { digimon: { lockCount: 2 } } },
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({
        rows: [
          { id: 'pp1', event_id: 'evt1', player_name: 'Player One', forecast_payload: { edge: 12, odds: -110 }, forecast_type: 'PLAYER_PROP', recommendation: 'OVER', league: 'nba' },
          { id: 'pp2', event_id: 'evt1', player_name: 'Player Two', forecast_payload: { edge: 11, odds: -105 }, forecast_type: 'PLAYER_PROP', recommendation: 'OVER', league: 'nba' },
          { id: 'pp3', event_id: 'evt1', player_name: 'Player Three', forecast_payload: { edge: 10, odds: 120 }, forecast_type: 'PLAYER_PROP', recommendation: 'OVER', league: 'nba' },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.leagues).toEqual(['nba']);
    expect(res.body.events.nba[0]).toMatchObject({
      id: 'evt1',
      homeTeam: 'Lakers',
      awayTeam: 'Celtics',
      oddsUpdatedAt: '2026-03-27T18:41:00.000Z',
      verificationLabel: 'Double-verified odds',
      forecastStatus: 'ready',
      playerPropsCount: 3,
      playerPropsAvailable: true,
      playerPropsStatus: 'ready',
      digimonLockCount: 2,
      forecastMeta: { hasForecast: true, forecastSide: 'Lakers' },
    });
    expect(res.body.events.nba[0].forecastMeta).not.toHaveProperty('marketBreakdown');
  });

  it('reuses the events response from route cache for identical requests inside the ttl window', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-cache',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 },
          spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } },
          total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Lakers' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);

    const first = await request(app).get('/?league=nba');
    const second = await request(app).get('/?league=nba');

    expect(first.status).toBe(200);
    expect(second.status).toBe(200);
    expect(first.headers['x-rainmaker-cache']).toBe('MISS');
    expect(second.headers['x-rainmaker-cache']).toBe('HIT');
    expect(poolQuery).toHaveBeenCalledTimes(4);
    expect(second.body).toEqual(first.body);
  });

  it('queries public composite confidence for event cards', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.58',
          fc_data: { forecast_side: 'Lakers' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.events.nba[0].forecastMeta.confidence).toBe(0.58);
    expect(String(poolQuery.mock.calls[0][0])).toContain('COALESCE(fc_exact.public_confidence, fc_fallback.public_confidence) AS fc_confidence');
    expect(String(poolQuery.mock.calls[0][0])).toContain("NULLIF(fc.composite_confidence, 'NaN'::float8)");
    expect(String(poolQuery.mock.calls[0][0])).toContain('fc_exact.matched_event_id IS NULL');
    expect(String(poolQuery.mock.calls[0][0])).not.toContain('WHERE fc.event_id = e.event_id\n                               OR');
  });

  it('does not mark NaN confidence rows as ready on event cards', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-nan',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: 'NaN',
          fc_data: { forecast_side: 'Lakers' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.events.nba[0].forecastStatus).toBe('generating');
    expect(res.body.events.nba[0].forecastMeta).toBeNull();
  });

  it('hides weak MLB forecasts on event cards even when cache confidence is present', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-mlb-weak',
          league: 'mlb',
          home_team: 'New York Yankees', home_short: 'NYY', away_team: 'Athletics', away_short: 'ATH',
          starts_at: '2026-04-08T23:00:00.000Z',
          moneyline: { home: -175, away: 150 },
          spread: { home: { line: -1.5, odds: 120 }, away: { line: 1.5, odds: -145 } },
          total: { over: { line: 8.5, odds: -110 }, under: { line: 8.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.74',
          fc_data: {
            forecast_side: 'New York Yankees',
            projected_lines: { moneyline: { home: -188, away: 188 } },
            spread_edge: 2.0,
          },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=mlb');

    expect(res.status).toBe(200);
    expect(res.body.events.mlb[0].forecastStatus).toBe('generating');
    expect(res.body.events.mlb[0].forecastMeta).toBeNull();
  });

  it('adds marketBreakdown when EVENTS_MARKET_BREAKDOWN is enabled', async () => {
    process.env.EVENTS_MARKET_BREAKDOWN = 'true';
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          prop_count: 0,
          player_props_ready_count: 0,
          team_props_count: 2,
          fc_confidence: '0.74',
          fc_data: { value_rating: 8, forecast_side: 'Lakers', spread_edge: 3.5, total_edge: 2.1, moneyline_edge: 1.2 },
          fc_model_signals: null,
          fc_input_quality: { digimon: { lockCount: 1 } },
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.events.nba[0].forecastMeta.marketBreakdown).toEqual({
      spread: 3.5,
      total: 2.1,
      moneyline: 1.2,
    });
    expect(res.body.events.nba[0].playerPropsStatus).toBe('empty_after_run');
    expect(res.body.events.nba[0].digimonLockCount).toBe(1);
  });

  it('does not mark cached prop highlights as ready when there are no visible player prop assets', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 2,
          fc_confidence: '0.74',
          fc_data: {
            forecast_side: 'Lakers',
            prop_highlights: [
              { player: 'Player One', prop: 'Points', recommendation: 'over' },
              { player: 'Player Two', prop: 'Rebounds', recommendation: 'over' },
              { player: 'Player Three', prop: 'Assists', recommendation: 'under' },
              { player: 'Player Four', prop: 'Points', recommendation: 'over' },
            ],
          },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.events.nba[0]).toMatchObject({
      playerPropsCount: 0,
      playerPropsAvailable: false,
      playerPropsStatus: 'empty_after_run',
    });
  });

  it('does not mark props as ready when every active player prop is filtered out', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 2,
          team_props_count: 0,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Lakers' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({
        rows: [
          { id: 'pp1', event_id: 'evt1', player_name: 'Player One', forecast_payload: { edge: 12 }, forecast_type: 'PLAYER_PROP', recommendation: 'OVER', league: 'nba' },
          { id: 'pp2', event_id: 'evt1', player_name: 'Player Two', forecast_payload: { edge: 11 }, forecast_type: 'PLAYER_PROP', recommendation: 'UNDER', league: 'nba' },
        ],
      })
      .mockResolvedValueOnce({
        rows: [
          { name: 'player one', league: 'nba' },
          { name: 'player two', league: 'nba' },
        ],
      });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.events.nba[0]).toMatchObject({
      playerPropsCount: 0,
      playerPropsAvailable: false,
      playerPropsStatus: 'empty_after_run',
    });
  });

  it('does not count unpriced player props as available on event cards', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 2,
          team_props_count: 1,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Lakers' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({
        rows: [
          { id: 'pp1', event_id: 'evt1', player_name: 'Player One', forecast_payload: { prop: 'Points Over 20.5', edge: 12, odds: null }, forecast_type: 'PLAYER_PROP', recommendation: 'OVER', league: 'nba' },
          { id: 'pp2', event_id: 'evt1', player_name: 'Player Two', forecast_payload: { prop: 'Assists Over 6.5', edge: 11 }, forecast_type: 'PLAYER_PROP', recommendation: 'UNDER', league: 'nba' },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.events.nba[0]).toMatchObject({
      playerPropsCount: 0,
      playerPropsAvailable: false,
      playerPropsStatus: 'empty_after_run',
    });
  });

  it('counts non-expired stale player props when active inventory is empty', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Lakers', home_short: 'LAL', away_team: 'Celtics', away_short: 'BOS',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 1,
          team_props_count: 0,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Lakers' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({
        rows: [
          { id: 'pp-stale', event_id: 'evt1', player_name: 'Jayson Tatum', forecast_payload: { prop: 'Points Over 27.5', edge: 12, odds: -108 }, forecast_type: 'PLAYER_PROP', league: 'nba', status: 'STALE' },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(res.body.events.nba[0]).toMatchObject({
      playerPropsCount: 1,
      playerPropsAvailable: true,
      playerPropsStatus: 'ready',
    });
  });

  it('nulls impossible same-sign moneylines instead of showing both teams as favorites', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-bad-ml',
          league: 'mlb',
          home_team: 'Cardinals', home_short: 'STL', away_team: 'Rays', away_short: 'TB',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -213, away: -257 },
          spread: { home: { line: -1.5, odds: 140 }, away: { line: 1.5, odds: -167 } },
          total: { over: { line: 8.5, odds: -105 }, under: { line: 8.5, odds: -115 } },
          opening_moneyline: { home: 115, away: 102 },
          opening_spread: null,
          opening_total: null,
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Cardinals' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=mlb');

    expect(res.status).toBe(200);
    expect(res.body.events.mlb[0].moneyline).toEqual({ home: null, away: null });
    expect(res.body.events.mlb[0].openingMoneyline).toEqual({ home: null, away: null });
  });

  it('keeps sane pickem-style negative moneylines visible on event cards', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-sane-ml',
          league: 'mlb',
          home_team: 'Cardinals', home_short: 'STL', away_team: 'Rays', away_short: 'TB',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -117, away: -104 },
          spread: { home: { line: -1.5, odds: 140 }, away: { line: 1.5, odds: -167 } },
          total: { over: { line: 8.5, odds: -105 }, under: { line: 8.5, odds: -115 } },
          opening_moneyline: { home: -118, away: -102 },
          opening_spread: null,
          opening_total: null,
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Cardinals' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=mlb');

    expect(res.status).toBe(200);
    expect(res.body.events.mlb[0].moneyline).toEqual({ home: -117, away: -104 });
    expect(res.body.events.mlb[0].openingMoneyline).toEqual({ home: -118, away: -102 });
  });

  it('falls back to cached odds_data moneyline when rm_events moneyline is missing', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-cached-ml',
          league: 'mlb',
          home_team: 'Mariners', home_short: 'SEA', away_team: 'Yankees', away_short: 'NYY',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: null, away: null },
          spread: { home: { line: 1.5, odds: -188 }, away: { line: -1.5, odds: 155 } },
          total: { over: { line: 7.5, odds: -117 }, under: { line: 7.5, odds: -105 } },
          opening_moneyline: { home: -113, away: -163 },
          opening_spread: null,
          opening_total: null,
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Yankees' },
          fc_odds_data: { moneyline: { home: -108, away: -111 } },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=mlb');

    expect(res.status).toBe(200);
    expect(String(poolQuery.mock.calls[0]?.[0] || '')).toContain('fc.odds_data AS fc_odds_data');
    expect(res.body.events.mlb[0].moneyline).toEqual({ home: -108, away: -111 });
  });

  it('drops absurd MLB current and cached odds before falling back to sane opening prices', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-mlb-sanitize-route',
          league: 'mlb',
          home_team: 'Colorado Rockies', home_short: 'COL', away_team: 'Houston Astros', away_short: 'HOU',
          starts_at: '2026-04-08T19:00:00.000Z',
          moneyline: { home: -10000, away: 3300 },
          spread: { home: { line: -6.5, odds: -130 }, away: { line: 6.5, odds: 100 } },
          total: { over: { line: 19.5, odds: 105 }, under: { line: 19.5, odds: -143 } },
          opening_moneyline: { home: 105, away: -125 },
          opening_spread: { home: { line: 1.5, odds: -115 }, away: { line: -1.5, odds: -115 } },
          opening_total: { over: { line: 9.5, odds: -110 }, under: { line: 9.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.61',
          fc_data: { forecast_side: 'Houston Astros' },
          fc_odds_data: {
            moneyline: { home: -5000, away: 2100 },
            spread: { home: { line: -4.5, odds: -400 }, away: { line: 4.5, odds: 250 } },
            total: { over: { line: 16.5, odds: 119 }, under: { line: 16.5, odds: -159 } },
          },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=mlb');

    expect(res.status).toBe(200);
    expect(res.body.events.mlb[0].moneyline).toEqual({ home: 105, away: -125 });
    expect(res.body.events.mlb[0].spread).toEqual({ home: null, away: null });
    expect(res.body.events.mlb[0].total).toEqual({
      over: { line: 9.5, odds: -110 },
      under: { line: 9.5, odds: -110 },
    });
    expect(res.body.events.mlb[0].openingSpread).toEqual({
      home: { line: 1.5, odds: -115 },
      away: { line: -1.5, odds: -115 },
    });
  });

  it('falls back to opening moneyline when current and cached moneylines are blank', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-opening-ml',
          league: 'nhl',
          home_team: 'Sharks', home_short: 'SJ', away_team: 'Blues', away_short: 'STL',
          starts_at: '2026-03-27T22:00:00.000Z',
          moneyline: { home: null, away: null },
          spread: { home: { line: -1.5, odds: 221 }, away: { line: 1.5, odds: -278 } },
          total: { over: { line: 6, odds: -105 }, under: { line: 6, odds: -115 } },
          opening_moneyline: { home: -112, away: -131 },
          opening_spread: null,
          opening_total: null,
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.68',
          fc_data: { forecast_side: 'Blues' },
          fc_odds_data: { moneyline: { home: null, away: null } },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nhl');

    expect(res.status).toBe(200);
    expect(res.body.events.nhl[0].moneyline).toEqual({ home: -112, away: -131 });
  });

  it('falls back to projected moneyline when current, cached, and opening moneylines are blank', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-projected-ml',
          league: 'la_liga',
          home_team: 'Rayo Vallecano', home_short: 'RAY', away_team: 'Elche', away_short: 'ELC',
          starts_at: '2026-04-03T19:00:00.000Z',
          moneyline: { home: null, away: null },
          spread: { home: { line: -1.5, odds: 217 }, away: { line: 1.5, odds: -299 } },
          total: { over: { line: 2.5, odds: 102 }, under: { line: 2.5, odds: -125 } },
          opening_moneyline: { home: null, away: null },
          opening_spread: null,
          opening_total: null,
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.545',
          fc_data: {
            forecast_side: 'Rayo Vallecano',
            projected_lines: {
              moneyline: { home: -138, away: 118 },
            },
          },
          fc_odds_data: { moneyline: { home: null, away: null } },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=la_liga');

    expect(res.status).toBe(200);
    expect(res.body.events.la_liga[0].moneyline).toEqual({ home: -138, away: 118 });
  });

  it('keeps same-sign positive soccer moneylines visible on event cards and preserves draw odds', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-soccer-plus-plus',
          league: 'epl',
          home_team: 'Brentford', home_short: 'BRE', away_team: 'Fulham', away_short: 'FUL',
          starts_at: '2026-04-05T16:30:00.000Z',
          moneyline: { home: 175, away: 165, draw: 230 },
          spread: { home: { line: 0, odds: -110 }, away: { line: 0, odds: -110 } },
          total: { over: { line: 2.5, odds: -105 }, under: { line: 2.5, odds: -115 } },
          opening_moneyline: { home: null, away: null },
          opening_spread: null,
          opening_total: null,
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.55',
          fc_data: { forecast_side: 'Brentford' },
          fc_odds_data: null,
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=epl');

    expect(res.status).toBe(200);
    expect(res.body.events.epl[0].moneyline).toEqual({ home: 175, away: 165, draw: 230 });
  });

  it('repairs same-sign projected moneylines when the projected spread names the favorite', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-projected-sign-fix',
          league: 'bundesliga',
          home_team: 'SV Werder Bremen', home_short: 'SVW', away_team: 'RB Leipzig', away_short: 'RBL',
          starts_at: '2026-04-04T13:30:00.000Z',
          moneyline: { home: null, away: null },
          spread: { home: { line: 1, odds: -110 }, away: { line: -1, odds: -110 } },
          total: { over: { line: 3, odds: -110 }, under: { line: 3, odds: -110 } },
          opening_moneyline: { home: null, away: null },
          opening_spread: null,
          opening_total: null,
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.542',
          fc_data: {
            forecast_side: 'RB Leipzig',
            projected_margin: -1,
            projected_lines: {
              spread: { home: 1, away: -1 },
              moneyline: { home: 175, away: 145 },
            },
          },
          fc_odds_data: { moneyline: { home: null, away: null } },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=bundesliga');

    expect(res.status).toBe(200);
    expect(res.body.events.bundesliga[0].moneyline).toEqual({ home: 175, away: -145 });
  });

  it('surfaces source-market suppression reasons on event cards when team props were suppressed upstream', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-suppressed',
          league: 'nba',
          home_team: 'Suns', home_short: 'PHX', away_team: 'Jazz', away_short: 'UTA',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 }, spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } }, total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 2,
          no_source_market_count: 2,
          no_valid_market_match_count: 0,
          fc_confidence: '0.74',
          fc_data: { forecast_side: 'Suns' },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=nba');

    expect(res.status).toBe(200);
    expect(String(poolQuery.mock.calls[0]?.[0] || '')).toContain('AS no_source_market_count');
    expect(String(poolQuery.mock.calls[0]?.[0] || '')).toContain('AS no_valid_market_match_count');
    expect(res.body.events.nba[0]).toMatchObject({
      playerPropsCount: 0,
      playerPropsAvailable: false,
      playerPropsStatus: 'no_source_market_candidates',
    });
  });

  it('sports endpoint only counts events with forecasts, matching the main events feed', async () => {
    const { default: router } = await import('../events');
    poolQuery.mockResolvedValue({
      rows: [
        { league: 'mlb', count: '3' },
        { league: 'nba', count: '6' },
      ],
    });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/sports');

    expect(res.status).toBe(200);
    expect(res.body.sports).toEqual([
      { key: 'mlb', label: 'MLB', todayCount: 3 },
      { key: 'nba', label: 'NBA', todayCount: 6 },
    ]);
    expect(res.body.total).toBe(9);
  });

  it('uses the EU lookahead window for soccer league event queries', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt-soccer',
          league: 'epl',
          home_team: 'Arsenal', home_short: 'ARS', away_team: 'Liverpool', away_short: 'LIV',
          starts_at: '2026-04-03T19:00:00.000Z',
          moneyline: { home: -120, away: 110 },
          spread: { home: { line: -0.5, odds: -110 }, away: { line: 0.5, odds: -110 } },
          total: { over: { line: 2.5, odds: -110 }, under: { line: 2.5, odds: -110 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: null,
          fc_data: null,
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=epl');

    expect(res.status).toBe(200);
    expect(res.body.events.epl[0]).toMatchObject({
      id: 'evt-soccer',
      forecastStatus: 'generating',
    });
    expect(String(poolQuery.mock.calls[0]?.[0] || '')).toContain('BETWEEN');
    expect(poolQuery.mock.calls[0]?.[1]).toEqual(['epl', 14]);
  });

  it('uses the EU lookahead window in the sports endpoint for soccer counts', async () => {
    const { default: router } = await import('../events');
    poolQuery.mockResolvedValue({
      rows: [
        { league: 'epl', count: '4' },
        { league: 'la_liga', count: '5' },
        { league: 'nba', count: '6' },
      ],
    });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/sports');

    expect(res.status).toBe(200);
    expect(String(poolQuery.mock.calls[0]?.[0] || '')).toContain('e.league IN');
    expect(poolQuery.mock.calls[0]?.[1]).toEqual([14]);
    expect(res.body.sports).toEqual([
      { key: 'epl', label: 'English Premier League', todayCount: 4 },
      { key: 'la_liga', label: 'La Liga', todayCount: 5 },
      { key: 'nba', label: 'NBA', todayCount: 6 },
    ]);
  });

  it('returns generating events even when no cached forecast exists yet', async () => {
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt2',
          league: 'mlb',
          home_team: 'Astros', home_short: 'HOU', away_team: 'Angels', away_short: 'LAA',
          starts_at: '2026-03-27T19:00:00.000Z',
          moneyline: { home: -120, away: 110 },
          spread: { home: { line: -1.5, odds: 145 }, away: { line: 1.5, odds: -175 } },
          total: { over: { line: 8.5, odds: -122 }, under: { line: 8.5, odds: 100 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: null,
          fc_data: null,
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=mlb');

    expect(res.status).toBe(200);
    expect(res.body.events.mlb[0]).toMatchObject({
      id: 'evt2',
      forecastStatus: 'generating',
      forecastMeta: null,
    });
  });

  it('marks steam and sharp badges as available when those insight unlocks are enabled', async () => {
    process.env.FEATURE_STEAM_UNLOCK = 'true';
    process.env.FEATURE_SHARP_UNLOCK = 'true';
    const { default: router } = await import('../events');
    poolQuery
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'mlb',
          home_team: 'Astros', home_short: 'HOU', away_team: 'Angels', away_short: 'LAA',
          starts_at: '2026-03-28T19:00:00.000Z',
          moneyline: { home: -120, away: 110 },
          spread: { home: { line: -1.5, odds: 145 }, away: { line: 1.5, odds: -175 } },
          total: { over: { line: 8.5, odds: -122 }, under: { line: 8.5, odds: 100 } },
          player_props_ready_count: 0,
          team_props_count: 0,
          fc_confidence: '0.62',
          fc_data: {
            value_rating: 7,
            forecast_side: 'Astros',
            projected_lines: { moneyline: { home: -118, away: 108 } },
            spread_edge: 1.4,
            sharp_money_indicator: 'No sharp market interest detected. Markets holding steady.',
            line_movement_analysis: 'Markets holding steady with no notable movement.',
          },
          fc_model_signals: null,
          fc_input_quality: null,
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/?league=mlb');

    expect(res.status).toBe(200);
    expect(res.body.events.mlb[0].forecastMeta).toMatchObject({
      hasSharpSignal: true,
      hasSteamSignal: true,
    });
  });
});
