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

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

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(); }),
  findUserById: vi.fn(),
  getPickBalance: vi.fn(),
  poolQuery: vi.fn(),
  isPlayerOnTeam: vi.fn<(playerName: string, teamShort: string, league: string) => boolean>(() => true),
  fetchTeamPropMarketCandidates: vi.fn(),
  fetchMlbPropCandidates: vi.fn(),
  getPiffPropsForGame: vi.fn(() => []),
  loadPiffPropsForDate: vi.fn(() => ({})),
  buildSourceBackedFallbackProps: vi.fn((params: any) => (params.candidates || []).map((candidate: any) => ({
    player: candidate.player,
    prop: `${candidate.propLabel || candidate.statType} Over ${candidate.marketLineValue}`,
    recommendation: 'over',
    reasoning: `Rain Man projects ${candidate.player}.`,
    edge: 12.5,
    prob: 64,
    odds: candidate.overOdds ?? null,
    projected_stat_value: Number(candidate.marketLineValue) + 0.7,
    stat_type: candidate.normalizedStatType || candidate.statType,
    market_line_value: candidate.marketLineValue,
    model_context: { context_summary: 'Mock fallback.' },
  }))),
  buildMlbFallbackProps: vi.fn((candidates: any[]) => (candidates || []).map((candidate: any) => ({
    player: candidate.player,
    prop: `${candidate.prop || candidate.statType} Over ${candidate.marketLineValue}`,
    recommendation: 'over',
    reasoning: `Rain Man projects ${candidate.player}.`,
    edge: 11.1,
    prob: 61,
    odds: candidate.overOdds ?? null,
    projected_stat_value: Number(candidate.marketLineValue) + 0.3,
    stat_type: candidate.statType,
    market_line_value: candidate.marketLineValue,
    model_context: { context_summary: 'Mock MLB fallback.' },
  }))),
}));

vi.mock('../../middleware/auth', () => ({ authMiddleware: mocked.authMiddleware, optionalAuth: mocked.optionalAuth }));
vi.mock('../../models/user', () => ({
  findUserById: mocked.findUserById,
  deductPick: vi.fn(),
  getPickBalance: mocked.getPickBalance,
  ensureDailyGrant: vi.fn(),
  isInGracePeriod: vi.fn(() => false),
}));
vi.mock('../../models/pick', () => ({ hasUserPurchasedPick: vi.fn(), recordPick: vi.fn() }));
vi.mock('../../models/forecast', () => ({
  getCachedForecast: vi.fn(),
  getCachedForecastByTeams: vi.fn(),
  cacheForecast: vi.fn(),
  updateCachedOdds: vi.fn(),
}));
vi.mock('../../models/ledger', () => ({ recordLedgerEntry: vi.fn(), LedgerReason: {} }));
vi.mock('../../services/sgo', () => ({ fetchEvents: vi.fn(async () => []), parseOdds: vi.fn(), LEAGUE_MAP: { nba: 'nba', mlb: 'mlb' } }));
vi.mock('../../services/grok', () => ({
  generateForecast: vi.fn(),
  generateTeamProps: vi.fn(),
  generateSteamInsight: vi.fn(),
  generateSharpInsight: vi.fn(),
  buildSourceBackedFallbackProps: mocked.buildSourceBackedFallbackProps,
  buildMlbFallbackProps: mocked.buildMlbFallbackProps,
}));
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('../../services/team-prop-market-candidates', () => ({ fetchTeamPropMarketCandidates: mocked.fetchTeamPropMarketCandidates }));
vi.mock('../../services/mlb-prop-markets', () => ({ fetchMlbPropCandidates: mocked.fetchMlbPropCandidates }));
vi.mock('../../services/piff', () => ({
  getPiffPropsForGame: mocked.getPiffPropsForGame,
  loadPiffPropsForDate: mocked.loadPiffPropsForDate,
}));
vi.mock('../../services/canonical-names', async () => {
  const actual = await vi.importActual<typeof import('../../services/canonical-names')>('../../services/canonical-names');
  return {
    ...actual,
    isPlayerOnTeam: mocked.isPlayerOnTeam,
  };
});
vi.mock('../../db', () => ({ default: { query: mocked.poolQuery } }));

async function loadRouter() {
  vi.resetModules();
  return (await import('../forecast')).default;
}

function collectUnexpectedSnakeCasePaths(
  value: any,
  allowedKeys: Set<string>,
  path = 'body',
): string[] {
  if (!value || typeof value !== 'object') return [];
  if (Array.isArray(value)) {
    return value.flatMap((entry, index) => collectUnexpectedSnakeCasePaths(entry, allowedKeys, `${path}[${index}]`));
  }

  return Object.entries(value).flatMap(([key, entry]) => {
    const current = /_/.test(key) && !allowedKeys.has(key) ? [`${path}.${key}`] : [];
    return [...current, ...collectUnexpectedSnakeCasePaths(entry, allowedKeys, `${path}.${key}`)];
  });
}

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

function snapshotPlayerPropsResponse(body: any) {
  return {
    contractVersion: body.contractVersion,
    mode: body.mode,
    count: body.count,
    fallbackReason: body.fallbackReason,
    forecastBalance: body.forecastBalance,
    forecast_balance: body.forecast_balance,
    playerProps: body.playerProps,
    marketProps: body.marketProps,
    players: body.players,
    featuredPlayers: body.featuredPlayers,
  };
}

