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

import {
  DEPRECATED_FORECAST_RESPONSE_ALIAS_MAP,
  FORECAST_PUBLIC_CONTRACT_VERSION,
} from '../../contracts/forecast-public-contract';

function expectDeprecatedAliasesToMirrorCanonical<T extends Record<string, any>>(
  value: T,
  aliasMap: Record<string, string>,
) {
  for (const [aliasKey, canonicalKey] of Object.entries(aliasMap)) {
    expect(value[aliasKey]).toEqual(value[canonicalKey]);
  }
}

function snapshotForecastResponse(body: any) {
  return {
    contractVersion: body.contractVersion,
    forecast: body.forecast,
    confidence: body.confidence,
    homeTeam: body.homeTeam,
    awayTeam: body.awayTeam,
    league: body.league,
    alreadyOwned: body.alreadyOwned,
    forecastBalance: body.forecastBalance,
    forecast_balance: body.forecast_balance,
    forecastVersion: body.forecastVersion,
    staticCommit: body.staticCommit,
    generatedAt: body.generatedAt,
    rieSignals: Array.isArray(body.rieSignals)
      ? body.rieSignals.map((signal: any) => signal?.signalId || null)
      : body.rieSignals,
    ragInsights: body.ragInsights,
    strategyId: body.strategyId,
    mlbPhaseContext: body.mlbPhaseContext,
    mlbMatchupContext: body.mlbMatchupContext,
    basemonSummary: body.basemonSummary,
  };
}

const mocked = vi.hoisted(() => ({
  authMiddleware: vi.fn((req: any, _res: any, next: any) => { req.user = { userId: 'user-1' }; next(); }),
  optionalAuth: vi.fn((_req: any, _res: any, next: any) => { next(); }),
  getCachedForecast: vi.fn(),
  getCachedForecastByTeams: vi.fn(),
  updateCachedOdds: vi.fn(),
  buildForecastFromCuratedEvent: vi.fn(),
  fetchEvents: vi.fn(),
  hasUserPurchasedPick: vi.fn(),
  deductPick: vi.fn(),
  getPickBalance: vi.fn(),
  findUserById: vi.fn(),
  ensureDailyGrant: vi.fn(),
  isInGracePeriod: vi.fn(() => false),
  recordPick: vi.fn(),
  parseOdds: vi.fn(),
  sanitizeGameOddsForLeague: vi.fn((league: string | null | undefined, odds: any) => {
    if (String(league || '').toLowerCase() !== 'mlb' || !odds) return odds;
    const totalLine = Number(odds.total?.over?.line);
    const underLine = Number(odds.total?.under?.line);
    const overOdds = Number(odds.total?.over?.odds);
    const underOdds = Number(odds.total?.under?.odds);
    const totalLooksBad = Number.isFinite(totalLine)
      && Number.isFinite(underLine)
      && (
        Math.abs(totalLine - underLine) > 0.15
        || totalLine < 4.5
        || totalLine > 12.5
        || Math.abs(overOdds) > 300
        || Math.abs(underOdds) > 300
      );
    return {
      ...odds,
      total: totalLooksBad ? { over: null, under: null } : odds.total,
    };
  }),
  sanitizeMoneylinePair: vi.fn((pair: any) => {
    if (!pair) return { home: null, away: null };
    const home = pair.home ?? null;
    const away = pair.away ?? null;
    if (home != null && away != null && ((home < 0 && away < 0) || (home > 0 && away > 0))) {
      return { home: null, away: null };
    }
    return { home, away };
  }),
  poolQuery: vi.fn(),
}));

vi.mock('../../middleware/auth', () => ({ authMiddleware: mocked.authMiddleware, optionalAuth: mocked.optionalAuth }));
vi.mock('../../models/user', () => ({ findUserById: mocked.findUserById, deductPick: mocked.deductPick, getPickBalance: mocked.getPickBalance, ensureDailyGrant: mocked.ensureDailyGrant, isInGracePeriod: mocked.isInGracePeriod }));
vi.mock('../../models/pick', () => ({ hasUserPurchasedPick: mocked.hasUserPurchasedPick, recordPick: mocked.recordPick }));
vi.mock('../../models/forecast', () => ({ getCachedForecast: mocked.getCachedForecast, getCachedForecastByTeams: mocked.getCachedForecastByTeams, cacheForecast: vi.fn(), updateCachedOdds: mocked.updateCachedOdds }));
vi.mock('../../models/ledger', () => ({ recordLedgerEntry: vi.fn(), LedgerReason: {} }));
vi.mock('../../services/sgo', () => ({
  fetchEvents: mocked.fetchEvents,
  parseOdds: mocked.parseOdds,
  sanitizeGameOddsForLeague: mocked.sanitizeGameOddsForLeague,
  sanitizeMoneylinePair: mocked.sanitizeMoneylinePair,
  LEAGUE_MAP: { nba: 'nba', mlb: 'mlb' },
}));
vi.mock('../../services/forecast-builder', () => ({ buildForecastFromCuratedEvent: mocked.buildForecastFromCuratedEvent }));
vi.mock('../../services/grok', () => ({ generateForecast: vi.fn(), generateTeamProps: vi.fn(), generateSteamInsight: vi.fn(), generateSharpInsight: vi.fn() }));
vi.mock('../../services/dvp', () => ({ generateDvpInsight: vi.fn(), hasDvpData: vi.fn(async () => false) }));
vi.mock('../../services/home-field-scout', () => ({ generateHcwInsight: vi.fn(), hasHcwData: vi.fn(() => false) }));
vi.mock('../../db', () => ({ default: { query: mocked.poolQuery } }));

