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

import { 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(),
}));

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: { mlb: 'mlb' } }));
vi.mock('../../services/grok', () => ({ generateForecast: vi.fn(), generateTeamProps: vi.fn(), generateSteamInsight: vi.fn(), generateSharpInsight: vi.fn() }));
vi.mock('../../services/dvp', () => ({ generateDvpInsight: vi.fn(), hasDvpData: vi.fn(async () => false) }));
vi.mock('../../services/home-field-scout', () => ({ generateHcwInsight: vi.fn(), hasHcwData: vi.fn(() => false) }));
vi.mock('../../db', () => ({ default: { query: mocked.poolQuery } }));

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

function collectSnakeCasePaths(value: any, path = 'body'): string[] {
  if (!value || typeof value !== 'object') return [];
  if (Array.isArray(value)) {
    return value.flatMap((entry, index) => collectSnakeCasePaths(entry, `${path}[${index}]`));
  }
  return Object.entries(value).flatMap(([key, entry]) => {
    const current = /_/.test(key) ? [`${path}.${key}`] : [];
    return [...current, ...collectSnakeCasePaths(entry, `${path}.${key}`)];
  });
}

function snapshotTopPropsResponse(body: any) {
  return {
    contractVersion: body.contractVersion,
    league: body.league,
    generatedAt: body.generatedAt,
    count: body.count,
    cards: body.cards,
    filters: body.filters,
    emptyMessage: body.emptyMessage,
  };
}

