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

const getCachedForecast = vi.fn();
const cacheForecast = vi.fn();
const updateCachedOdds = vi.fn();
const parseOdds = vi.fn();
const generateForecast = vi.fn();
const getPiffPropsForGame = vi.fn(() => []);
const loadPiffPropsForDate = vi.fn(() => ({}));
const getDigimonForGame = vi.fn(() => []);
const loadDigimonPicksForDate = vi.fn(() => ({}));
const calculateComposite = vi.fn();
const getCornerScoutForGame = vi.fn();
const isSoccerLeague = vi.fn(() => false);
const buildIntelligence = vi.fn();
const mapToLegacyComposite = vi.fn();
const formatStoredModelSignals = vi.fn((_intel, legacy) => legacy);
const getLegacyCompositeView = vi.fn((stored) => stored);
const applyLegacyConfidenceAdjustment = vi.fn((legacy, adjustment) => {
  legacy.compositeConfidence = Math.round((legacy.compositeConfidence + adjustment) * 1000) / 1000;
  legacy.edgeBreakdown = {
    ...legacy.edgeBreakdown,
    adjustmentContribution: Math.round((((legacy.edgeBreakdown?.adjustmentContribution) || 0) + adjustment) * 1000) / 1000,
  };
  return legacy;
});
const dbQuery = vi.fn();
const isTournamentActive = vi.fn(async () => false);
const computeTournamentContext = vi.fn();

vi.mock('../../db', () => ({ default: { query: dbQuery } }));
vi.mock('../sgo', () => ({ parseOdds, SgoEvent: {} }));
vi.mock('../grok', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../grok')>();
  return { ...actual, generateForecast };
});
vi.mock('../../models/forecast', () => ({ getCachedForecast, cacheForecast, updateCachedOdds }));
vi.mock('../piff', () => ({ getPiffPropsForGame, loadPiffPropsForDate, loadTodaysPiffProps: vi.fn(() => ({})) }));
vi.mock('../digimon', () => ({ getDigimonForGame, loadDigimonPicksForDate, loadTodaysDigimonPicks: vi.fn(() => ({})) }));
vi.mock('../dvp', () => ({ getDvpForMatchup: vi.fn(async () => ({ home: null, away: null })) }));
vi.mock('../composite-score', () => ({ calculateComposite }));
vi.mock('../kenpom', () => ({ getKenPomMatchup: vi.fn() }));
vi.mock('../corner-scout', () => ({ getCornerScoutForGame, isSoccerLeague }));
vi.mock('../../tournament/context-engine', () => ({ isTournamentActive, computeTournamentContext }));
vi.mock('../narrative-engine', () => ({ runNarrativeEngine: vi.fn(() => ({ processedFields: {}, metadata: null })) }));
vi.mock('../rie', () => ({ buildIntelligence, mapToLegacyComposite, formatStoredModelSignals, getLegacyCompositeView, applyLegacyConfidenceAdjustment }));