describe('/forecast contract', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.resetModules();
    delete process.env.FORECAST_PAYLOAD_V2;
    delete process.env.MLB_MARKETS_ENABLED;
    mocked.fetchEvents.mockResolvedValue([]);
    mocked.parseOdds.mockReturnValue({ 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 } } });
    mocked.findUserById.mockResolvedValue({ id: 'user-1', email: 'test@rainmaker.local', is_weatherman: false, email_verified: true });
  });

  it('returns the owned forecast payload contract', async () => {
    process.env.FORECAST_PAYLOAD_V2 = 'false';
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Lakers' },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Lakers',
      away_team: 'Celtics',
      league: 'nba',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      forecast: { winner_pick: 'Lakers' },
      confidence: 0.76,
      homeTeam: 'Lakers',
      awayTeam: 'Celtics',
      league: 'nba',
      alreadyOwned: true,
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      forecastVersion: '1.0',
      forecastBalance: 2,
      forecast_balance: 2,
      staticCommit: true,
    });
    expect(res.body).not.toHaveProperty('rieSignals');
    expect(res.body).not.toHaveProperty('ragInsights');
    expect(res.body).not.toHaveProperty('strategyId');
    expect(res.body).not.toHaveProperty('modelSignals');
    expect(res.body).not.toHaveProperty('inputQuality');
    expectDeprecatedAliasesToMirrorCanonical(res.body, DEPRECATED_FORECAST_RESPONSE_ALIAS_MAP);
    expect(snapshotForecastResponse(res.body)).toMatchInlineSnapshot(`
      {
        "alreadyOwned": true,
        "awayTeam": "Celtics",
        "basemonSummary": null,
        "confidence": 0.76,
        "contractVersion": "forecast-public-v1",
        "forecast": {
          "winner_pick": "Lakers",
        },
        "forecastBalance": 2,
        "forecastVersion": "1.0",
        "forecast_balance": 2,
        "generatedAt": "2026-03-27T19:00:00.000Z",
        "homeTeam": "Lakers",
        "league": "nba",
        "mlbMatchupContext": undefined,
        "mlbPhaseContext": undefined,
        "ragInsights": undefined,
        "rieSignals": undefined,
        "staticCommit": true,
        "strategyId": undefined,
      }
    `);
  });

  it('does not hit SGO when an exact cached forecast already exists', async () => {
    process.env.FORECAST_PAYLOAD_V2 = 'false';
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      event_id: 'evt1',
      forecast_data: { winner_pick: 'Lakers' },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Lakers',
      away_team: 'Celtics',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'nba',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(mocked.fetchEvents).not.toHaveBeenCalled();
    expect(mocked.getCachedForecastByTeams).not.toHaveBeenCalled();
  });

  it('preserves a zero composite confidence instead of falling back to legacy confidence', async () => {
    process.env.FORECAST_PAYLOAD_V2 = 'false';
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Lakers' },
      confidence_score: 0.72,
      composite_confidence: 0,
      home_team: 'Lakers',
      away_team: 'Celtics',
      league: 'nba',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.confidence).toBe(0);
  });

  it('regenerates a curated forecast through the shared builder path', async () => {
    const { default: router } = await import('../forecast');

    mocked.findUserById.mockResolvedValue({
      id: 'user-1',
      email: 'admin@rainmakersports.app',
      is_weatherman: true,
      email_verified: true,
    });
    mocked.poolQuery.mockResolvedValue({
      rows: [{
        event_id: 'evt-admin-1',
        league: 'nba',
        home_team: 'Lakers',
        away_team: 'Celtics',
        home_short: 'LAL',
        away_short: 'BOS',
        starts_at: '2026-03-28T23:30:00.000Z',
        moneyline: { home: -130, away: 110 },
        spread: { home: { line: -3.5, odds: -110 }, away: { line: 3.5, odds: -110 } },
        total: { over: { line: 226.5, odds: -110 }, under: { line: 226.5, odds: -110 } },
      }],
    });
    mocked.buildForecastFromCuratedEvent.mockResolvedValue({
      forecast: { winner_pick: 'Lakers' },
      forecastId: 'fc-admin-1',
      confidenceScore: 0.81,
      cached: false,
      odds: {},
    });
    mocked.getCachedForecast.mockResolvedValue({
      id: 'fc-admin-1',
      created_at: '2026-03-28T22:45:00.000Z',
      last_refresh_at: null,
      last_refresh_type: null,
      material_change: null,
    });

    const app = express();
    app.use('/', router);
    const res = await request(app).post('/evt-admin-1/regenerate');

    expect(res.status).toBe(200);
    expect(mocked.buildForecastFromCuratedEvent).toHaveBeenCalledWith(expect.objectContaining({
      event_id: 'evt-admin-1',
      home_team: 'Lakers',
      away_team: 'Celtics',
      league: 'nba',
    }));
    expect(res.body).toMatchObject({
      status: 'ok',
      eventId: 'evt-admin-1',
      forecastId: 'fc-admin-1',
      confidenceScore: 0.81,
      homeTeam: 'Lakers',
      awayTeam: 'Celtics',
      league: 'nba',
    });
  });

  it('treats a cached forecast alias as already owned on the SGO path', async () => {
    const { default: router } = await import('../forecast');

    mocked.fetchEvents.mockResolvedValue([
      {
        eventID: 'sgo-evt-1',
        teams: {
          home: { names: { long: 'Dodgers', short: 'LAD' } },
          away: { names: { long: 'Diamondbacks', short: 'ARI' } },
        },
        status: {
          startsAt: '2026-03-27T19:00:00.000Z',
          displayShort: '7:00 PM',
          oddsPresent: true,
        },
        odds: {},
      },
    ]);
    mocked.getCachedForecast.mockResolvedValue(null);
    mocked.getCachedForecastByTeams.mockResolvedValue({
      id: 'fc-1',
      event_id: 'curated-evt-1',
      forecast_data: { winner_pick: 'Dodgers' },
      confidence_score: 0.64,
      composite_confidence: 0.68,
      home_team: 'Dodgers',
      away_team: 'Diamondbacks',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T18:00:00.000Z',
    });
    mocked.hasUserPurchasedPick.mockImplementation(async (_userId: string, resolvedEventId: string, forecastId?: string | null) => (
      resolvedEventId === 'curated-evt-1' && forecastId === 'fc-1'
    ));
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(mocked.hasUserPurchasedPick).toHaveBeenCalledWith('user-1', 'curated-evt-1', 'fc-1');
    expect(res.body).toMatchObject({
      homeTeam: 'Dodgers',
      awayTeam: 'Diamondbacks',
      league: 'mlb',
      alreadyOwned: true,
    });
    expect(mocked.recordPick).not.toHaveBeenCalled();
  });

  it('synthesizes model lines from projected margin and projected total when projected_lines are missing', async () => {
    process.env.FORECAST_PAYLOAD_V2 = 'false';
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        projected_margin: 0.2,
        projected_total_points: 9.5,
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Diamondbacks',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.modelLines).toMatchObject({
      spread: { home: -0.2, away: 0.2 },
      total: 9.5,
    });
  });

  it('contains the nested legacy forecast bag behind the serializer boundary', async () => {
    process.env.FORECAST_PAYLOAD_V2 = 'true';
    process.env.MLB_MARKETS_ENABLED = 'true';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        model_signals: { should_not_leak: true },
        input_quality: { should_not_leak: true },
        rie_signals: [{ should_not_leak: true }],
        rag_insights: [{ should_not_leak: true }],
        strategy_id: 'legacy-internal',
        mlb_phase_context: {
          phase_label: 'Early',
          k_rank: 4,
          lineup_certainty: 'confirmed',
          park_factor: 108,
          weather_impact: 'positive',
        },
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: { rieSignals: [{ signalId: 'mlb_phase' }], ragInsights: [{ query: 'bullpen' }], strategyId: 'mlb' },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.forecast).toMatchObject({
      winner_pick: 'Dodgers',
      mlb_phase_context: {
        phase_label: 'Early',
        k_rank: 4,
        lineup_certainty: 'confirmed',
        park_factor: 108,
        weather_impact: 'positive',
      },
    });
    expect(res.body.forecast).not.toHaveProperty('model_signals');
    expect(res.body.forecast).not.toHaveProperty('input_quality');
    expect(res.body.forecast).not.toHaveProperty('rie_signals');
    expect(res.body.forecast).not.toHaveProperty('rag_insights');
    expect(res.body.forecast).not.toHaveProperty('strategy_id');
    expect(res.body.forecast).not.toHaveProperty('prop_highlights');
  });

  it('returns not_ready when a curated event exists but cache is cold', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(false);
    mocked.findUserById.mockResolvedValue({ id: 'user-1', is_weatherman: false, email_verified: true });
    mocked.ensureDailyGrant.mockResolvedValue(undefined);
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 });
    mocked.getCachedForecast.mockResolvedValue(null);
    mocked.getCachedForecastByTeams.mockResolvedValue(null);
    mocked.poolQuery.mockResolvedValue({ rows: [] });
    mocked.poolQuery.mockResolvedValueOnce({ rows: [{ event_id: 'evt1', home_team: 'Lakers', away_team: 'Celtics', home_short: 'LAL', away_short: 'BOS', league: 'nba', moneyline: null, spread: null, total: null, starts_at: '2026-03-27T19:00:00.000Z' }] });

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

    expect(res.status).toBe(202);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      status: 'not_ready',
      eventId: 'evt1',
      homeTeam: 'Lakers',
      awayTeam: 'Celtics',
      league: 'nba',
      insightAvailability: { steam: true, sharp: true, dvp: false, hcw: false },
      unlockedInsights: [],
    });
    expect(res.body).toMatchInlineSnapshot(`
      {
        "awayShort": "BOS",
        "awayTeam": "Celtics",
        "contractVersion": "forecast-public-v1",
        "eventId": "evt1",
        "homeShort": "LAL",
        "homeTeam": "Lakers",
        "insightAvailability": {
          "dvp": false,
          "hcw": false,
          "sharp": true,
          "steam": true,
        },
        "league": "nba",
        "message": "Forecast is being generated. Check back shortly.",
        "odds": {
          "moneyline": {
            "away": null,
            "home": null,
          },
          "spread": {
            "away": null,
            "home": null,
          },
          "total": {
            "over": null,
            "under": null,
          },
        },
        "status": "not_ready",
        "unlockedInsightData": {},
        "unlockedInsights": [],
      }
    `);
  });

  it('serves cached MLB forecasts for logged-in users instead of forcing not_ready', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(false);
    mocked.findUserById.mockResolvedValue({ id: 'user-1', is_weatherman: false, email_verified: true });
    mocked.ensureDailyGrant.mockResolvedValue(undefined);
    mocked.recordPick.mockResolvedValue({ inserted: true });
    mocked.deductPick.mockResolvedValue({ success: true, source: 'single_pick' });
    mocked.getPickBalance
      .mockResolvedValueOnce({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 })
      .mockResolvedValueOnce({ single_picks: 0, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 });
    mocked.getCachedForecast.mockResolvedValue({
      id: 'fc-mlb-weak',
      event_id: 'evt-mlb-weak',
      forecast_data: {
        forecast_side: 'New York Yankees',
        winner_pick: 'New York Yankees',
        projected_lines: { moneyline: { home: -188, away: 188 } },
        spread_edge: 2.0,
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'New York Yankees',
      away_team: 'Athletics',
      starts_at: '2026-04-08T23:00:00.000Z',
      league: 'mlb',
      odds_data: { moneyline: { home: -175, away: 150 } },
      model_signals: { modelSignals: null },
      created_at: '2026-04-08T18:00:00.000Z',
    });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      alreadyOwned: true,
      homeTeam: 'New York Yankees',
      awayTeam: 'Athletics',
      league: 'mlb',
    });
    expect(mocked.ensureDailyGrant).toHaveBeenCalled();
    expect(mocked.recordPick).toHaveBeenCalled();
    expect(mocked.deductPick).toHaveBeenCalled();
  });

  it('returns canonical and deprecated balance fields for cached insight unlock responses', async () => {
    const { default: router } = await import('../forecast');
    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [{ id: 'unlock-1' }] })
      .mockResolvedValueOnce({ rows: [{ insight_data: { title: 'Steam Move Alert' } }] });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 2 });

    const app = express();
    app.use(express.json());
    app.use('/', router);
    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt1', type: 'STEAM_INSIGHT', league: 'nba' });

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      insightType: 'STEAM',
      insightData: { title: 'Steam Move Alert' },
      forecastBalance: 3,
      forecast_balance: 3,
    });
    expectDeprecatedAliasesToMirrorCanonical(res.body, DEPRECATED_FORECAST_RESPONSE_ALIAS_MAP);
  });

  it('exposes additive payload fields when Phase 4 flags are on', async () => {
    process.env.FORECAST_PAYLOAD_V2 = 'true';
    process.env.MLB_MARKETS_ENABLED = 'true';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Dodgers', mlb_phase_context: { k_rank: 4 } },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'mlb',
      odds_data: null,
      model_signals: {
        modelSignals: null,
        rieSignals: [
          { signalId: 'mlb_phase' },
          {
            signalId: 'mlb_matchup',
            available: true,
            rawData: {
              park: { runs: 1.08, hr: 1.12 },
              log5: { homeWinProbHFA: 0.58, impliedSpread: -0.7, strengthDiff: 'Slight edge' },
              runsCreated: { home: 5.1, away: 4.4 },
              pythagorean: { home: { winPct: 0.57 }, away: { winPct: 0.49 } },
              projections: { homeBat: { projWrcPlus: 109 }, awayBat: { projWrcPlus: 96 } },
              starters: { home: { fangraphs: { fip: 3.41 } }, away: { fangraphs: { fip: 4.18 } } },
            },
          },
          {
            signalId: 'fangraphs',
            available: true,
            rawData: {
              homeBat: { wrcPlus: 112 },
              awayBat: { wrcPlus: 94 },
            },
          },
        ],
        ragInsights: [{ query: 'bullpen' }],
        strategyId: 'mlb',
      },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt1',
            home_team: 'Dodgers',
            away_team: 'Giants',
            home_short: 'LAD',
            away_short: 'SF',
            league: 'mlb',
            moneyline: null,
            spread: null,
            total: null,
            starts_at: '2026-03-27T19:00:00.000Z',
          }],
        };
      }
      if (sql.includes('FROM sportsclaw.basemon_picks')) {
        return {
          rows: [
            {
              market: 'pitcher_strikeouts',
              player_name: 'David Peterson',
              line: 4.5,
              direction: 'over',
              verdict: 'LOCK',
              edge: 0.12,
              model_prob: 0.63,
              implied_prob: 0.54,
              note: 'Clean spot.',
            },
          ],
        };
      }
      return { rows: [] };
    });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      rieSignals: expect.arrayContaining([{ signalId: 'mlb_phase' }]),
      ragInsights: [{ query: 'bullpen' }],
      strategyId: 'mlb',
      mlbPhaseContext: {
        phaseLabel: null,
        kRank: 4,
        lineupCertainty: null,
        parkFactor: 108,
        weatherImpact: null,
      },
      mlbMatchupContext: {
        log5HomeWinProb: 0.58,
        impliedSpread: -0.7,
        strengthDiff: 'Slight edge',
        parkRunsFactor: 1.08,
        parkHrFactor: 1.12,
        homeRunsCreated: 5.1,
        awayRunsCreated: 4.4,
        homePythagWinPct: 0.57,
        awayPythagWinPct: 0.49,
        homeProjectedWrcPlus: 109,
        awayProjectedWrcPlus: 96,
        homeTeamWrcPlus: 112,
        awayTeamWrcPlus: 94,
        homeStarterFip: 3.41,
        awayStarterFip: 4.18,
      },
    });
    expect(res.body.forecast.prop_highlights).toEqual([
      expect.objectContaining({
        player: 'David Peterson',
        prop: 'Pitcher Ks OVER 4.5',
        recommendation: 'LOCK',
      }),
    ]);
    expectDeprecatedAliasesToMirrorCanonical(res.body, DEPRECATED_FORECAST_RESPONSE_ALIAS_MAP);
    expect(snapshotForecastResponse(res.body)).toMatchInlineSnapshot(`
      {
        "alreadyOwned": true,
        "awayTeam": "Giants",
        "basemonSummary": {
          "available": true,
          "markets": [
            {
              "leanCount": 0,
              "lockCount": 1,
              "market": "Pitcher Ks",
              "playCount": 0,
              "skipCount": 0,
            },
          ],
          "topPicks": [
            {
              "direction": "over",
              "edge": 12,
              "impliedProb": 0.54,
              "line": 4.5,
              "market": "Pitcher Ks",
              "modelProb": 0.63,
              "note": "Clean spot.",
              "player": "David Peterson",
              "verdict": "LOCK",
            },
          ],
          "totalLockCount": 1,
          "totalPlayCount": 0,
        },
        "confidence": 0.76,
        "contractVersion": "forecast-public-v1",
        "forecast": {
          "mlb_phase_context": {
            "k_rank": 4,
            "lineup_certainty": null,
            "park_factor": null,
            "phase_label": null,
            "weather_impact": null,
          },
          "prop_highlights": [
            {
              "player": "David Peterson",
              "prop": "Pitcher Ks OVER 4.5",
              "reasoning": "Clean spot.",
              "recommendation": "LOCK",
            },
          ],
          "winner_pick": "Dodgers",
        },
        "forecastBalance": 2,
        "forecastVersion": "1.0",
        "forecast_balance": 2,
        "generatedAt": "2026-03-27T19:00:00.000Z",
        "homeTeam": "Dodgers",
        "league": "mlb",
        "mlbMatchupContext": {
          "awayProjectedWrcPlus": 96,
          "awayPythagWinPct": 0.49,
          "awayRunsCreated": 4.4,
          "awayStarterFip": 4.18,
          "awayTeamWrcPlus": 94,
          "homeProjectedWrcPlus": 109,
          "homePythagWinPct": 0.57,
          "homeRunsCreated": 5.1,
          "homeStarterFip": 3.41,
          "homeTeamWrcPlus": 112,
          "impliedSpread": -0.7,
          "log5HomeWinProb": 0.58,
          "parkHrFactor": 1.12,
          "parkRunsFactor": 1.08,
          "strengthDiff": "Slight edge",
        },
        "mlbPhaseContext": {
          "kRank": 4,
          "lineupCertainty": null,
          "parkFactor": 108,
          "phaseLabel": null,
          "weatherImpact": null,
        },
        "ragInsights": [
          {
            "query": "bullpen",
          },
        ],
        "rieSignals": [
          "mlb_phase",
          "mlb_matchup",
          "fangraphs",
        ],
        "staticCommit": true,
        "strategyId": "mlb",
      }
    `);
  });

  it('omits mlbPhaseContext when MLB_MARKETS_ENABLED is false', async () => {
    process.env.MLB_MARKETS_ENABLED = 'false';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Dodgers', mlb_phase_context: { k_rank: 4 } },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).not.toHaveProperty('mlbPhaseContext');
    expectDeprecatedAliasesToMirrorCanonical(res.body, DEPRECATED_FORECAST_RESPONSE_ALIAS_MAP);
    expect(res.body.forecast).toEqual({
      winner_pick: 'Dodgers',
      mlb_phase_context: {
        k_rank: 4,
        lineup_certainty: null,
        park_factor: null,
        phase_label: null,
        weather_impact: null,
      },
    });
  });

  it('derives public mlb phase context from nested raw signal payloads', async () => {
    process.env.MLB_MARKETS_ENABLED = 'true';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        mlb_phase_context: {
          firstFive: {
            homeStarter: { kBbPct: 0.21 },
            awayStarter: { kBbPct: 0.09 },
          },
          context: {
            lineups: { homeStatus: 'Confirmed', awayStatus: 'Confirmed' },
            weather: { temperatureF: 62, windMph: 12, conditions: 'Clear' },
            weatherRunBias: 0.06,
          },
          applicationHints: {
            propMatrixReady: true,
            fullGameSideReady: true,
            f5SideReady: true,
          },
        },
        mlb_matchup_context: {
          park_runs_factor: 1.08,
        },
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.mlbPhaseContext).toEqual({
      phaseLabel: 'Ready',
      kRank: 8,
      lineupCertainty: 'confirmed',
      parkFactor: 108,
      weatherImpact: 'positive (62F, 12 mph wind, clear)',
    });
    expect(res.body.forecast.mlb_phase_context).toEqual({
      phase_label: 'Ready',
      k_rank: 8,
      lineup_certainty: 'confirmed',
      park_factor: 108,
      weather_impact: 'positive (62F, 12 mph wind, clear)',
    });
  });

  it('derives mlb matchup context from rie signals when old cached rows do not store it yet', async () => {
    process.env.MLB_MARKETS_ENABLED = 'true';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Dodgers' },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: {
        rieSignals: [
          {
            signalId: 'mlb_matchup',
            available: true,
            rawData: {
              log5: { homeWinProbHFA: 0.61, impliedSpread: -1.1, strengthDiff: 'Clear edge' },
              park: { runs: 0.98, hr: 1.03 },
              runsCreated: { home: 5.4, away: 4.6 },
              pythagorean: { home: { winPct: 0.59 }, away: { winPct: 0.48 } },
              projections: { homeBat: { projWrcPlus: 111 }, awayBat: { projWrcPlus: 97 } },
            },
          },
          {
            signalId: 'fangraphs',
            available: true,
            rawData: {
              homeBat: { wrcPlus: 114 },
              awayBat: { wrcPlus: 92 },
              homePit: { fip: 3.22 },
              awayPit: { fip: 4.05 },
            },
          },
        ],
      },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.mlbMatchupContext).toMatchObject({
      log5HomeWinProb: 0.61,
      impliedSpread: -1.1,
      strengthDiff: 'Clear edge',
      parkRunsFactor: 0.98,
      parkHrFactor: 1.03,
      homeRunsCreated: 5.4,
      awayRunsCreated: 4.6,
      homePythagWinPct: 0.59,
      awayPythagWinPct: 0.48,
      homeProjectedWrcPlus: 111,
      awayProjectedWrcPlus: 97,
      homeTeamWrcPlus: 114,
      awayTeamWrcPlus: 92,
      homeStarterFip: 3.22,
      awayStarterFip: 4.05,
    });
  });

  it('returns null basemonSummary when no basemon picks exist for an MLB forecast', async () => {
    process.env.MLB_MARKETS_ENABLED = 'true';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Dodgers' },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      home_short: 'LAD',
      away_short: 'SF',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.basemonSummary).toBeNull();
  });

  it('omits mlbPhaseContext for non-MLB forecasts even when MLB_MARKETS_ENABLED is true', async () => {
    process.env.MLB_MARKETS_ENABLED = 'true';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Lakers', mlb_phase_context: { k_rank: 4 } },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Lakers',
      away_team: 'Celtics',
      league: 'nba',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).not.toHaveProperty('mlbPhaseContext');
  });

  it('reads native model_signals shape without breaking legacy response fields', async () => {
    process.env.FORECAST_PAYLOAD_V2 = 'false';
    const { default: router } = await import('../forecast');

    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: { winner_pick: 'Lakers', value_rating: 8 },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Lakers',
      away_team: 'Celtics',
      league: 'nba',
      odds_data: null,
      model_signals: {
        compositeConfidence: 0.76,
        stormCategory: 4,
        signals: [],
        strategyProfile: { league: 'nba', signalWeights: {}, requiredSignals: [], optionalSignals: [], ragQueries: [] },
        ragInsights: [],
        edgeBreakdown: { grok: { score: 0.77, weight: 1, contribution: 0.77 } },
        inputQuality: { piff: 'A', dvp: 'B', digimon: 'N/A', odds: 'A', rag: 'A', overall: 'A' },
        compositeVersion: 'rm_2.0',
      },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.modelSignals).toMatchObject({
      grok: {
        confidence: 0.77,
        valueRating: 8,
      },
      piff: null,
      digimon: null,
      dvp: null,
    });
    expect(res.body.inputQuality).toMatchObject({
      piff: 'A',
      dvp: 'B',
      digimon: 'N/A',
      odds: 'A',
      rag: 'A',
      overall: 'A',
    });
    expect(res.body.forecastVersion).toBe('rm_2.0');
  });

  it('sanitizes public forecast copy and limits Rainmaker branding repetition', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        summary: 'Rainmaker sees value here. Rainman also likes the total. Model Stack says Grok and PIFF agree.',
        key_factors: [
          'Data Quality remains stable for this spot.',
          'Rainmaker likes the matchup, but Rainmaker should not be repeated.',
        ],
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      input_quality: { overall: 'A' },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).not.toHaveProperty('modelSignals');
    expect(res.body.inputQuality).toMatchObject({ overall: 'A' });
    expect(JSON.stringify(res.body.forecast)).not.toMatch(/Model Stack|Data Quality|Grok|PIFF/i);

    const text = JSON.stringify(res.body.forecast);
    const brandMentions = (text.match(/\bRainmaker\b|\bRain\s?Man\b|\bRainman\b/g) || []).length;
    expect(brandMentions).toBeLessThanOrEqual(2);
  });

  it('strips internal suppression audit fields from the public forecast payload', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        _suppressed_props: [
          { player: 'J.T. Realmuto', prop: 'total_bases', recommendation: 'under 1.5' },
        ],
        _suppression_at: '2026-03-28T17:00:08.899550+00:00',
        _suppression_run_id: '9ac9b623-194',
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.forecast).not.toHaveProperty('_suppressed_props');
    expect(res.body.forecast).not.toHaveProperty('_suppression_at');
    expect(res.body.forecast).not.toHaveProperty('_suppression_run_id');
  });

  it('hydrates empty prop_highlights from active player prop assets', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        prop_highlights: [],
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_events')) return { rows: [] };
      if (sql.includes('FROM rm_insight_unlocks')) return { rows: [] };
      if (sql.includes('FROM rm_forecast_precomputed')) {
        return {
          rows: [
            {
              id: 'asset-1',
              player_name: 'Mookie Betts',
              confidence_score: 0.88,
              forecast_payload: {
                prop: 'Hits Over 1.5',
                recommendation: 'over',
                reasoning: 'Lead-off profile and matchup keep the over live.',
              },
            },
          ],
        };
      }
      return { rows: [] };
    });

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

    expect(res.status).toBe(200);
    expect(res.body.forecast.prop_highlights).toEqual([
      expect.objectContaining({
        player: 'Mookie Betts',
        prop: 'Hits Over 1.5',
        recommendation: 'over',
      }),
    ]);
  });

  it('suppresses cached prop_highlights when no source-backed assets survive', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Nets',
        prop_highlights: [
          {
            player: 'LaMelo Ball',
            prop: 'Points Over 5.5',
            recommendation: 'over',
            reasoning: 'Bad alt line leaked through.',
          },
        ],
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Nets',
      away_team: 'Hornets',
      league: 'nba',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_events')) return { rows: [] };
      if (sql.includes('FROM rm_insight_unlocks')) return { rows: [] };
      if (sql.includes('FROM rm_forecast_precomputed')) return { rows: [] };
      return { rows: [] };
    });

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

    expect(res.status).toBe(200);
    expect(res.body.forecast.prop_highlights || []).toEqual([]);
  });

  it('falls back to team props bundles when player prop assets are unavailable', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        prop_highlights: [],
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_events')) return { rows: [] };
      if (sql.includes('FROM rm_insight_unlocks')) return { rows: [] };
      if (sql.includes("forecast_type = 'PLAYER_PROP'")) return { rows: [] };
      if (sql.includes("forecast_type = 'TEAM_PROPS'")) {
        return {
          rows: [
            {
              team_side: 'home',
              status: 'STALE',
              forecast_payload: {
                props: [
                  {
                    player: 'Shohei Ohtani',
                    prop: 'Total Bases Over 1.5',
                    recommendation: 'over',
                    reasoning: 'Barrel profile and matchup still support the over.',
                  },
                ],
              },
            },
          ],
        };
      }
      return { rows: [] };
    });

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

    expect(res.status).toBe(200);
    expect(res.body.forecast.prop_highlights).toEqual([
      expect.objectContaining({
        player: 'Shohei Ohtani',
        prop: 'Total Bases Over 1.5',
        recommendation: 'over',
      }),
    ]);
  });

  it('does not expose actionable side, edge, or confidence fields in preview payloads', async () => {
    const { default: router } = await import('../forecast');
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Braves',
        forecast_side: 'Royals',
        projected_winner: 'Braves',
        summary: 'Lean Braves, but Royals have spread value.',
        spread_edge: 2.5,
        value_rating: 7,
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Atlanta Braves',
      away_team: 'Kansas City Royals',
      league: 'mlb',
      odds_data: {
        moneyline: { home: -152, away: 125 },
        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 } },
      },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      winnerPick: null,
      forecastSide: null,
      projectedWinner: null,
      confidence: null,
      modelEdge: null,
      valueRating: null,
      basemonSummary: null,
    });
    expect(res.body).not.toHaveProperty('modelSignals');
    expect(res.body).not.toHaveProperty('inputQuality');
    expect(res.body).not.toHaveProperty('rieSignals');
    expect(res.body).not.toHaveProperty('ragInsights');
    expect(res.body).not.toHaveProperty('strategyId');
    expect(res.body).not.toHaveProperty('mlbPhaseContext');
    expect(res.body).not.toHaveProperty('mlbMatchupContext');
    expect(res.body).not.toHaveProperty('forecastVersion');
    expect(res.body).not.toHaveProperty('generatedAt');
    expect(res.body).not.toHaveProperty('lastRefreshAt');
    expect(res.body).not.toHaveProperty('lastRefreshType');
    expect(res.body).not.toHaveProperty('materialChange');
    expect(res.body).not.toHaveProperty('contractVersion');
    expect(res.body).not.toHaveProperty('staticCommit');
    expect(res.body).not.toHaveProperty('compositeConfidence');
  });

  it('returns preview metadata needed by the no-credits popup', async () => {
    const { default: router } = await import('../forecast');
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Braves',
        forecast_side: 'Royals',
        projected_winner: 'Braves',
        summary: 'Lean Braves, but Royals have spread value.',
        value_rating: 7,
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Atlanta Braves',
      away_team: 'Kansas City Royals',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'mlb',
      odds_data: {
        moneyline: { home: -152, away: 125 },
        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 } },
      },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.poolQuery.mockImplementation(async (query: string) => {
      if (query.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt1',
            home_team: 'Atlanta Braves',
            away_team: 'Kansas City Royals',
            home_short: 'ATL',
            away_short: 'KC',
            starts_at: '2026-03-27T19:00:00.000Z',
            league: 'mlb',
            moneyline: { home: -152, away: 125 },
            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 } },
          }],
        };
      }
      if (query.includes('FROM sportsclaw.basemon_picks')) {
        return {
          rows: [{
            market: 'pitcher_ks',
            player_name: 'Cole Ragans',
            line: 6.5,
            direction: 'over',
            verdict: 'LOCK',
            edge: 0.08,
            model_prob: 0.61,
            implied_prob: 0.53,
            note: 'Whiff profile stays live.',
          }],
        };
      }
      return { rows: [] };
    });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      homeTeam: 'Atlanta Braves',
      awayTeam: 'Kansas City Royals',
      homeShort: 'ATL',
      awayShort: 'KC',
      summaryPreview: expect.stringContaining('Atlanta Braves'),
      insightAvailability: {
        steam: true,
        sharp: true,
        dvp: false,
        hcw: false,
      },
      basemonSummary: null,
    });
  });

  it('fills preview odds gaps from cached odds when curated event odds are incomplete', async () => {
    const { default: router } = await import('../forecast');
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {},
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Atlanta Braves',
      away_team: 'Kansas City Royals',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'mlb',
      odds_data: {
        moneyline: { home: -152, away: 125 },
        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 } },
      },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.poolQuery.mockImplementation(async (query: string) => {
      if (query.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt1',
            home_team: 'Atlanta Braves',
            away_team: 'Kansas City Royals',
            home_short: 'ATL',
            away_short: 'KC',
            starts_at: '2026-03-27T19:00:00.000Z',
            league: 'mlb',
            moneyline: { home: null, away: null },
            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 } },
          }],
        };
      }
      return { rows: [] };
    });

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

    expect(res.status).toBe(200);
    expect(res.body.odds).toMatchObject({
      moneyline: { home: -152, away: 125 },
      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 } },
    });
  });

  it('sanitizes corrupt MLB totals in preview odds', async () => {
    const { default: router } = await import('../forecast');
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {},
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Houston Astros',
      away_team: 'Oakland Athletics',
      starts_at: '2026-04-05T20:06:00.000Z',
      league: 'mlb',
      odds_data: {
        moneyline: { home: -152, away: 128 },
        spread: { home: { line: -1.5, odds: 140 }, away: { line: 1.5, odds: -167 } },
        total: { over: { line: 16.5, odds: 119 }, under: { line: 16.5, odds: -159 } },
      },
      created_at: '2026-04-05T19:00:00.000Z',
    });
    mocked.poolQuery.mockImplementation(async (query: string) => {
      if (query.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-mlb-total-preview',
            home_team: 'Houston Astros',
            away_team: 'Oakland Athletics',
            home_short: 'HOU',
            away_short: 'OAK',
            starts_at: '2026-04-05T20:06:00.000Z',
            league: 'mlb',
            moneyline: { home: -152, away: 128 },
            spread: { home: { line: -1.5, odds: 140 }, away: { line: 1.5, odds: -167 } },
            total: { over: { line: 19.5, odds: 105 }, under: { line: 19.5, odds: -143 } },
            opening_moneyline: { home: -145, away: 122 },
            opening_spread: { home: { line: -1.5, odds: 138 }, away: { line: 1.5, odds: -165 } },
            opening_total: { over: { line: 9.5, odds: -110 }, under: { line: 9.5, odds: -110 } },
          }],
        };
      }
      return { rows: [] };
    });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/evt-mlb-total-preview/preview');

    expect(res.status).toBe(200);
    expect(res.body.odds.total).toEqual({ over: null, under: null });
  });

  it('normalizes stale summary and clip metadata when forecast side differs from projected winner', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Braves',
        forecast_side: 'Royals',
        projected_winner: 'Braves',
        summary: 'Lean Braves, but Royals have spread value.',
        spread_edge: 2.5,
        projected_margin: 0.8,
        clip_metadata: [
          {
            clip_type: 'GAME_FORECAST',
            display_text: 'KC @ ATL | Value: ATL | Edge +2.5pts | Winner: Braves',
            clip_data: { forecast_side: 'Braves', winner: 'Braves' },
          },
          {
            clip_type: 'SPREAD',
            display_text: 'KC @ ATL | Forecast: ATL -1.5 | Model margin: -0.8pts | Edge +2.5pts',
            clip_data: { forecast_side: 'Braves' },
          },
        ],
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Atlanta Braves',
      away_team: 'Kansas City Royals',
      league: 'mlb',
      odds_data: {
        moneyline: { home: -152, away: 125 },
        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 } },
      },
      model_signals: null,
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.forecast.summary).toContain('Outcome lean: Atlanta Braves. Best value: Royals against the run line.');
    expect(res.body.forecast.summary).toContain('Treat this as a run line-value call, not an outright winner flip.');
    expect(res.body.forecast.summary).not.toContain('Lean Braves, but Royals have spread value.');
    expect(res.body.forecast.clip_metadata[0]).toMatchObject({
      display_text: 'ROYALS @ BRAVES | Value: Royals | Edge +2.5 runs | Winner: Atlanta Braves',
      clip_data: {
        forecast_side: 'Royals',
        winner: 'Atlanta Braves',
        projected_winner: 'Atlanta Braves',
      },
    });
    expect(res.body.forecast.clip_metadata[1]).toMatchObject({
      display_text: 'ROYALS @ BRAVES | Forecast: ROYALS +1.5 | Model margin: 0.8 runs | Edge +2.5 runs',
      clip_data: {
        forecast_side: 'Royals',
        projected_winner: 'Atlanta Braves',
      },
    });
  });

  it('reconciles stale mlb winner and projected scores from the calibrated margin on serve', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Braves',
        forecast_side: 'Royals',
        projected_winner: 'Braves',
        summary: 'Rain Man forecasts Braves to outscore Royals by ~2 runs.\nProjected combined score: ~8.4 runs.\nHere\'s how Rain Man got there: Boston had the stronger lineup card in the original writeup.',
        spread_edge: 2.3,
        projected_margin: -0.8,
        projected_total_points: 8.4,
        projected_team_score_home: 5.2,
        projected_team_score_away: 3.2,
        clip_metadata: [
          {
            clip_type: 'GAME_FORECAST',
            display_text: 'KC @ ATL | Value: KC | Edge +2.3pts | Winner: Braves',
            clip_data: { forecast_side: 'Royals', winner: 'Braves' },
          },
        ],
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Atlanta Braves',
      away_team: 'Kansas City Royals',
      league: 'mlb',
      odds_data: {
        moneyline: { home: -152, away: 125 },
        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 } },
      },
      model_signals: null,
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.forecast.winner_pick).toBe('Kansas City Royals');
    expect(res.body.forecast.projected_winner).toBe('Kansas City Royals');
    expect(res.body.forecast.projected_margin).toBe(-0.8);
    expect(res.body.forecast.projected_team_score_home).toBe(3.8);
    expect(res.body.forecast.projected_team_score_away).toBe(4.6);
    expect(res.body.forecast.summary).toContain('Rain Man forecasts Kansas City Royals to outscore Atlanta Braves by ~0.8 runs.');
    expect(res.body.forecast.clip_metadata[0]).toMatchObject({
      display_text: 'ROYALS @ BRAVES | Value: Royals | Edge +2.3 runs | Winner: Kansas City Royals',
      clip_data: {
        forecast_side: 'Royals',
        winner: 'Kansas City Royals',
        projected_winner: 'Kansas City Royals',
      },
    });
  });

  it('keeps steam and sharp insights unlockable even when forecast text has no prequalified signal', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(true);
    mocked.getCachedForecast.mockResolvedValue({
      forecast_data: {
        winner_pick: 'Dodgers',
        sharp_money_indicator: 'No sharp market interest detected amid stable pricing.',
        line_movement_analysis: 'Markets holding steady with no notable movement, signaling consensus on the slim favorite.',
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.insightAvailability).toEqual({
      steam: true,
      sharp: true,
      dvp: false,
      hcw: false,
    });
  });

  it('returns canonical and deprecated balance fields for cached insight unlock responses', async () => {
    const { default: router } = await import('../forecast');
    mocked.getPickBalance.mockResolvedValue({
      single_picks: 1,
      daily_pass_picks: 0,
      daily_pass_valid: false,
      daily_free_forecasts: 1,
    });
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_insight_unlocks')) {
        return { rows: [{ id: 'unlock-1' }] };
      }
      if (sql.includes('SELECT insight_data FROM rm_insight_cache')) {
        return { rows: [{ insight_data: { matchup: 'Dodgers vs Giants', keyTakeaways: ['Test'] } }] };
      }
      return { rows: [] };
    });

    const app = express();
    app.use(express.json());
    app.use('/', router);
    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt1', type: 'DVP_INSIGHT', league: 'mlb' });

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      insightType: 'DVP',
      insightData: { matchup: 'Dodgers vs Giants', keyTakeaways: ['Test'] },
      forecastBalance: 2,
      forecast_balance: 2,
    });
  });

  it('returns alreadyOwned true after a successful fallback cached unlock', async () => {
    const { default: router } = await import('../forecast');
    mocked.hasUserPurchasedPick.mockResolvedValue(false);
    mocked.findUserById.mockResolvedValue({ id: 'user-1', is_weatherman: false, email_verified: true });
    mocked.ensureDailyGrant.mockResolvedValue(undefined);
    mocked.deductPick.mockResolvedValue({ success: true, source: 'single_pick' });
    mocked.recordPick.mockResolvedValue(undefined);
    mocked.getPickBalance
      .mockResolvedValueOnce({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 })
      .mockResolvedValueOnce({ single_picks: 0, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 });
    mocked.getCachedForecast.mockResolvedValue({
      id: 'fc-1',
      forecast_data: {
        winner_pick: 'Dodgers',
        forecast_side: 'Dodgers',
        projected_lines: { moneyline: { home: -102, away: -108 } },
        spread_edge: 1.2,
      },
      confidence_score: 0.72,
      composite_confidence: 0.76,
      home_team: 'Dodgers',
      away_team: 'Giants',
      starts_at: '2026-03-27T19:00:00.000Z',
      league: 'mlb',
      odds_data: null,
      model_signals: { modelSignals: null },
      created_at: '2026-03-27T19:00:00.000Z',
    });
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.alreadyOwned).toBe(true);
  });
});