describe('/forecast/:eventId/player-props contract', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mocked.poolQuery.mockReset();
    mocked.poolQuery.mockResolvedValue({ rows: [] });
    mocked.fetchTeamPropMarketCandidates.mockResolvedValue([]);
    mocked.fetchMlbPropCandidates.mockResolvedValue([]);
    mocked.isPlayerOnTeam.mockReturnValue(true);
    mocked.getPiffPropsForGame.mockReturnValue([]);
    mocked.loadPiffPropsForDate.mockReturnValue({});
    mocked.buildSourceBackedFallbackProps.mockClear();
    mocked.buildMlbFallbackProps.mockClear();
    process.env.FEATURE_PLAYER_PROPS_PER_PLAYER_UNLOCK = 'true';
    process.env.FEATURE_PLAYER_PROPS_DUAL_READ = 'false';
    process.env.PROP_DEDUP_ENABLED = 'false';
    process.env.PI_SIBLING_SUPPRESSION_ENABLED = 'false';
    delete process.env.MLB_PROP_CONTEXT_V2;
    mocked.findUserById.mockResolvedValue({
      id: 'user-1',
      email: 'admin@rainmakersports.app',
      is_weatherman: true,
      email_verified: true,
    });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
  });

  it('returns a versioned per-player contract with grouped rows and only deprecated snake_case aliases', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-1',
            player_name: 'Luka Doncic',
            team_id: 'LAL',
            team_side: 'home',
            league: 'nba',
            confidence_score: 0.7,
            forecast_payload: {
              prop: 'Points Over 31.5',
              recommendation: 'over',
              edge: 16.4,
              prob: 68,
              odds: -110,
              line: 31.5,
              market_line_value: 31.5,
              projected_stat_value: 34.2,
              stat_type: 'points',
              normalized_stat_type: 'points',
              market_type: 'PLAYER_PROP',
              grading_category: 'PLAYER_PROPS',
            },
          },
          {
            id: 'asset-2',
            player_name: 'Luka Doncic',
            team_id: 'LAL',
            team_side: 'home',
            league: 'nba',
            confidence_score: 0.68,
            forecast_payload: {
              prop: 'Assists Over 8.5',
              recommendation: 'over',
              edge: 25.2,
              prob: 76,
              odds: 120,
              line: 8.5,
              market_line_value: 8.5,
              projected_stat_value: 9.7,
              stat_type: 'assists',
              normalized_stat_type: 'assists',
              market_type: 'PLAYER_PROP',
              grading_category: 'PLAYER_PROPS',
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      mode: 'per_player',
      count: 2,
      forecastBalance: 2,
      forecast_balance: 2,
    });
    expect(res.body.playerProps).toHaveLength(2);
    expect(res.body.featuredPlayers).toHaveLength(1);
    expect(res.body.featuredPlayers[0]).toMatchObject({
      availability: 'unknown',
    });
    expect(res.body.playerProps[0]).toMatchObject({
      assetId: 'asset-1',
      gradingCategory: 'PLAYER_PROPS',
      forecastDirection: 'OVER',
      marketLine: 31.5,
      marketLineValue: 31.5,
      normalizedStatType: 'points',
      projectedOutcome: 34.2,
      resultOutcome: null,
      closingLineValue: null,
      closingOddsSnapshot: null,
    });
    expect(res.body.players).toEqual([
      expect.objectContaining({
        player: 'Luka Doncic',
        strongestSignal: 'STRONG',
        props: [
          expect.objectContaining({ propType: 'Assists', signal: 'STRONG' }),
          expect.objectContaining({ propType: 'Points', signal: 'GOOD' }),
        ],
      }),
    ]);

    const allowedSnakeCase = new Set([...DEPRECATED_PLAYER_PROP_FIELDS]);
    expect(collectUnexpectedSnakeCasePaths(res.body.playerProps[0], allowedSnakeCase)).toEqual([]);
    expectDeprecatedAliasesToMirrorCanonical(res.body, { forecast_balance: 'forecastBalance' });
    expectDeprecatedAliasesToMirrorCanonical(res.body.playerProps[0], DEPRECATED_PLAYER_PROP_ALIAS_MAP);
    expect(snapshotPlayerPropsResponse(res.body)).toMatchInlineSnapshot(`
      {
        "contractVersion": "forecast-public-v1",
        "count": 2,
        "fallbackReason": null,
        "featuredPlayers": [
          {
            "analysis": null,
            "availability": "unknown",
            "hasPick": true,
            "maxVarianceEdgePct": 14.1,
            "player": "Luka Doncic",
            "playerRole": "batter",
            "projectedMinutes": null,
            "props": [
              {
                "analysis": null,
                "assetId": "asset-2",
                "forecastDirection": "OVER",
                "hasPick": true,
                "locked": false,
                "marketImpliedProbability": 45.5,
                "marketLine": 8.5,
                "odds": 120,
                "projectedOutcome": 9.7,
                "projectedProbability": 76,
                "propLabel": "Assists Over 8.5",
                "propType": "Assists",
                "signalTier": "STRONG",
                "varianceEdgePct": 14.1,
                "varianceSignal": "FAIR",
              },
              {
                "analysis": null,
                "assetId": "asset-1",
                "forecastDirection": "OVER",
                "hasPick": true,
                "locked": false,
                "marketImpliedProbability": 52.4,
                "marketLine": 31.5,
                "odds": -110,
                "projectedOutcome": 34.2,
                "projectedProbability": 68,
                "propLabel": "Points Over 31.5",
                "propType": "Points",
                "signalTier": "GOOD",
                "varianceEdgePct": 8.6,
                "varianceSignal": "COIN_FLIP",
              },
            ],
            "signalStrength": "FAIR",
            "team": "LAL",
            "teamSide": "home",
          },
        ],
        "forecastBalance": 2,
        "forecast_balance": 2,
        "marketProps": [],
        "mode": "per_player",
        "playerProps": [
          {
            "agreementLabel": "MEDIUM",
            "agreementScore": 61,
            "agreementSources": [
              "model",
              "market",
            ],
            "assetId": "asset-1",
            "closingLineValue": null,
            "closingOddsSnapshot": null,
            "confidence": 0.7,
            "edge": 16.4,
            "edgePct": 16.4,
            "forecastDirection": "OVER",
            "gradingCategory": "PLAYER_PROPS",
            "league": "nba",
            "line": 31.5,
            "locked": false,
            "marketFamily": "batter_props",
            "marketImpliedProbability": 52.4,
            "marketLine": 31.5,
            "marketLineValue": 31.5,
            "marketOrigin": "source_backed",
            "marketQualityLabel": "FAIR",
            "marketQualityScore": 65,
            "marketType": "PLAYER_PROP",
            "market_line_value": 31.5,
            "modelContext": null,
            "normalizedStatType": "points",
            "normalized_stat_type": "points",
            "odds": -110,
            "player": "Luka Doncic",
            "playerRole": "batter",
            "prob": 68,
            "projectedOutcome": 34.2,
            "projectedProbability": 68,
            "projected_stat_value": 34.2,
            "prop": "Points Over 31.5",
            "reasoning": null,
            "recommendation": "over",
            "resultOutcome": null,
            "signalTableRow": {
              "edgePct": 15.6,
              "forecastDirection": "OVER",
              "marketImpliedProbability": 52.4,
              "marketLine": 31.5,
              "odds": -110,
              "projectedOutcome": 34.2,
              "projectedProbability": 68,
              "propType": "Points",
              "signal": "GOOD",
            },
            "signalTier": "GOOD",
            "sourceBacked": true,
            "statType": "points",
            "stat_type": "points",
            "team": "LAL",
            "teamSide": "home",
          },
          {
            "agreementLabel": "HIGH",
            "agreementScore": 94,
            "agreementSources": [
              "model",
              "market",
            ],
            "assetId": "asset-2",
            "closingLineValue": null,
            "closingOddsSnapshot": null,
            "confidence": 0.68,
            "edge": 25.2,
            "edgePct": 25.2,
            "forecastDirection": "OVER",
            "gradingCategory": "PLAYER_PROPS",
            "league": "nba",
            "line": 8.5,
            "locked": false,
            "marketFamily": "player_props",
            "marketImpliedProbability": 45.5,
            "marketLine": 8.5,
            "marketLineValue": 8.5,
            "marketOrigin": "source_backed",
            "marketQualityLabel": "FAIR",
            "marketQualityScore": 65,
            "marketType": "PLAYER_PROP",
            "market_line_value": 8.5,
            "modelContext": null,
            "normalizedStatType": "assists",
            "normalized_stat_type": "assists",
            "odds": 120,
            "player": "Luka Doncic",
            "playerRole": "unknown",
            "prob": 76,
            "projectedOutcome": 9.7,
            "projectedProbability": 76,
            "projected_stat_value": 9.7,
            "prop": "Assists Over 8.5",
            "reasoning": null,
            "recommendation": "over",
            "resultOutcome": null,
            "signalTableRow": {
              "edgePct": 30.5,
              "forecastDirection": "OVER",
              "marketImpliedProbability": 45.5,
              "marketLine": 8.5,
              "odds": 120,
              "projectedOutcome": 9.7,
              "projectedProbability": 76,
              "propType": "Assists",
              "signal": "STRONG",
            },
            "signalTier": "STRONG",
            "sourceBacked": true,
            "statType": "assists",
            "stat_type": "assists",
            "team": "LAL",
            "teamSide": "home",
          },
        ],
        "players": [
          {
            "maxEdgePct": 30.5,
            "player": "Luka Doncic",
            "playerRole": "batter",
            "props": [
              {
                "edgePct": 30.5,
                "forecastDirection": "OVER",
                "marketImpliedProbability": 45.5,
                "marketLine": 8.5,
                "odds": 120,
                "projectedOutcome": 9.7,
                "projectedProbability": 76,
                "propType": "Assists",
                "signal": "STRONG",
              },
              {
                "edgePct": 15.6,
                "forecastDirection": "OVER",
                "marketImpliedProbability": 52.4,
                "marketLine": 31.5,
                "odds": -110,
                "projectedOutcome": 34.2,
                "projectedProbability": 68,
                "propType": "Points",
                "signal": "GOOD",
              },
            ],
            "strongestSignal": "STRONG",
            "team": "LAL",
            "teamSide": "home",
          },
        ],
      }
    `);
  });

  it('falls back to team mode when all precomputed player props are unpriced', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-legacy-1',
            player_name: 'Jalen Johnson',
            team_id: 'ATL',
            team_side: 'home',
            league: 'nba',
            confidence_score: 0.67,
            forecast_payload: {
              prop: 'Points Over 24.5',
              recommendation: 'over',
              edge: 6.2,
              prob: 67,
              line: 24.5,
              market_line_value: 24.5,
              projected_stat_value: 27,
              stat_type: 'points',
              normalized_stat_type: 'points',
              grading_category: 'PLAYER_PROPS',
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      mode: 'team',
      count: 0,
      fallbackReason: 'all_assets_filtered_out',
    });
    expect(res.body.playerProps).toEqual([]);
    expect(res.body.players).toEqual([]);
  });

  it('surfaces borderline FAIR player props without letting them outrank stronger signals', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-runs-1',
            player_name: 'Shohei Ohtani',
            team_id: 'LAD',
            team_side: 'home',
            league: 'mlb',
            confidence_score: 0.82,
            forecast_payload: {
              prop: 'Points Over 1.5',
              recommendation: 'over',
              prob: 62,
              odds: 120,
              line: 1.5,
              market_line_value: 1.5,
              projected_stat_value: 1.9,
              stat_type: 'points',
              normalized_stat_type: 'points',
              market_type: 'PLAYER_PROP',
              model_context: { k_rank: 4, park_factor: 108, weather_impact: 'positive' },
            },
          },
          {
            id: 'asset-average-1',
            player_name: 'Juan Soto',
            team_id: 'NYY',
            team_side: 'away',
            league: 'mlb',
            confidence_score: 0.74,
            forecast_payload: {
              prop: 'Walks Over 0.5',
              recommendation: 'over',
              prob: 63,
              odds: -110,
              line: 0.5,
              market_line_value: 0.5,
              projected_stat_value: 0.8,
              stat_type: 'batting_basesOnBalls',
              normalized_stat_type: 'batting_basesOnBalls',
              market_type: 'PLAYER_PROP',
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.count).toBe(2);
    expect(res.body.playerProps).toHaveLength(2);
    expect(res.body.playerProps[0].player).toBe('Shohei Ohtani');
    expect(res.body.playerProps[0].signalTableRow.propType).toBe('Runs');
    expect(JSON.stringify(res.body.playerProps[0])).not.toMatch(/"Points"/);
    expect(res.body.playerProps[1]).toMatchObject({
      player: 'Juan Soto',
      signalTier: 'FAIR',
      signalTableRow: expect.objectContaining({
        propType: 'Walks',
        signal: 'FAIR',
      }),
    });
    expect(res.body.players).toEqual([
      expect.objectContaining({
        player: 'Shohei Ohtani',
        props: [expect.objectContaining({ propType: 'Runs', signal: 'GOOD' })],
      }),
      expect.objectContaining({
        player: 'Juan Soto',
        strongestSignal: 'FAIR',
        props: [expect.objectContaining({ propType: 'Walks', signal: 'FAIR' })],
      }),
    ]);
  });

  it('keeps priced informational props on the public surface even without a signal tier', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-info-1',
            player_name: 'Kevin Durant',
            team_id: 'PHX',
            team_side: 'home',
            league: 'nba',
            confidence_score: 0.61,
            forecast_payload: {
              prop: 'Pts + Reb + Ast Under 34.5',
              recommendation: 'under',
              prob: 57,
              odds: -108,
              line: 34.5,
              market_line_value: 34.5,
              projected_stat_value: 32.8,
              stat_type: 'pointsreboundsassists',
              normalized_stat_type: 'pointsreboundsassists',
              market_source: 'fanduel',
              signal_tier: null,
              signal_table_row: null,
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      mode: 'per_player',
      count: 1,
      fallbackReason: null,
    });
    expect(res.body.playerProps).toHaveLength(1);
    expect(res.body.playerProps[0]).toMatchObject({
      player: 'Kevin Durant',
      prop: 'Pts + Reb + Ast Under 34.5',
      marketLineValue: 34.5,
      projectedOutcome: 32.8,
    });
    expect(res.body.playerProps[0].signalTier).toBeUndefined();
    expect(res.body.playerProps[0].signalTableRow).toBeUndefined();
  });

  it('surfaces lineup-backed featured players even when no player prop assets exist', async () => {
    const router = await loadRouter();

    mocked.fetchTeamPropMarketCandidates.mockImplementation(async (params: any) => {
      if (params.teamShort !== 'NYK') return [];
      return [
        {
          player: 'Jalen Brunson',
          statType: 'points',
          normalizedStatType: 'points',
          marketLineValue: 28.5,
          overOdds: -118,
          underOdds: -102,
          availableSides: ['over', 'under'],
          source: 'fanduel',
          marketName: 'Jalen Brunson Points Over/Under',
          propLabel: 'Points',
          sourceMap: [
            { side: 'over', source: 'fanduel', odds: -118, timestamp: null, rawMarketId: null },
            { side: 'under', source: 'fanduel', odds: -102, timestamp: null, rawMarketId: null },
          ],
        },
      ];
    });

    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          homeStatus: 'confirmed',
          awayStatus: 'projected',
          homePlayers: [{ name: 'Jayson Tatum' }, { name: 'Jaylen Brown' }],
          awayPlayers: [{ name: 'Jalen Brunson' }],
          homeProjMinutes: { 'Jayson Tatum': 37, 'Jaylen Brown': 35 },
          awayProjMinutes: { 'Jalen Brunson': 38 },
        }],
      });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      mode: 'team',
      count: 0,
      fallbackReason: 'no_precomputed_assets',
    });
    expect(res.body.playerProps).toEqual([]);
    expect(res.body.marketProps).toEqual([
      expect.objectContaining({
        player: 'Jalen Brunson',
        team: 'NYK',
        teamSide: 'away',
        prop: 'Points',
        line: 28.5,
        overOdds: -118,
        underOdds: -102,
        source: 'fanduel',
        books: 1,
      }),
    ]);
    expect(res.body.featuredPlayers).toEqual([
      expect.objectContaining({
        player: 'Jalen Brunson',
        team: 'NYK',
        teamSide: 'away',
        availability: 'projected',
        projectedMinutes: 38,
      }),
      expect.objectContaining({
        player: 'Jayson Tatum',
        team: 'BOS',
        teamSide: 'home',
        availability: 'confirmed',
        projectedMinutes: 37,
      }),
      expect.objectContaining({
        player: 'Jaylen Brown',
        team: 'BOS',
        teamSide: 'home',
        availability: 'confirmed',
        projectedMinutes: 35,
      }),
    ]);
  });

  it('filters polluted lineup-seeded players that are not on the event team roster', async () => {
    mocked.isPlayerOnTeam.mockImplementation((playerName: string) => !['Kevin Durant', 'Nikola Jokic'].includes(playerName));

    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          homeStatus: 'confirmed',
          awayStatus: 'confirmed',
          homePlayers: [
            { name: 'Jayson Tatum' },
            { name: 'Nikola Jokic' },
          ],
          awayPlayers: [
            { name: 'Jalen Brunson' },
            { name: 'Kevin Durant' },
          ],
          homeProjMinutes: {},
          awayProjMinutes: {},
        }],
      });

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

    expect(res.status).toBe(200);
    expect(res.body.featuredPlayers).toEqual(expect.arrayContaining([
      expect.objectContaining({
        player: 'Jalen Brunson',
        team: 'NYK',
        teamSide: 'away',
      }),
      expect.objectContaining({
        player: 'Jayson Tatum',
        team: 'BOS',
        teamSide: 'home',
      }),
    ]));
    expect(res.body.featuredPlayers.map((player: any) => player.player)).not.toContain('Kevin Durant');
    expect(res.body.featuredPlayers.map((player: any) => player.player)).not.toContain('Nikola Jokic');
  });

  it('filters lineup-seeded players that lack event-team market candidates even if roster validation passes', async () => {
    mocked.isPlayerOnTeam.mockReturnValue(true);
    mocked.fetchTeamPropMarketCandidates.mockImplementation(async (params: any) => {
      if (params.teamShort === 'BOS') {
        return [
          {
            player: 'Jayson Tatum',
            statType: 'rebounds',
            normalizedStatType: 'rebounds',
            marketLineValue: 8.5,
            overOdds: -110,
            underOdds: -110,
            availableSides: ['over', 'under'],
            source: 'draftkings',
            marketName: 'Jayson Tatum Rebounds Over/Under',
            propLabel: 'Rebounds',
          },
        ];
      }
      if (params.teamShort === 'NYK') {
        return [
          {
            player: 'Jalen Brunson',
            statType: 'points',
            normalizedStatType: 'points',
            marketLineValue: 28.5,
            overOdds: -118,
            underOdds: -102,
            availableSides: ['over', 'under'],
            source: 'fanduel',
            marketName: 'Jalen Brunson Points Over/Under',
            propLabel: 'Points',
          },
        ];
      }
      return [];
    });

    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          source: 'rotowire',
          homeStatus: 'confirmed',
          awayStatus: 'confirmed',
          homePlayers: [
            { name: 'Jayson Tatum' },
            { name: 'Nikola Jokic' },
          ],
          awayPlayers: [
            { name: 'Jalen Brunson' },
            { name: 'Kevin Durant' },
          ],
          homeProjMinutes: {},
          awayProjMinutes: {},
        }],
      });

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

    expect(res.status).toBe(200);
    expect(res.body.featuredPlayers).toEqual(expect.arrayContaining([
      expect.objectContaining({ player: 'Jayson Tatum', team: 'BOS', teamSide: 'home' }),
      expect.objectContaining({ player: 'Jalen Brunson', team: 'NYK', teamSide: 'away' }),
    ]));
    expect(res.body.featuredPlayers.map((player: any) => player.player)).not.toContain('Nikola Jokic');
    expect(res.body.featuredPlayers.map((player: any) => player.player)).not.toContain('Kevin Durant');
  });

  it('attaches source-backed fallback props to lineup-backed featured players without stored assets', async () => {
    const router = await loadRouter();

    mocked.fetchTeamPropMarketCandidates.mockResolvedValue([
      {
        player: 'Jalen Brunson',
        statType: 'points',
        normalizedStatType: 'points',
        marketLineValue: 28.5,
        overOdds: -118,
        underOdds: -102,
        availableSides: ['over', 'under'],
        source: 'fanduel',
        marketName: 'Jalen Brunson Points Over/Under',
        propLabel: 'Points',
      },
      {
        player: 'Jayson Tatum',
        statType: 'rebounds',
        normalizedStatType: 'rebounds',
        marketLineValue: 8.5,
        overOdds: -110,
        underOdds: -110,
        availableSides: ['over', 'under'],
        source: 'draftkings',
        marketName: 'Jayson Tatum Rebounds Over/Under',
        propLabel: 'Rebounds',
      },
    ]);

    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          homeStatus: 'confirmed',
          awayStatus: 'confirmed',
          homePlayers: [{ name: 'Jayson Tatum' }],
          awayPlayers: [{ name: 'Jalen Brunson' }],
          homeProjMinutes: { 'Jayson Tatum': 37 },
          awayProjMinutes: { 'Jalen Brunson': 38 },
        }],
      });

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

    expect(res.status).toBe(200);
    expect(res.body.playerProps).toEqual([]);
    expect(res.body.featuredPlayers).toEqual([
      expect.objectContaining({
        player: 'Jalen Brunson',
        props: [
          expect.objectContaining({
            propType: 'Points',
            marketLine: 28.5,
            projectedOutcome: 29.2,
            locked: false,
          }),
        ],
      }),
      expect.objectContaining({
        player: 'Jayson Tatum',
        props: [
          expect.objectContaining({
            propType: 'Rebounds',
            marketLine: 8.5,
            projectedOutcome: 9.2,
            locked: false,
          }),
        ],
      }),
    ]);
  });

  it('spreads fallback props across top lineup players before adding a second prop to the same player', async () => {
    const router = await loadRouter();

    mocked.fetchTeamPropMarketCandidates.mockResolvedValue([
      {
        player: 'Jalen Brunson',
        statType: 'points',
        normalizedStatType: 'points',
        marketLineValue: 28.5,
        overOdds: -118,
        underOdds: -102,
        availableSides: ['over', 'under'],
        source: 'fanduel',
        marketName: 'Jalen Brunson Points Over/Under',
        propLabel: 'Points',
      },
      {
        player: 'Jalen Brunson',
        statType: 'assists',
        normalizedStatType: 'assists',
        marketLineValue: 7.5,
        overOdds: -110,
        underOdds: -110,
        availableSides: ['over', 'under'],
        source: 'fanduel',
        marketName: 'Jalen Brunson Assists Over/Under',
        propLabel: 'Assists',
      },
      {
        player: 'Mikal Bridges',
        statType: 'rebounds',
        normalizedStatType: 'rebounds',
        marketLineValue: 5.5,
        overOdds: -105,
        underOdds: -115,
        availableSides: ['over', 'under'],
        source: 'draftkings',
        marketName: 'Mikal Bridges Rebounds Over/Under',
        propLabel: 'Rebounds',
      },
      {
        player: 'Jayson Tatum',
        statType: 'rebounds',
        normalizedStatType: 'rebounds',
        marketLineValue: 8.5,
        overOdds: -110,
        underOdds: -110,
        availableSides: ['over', 'under'],
        source: 'draftkings',
        marketName: 'Jayson Tatum Rebounds Over/Under',
        propLabel: 'Rebounds',
      },
    ]);
    mocked.buildSourceBackedFallbackProps.mockReturnValue([
      {
        player: 'Jalen Brunson',
        prop: 'Points Over 28.5',
        recommendation: 'over',
        reasoning: 'Rain Man projects Jalen Brunson.',
        edge: 12.5,
        prob: 64,
        odds: -118,
        projected_stat_value: 29.2,
        stat_type: 'points',
        market_line_value: 28.5,
        model_context: { context_summary: 'Mock fallback.' },
      },
      {
        player: 'Jalen Brunson',
        prop: 'Assists Over 7.5',
        recommendation: 'over',
        reasoning: 'Rain Man projects Jalen Brunson.',
        edge: 11.4,
        prob: 61,
        odds: -110,
        projected_stat_value: 8.2,
        stat_type: 'assists',
        market_line_value: 7.5,
        model_context: { context_summary: 'Mock fallback.' },
      },
      {
        player: 'Mikal Bridges',
        prop: 'Rebounds Over 5.5',
        recommendation: 'over',
        reasoning: 'Rain Man projects Mikal Bridges.',
        edge: 10.8,
        prob: 60,
        odds: -105,
        projected_stat_value: 6.1,
        stat_type: 'rebounds',
        market_line_value: 5.5,
        model_context: { context_summary: 'Mock fallback.' },
      },
    ]);

    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2099-04-01T23:30:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          homeStatus: 'confirmed',
          awayStatus: 'confirmed',
          homePlayers: [{ name: 'Jayson Tatum' }],
          awayPlayers: [{ name: 'Jalen Brunson' }, { name: 'Mikal Bridges' }],
          homeProjMinutes: { 'Jayson Tatum': 37 },
          awayProjMinutes: { 'Jalen Brunson': 38, 'Mikal Bridges': 36 },
        }],
      });

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

    expect(res.status).toBe(200);
    expect(res.body.featuredPlayers).toHaveLength(3);
    expect(res.body.featuredPlayers[0]).toMatchObject({
      player: 'Mikal Bridges',
      props: [expect.objectContaining({ propType: 'Rebounds' })],
    });
    expect(res.body.featuredPlayers[1]).toMatchObject({
      player: 'Jalen Brunson',
      props: [expect.objectContaining({ propType: 'Assists' }), expect.objectContaining({ propType: 'Points' })],
    });
    expect(res.body.featuredPlayers[2]).toMatchObject({
      player: 'Jayson Tatum',
      props: [],
    });
  });

  it('deduplicates same-market siblings when PROP_DEDUP_ENABLED is true', async () => {
    process.env.PROP_DEDUP_ENABLED = 'true';
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-1',
            player_name: 'Luka Doncic',
            team_id: 'LAL',
            team_side: 'home',
            league: 'nba',
            confidence_score: 0.7,
            forecast_payload: {
              prop: 'Points Over 31.5',
              recommendation: 'over',
              prob: 68,
              odds: -110,
              line: 31.5,
              market_line_value: 31.5,
              projected_stat_value: 34.2,
              stat_type: 'points',
              market_type: 'PLAYER_PROP',
            },
          },
          {
            id: 'asset-2',
            player_name: 'Luka Doncic',
            team_id: 'LAL',
            team_side: 'home',
            league: 'nba',
            confidence_score: 0.68,
            forecast_payload: {
              prop: 'Points Over 32.5',
              recommendation: 'over',
              prob: 66,
              odds: -110,
              line: 32.5,
              market_line_value: 32.5,
              projected_stat_value: 33.8,
              stat_type: 'points',
              market_type: 'PLAYER_PROP',
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.count).toBe(1);
    expect(res.body.playerProps[0].assetId).toBe('asset-1');
  });

  it('keeps locked props on the public surface without leaking unlocked detail fields', async () => {
    mocked.findUserById.mockResolvedValue({ id: 'user-1', is_weatherman: false, email_verified: true });
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-locked-1',
            player_name: 'Luka Doncic',
            team_id: 'LAL',
            team_side: 'home',
            league: 'nba',
            confidence_score: 0.7,
            forecast_payload: {
              prop: 'Points Over 31.5',
              recommendation: 'over',
              prob: 68,
              odds: -110,
              line: 31.5,
              market_line_value: 31.5,
              projected_stat_value: 34.2,
              stat_type: 'points',
              market_type: 'PLAYER_PROP',
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.count).toBe(1);
    expect(res.body.playerProps[0]).toMatchObject({
      assetId: 'asset-locked-1',
      player: 'Luka Doncic',
      locked: true,
      confidence: null,
    });
    expect(res.body.playerProps[0]).not.toHaveProperty('recommendation');
    expect(res.body.playerProps[0]).not.toHaveProperty('marketLineValue');
    expect(res.body.playerProps[0]).not.toHaveProperty('signalTableRow');
    expect(res.body.players).toEqual([]);
    expect(res.body.featuredPlayers).toHaveLength(1);
    expect(res.body.featuredPlayers[0]).toMatchObject({
      signalStrength: null,
      maxVarianceEdgePct: null,
      hasPick: false,
      analysis: null,
    });
    expect(res.body.featuredPlayers[0].props[0]).toMatchObject({
      assetId: 'asset-locked-1',
      marketLine: null,
      odds: null,
      forecastDirection: null,
      projectedOutcome: null,
      projectedProbability: null,
      marketImpliedProbability: null,
      varianceEdgePct: null,
      varianceSignal: null,
      hasPick: false,
      signalTier: null,
      analysis: null,
    });
    expect(snapshotPlayerPropsResponse(res.body)).toMatchInlineSnapshot(`
      {
        "contractVersion": "forecast-public-v1",
        "count": 1,
        "fallbackReason": null,
        "featuredPlayers": [
          {
            "analysis": null,
            "availability": "unknown",
            "hasPick": false,
            "maxVarianceEdgePct": null,
            "player": "Luka Doncic",
            "playerRole": "batter",
            "projectedMinutes": null,
            "props": [
              {
                "analysis": null,
                "assetId": "asset-locked-1",
                "forecastDirection": null,
                "hasPick": false,
                "locked": true,
                "marketImpliedProbability": null,
                "marketLine": null,
                "odds": null,
                "projectedOutcome": null,
                "projectedProbability": null,
                "propLabel": "Points Over 31.5",
                "propType": "Points",
                "signalTier": null,
                "varianceEdgePct": null,
                "varianceSignal": null,
              },
            ],
            "signalStrength": null,
            "team": "LAL",
            "teamSide": "home",
          },
        ],
        "forecastBalance": 2,
        "forecast_balance": 2,
        "marketProps": [],
        "mode": "per_player",
        "playerProps": [
          {
            "assetId": "asset-locked-1",
            "confidence": null,
            "league": "nba",
            "locked": true,
            "player": "Luka Doncic",
            "prop": "Points Over 31.5",
            "team": "LAL",
            "teamSide": "home",
          },
        ],
        "players": [],
      }
    `);
  });

  it('surfaces mlbPropContext for MLB by default', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-mlb-1',
            player_name: 'Shohei Ohtani',
            team_id: 'LAD',
            team_side: 'home',
            league: 'mlb',
            confidence_score: 0.7,
            forecast_payload: {
              prop: 'Hits Over 1.5',
              recommendation: 'over',
              prob: 70,
              odds: -115,
              line: 1.5,
              market_line_value: 1.5,
              projected_stat_value: 1.9,
              stat_type: 'hits',
              market_type: 'PLAYER_PROP',
              model_context: {
                k_rank: 4,
                park_factor: 108,
                weather_impact: 'positive',
                handedness_split: 'favorable',
                lineup_certainty: 'confirmed',
              },
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.playerProps[0]).toMatchObject({
      marketType: 'PLAYER_PROP',
      marketFamily: 'batter_props',
      marketOrigin: 'source_backed',
      sourceBacked: true,
      playerRole: 'batter',
      signalTier: 'GOOD',
      mlbPropContext: {
        kRank: 4,
        parkFactor: 108,
        weatherImpact: 'positive',
        handednessSplit: 'favorable',
        lineupCertainty: 'confirmed',
      },
    });
  });

  it('omits mlbPropContext only when the flag is explicitly off or the league is not MLB', async () => {
    process.env.MLB_PROP_CONTEXT_V2 = 'false';
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-off-1',
            player_name: 'Shohei Ohtani',
            team_id: 'LAD',
            team_side: 'home',
            league: 'mlb',
            confidence_score: 0.7,
            forecast_payload: {
              prop: 'Hits Over 1.5',
              recommendation: 'over',
              prob: 70,
              odds: -115,
              line: 1.5,
              market_line_value: 1.5,
              projected_stat_value: 1.9,
              stat_type: 'hits',
              market_type: 'PLAYER_PROP',
              model_context: { k_rank: 4 },
            },
          },
          {
            id: 'asset-nba-1',
            player_name: 'Luka Doncic',
            team_id: 'DAL',
            team_side: 'away',
            league: 'nba',
            confidence_score: 0.7,
            forecast_payload: {
              prop: 'Points Over 31.5',
              recommendation: 'over',
              prob: 68,
              odds: -110,
              line: 31.5,
              market_line_value: 31.5,
              projected_stat_value: 34.2,
              stat_type: 'points',
              market_type: 'PLAYER_PROP',
              model_context: { projected_minutes: 36 },
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.playerProps[0]).not.toHaveProperty('mlbPropContext');
    expect(res.body.playerProps[1]).not.toHaveProperty('mlbPropContext');
  });

  it('falls back to team mode when no precomputed player props exist', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rows: [{ starts_at: '2099-03-28T22:00:00.000Z' }] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      mode: 'team',
      count: 0,
      fallbackReason: 'no_precomputed_assets',
      forecastBalance: 2,
      forecast_balance: 2,
    });
  });

  it('reports game_started when priced player props are no longer available after first pitch', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2000-03-28T22:00:00.000Z',
        }],
      })
      .mockResolvedValueOnce({
        rows: [{
          event_id: 'evt1',
          league: 'nba',
          home_team: 'Boston Celtics',
          away_team: 'New York Knicks',
          home_short: 'BOS',
          away_short: 'NYK',
          starts_at: '2000-03-28T22:00:00.000Z',
        }],
      });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      mode: 'team',
      count: 0,
      fallbackReason: 'game_started',
      forecastBalance: 2,
      forecast_balance: 2,
    });
  });

  it('falls back to non-expired stale player props when active inventory is empty', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [
          {
            id: 'asset-stale-1',
            player_name: 'Jayson Tatum',
            team_id: 'BOS',
            team_side: 'home',
            league: 'nba',
            status: 'STALE',
            confidence_score: 0.74,
            forecast_payload: {
              prop: 'Points Over 27.5',
              recommendation: 'over',
              edge: 14.1,
              prob: 69,
              odds: -108,
              line: 27.5,
              market_line_value: 27.5,
              projected_stat_value: 30.1,
              stat_type: 'points',
              normalized_stat_type: 'points',
              market_type: 'PLAYER_PROP',
              grading_category: 'PLAYER_PROPS',
            },
          },
        ],
      })
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      mode: 'per_player',
      count: 1,
      fallbackReason: null,
    });
    expect(res.body.playerProps[0]).toMatchObject({
      assetId: 'asset-stale-1',
      player: 'Jayson Tatum',
      prop: 'Points Over 27.5',
    });
  });

  it('backfills missing mlb player props on read before falling back to no_precomputed_assets', async () => {
    const router = await loadRouter();
    const { generateTeamProps } = await import('../../services/grok');
    vi.mocked(generateTeamProps)
      .mockResolvedValueOnce({
        team: 'Atlanta Braves',
        summary: 'Home props',
        props: [
          {
            player: 'Ronald Acuna Jr.',
            prop: 'Runs Over 1.5',
            recommendation: 'over',
            reasoning: 'Lead-off spot with source-backed run environment.',
            edge: 18.2,
            prob: 72,
            odds: -110,
            market_line_value: 1.5,
            projected_stat_value: 1.9,
            stat_type: 'batting_runs',
          },
        ],
      } as any)
      .mockResolvedValueOnce({
        team: 'Kansas City Royals',
        summary: 'Away props',
        props: [
          {
            player: 'Bobby Witt Jr.',
            prop: 'Hits Over 1.5',
            recommendation: 'over',
            reasoning: 'Contact profile against a hittable arm.',
            edge: 16.1,
            prob: 69,
            odds: -105,
            market_line_value: 1.5,
            projected_stat_value: 1.8,
            stat_type: 'batting_hits',
          },
        ],
      } as any);

    const insertedRows: any[] = [];
    mocked.poolQuery.mockImplementation(async (sql: string, params?: any[]) => {
      if (sql.includes('forecast_type = \'PLAYER_PROP\'') && sql.includes('FROM rm_forecast_precomputed')) {
        return { rows: insertedRows };
      }
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-mlb-read-1',
            league: 'mlb',
            home_team: 'Atlanta Braves',
            away_team: 'Kansas City Royals',
            home_short: 'ATL',
            away_short: 'KC',
            starts_at: '2099-03-28T23:15:00.000Z',
            moneyline: {},
            spread: {},
            total: {},
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) return { rows: [] };
      if (sql.includes("forecast_type = 'TEAM_PROPS'") && sql.includes('ORDER BY generated_at DESC')) return { rows: [] };
      if (sql.includes('FROM rm_mlb_normalized_player_prop_markets')) {
        return {
          rows: [
            {
              player_name: 'Ronald Acuna Jr.',
              stat_type: 'batting_runs',
              line: 1.5,
              over_payload: { odds: -110 },
              under_payload: { odds: -110 },
            },
            {
              player_name: 'Bobby Witt Jr.',
              stat_type: 'batting_hits',
              line: 1.5,
              over_payload: { odds: -105 },
              under_payload: { odds: -115 },
            },
          ],
        };
      }
      if (sql.includes('INSERT INTO rm_team_props_cache')) return { rows: [] };
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) return { rows: [] };
      if (sql.includes('jsonb_to_recordset($1::jsonb)') && sql.includes('UPDATE rm_forecast_precomputed existing')) {
        return { rows: [], rowCount: 0 };
      }
      if (sql.includes('jsonb_to_recordset($1::jsonb)') && sql.includes('INSERT INTO rm_forecast_precomputed')) {
        const records = JSON.parse(String(params?.[0] ?? '[]'));
        for (const record of records) {
          insertedRows.push({
            id: `asset-${insertedRows.length + 1}`,
            player_name: record.player_name,
            team_id: record.team_id,
            team_side: record.team_side,
            league: record.league,
            confidence_score: record.confidence_score,
            forecast_payload: record.forecast_payload,
          });
        }
        return { rows: records.map((_: any, index: number) => ({ id: `asset-${index + 1}` })) };
      }
      if (sql.includes('FROM "PlayerInjury"')) return { rows: [] };
      return { rows: [] };
    });

    const app = express();
    app.use('/', router);
    const res = await request(app).get('/evt-mlb-read-1/player-props');

    expect(res.status).toBe(200);
    expect(generateTeamProps).toHaveBeenCalledTimes(2);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      mode: 'per_player',
      count: 2,
      fallbackReason: null,
      forecastBalance: 2,
      forecast_balance: 2,
    });
    expect(res.body.playerProps).toEqual(expect.arrayContaining([
      expect.objectContaining({ player: 'Ronald Acuña Jr.', recommendation: 'over' }),
      expect.objectContaining({ player: 'Bobby Witt Jr.', recommendation: 'over' }),
    ]));
  });

  it('persists regenerated mlb player props when team props are generated on demand', async () => {
    const router = await loadRouter();
    const grok = await import('../../services/grok');
    vi.mocked(grok.generateTeamProps).mockResolvedValue({
      team: 'Atlanta Braves',
      summary: 'Live MLB generation path.',
      props: [
        {
          player: 'Ronald Acuna Jr.',
          prop: 'Runs Over 1.5',
          recommendation: 'over',
          reasoning: 'Lead-off spot with source-backed run environment.',
          edge: 18.2,
          prob: 72,
          odds: -110,
          market_line_value: 1.5,
          projected_stat_value: 1.9,
          stat_type: 'batting_runs',
          model_context: {
            k_rank: 3,
            park_factor: 107,
            weather_impact: 'positive',
          },
        },
      ],
    } as any);

    const insertedForecastTypes: string[] = [];
    let insertedPlayerPropRecords: any[] = [];

    mocked.poolQuery.mockImplementation(async (sql: string, params?: any[]) => {
      if (sql.includes('SELECT id FROM rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes('FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-mlb-1',
            league: 'mlb',
            home_team: 'Atlanta Braves',
            away_team: 'Kansas City Royals',
            home_short: 'ATL',
            away_short: 'KC',
            starts_at: '2026-03-28T23:15:00.000Z',
            moneyline: null,
            spread: null,
            total: null,
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) return { rows: [] };
      if (sql.includes("forecast_type = 'TEAM_PROPS'") && sql.includes('ORDER BY generated_at DESC')) {
        return { rows: [] };
      }
      if (sql.includes('FROM rm_mlb_normalized_player_prop_markets')) {
        return {
          rows: [{
            player_name: 'Ronald Acuna Jr.',
            stat_type: 'batting_runs',
            line: 1.5,
            over_payload: { odds: -110 },
            under_payload: { odds: -110 },
          }],
        };
      }
      if (sql.includes('INSERT INTO rm_team_props_cache')) return { rows: [] };
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) {
        insertedForecastTypes.push('TEAM_PROPS');
        return { rows: [{ id: 'team-props-asset' }] };
      }
      if (sql.includes('jsonb_to_recordset($1::jsonb)') && sql.includes('UPDATE rm_forecast_precomputed existing')) {
        return { rows: [], rowCount: 0 };
      }
      if (sql.includes('jsonb_to_recordset($1::jsonb)') && sql.includes('INSERT INTO rm_forecast_precomputed')) {
        insertedForecastTypes.push('PLAYER_PROP');
        insertedPlayerPropRecords = JSON.parse(String(params?.[0] ?? '[]'));
        return { rows: [{ id: 'player-prop-asset' }] };
      }
      if (sql.includes('INSERT INTO rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes('SELECT player_name, forecast_payload, confidence_score')) {
        return {
          rows: [{
            player_name: 'Ronald Acuna Jr.',
            confidence_score: 0.72,
            forecast_payload: {
              prop: 'Runs Over 1.5',
              recommendation: 'over',
              reasoning: 'Lead-off spot with source-backed run environment.',
              edge: 18.2,
              prob: 72,
              projected_probability: 72,
              market_implied_probability: 52.4,
              signal_tier: 'GOOD',
              forecast_direction: 'OVER',
              odds: -110,
              line: 1.5,
              market_line_value: 1.5,
              projected_stat_value: 1.9,
              stat_type: 'batting_runs',
              normalized_stat_type: 'batting_runs',
              market_type: 'PLAYER_PROP',
              grading_category: 'PLAYER_PROPS',
              player_role: 'batter',
              signal_table_row: {
                propType: 'Runs',
                marketLine: 1.5,
                odds: -110,
                marketImpliedProbability: 52.4,
                forecastDirection: 'OVER',
                projectedProbability: 72,
                projectedOutcome: 1.9,
                edgePct: 19.6,
                signal: 'GOOD',
              },
            },
          }],
        };
      }
      return { rows: [] };
    });

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

    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt-mlb-1', type: 'TEAM_PROPS', team: 'home', league: 'mlb' });

    expect(res.status).toBe(200);
    expect(insertedForecastTypes).toEqual(expect.arrayContaining(['PLAYER_PROP']));
    const teamPropsInsert = mocked.poolQuery.mock.calls.find(([sql]) => String(sql).includes("VALUES ($1,$2,$3,'TEAM_PROPS'"));
    const playerPropUpdate = mocked.poolQuery.mock.calls.find(([sql]) => String(sql).includes('jsonb_to_recordset($1::jsonb)') && String(sql).includes('UPDATE rm_forecast_precomputed existing'));
    const playerPropInsert = mocked.poolQuery.mock.calls.find(([sql]) => String(sql).includes('jsonb_to_recordset($1::jsonb)') && String(sql).includes('INSERT INTO rm_forecast_precomputed'));
    expect(String(teamPropsInsert?.[0])).not.toContain('ON CONFLICT');
    expect(String(playerPropUpdate?.[0])).not.toContain('ON CONFLICT');
    expect(String(playerPropInsert?.[0])).not.toContain('ON CONFLICT');
    expect(insertedPlayerPropRecords).toHaveLength(1);
    expect(insertedPlayerPropRecords[0].forecast_payload).toMatchObject({
      prop: 'Runs Over 1.5',
      forecast: 'Runs Over 1.5',
      prop_type: 'Runs',
      sportsbook_display: 'Runs Over 1.5 (-110)',
      forecast_direction: 'OVER',
      agreement_score: 76,
      agreement_label: 'HIGH',
      agreement_sources: ['model', 'market'],
      market_quality_label: 'FAIR',
    });
    expect(res.body.playerProps).toHaveLength(1);
    expect(res.body.playerProps[0]).toMatchObject({
      player: 'Ronald Acuna Jr.',
      recommendation: 'over',
      marketOrigin: 'source_backed',
      sourceBacked: true,
    });
  });

  it('casts date_et comparisons when upserting regenerated mlb team and player props', async () => {
    const router = await loadRouter();
    const grok = await import('../../services/grok');
    vi.mocked(grok.generateTeamProps).mockResolvedValue({
      team: 'Atlanta Braves',
      summary: 'Live MLB generation path.',
      props: [
        {
          player: 'Ronald Acuna Jr.',
          prop: 'Runs Over 1.5',
          recommendation: 'over',
          reasoning: 'Lead-off spot with source-backed run environment.',
          edge: 18.2,
          prob: 72,
          odds: -110,
          market_line_value: 1.5,
          projected_stat_value: 1.9,
          stat_type: 'batting_runs',
        },
      ],
    } as any);

    const rmForecastSql: string[] = [];

    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('SELECT id FROM rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes('FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-mlb-1',
            league: 'mlb',
            home_team: 'Atlanta Braves',
            away_team: 'Kansas City Royals',
            home_short: 'ATL',
            away_short: 'KC',
            starts_at: '2026-03-28T23:15:00.000Z',
            moneyline: null,
            spread: null,
            total: null,
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) return { rows: [] };
      if (sql.includes("forecast_type = 'TEAM_PROPS'") && sql.includes('ORDER BY generated_at DESC')) {
        return { rows: [] };
      }
      if (sql.includes('FROM rm_mlb_normalized_player_prop_markets')) {
        return {
          rows: [{
            player_name: 'Ronald Acuna Jr.',
            stat_type: 'batting_runs',
            line: 1.5,
            over_payload: { odds: -110 },
            under_payload: { odds: -110 },
          }],
        };
      }
      if (sql.includes('INSERT INTO rm_team_props_cache')) return { rows: [] };
      if (sql.includes('rm_forecast_precomputed')) {
        rmForecastSql.push(sql);
      }
      if (sql.includes('UPDATE rm_forecast_precomputed')) return { rows: [], rowCount: 0 };
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) return { rows: [{ id: 'team-props-asset' }] };
      if (sql.includes('jsonb_to_recordset($1::jsonb)') && sql.includes('INSERT INTO rm_forecast_precomputed')) {
        return { rows: [{ id: 'player-prop-asset' }] };
      }
      if (sql.includes('INSERT INTO rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes('SELECT player_name, forecast_payload, confidence_score')) {
        return {
          rows: [{
            player_name: 'Ronald Acuna Jr.',
            confidence_score: 0.72,
            forecast_payload: {
              prop: 'Runs Over 1.5',
              recommendation: 'over',
              reasoning: 'Lead-off spot with source-backed run environment.',
              edge: 18.2,
              prob: 72,
              projected_probability: 72,
              market_implied_probability: 52.4,
              signal_tier: 'GOOD',
              forecast_direction: 'OVER',
              odds: -110,
              line: 1.5,
              market_line_value: 1.5,
              projected_stat_value: 1.9,
              stat_type: 'batting_runs',
              normalized_stat_type: 'batting_runs',
              market_type: 'PLAYER_PROP',
              grading_category: 'PLAYER_PROPS',
              player_role: 'batter',
              signal_table_row: {
                propType: 'Runs',
                marketLine: 1.5,
                odds: -110,
                marketImpliedProbability: 52.4,
                forecastDirection: 'OVER',
                projectedProbability: 72,
                projectedOutcome: 1.9,
                edgePct: 19.6,
                signal: 'GOOD',
              },
            },
          }],
        };
      }
      return { rows: [] };
    });

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

    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt-mlb-1', type: 'TEAM_PROPS', team: 'home', league: 'mlb' });

    expect(res.status).toBe(200);
    const forecastUpserts = rmForecastSql.filter((sql) => sql.includes('rm_forecast_precomputed'));
    expect(forecastUpserts.some((sql) => sql.includes('WHERE date_et = $1::date'))).toBe(true);
    expect(forecastUpserts.some((sql) => sql.includes('existing.date_et = incoming.date_et'))).toBe(true);
    expect(forecastUpserts.some((sql) => sql.includes("VALUES ($1::date,$2,$3,'TEAM_PROPS'"))).toBe(true);
    expect(forecastUpserts.some((sql) => sql.includes('jsonb_to_recordset($1::jsonb)'))).toBe(true);
  });

  it('regenerates mlb player props on read when only stale rows are available', async () => {
    const router = await loadRouter();
    const { generateTeamProps } = await import('../../services/grok');
    vi.mocked(generateTeamProps).mockResolvedValue({
      team: 'Los Angeles Dodgers',
      summary: 'Fresh MLB props on stale fallback.',
      props: [
        {
          player: 'Mookie Betts',
          prop: 'Hits Over 1.5',
          recommendation: 'over',
          reasoning: 'Fresh source-backed edge.',
          edge: 15.4,
          prob: 68,
          odds: -108,
          market_line_value: 1.5,
          projected_stat_value: 1.8,
          stat_type: 'batting_hits',
        },
      ],
    } as any);

    let playerPropReadCount = 0;
    const insertedRows: any[] = [];

    mocked.poolQuery.mockImplementation(async (sql: string, params?: any[]) => {
      if (sql.includes("forecast_type = 'PLAYER_PROP'") && sql.includes('FROM rm_forecast_precomputed')) {
        playerPropReadCount += 1;
        if (playerPropReadCount === 1) {
          return {
            rows: [
              {
                id: 'stale-asset-1',
                event_id: 'mlb-cle-lad-20260330',
                player_name: 'Mookie Betts',
                team_id: 'LAD',
                team_side: 'home',
                league: 'mlb',
                status: 'STALE',
                expires_at: '2099-03-30T23:15:00.000Z',
                confidence_score: 0.61,
                forecast_payload: {
                  prop: 'Hits Over 1.5',
                  recommendation: 'over',
                  reasoning: 'Stale reasoning',
                  prob: 61,
                  odds: -110,
                  market_line_value: 1.5,
                  projected_stat_value: 1.7,
                  stat_type: 'batting_hits',
                  normalized_stat_type: 'batting_hits',
                  signal_table_row: { odds: -110 },
                },
              },
            ],
          };
        }

        return { rows: insertedRows };
      }
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'mlb-cle-lad-20260330',
            league: 'mlb',
            home_team: 'Los Angeles Dodgers',
            away_team: 'Cleveland Guardians',
            home_short: 'LAD',
            away_short: 'CLE',
            starts_at: '2099-03-30T23:15:00.000Z',
            moneyline: {},
            spread: {},
            total: {},
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) return { rows: [] };
      if (sql.includes("forecast_type = 'TEAM_PROPS'") && sql.includes('ORDER BY generated_at DESC')) return { rows: [] };
      if (sql.includes('FROM rm_mlb_normalized_player_prop_markets')) {
        return {
          rows: [{
            player_name: 'Mookie Betts',
            stat_type: 'batting_hits',
            line: 1.5,
            over_payload: { odds: -108 },
            under_payload: { odds: -118 },
          }],
        };
      }
      if (sql.includes('INSERT INTO rm_team_props_cache')) return { rows: [] };
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) return { rows: [{ id: 'team-props-asset' }] };
      if (sql.includes('jsonb_to_recordset($1::jsonb)') && sql.includes('UPDATE rm_forecast_precomputed existing')) {
        return { rows: [], rowCount: 0 };
      }
      if (sql.includes('jsonb_to_recordset($1::jsonb)') && sql.includes('INSERT INTO rm_forecast_precomputed')) {
        const records = JSON.parse(String(params?.[0] ?? '[]'));
        for (const record of records) {
          insertedRows.push({
            id: `fresh-asset-${insertedRows.length + 1}`,
            event_id: record.event_id,
            player_name: record.player_name,
            team_id: record.team_id,
            team_side: record.team_side,
            league: record.league,
            status: 'ACTIVE',
            expires_at: record.expires_at,
            confidence_score: record.confidence_score,
            forecast_payload: record.forecast_payload,
          });
        }
        return { rows: records.map((_: any, index: number) => ({ id: `player-prop-asset-${index + 1}` })) };
      }
      if (sql.includes('FROM "PlayerInjury"')) return { rows: [] };
      return { rows: [] };
    });

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

    const res = await request(app).get('/mlb-cle-lad-20260330/player-props');

    expect(res.status).toBe(200);
    expect(generateTeamProps).toHaveBeenCalled();
    expect(playerPropReadCount).toBeGreaterThanOrEqual(2);
    expect(res.body.playerProps[0]).toMatchObject({
      player: 'Mookie Betts',
      confidence: 0.68,
      recommendation: 'over',
    });
  });

  it('keeps legacy playerProps present in team mode while exposing the canonical balance field', async () => {
    process.env.FEATURE_PLAYER_PROPS_PER_PLAYER_UNLOCK = 'false';
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [{
          id: 'asset-1',
          player_name: 'Luka Doncic',
          team_id: 'LAL',
          team_side: 'home',
          league: 'nba',
          confidence_score: 0.7,
          forecast_payload: {
            prop: 'Points Over 31.5',
            recommendation: 'over',
            edge: 16.4,
            prob: 68,
            odds: -110,
            line: 31.5,
            market_line_value: 31.5,
            projected_stat_value: 34.2,
            stat_type: 'points',
            normalized_stat_type: 'points',
            grading_category: 'PLAYER_PROPS',
            market_type: 'PLAYER_PROP',
          },
        }],
      })
      .mockResolvedValueOnce({ rows: [] });

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

    expect(res.status).toBe(200);
    expect(res.body.mode).toBe('team');
    expect(Array.isArray(res.body.playerProps)).toBe(true);
    expect(Array.isArray(res.body.players)).toBe(true);
    expect(res.body).toHaveProperty('count');
    expect(typeof res.body.forecastBalance).toBe('number');
    expect(typeof res.body.forecast_balance).toBe('number');
  });

  it('adds mlbPropContext to cached legacy team props payloads', async () => {
    const router = await loadRouter();

    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_team_props_unlocks')) {
        return { rows: [{ id: 'unlock-1' }] };
      }
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-mlb-legacy',
            home_team: 'Dodgers',
            away_team: 'Diamondbacks',
            home_short: 'LAD',
            away_short: 'ARI',
            league: 'mlb',
            starts_at: '2026-03-28T19:00:00Z',
            moneyline: {},
            spread: {},
            total: {},
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) {
        return {
          rows: [{
            props_data: {
              team: 'Dodgers',
              props: [
                {
                  player: 'Shohei Ohtani',
                  prop: 'Hits Over 1.5',
                  recommendation: 'over',
                  prob: 72,
                  model_context: {
                    k_rank: 4,
                    park_factor: 112,
                    weather_impact: 'positive',
                    lineup_certainty: 'confirmed',
                  },
                },
              ],
            },
          }],
        };
      }
      if (sql.includes("forecast_type = 'TEAM_PROPS'")) {
        return { rows: [{ status: 'ACTIVE', generated_at: '2026-03-28T19:00:00Z' }] };
      }
      if (sql.includes("forecast_type = 'PLAYER_PROP'")) {
        return { rows: [] };
      }
      return { rows: [] };
    });
    mocked.getPickBalance.mockResolvedValueOnce({ single_picks: 2, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });

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

    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt-mlb-legacy', type: 'TEAM_PROPS', team: 'home', league: 'mlb' });

    expect(res.status).toBe(200);
    expect(res.body.forecastBalance).toBe(3);
    expect(res.body.forecast_balance).toBe(3);
    expect(Array.isArray(res.body.playerProps)).toBe(true);
    expect(res.body.props[0]).toMatchObject({
      player: 'Shohei Ohtani',
      confidence: 0.72,
      modelContext: {
        kRank: 4,
        parkFactor: 112,
        weatherImpact: 'positive',
        lineupCertainty: 'confirmed',
      },
      mlbPropContext: {
        kRank: 4,
        parkFactor: 112,
        weatherImpact: 'positive',
        handednessSplit: null,
        lineupCertainty: 'confirmed',
      },
    });
    expect(res.body.playerProps[0]).toMatchObject({
      player: 'Shohei Ohtani',
      prop: 'Hits Over 1.5',
      confidence: 0.72,
      mlbPropContext: {
        kRank: 4,
        parkFactor: 112,
        weatherImpact: 'positive',
        handednessSplit: null,
        lineupCertainty: 'confirmed',
      },
    });
  });

  it('normalizes legacy recommendations arrays into props for team unlock responses', async () => {
    const router = await loadRouter();

    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_team_props_unlocks')) {
        return { rows: [] };
      }
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-1',
            home_team: 'Dodgers',
            away_team: 'Diamondbacks',
            home_short: 'LAD',
            away_short: 'ARI',
            league: 'mlb',
            starts_at: '2026-03-28T19:00:00Z',
            moneyline: {},
            spread: {},
            total: {},
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) {
        return { rows: [] };
      }
      if (sql.includes("forecast_type = 'TEAM_PROPS'")) {
        return { rows: [] };
      }
      if (sql.includes('INSERT INTO rm_team_props_cache')) {
        return { rows: [] };
      }
      if (sql.includes('INSERT INTO rm_team_props_unlocks')) {
        return { rows: [] };
      }
      if (sql.includes("forecast_type = 'PLAYER_PROP'")) {
        return { rows: [] };
      }
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) {
        return { rows: [] };
      }
      return { rows: [] };
    });

    const { generateTeamProps } = await import('../../services/grok');
    vi.mocked(generateTeamProps).mockResolvedValue({
      team: 'Dodgers',
      summary: 'Test summary',
      recommendations: [
        {
          player: 'Shohei Ohtani',
          prop: 'Hits Over 1.5',
          recommendation: 'over',
          reasoning: 'Test reasoning',
        },
      ],
    } as any);

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

    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt-1', type: 'TEAM_PROPS', team: 'home', league: 'mlb' });

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

  it('regenerates stale empty MLB team props bundles even for already-unlocked users', async () => {
    const router = await loadRouter();

    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_team_props_unlocks')) {
        return { rows: [{ id: 'unlock-1' }] };
      }
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'mlb-tb-stl-20260328',
            home_team: 'St. Louis Cardinals',
            away_team: 'Tampa Bay Rays',
            home_short: 'STL',
            away_short: 'TB',
            league: 'mlb',
            starts_at: '2026-03-28T23:15:00Z',
            moneyline: {},
            spread: {},
            total: {},
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) {
        return {
          rows: [{
            props_data: {
              team: 'St. Louis Cardinals',
              props: [],
              metadata: {
                mlb_publishable_candidate_count: 12,
              },
            },
          }],
        };
      }
      if (sql.includes("forecast_type = 'TEAM_PROPS'")) {
        return { rows: [{ status: 'EXPIRED', generated_at: '2026-03-28T20:45:40Z' }] };
      }
      if (sql.includes('INSERT INTO rm_team_props_cache')) {
        return { rows: [] };
      }
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) {
        return { rows: [] };
      }
      if (sql.includes("forecast_type = 'PLAYER_PROP'")) {
        return { rows: [] };
      }
      return { rows: [] };
    });

    const { generateTeamProps } = await import('../../services/grok');
    vi.mocked(generateTeamProps).mockResolvedValue({
      team: 'St. Louis Cardinals',
      summary: 'Fresh props',
      props: [
        {
          player: 'Michael McGreevy',
          prop: 'Pitching Earned Runs Under 1.5',
          recommendation: 'under',
          reasoning: 'Fresh reasoning',
        },
      ],
      metadata: { mlb_publishable_candidate_count: 12 },
    } as any);

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

    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'mlb-tb-stl-20260328', type: 'TEAM_PROPS', team: 'home', league: 'mlb' });

    expect(res.status).toBe(200);
    expect(generateTeamProps).toHaveBeenCalledTimes(1);
    expect(res.body.props).toEqual([
      expect.objectContaining({
        player: 'Michael McGreevy',
        prop: 'Pitching Earned Runs Under 1.5',
        recommendation: 'under',
      }),
    ]);
  });

  it('surfaces generic suppressed_reason for non-MLB team unlock responses', async () => {
    const router = await loadRouter();

    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-nba-1',
            home_team: 'Phoenix Suns',
            away_team: 'Utah Jazz',
            home_short: 'PHX',
            away_short: 'UTA',
            league: 'nba',
            starts_at: '2026-03-28T23:30:00.000Z',
            moneyline: {},
            spread: {},
            total: {},
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) return { rows: [] };
      if (sql.includes("forecast_type = 'TEAM_PROPS'")) return { rows: [] };
      if (sql.includes('INSERT INTO rm_team_props_cache')) return { rows: [] };
      if (sql.includes('INSERT INTO rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes("forecast_type = 'PLAYER_PROP'")) return { rows: [] };
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) return { rows: [] };
      return { rows: [] };
    });

    const { generateTeamProps } = await import('../../services/grok');
    vi.mocked(generateTeamProps).mockResolvedValue({
      team: 'Phoenix Suns',
      summary: 'Props suppressed for Phoenix Suns due to no source market candidates in the source market feed.',
      props: [],
      metadata: {
        suppressed_reason: 'no_source_market_candidates',
        source_market_candidate_count: 0,
      },
    } as any);

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

    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt-nba-1', type: 'TEAM_PROPS', team: 'home', league: 'nba' });

    expect(res.status).toBe(200);
    expect(res.body.suppressed_reason).toBe('no_source_market_candidates');
    expect(res.body.playerProps).toEqual([]);
  });

  it('does not regenerate non-MLB team props on unlock when an active empty cache already exists', async () => {
    const router = await loadRouter();

    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-nba-cached-empty',
            home_team: 'Phoenix Suns',
            away_team: 'Utah Jazz',
            home_short: 'PHX',
            away_short: 'UTA',
            league: 'nba',
            starts_at: '2026-03-28T23:30:00.000Z',
            moneyline: {},
            spread: {},
            total: {},
          }],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache')) {
        return {
          rows: [{
            props_data: {
              team: 'Phoenix Suns',
              summary: 'No props today.',
              props: [],
            },
          }],
        };
      }
      if (sql.includes("forecast_type = 'TEAM_PROPS'") && sql.includes('ORDER BY generated_at DESC')) {
        return { rows: [{ status: 'ACTIVE', generated_at: '2026-03-30T15:00:00.000Z' }] };
      }
      if (sql.includes('INSERT INTO rm_team_props_unlocks')) return { rows: [] };
      if (sql.includes("forecast_type = 'PLAYER_PROP'")) return { rows: [] };
      if (sql.includes('INSERT INTO rm_team_props_cache')) throw new Error('should not rewrite team props cache');
      if (sql.includes("VALUES ($1,$2,$3,'TEAM_PROPS'")) throw new Error('should not regenerate team props assets');
      return { rows: [] };
    });

    const { generateTeamProps } = await import('../../services/grok');
    vi.mocked(generateTeamProps).mockResolvedValue({
      team: 'Phoenix Suns',
      summary: 'Should not be called.',
      props: [{ player: 'Devin Booker', prop: 'Points Over 26.5' }],
    } as any);

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

    const res = await request(app)
      .post('/unlock')
      .send({ eventId: 'evt-nba-cached-empty', type: 'TEAM_PROPS', team: 'home', league: 'nba' });

    expect(res.status).toBe(200);
    expect(generateTeamProps).not.toHaveBeenCalled();
    expect(res.body.playerProps).toEqual([]);
  });

  it('returns empty or suppressed precomputed team bundles in the breakdown route instead of dropping them', async () => {
    const router = await loadRouter();

    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-nba-breakdown-1',
            home_team: 'Phoenix Suns',
            away_team: 'Utah Jazz',
            home_short: 'PHX',
            away_short: 'UTA',
            league: 'nba',
            starts_at: '2026-03-28T23:30:00.000Z',
          }],
        };
      }
      if (sql.includes("forecast_type = 'TEAM_PROPS'")) {
        return {
          rows: [
            {
              team_side: 'home',
              player_name: null,
              confidence_score: null,
              forecast_payload: {
                props: [],
                metadata: { suppressed_reason: 'no_source_market_candidates' },
              },
            },
            {
              team_side: 'away',
              player_name: null,
              confidence_score: 0.61,
              forecast_payload: {
                props: [
                  {
                    player: 'Walker Kessler',
                    prop: 'Rebounds Under 7.5',
                    recommendation: 'under',
                    prob: 62,
                    line: 7.5,
                    odds: -115,
                  },
                  {
                    player: 'Lauri Markkanen',
                    prop: 'Points Over 24.5',
                    recommendation: 'over',
                    edge: 7.2,
                    prob: 61,
                    line: 24.5,
                    odds: -110,
                    forecast_direction: 'OVER',
                    agreement_score: 68,
                    market_quality_label: 'GOOD',
                  },
                ],
              },
            },
          ],
        };
      }
      if (sql.includes('FROM rm_team_props_cache')) return { rows: [] };
      return { rows: [] };
    });

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

    const res = await request(app).get('/evt-nba-breakdown-1/team-props-breakdown');

    expect(res.status).toBe(200);
    expect(res.body.available).toBe(true);
    expect(res.body.home).toMatchObject({
      team: 'Phoenix Suns',
      short: 'PHX',
      side: 'home',
      props: [],
      suppressed_reason: 'no_source_market_candidates',
    });
    expect(res.body.away).toMatchObject({
      team: 'Utah Jazz',
      short: 'UTA',
      side: 'away',
    });
    expect(res.body.away.props).toHaveLength(1);
    expect(res.body.away.props[0]).toMatchObject({
      player: 'Lauri Markkanen',
      prop: 'Points Over 24.5',
      recommendation: 'over',
      edge: 7.2,
    });
  });

  it('regenerates poisoned team-props bundles in the breakdown route instead of serving source-market fallbacks', async () => {
    const router = await loadRouter();
    const { generateTeamProps } = await import('../../services/grok');

    vi.mocked(generateTeamProps).mockResolvedValue({
      team: 'Phoenix Suns',
      summary: 'Clean regenerated bundle.',
      props: [
        {
          player: 'Devin Booker',
          prop: 'Points Over 28.5',
          recommendation: 'over',
          reasoning: 'Regenerated from valid support.',
          edge: 12.4,
          prob: 66,
          odds: -118,
          market_line_value: 28.5,
          projected_stat_value: 31.1,
          stat_type: 'points',
          forecast_direction: 'OVER',
          agreement_score: 72,
          market_quality_label: 'GOOD',
          model_context: {
            projection_basis: 'source_market_piff_exact',
          },
        },
      ],
    } as any);

    mocked.poolQuery.mockImplementation(async (sql: string, params?: any[]) => {
      if (sql.includes('SELECT * FROM rm_events')) {
        return {
          rows: [{
            event_id: 'evt-nba-breakdown-2',
            home_team: 'Phoenix Suns',
            away_team: 'Utah Jazz',
            home_short: 'PHX',
            away_short: 'UTA',
            league: 'nba',
            starts_at: '2026-03-28T23:30:00.000Z',
            moneyline: null,
            spread: null,
            total: null,
          }],
        };
      }
      if (sql.includes('SELECT team_side, player_name, forecast_payload, confidence_score')) {
        return {
          rows: [
            {
              team_side: 'home',
              player_name: null,
              confidence_score: null,
              forecast_payload: {
                props: [
                  {
                    player: 'Devin Booker',
                    prop: 'Points Under 28.5',
                    recommendation: 'under',
                    reasoning: 'This fallback is anchored to an exact source-backed market after validation failed.',
                    model_context: {
                      projection_basis: 'source_market_fallback',
                    },
                  },
                ],
              },
            },
            {
              team_side: 'away',
              player_name: null,
              confidence_score: 0.61,
              forecast_payload: {
                props: [
                  {
                    player: 'Lauri Markkanen',
                    prop: 'Points Over 24.5',
                    recommendation: 'over',
                    reasoning: 'Clean precomputed bundle.',
                    edge: 7.2,
                    prob: 61,
                    line: 24.5,
                    odds: -110,
                    forecast_direction: 'OVER',
                    agreement_score: 68,
                    market_quality_label: 'GOOD',
                    model_context: {
                      projection_basis: 'source_market_piff_exact',
                    },
                  },
                ],
              },
            },
          ],
        };
      }
      if (sql.includes('SELECT team, props_data FROM rm_team_props_cache')) {
        return {
          rows: [
            {
              team: 'home',
              props_data: {
                props: [
                  {
                    player: 'Devin Booker',
                    prop: 'Points Under 28.5',
                    recommendation: 'under',
                    reasoning: 'This fallback is anchored to an exact source-backed market after validation failed.',
                    model_context: {
                      projection_basis: 'source_market_fallback',
                    },
                  },
                ],
              },
            },
          ],
        };
      }
      if (sql.includes('SELECT * FROM rm_team_props_cache WHERE event_id = $1 AND team = $2')) {
        return {
          rows: [
            {
              props_data: {
                props: [
                  {
                    player: 'Devin Booker',
                    prop: 'Points Under 28.5',
                    recommendation: 'under',
                    reasoning: 'This fallback is anchored to an exact source-backed market after validation failed.',
                    model_context: {
                      projection_basis: 'source_market_fallback',
                    },
                  },
                ],
              },
            },
          ],
        };
      }
      if (sql.includes("forecast_type = 'TEAM_PROPS'") && sql.includes('ORDER BY generated_at DESC')) {
        return { rows: [{ status: 'ACTIVE', generated_at: '2026-03-30T15:00:00.000Z' }] };
      }
      if (sql.includes('INSERT INTO rm_team_props_cache')) {
        expect(params?.[0]).toBe('evt-nba-breakdown-2');
        expect(params?.[1]).toBe('home');
        expect(JSON.parse(String(params?.[5]))).toMatchObject({
          summary: 'Clean regenerated bundle.',
        });
        return { rows: [] };
      }
      return { rows: [] };
    });

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

    const res = await request(app).get('/evt-nba-breakdown-2/team-props-breakdown');

    expect(res.status).toBe(200);
    expect(generateTeamProps).toHaveBeenCalledTimes(1);
    expect(res.body.home).toMatchObject({
      team: 'Phoenix Suns',
      short: 'PHX',
      side: 'home',
    });
    expect(res.body.home.props).toEqual([
      expect.objectContaining({
        player: 'Devin Booker',
        prop: 'Points Over 28.5',
        recommendation: 'over',
      }),
    ]);
  });
});