describe('forecast-builder', () => {
  beforeEach(() => {
    vi.resetModules();
    vi.clearAllMocks();
    process.env.RIE_ENABLED = 'true';
    parseOdds.mockReturnValue({
      moneyline: { home: -120, away: 110 },
      spread: { home: { line: -4.5, odds: -110 }, away: { line: 4.5, odds: -110 } },
      total: { over: { line: 220.5, odds: -110 }, under: { line: 220.5, odds: -110 } },
    });
    dbQuery.mockResolvedValue({ rows: [] });
  });

  it('returns cached forecasts without rebuilding the composite', async () => {
    getCachedForecast.mockResolvedValue({
      id: 'fc-cache',
      forecast_data: { winner_pick: 'Lakers' },
      confidence_score: 0.71,
      composite_confidence: 0.74,
      odds_data: parseOdds(),
      model_signals: { stormCategory: 3, modelSignals: { grok: null, piff: null, digimon: null, dvp: null }, edgeBreakdown: { grokScore: 0.7, piffScore: 0.6, digimonScore: null, weights: { grok: 0.6, piff: 0.4, digimon: 0 } } },
      composite_version: 'v2',
    });

    const { buildForecast } = await import('../forecast-builder');
    const result = await buildForecast({
      eventID: 'evt1',
      teams: { home: { names: { long: 'Lakers', short: 'LAL' } }, away: { names: { long: 'Celtics', short: 'BOS' } } },
      status: { startsAt: '2026-03-27T19:00:00.000Z' },
    } as any, 'nba');

    expect(result.cached).toBe(true);
    expect(updateCachedOdds).toHaveBeenCalled();
    expect(result.confidenceScore).toBe(0.74);
  });

  it('builds and stores a new RIE composite when cache is missing', async () => {
    getCachedForecast.mockResolvedValue(null);
    generateForecast.mockResolvedValue({
      confidence: 0.78,
      value_rating: 8,
      winner_pick: 'Lakers',
      forecast_side: 'Lakers',
      projected_margin: 6,
      projected_total_points: 221,
    });
    buildIntelligence.mockResolvedValue({ signals: [], inputQuality: { piff: 'A', dvp: 'B', digimon: 'N/A', odds: 'A', overall: 'A' }, ragInsights: [], strategyProfile: { league: 'nba' } });
    mapToLegacyComposite.mockReturnValue({
      compositeConfidence: 0.82,
      stormCategory: 5,
      modelSignals: { grok: { confidence: 0.78, valueRating: 8 }, piff: null, digimon: null, dvp: null },
      edgeBreakdown: { grokScore: 0.78, piffScore: 0.5, digimonScore: null, weights: { grok: 1 } },
      compositeVersion: 'rm_2.0',
    });
    cacheForecast.mockResolvedValue({ id: 'saved-fc' });

    const { buildForecast } = await import('../forecast-builder');
    const result = await buildForecast({
      eventID: 'evt2',
      teams: { home: { names: { long: 'Lakers', short: 'LAL' } }, away: { names: { long: 'Celtics', short: 'BOS' } } },
      status: { startsAt: '2026-03-27T19:00:00.000Z' },
    } as any, 'nba');

    expect(result.cached).toBe(false);
    expect(buildIntelligence).toHaveBeenCalled();
    expect(cacheForecast).toHaveBeenCalled();
    expect(cacheForecast).toHaveBeenCalledWith(expect.objectContaining({
      confidenceScore: 0.78,
    }));
    expect(dbQuery).toHaveBeenCalled();
    expect(result.confidenceScore).toBe(0.82);
    expect(result.rawConfidenceScore).toBe(0.78);
  });

  it('loads legacy soccer side-data using the event ET date', async () => {
    process.env.RIE_ENABLED = 'false';
    getCachedForecast.mockResolvedValue(null);
    generateForecast.mockResolvedValue({
      confidence: 0.63,
      value_rating: 6,
      winner_pick: 'Elche',
      forecast_side: 'Elche',
      projected_margin: 0.4,
      projected_total_points: 2.3,
    });
    isSoccerLeague.mockReturnValue(true);
    calculateComposite.mockReturnValue({
      compositeConfidence: 0.67,
      stormCategory: 3,
      modelSignals: { grok: { confidence: 0.63, valueRating: 6 }, piff: null, digimon: null, dvp: null },
      edgeBreakdown: { grokScore: 0.63, piffScore: 0.5, digimonScore: null, cornerScoutScore: 0.54, weights: { grok: 0.4, piff: 0.4, cornerScout: 0.2 } },
      compositeVersion: 'v2',
    });
    cacheForecast.mockResolvedValue({ id: 'saved-soccer' });

    const { buildForecast } = await import('../forecast-builder');
    await buildForecast({
      eventID: 'evt-soccer',
      teams: {
        home: { names: { long: 'Rayo Vallecano', short: 'RAY' } },
        away: { names: { long: 'Elche', short: 'ELC' } },
      },
      status: { startsAt: '2026-04-03T19:00:00.000Z' },
    } as any, 'la_liga');

    expect(loadPiffPropsForDate).toHaveBeenCalledWith('2026-04-03');
    expect(getCornerScoutForGame).toHaveBeenCalledWith('Rayo Vallecano', 'Elche', undefined, '2026-04-03');
  });

  it('builds from curated rm_events rows by synthesizing parseable SGO odds keys', async () => {
    getCachedForecast.mockResolvedValue(null);
    generateForecast.mockResolvedValue({
      confidence: 0.74,
      value_rating: 7,
      winner_pick: 'Lakers',
      forecast_side: 'Lakers',
      projected_margin: 4,
      projected_total_points: 227,
    });
    buildIntelligence.mockResolvedValue({ signals: [], inputQuality: { piff: 'A', dvp: 'B', digimon: 'N/A', odds: 'A', overall: 'A' }, ragInsights: [], strategyProfile: { league: 'nba' } });
    mapToLegacyComposite.mockReturnValue({
      compositeConfidence: 0.79,
      stormCategory: 4,
      modelSignals: { grok: { confidence: 0.74, valueRating: 7 }, piff: null, digimon: null, dvp: null },
      edgeBreakdown: { grokScore: 0.74, piffScore: 0.5, digimonScore: null, weights: { grok: 1 } },
      compositeVersion: 'rm_2.0',
    });
    cacheForecast.mockResolvedValue({ id: 'saved-curated' });

    const { buildForecastFromCuratedEvent } = await import('../forecast-builder');
    const result = await buildForecastFromCuratedEvent({
      event_id: 'evt-curated-1',
      league: 'nba',
      home_team: 'Lakers',
      away_team: 'Celtics',
      home_short: 'LAL',
      away_short: 'BOS',
      starts_at: '2026-03-27T19:00:00.000Z',
      moneyline: { home: -125, away: 105 },
      spread: { home: { line: -3.5, odds: -110 }, away: { line: 3.5, odds: -110 } },
      total: { over: { line: 226.5, odds: -110 }, under: { line: 226.5, odds: -110 } },
    });

    expect(parseOdds).toHaveBeenCalledWith(expect.objectContaining({
      eventID: 'evt-curated-1',
      odds: expect.objectContaining({
        'points-home-game-ml-home': expect.objectContaining({ bookOdds: '-125' }),
        'points-away-game-ml-away': expect.objectContaining({ bookOdds: '105' }),
        'points-home-game-sp-home': expect.objectContaining({ bookSpread: '-3.5', bookOdds: '-110' }),
        'points-away-game-sp-away': expect.objectContaining({ bookSpread: '3.5', bookOdds: '-110' }),
        'points-all-game-ou-over': expect.objectContaining({ bookOverUnder: '226.5', bookOdds: '-110' }),
        'points-all-game-ou-under': expect.objectContaining({ bookOverUnder: '226.5', bookOdds: '-110' }),
      }),
    }));
    expect(result.forecastId).toBe('saved-curated');
    expect(result.confidenceScore).toBe(0.79);
  });

  it('surfaces mlb_phase_context for mlb whenever RIE is enabled', async () => {
    vi.resetModules();
    vi.clearAllMocks();
    process.env.RIE_ENABLED = 'true';
    process.env.MLB_MARKETS_ENABLED = 'false';

    getCachedForecast.mockResolvedValue(null);
    generateForecast.mockResolvedValue({
      confidence: 0.78,
      value_rating: 8,
      winner_pick: 'Dodgers',
      forecast_side: 'Dodgers',
      projected_margin: 1,
      projected_total_points: 8,
    });
    buildIntelligence.mockResolvedValue({
      signals: [{ signalId: 'mlb_phase', available: true, rawData: { k_rank: 7, lineup_certainty: 'high' } }],
      inputQuality: { piff: 'A', dvp: 'B', digimon: 'N/A', odds: 'A', overall: 'A' },
      ragInsights: [],
      strategyProfile: { league: 'mlb' },
    });
    mapToLegacyComposite.mockReturnValue({
      compositeConfidence: 0.82,
      stormCategory: 5,
      modelSignals: { grok: { confidence: 0.78, valueRating: 8 }, piff: null, digimon: null, dvp: null },
      edgeBreakdown: { grokScore: 0.78, piffScore: 0.5, digimonScore: null, weights: { grok: 1 } },
      compositeVersion: 'rm_2.0',
    });
    cacheForecast.mockResolvedValue({ id: 'saved-fc' });

    const { buildForecast } = await import('../forecast-builder');
    const result = await buildForecast({
      eventID: 'evt3',
      teams: { home: { names: { long: 'Dodgers', short: 'LAD' } }, away: { names: { long: 'Giants', short: 'SF' } } },
      status: { startsAt: '2026-03-27T19:00:00.000Z' },
    } as any, 'mlb');

    expect(result.forecast.mlb_phase_context).toEqual({ k_rank: 7, lineup_certainty: 'high' });
  });

  it('re-calibrates mlb projected margin from deterministic RIE signals', async () => {
    vi.resetModules();
    vi.clearAllMocks();
    process.env.RIE_ENABLED = 'true';
    parseOdds.mockReturnValue({
      moneyline: { home: -145, away: 125 },
      spread: { home: { line: -1.5, odds: -110 }, away: { line: 1.5, odds: -110 } },
      total: { over: { line: 8, odds: -110 }, under: { line: 8, odds: -110 } },
    });

    getCachedForecast.mockResolvedValue(null);
    generateForecast.mockResolvedValue({
      confidence: 0.78,
      value_rating: 8,
      winner_pick: 'Diamondbacks',
      forecast_side: 'Diamondbacks',
      projected_winner: 'Diamondbacks',
      projected_margin: -2,
      projected_total_points: 8,
      projected_team_score_home: 3,
      projected_team_score_away: 5,
      summary: 'Rain Man forecasts Diamondbacks to outscore Dodgers by ~2 runs.\nProjected combined score: ~8 runs.\nHere\'s how Rain Man got there: Arizona has the better starter edge.',
      projected_lines: {
        moneyline: { home: -145, away: 125 },
        spread: { home: -1.5, away: 1.5 },
        total: 8,
      },
      spread_edge: 0.5,
    });
    buildIntelligence.mockResolvedValue({
      signals: [
        {
          signalId: 'mlb_matchup',
          available: true,
          score: 0.64,
          rawData: {
            log5: { impliedSpread: -1.9 },
            runsCreated: { home: 4.8, away: 3.9 },
            starters: {
              home: { fangraphs: { fip: 3.2 } },
              away: { fangraphs: { fip: 4.4 } },
            },
            projections: {
              homeBat: { projWrcPlus: 111 },
              awayBat: { projWrcPlus: 98 },
            },
          },
        },
        {
          signalId: 'mlb_phase',
          available: true,
          score: 0.61,
          rawData: {
            k_rank: 7,
            lineup_certainty: 'high',
            bullpen: { sideScore: 0.59 },
            context: { sideScore: 0.56 },
          },
        },
        {
          signalId: 'fangraphs',
          available: true,
          score: 0.58,
          rawData: {},
        },
      ],
      inputQuality: { piff: 'A', dvp: 'B', digimon: 'N/A', odds: 'A', overall: 'A' },
      ragInsights: [],
      strategyProfile: { league: 'mlb' },
    });
    mapToLegacyComposite.mockReturnValue({
      compositeConfidence: 0.82,
      stormCategory: 5,
      modelSignals: { grok: { confidence: 0.78, valueRating: 8 }, piff: null, digimon: null, dvp: null },
      edgeBreakdown: { grokScore: 0.78, piffScore: 0.5, digimonScore: null, weights: { grok: 1 } },
      compositeVersion: 'rm_2.0',
    });
    cacheForecast.mockResolvedValue({ id: 'saved-fc' });

    const { buildForecast } = await import('../forecast-builder');
    const result = await buildForecast({
      eventID: 'evt4',
      teams: { home: { names: { long: 'Dodgers', short: 'LAD' } }, away: { names: { long: 'Diamondbacks', short: 'ARI' } } },
      status: { startsAt: '2026-03-27T19:00:00.000Z' },
      odds: [],
    } as any, 'mlb');

    expect(result.forecast.projected_margin).toBe(2.8);
    expect(result.forecast.projected_lines?.spread).toEqual({ home: -2.8, away: 2.8 });
    expect(result.forecast.spread_edge).toBe(1.3);
    expect(result.forecast.winner_pick).toBe('Dodgers');
    expect(result.forecast.projected_winner).toBe('Dodgers');
    expect(result.forecast.projected_team_score_home).toBe(5.4);
    expect(result.forecast.projected_team_score_away).toBe(2.6);
    expect(result.forecast.summary).toContain('Rain Man forecasts Dodgers to outscore Diamondbacks by ~2.8 runs.');
  });

  it('anchors mlb deterministic margins to the market spread when log5 implied spread is missing', async () => {
    vi.resetModules();
    vi.clearAllMocks();
    process.env.RIE_ENABLED = 'true';
    parseOdds.mockReturnValue({
      moneyline: { home: -145, away: 125 },
      spread: { home: { line: -1.5, odds: -110 }, away: { line: 1.5, odds: -110 } },
      total: { over: { line: 8, odds: -110 }, under: { line: 8, odds: -110 } },
    });

    getCachedForecast.mockResolvedValue(null);
    generateForecast.mockResolvedValue({
      confidence: 0.72,
      value_rating: 7,
      winner_pick: 'Braves',
      forecast_side: 'Royals',
      projected_winner: 'Braves',
      projected_margin: 0.3,
      projected_total_points: 8,
      projected_team_score_home: 4.15,
      projected_team_score_away: 3.85,
      summary: 'Rain Man forecasts Braves to outscore Royals by ~0.3 runs.',
      projected_lines: {
        moneyline: { home: -145, away: 125 },
        spread: { home: -1.5, away: 1.5 },
        total: 8,
      },
      spread_edge: 1.2,
    });
    buildIntelligence.mockResolvedValue({
      signals: [
        {
          signalId: 'mlb_matchup',
          available: true,
          score: 0.564,
          rawData: {
            log5: null,
            runsCreated: { home: 4.8, away: 4.1 },
            starters: {
              home: { fangraphs: { fip: 4.39 } },
              away: { fangraphs: { fip: 5.09 } },
            },
            projections: {
              homeBat: { projWrcPlus: 110.2 },
              awayBat: { projWrcPlus: 105.4 },
            },
          },
        },
        {
          signalId: 'mlb_phase',
          available: true,
          score: 0.547,
          rawData: {
            bullpen: { sideScore: 0.532 },
            context: { sideScore: 0.533 },
          },
        },
        {
          signalId: 'fangraphs',
          available: true,
          score: 0.503,
          rawData: {},
        },
      ],
      inputQuality: { piff: 'A', dvp: 'B', digimon: 'N/A', odds: 'A', overall: 'A' },
      ragInsights: [],
      strategyProfile: { league: 'mlb' },
    });
    mapToLegacyComposite.mockReturnValue({
      compositeConfidence: 0.74,
      stormCategory: 4,
      modelSignals: { grok: { confidence: 0.72, valueRating: 7 }, piff: null, digimon: null, dvp: null },
      edgeBreakdown: { grokScore: 0.72, piffScore: 0.5, digimonScore: null, weights: { grok: 1 } },
      compositeVersion: 'rm_2.0',
    });
    cacheForecast.mockResolvedValue({ id: 'saved-fc' });

    const { buildForecast } = await import('../forecast-builder');
    const result = await buildForecast({
      eventID: 'evt5',
      teams: { home: { names: { long: 'Braves', short: 'ATL' } }, away: { names: { long: 'Royals', short: 'KC' } } },
      status: { startsAt: '2026-03-29T19:00:00.000Z' },
      odds: [],
    } as any, 'mlb');

    expect(result.forecast.projected_margin).toBe(1.9);
    expect(result.forecast.forecast_side).toBe('Braves');
    expect(result.forecast.spread_edge).toBe(0);
    expect(result.forecast.summary).toContain('Rain Man forecasts Braves to outscore Royals by ~1.9 runs.');
  });

  it('applies tournament adjustments through the confidence alignment helper for RIE composites', async () => {
    vi.resetModules();
    vi.clearAllMocks();
    process.env.RIE_ENABLED = 'true';

    getCachedForecast.mockResolvedValue(null);
    isTournamentActive.mockResolvedValue(true);
    computeTournamentContext.mockResolvedValue({ finalAdjustment: 0.012 });
    generateForecast.mockResolvedValue({
      confidence: 0.78,
      value_rating: 8,
      winner_pick: 'Michigan',
      forecast_side: 'Michigan',
      projected_margin: 6,
      projected_total_points: 151,
    });
    buildIntelligence.mockResolvedValue({
      signals: [],
      inputQuality: { piff: 'A', dvp: 'B', digimon: 'N/A', odds: 'A', overall: 'A' },
      ragInsights: [],
      strategyProfile: { league: 'ncaab' },
    });
    mapToLegacyComposite.mockReturnValue({
      compositeConfidence: 0.694,
      stormCategory: 3,
      modelSignals: { grok: { confidence: 0.78, valueRating: 8 }, piff: null, digimon: null, dvp: null },
      edgeBreakdown: { grokScore: 0.68, kenpomScore: 0.726, weights: { grok: 0.421, kenpom: 0.474, rag: 0.105 } },
      compositeVersion: 'rm_2.0',
    });
    cacheForecast.mockResolvedValue({ id: 'saved-fc' });

    const { buildForecast } = await import('../forecast-builder');
    const result = await buildForecast({
      eventID: 'evt4',
      teams: { home: { names: { long: 'Michigan Wolverines', short: 'MICH' } }, away: { names: { long: 'Alabama Crimson Tide', short: 'BAMA' } } },
      status: { startsAt: '2026-03-27T19:00:00.000Z' },
    } as any, 'ncaab');

    expect(applyLegacyConfidenceAdjustment).toHaveBeenCalledWith(expect.any(Object), 0.012);
    expect(result.confidenceScore).toBe(0.706);
  });
});
