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: { nba: 'nba' } }));
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}`)];
  });
}

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

  it('returns a unified ranked board that mixes games and props', async () => {
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_forecast_precomputed fp')) {
        return {
          rows: [
            {
              id: 'asset-1',
              event_id: 'evt-prop-1',
              league: 'nba',
              starts_at: '2026-03-28T23:00:00.000Z',
              player_name: 'Jalen Brunson',
              team_id: 'NYK',
              team_side: 'away',
              confidence_score: 0.73,
              generated_at: '2026-03-28T14:10:00.000Z',
              home_team: 'Boston Celtics',
              away_team: 'New York Knicks',
              odds_snapshot: -110,
              forecast_payload: {
                prop: 'Points',
                stat_type: 'points',
                normalized_stat_type: 'points',
                market_line_value: 27.5,
                market_source: 'fanduel',
                projected_stat_value: 29.4,
                prob: 69,
                recommendation: 'OVER',
                player_role: 'guard',
              },
            },
            {
              id: 'asset-ignored',
              event_id: 'evt-prop-2',
              league: 'nba',
              starts_at: '2026-03-28T23:10:00.000Z',
              player_name: 'Missing Source',
              team_id: 'NYK',
              team_side: 'away',
              confidence_score: 0.72,
              generated_at: '2026-03-28T14:11:00.000Z',
              home_team: 'Boston Celtics',
              away_team: 'New York Knicks',
              odds_snapshot: -112,
              forecast_payload: {
                prop: 'Assists',
                stat_type: 'assists',
                normalized_stat_type: 'assists',
                market_line_value: 6.5,
                projected_stat_value: 7.1,
                prob: 67,
                recommendation: 'OVER',
                player_role: 'guard',
              },
            },
          ],
        };
      }

      if (sql.includes('FROM rm_events e')) {
        return {
          rows: [
            {
              event_id: 'evt-game-1',
              league: 'nba',
              starts_at: '2026-03-28T23:00:00.000Z',
              home_team: 'Boston Celtics',
              away_team: 'New York Knicks',
              home_short: 'BOS',
              away_short: 'NYK',
              source: 'sgo-auto+theodds',
              spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } },
              fc_confidence: 0.71,
              fc_data: {
                forecast_side: 'Celtics -4.5',
                spread_edge: 4.2,
                value_rating: 86,
              },
            },
          ],
        };
      }

      throw new Error(`Unexpected SQL in top-picks test: ${sql}`);
    });

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

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      league: 'nba',
      count: 2,
      filters: {
        kinds: ['game', 'prop'],
      },
    });
    expect(res.body.entries[0]).toMatchObject({
      kind: 'prop',
      rankPosition: 1,
      prop: {
        assetId: 'asset-1',
        playerName: 'Jalen Brunson',
        clvFitLabel: 'TRACKED',
        verificationLabel: 'Double-verified odds',
      },
    });
    expect(res.body.entries[1]).toMatchObject({
      kind: 'game',
      rankPosition: 2,
      game: {
        eventId: 'evt-game-1',
        forecastSide: 'Celtics -4.5',
        verificationLabel: 'Double-verified odds',
      },
    });
    expect(res.body.entries[0].clvMetric).toBeGreaterThan(res.body.entries[1].clvMetric);
    expect(collectSnakeCasePaths(res.body)).toEqual([]);
  });

  it('returns the stable empty state when no top picks are available', async () => {
    mocked.poolQuery.mockResolvedValue({ rows: [] });

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

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      league: 'nba',
      count: 0,
      entries: [],
      emptyMessage: 'No pregame picks available in this window',
    });
  });

  it('filters weak MLB game forecasts out of the public top picks board', async () => {
    mocked.poolQuery.mockImplementation(async (sql: string) => {
      if (sql.includes('FROM rm_forecast_precomputed fp')) {
        return { rows: [] };
      }

      if (sql.includes('FROM rm_events e')) {
        return {
          rows: [
            {
              event_id: 'evt-mlb-keep',
              league: 'mlb',
              starts_at: '2026-04-08T23:10:00.000Z',
              home_team: 'Texas Rangers',
              away_team: 'Seattle Mariners',
              home_short: 'TEX',
              away_short: 'SEA',
              source: 'sgo-auto+theodds',
              spread: { home: { line: 1.5, odds: -170 }, away: { line: -1.5, odds: 145 } },
              fc_confidence: 0.66,
              fc_data: {
                forecast_side: 'Texas Rangers',
                projected_lines: { moneyline: { home: 101, away: -111 } },
                spread_edge: 0.1,
                value_rating: 72,
              },
            },
            {
              event_id: 'evt-mlb-drop',
              league: 'mlb',
              starts_at: '2026-04-08T23:20:00.000Z',
              home_team: 'New York Yankees',
              away_team: 'Athletics',
              home_short: 'NYY',
              away_short: 'ATH',
              source: 'sgo-auto+theodds',
              spread: { home: { line: -1.5, odds: 120 }, away: { line: 1.5, odds: -145 } },
              fc_confidence: 0.74,
              fc_data: {
                forecast_side: 'New York Yankees',
                projected_lines: { moneyline: { home: -188, away: 188 } },
                spread_edge: 2.0,
                value_rating: 86,
              },
            },
          ],
        };
      }

      throw new Error(`Unexpected SQL in top-picks MLB gate test: ${sql}`);
    });

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

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

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      league: 'mlb',
      count: 1,
    });
    expect(res.body.entries).toHaveLength(1);
    expect(res.body.entries[0]).toMatchObject({
      kind: 'game',
      game: {
        eventId: 'evt-mlb-keep',
        forecastSide: 'Texas Rangers',
      },
    });
  });

  it('supports sport-scoped soccer boards with the forward lookahead window', async () => {
    const sqlCalls: Array<{ sql: string; params: any[] | undefined }> = [];
    mocked.poolQuery.mockImplementation(async (sql: string, params?: any[]) => {
      sqlCalls.push({ sql, params });

      if (sql.includes('FROM rm_forecast_precomputed fp')) {
        return {
          rows: [
            {
              id: 'asset-soccer-1',
              event_id: 'evt-soccer-prop-1',
              league: 'epl',
              date_et: '2026-04-10',
              starts_at: '2026-04-10T19:00:00.000Z',
              player_name: 'Jarrod Bowen',
              team_id: 'WHU',
              team_side: 'away',
              confidence_score: 0.7,
              generated_at: '2026-03-28T14:10:00.000Z',
              home_team: 'Wolverhampton',
              away_team: 'West Ham',
              odds_snapshot: 120,
              forecast_payload: {
                prop: 'Shots',
                stat_type: 'shots',
                normalized_stat_type: 'shots',
                market_line_value: 2.5,
                market_source: 'draftkings',
                projected_stat_value: 3.1,
                prob: 67,
                recommendation: 'OVER',
                player_role: 'forward',
              },
            },
          ],
        };
      }

      if (sql.includes('FROM rm_events e')) {
        return {
          rows: [
            {
              event_id: 'evt-soccer-game-1',
              league: 'epl',
              starts_at: '2026-04-10T19:00:00.000Z',
              home_team: 'Wolverhampton',
              away_team: 'West Ham',
              home_short: 'WOL',
              away_short: 'WHU',
              source: 'theodds',
              spread: null,
              fc_confidence: 0.68,
              fc_data: {
                forecast_side: 'West Ham draw no bet',
                projected_margin: -0.4,
                value_rating: 82,
              },
            },
          ],
        };
      }

      throw new Error(`Unexpected SQL in top-picks test: ${sql}`);
    });

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

    const res = await request(app).get('/top-picks?sport=soccer&limit=5');

    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      league: 'soccer',
      count: 1,
    });
    expect(res.body.entries).toHaveLength(1);
    expect(res.body.entries[0]).toMatchObject({
      kind: 'game',
      game: {
        eventId: 'evt-soccer-game-1',
      },
    });

    const propQuery = sqlCalls.find((entry) => entry.sql.includes('FROM rm_forecast_precomputed fp'));
    const gameQuery = sqlCalls.find((entry) => entry.sql.includes('FROM rm_events e'));
    expect(propQuery?.params?.[0]).toEqual(['epl', 'la_liga', 'bundesliga', 'serie_a', 'ligue_1', 'champions_league', 'mls']);
    expect(propQuery?.params?.[1]).toBe(14);
    expect(gameQuery?.params?.[0]).toEqual(['epl', 'la_liga', 'bundesliga', 'serie_a', 'ligue_1', 'champions_league', 'mls']);
    expect(gameQuery?.params?.[1]).toBe(14);
    expect(gameQuery?.sql).toContain('WHERE 1=1');
    expect(gameQuery?.sql).toContain('::int');
    expect(gameQuery?.sql).toContain('fc_exact.matched_event_id IS NULL');
    expect(gameQuery?.sql).not.toContain('WHERE fc.event_id = e.event_id\n     OR');
  });
});