describe('/forecast/top-props contract', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-03-28T15:00:00.000Z'));
  });

  it('returns a versioned camelCase contract for tracked MLB pitcher-under props with FAIR cards below stronger edges', async () => {
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('fp.date =')) {
        throw new Error('top-props query regressed to removed fp.date column');
      }
      if (!sql.includes('fp.date_et =')) {
        throw new Error('top-props query must filter by fp.date_et');
      }
      if (!sql.includes('e.starts_at > NOW()')) {
        throw new Error('top-props query must exclude props after game start');
      }
      return {
        rows: [
          {
            id: 'asset-1',
            event_id: 'evt-1',
            league: 'mlb',
            starts_at: '2026-03-28T19:05:00.000Z',
            player_name: 'Gerrit Cole',
            team_id: 'NYY',
            team_side: 'home',
            confidence_score: 0.88,
            generated_at: '2026-03-28T14:00:00.000Z',
            home_team: 'Yankees',
            away_team: 'Red Sox',
            odds_snapshot: -105,
            forecast_payload: {
              prop: 'Strikeouts',
              stat_type: 'pitching_strikeouts',
              normalized_stat_type: 'pitching_strikeouts',
              market_line_value: 7.5,
              projected_stat_value: 6.1,
              prob: 79,
              recommendation: 'UNDER',
              player_role: 'pitcher',
              market_source: 'fanduel',
            },
          },
          {
            id: 'asset-2',
            event_id: 'evt-2',
            league: 'mlb',
            starts_at: '2026-03-28T19:05:00.000Z',
            player_name: 'Framber Valdez',
            team_id: 'HOU',
            team_side: 'home',
            confidence_score: 0.75,
            generated_at: '2026-03-28T14:05:00.000Z',
            home_team: 'Astros',
            away_team: 'Mariners',
            odds_snapshot: -110,
            forecast_payload: {
              prop: 'Hits Allowed',
              stat_type: 'pitching_hits',
              normalized_stat_type: 'pitching_hits',
              market_line_value: 5.5,
              projected_stat_value: 4.8,
              prob: 68,
              recommendation: 'UNDER',
              player_role: 'pitcher',
              market_source: 'draftkings',
            },
          },
          {
            id: 'asset-3',
            event_id: 'evt-3',
            league: 'mlb',
            starts_at: '2026-03-28T22:40:00.000Z',
            player_name: 'Joe Boyle',
            team_id: 'OAK',
            team_side: 'away',
            confidence_score: 0.72,
            generated_at: '2026-03-28T14:06:00.000Z',
            home_team: 'Rangers',
            away_team: 'Athletics',
            odds_snapshot: -102,
            forecast_payload: {
              prop: 'Walks Allowed',
              stat_type: 'pitching_basesOnBalls',
              normalized_stat_type: 'pitching_basesOnBalls',
              market_line_value: 1.5,
              projected_stat_value: 1.1,
              prob: 63,
              recommendation: 'UNDER',
              player_role: 'pitcher',
              market_source: 'betmgm',
            },
          },
          {
            id: 'asset-4',
            event_id: 'evt-4',
            league: 'mlb',
            starts_at: '2026-03-28T21:10:00.000Z',
            player_name: 'Zack Wheeler',
            team_id: 'PHI',
            team_side: 'home',
            confidence_score: 0.70,
            generated_at: '2026-03-28T14:10:00.000Z',
            home_team: 'Phillies',
            away_team: 'Braves',
            odds_snapshot: -108,
            forecast_payload: {
              prop: 'Earned Runs',
              stat_type: 'pitching_earnedruns',
              normalized_stat_type: 'pitching_earnedruns',
              market_line_value: 2.5,
              projected_stat_value: 2.0,
              prob: 62,
              recommendation: 'UNDER',
              player_role: 'pitcher',
              market_source: 'fanduel',
            },
          },
          {
            id: 'asset-5',
            event_id: 'evt-5',
            league: 'mlb',
            starts_at: '2026-03-28T20:10:00.000Z',
            player_name: 'Spencer Strider',
            team_id: 'ATL',
            team_side: 'away',
            confidence_score: 0.74,
            generated_at: '2026-03-28T14:12:00.000Z',
            home_team: 'Phillies',
            away_team: 'Braves',
            odds_snapshot: -101,
            forecast_payload: {
              prop: 'Strikeouts',
              stat_type: 'pitching_strikeouts',
              normalized_stat_type: 'pitching_strikeouts',
              market_line_value: 8.5,
              projected_stat_value: 7.4,
              prob: 66,
              recommendation: 'UNDER',
              player_role: 'pitcher',
              market_source: 'fanduel',
            },
          },
        ],
      };
    });

    const router = await loadRouter();


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

    const res = await request(app).get('/top-props?league=mlb&limit=4');

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      league: 'mlb',
      count: 4,
    });
    expect(res.body.cards).toHaveLength(4);
    expect(res.body.cards[0]).toMatchObject({
      playerName: 'Gerrit Cole',
      propType: 'Strikeouts',
      signal: 'STRONG',
      rankPosition: 1,
      forecastDirection: 'UNDER',
    });
    expect(res.body.cards.slice(1)).toEqual(expect.arrayContaining([
      expect.objectContaining({
        playerName: 'Framber Valdez',
        propType: 'Hits Allowed',
        signal: 'GOOD',
      }),
      expect.objectContaining({
        playerName: 'Spencer Strider',
        propType: 'Strikeouts',
        signal: 'GOOD',
      }),
      expect.objectContaining({
        playerName: 'Joe Boyle',
        propType: 'Walks Allowed',
        signal: 'FAIR',
      }),
    ]));
    expect(res.body.cards.find((card: any) => card.playerName === 'Zack Wheeler')).toBeUndefined();
    expect(res.body.cards[0]).toMatchObject({
      clvFitLabel: 'TRACKED',
      clvTracked: true,
      verificationLabel: 'Double-verified odds',
      verificationType: 'sportsbook',
    });
    expect(res.body.cards.every((card: any) => card.clvFitLabel === 'TRACKED')).toBe(true);
    expect(collectSnakeCasePaths(res.body)).toEqual([]);
    expect(snapshotTopPropsResponse(res.body)).toMatchObject({
      contractVersion: 'forecast-public-v1',
      count: 4,
      emptyMessage: 'No player prop edges available in this window',
      filters: {
        players: ['Framber Valdez', 'Gerrit Cole', 'Joe Boyle', 'Spencer Strider'],
        propTypes: ['Hits Allowed', 'Strikeouts', 'Walks Allowed'],
        teams: ['ATL', 'HOU', 'NYY', 'OAK'],
      },
      league: 'mlb',
    });
  });

  it('returns the stable empty state when no qualifying top props exist', async () => {
    const router = await loadRouter();

    mocked.poolQuery.mockResolvedValue({
      rows: [
        {
          id: 'asset-1',
          event_id: 'evt-1',
          league: 'mlb',
          starts_at: '2026-03-28T19:05:00.000Z',
          player_name: 'Juan Soto',
          team_id: 'NYY',
          team_side: 'home',
          confidence_score: 0.64,
          generated_at: '2026-03-28T14:12:00.000Z',
          home_team: 'Yankees',
          away_team: 'Red Sox',
          odds_snapshot: -120,
          forecast_payload: {
            prop: 'Walks',
            stat_type: 'batting_basesOnBalls',
            normalized_stat_type: 'batting_basesOnBalls',
            market_line_value: 0.5,
            projected_stat_value: 0.7,
            prob: 55,
            recommendation: 'OVER',
            player_role: 'batter',
            market_source: 'fanduel',
          },
        },
      ],
    });

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

    const res = await request(app).get('/top-props?league=mlb&limit=5');

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      league: 'mlb',
      count: 0,
      cards: [],
      emptyMessage: 'No player prop edges available in this window',
    });
    expect(snapshotTopPropsResponse(res.body)).toMatchObject({
      cards: [],
      contractVersion: 'forecast-public-v1',
      count: 0,
      emptyMessage: 'No player prop edges available in this window',
      filters: {
        players: [],
        propTypes: [],
        teams: [],
      },
      league: 'mlb',
    });
  });
});
