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

const { query, settleArchivedForecast, refreshBucketStats } = vi.hoisted(() => ({
  query: vi.fn(),
  settleArchivedForecast: vi.fn(),
  refreshBucketStats: vi.fn(),
}));

vi.mock('../../db', () => ({
  default: { query },
}));

vi.mock('../archive', () => ({
  settleArchivedForecast,
  refreshBucketStats,
}));

vi.mock('../../lib/team-abbreviations', () => ({
  getTeamAbbr: vi.fn((team: string | null) => {
    if (team === 'Arsenal') return 'ARS';
    if (team === 'Chelsea') return 'CHE';
    return '???';
  }),
}));

async function loadGradingService() {
  vi.resetModules();
  return import('../grading-service');
}

describe('grading-service', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    delete process.env.GRADE_TOTALS_ENABLED;
    vi.stubGlobal('fetch', vi.fn());
  });

  it('grades a spread winner using benchmark grading data', async () => {
    const { gradeForcast } = await loadGradingService();

    query.mockResolvedValueOnce({
      rows: [{ homeTeam: 'Lakers', awayTeam: 'Celtics', homeScore: 112, awayScore: 101, status: 'final' }],
    } as any);

    const result = await gradeForcast({
      id: 'fc1',
      event_id: 'evt1',
      league: 'nba',
      starts_at: '2026-03-27T19:00:00.000Z',
      home_team: 'Lakers',
      away_team: 'Celtics',
      confidence_score: '0.86',
      forecast_data: {
        winner_pick: 'Lakers',
        forecast_side: 'Lakers',
        projected_margin: 8,
        projected_total_points: 221,
        projected_lines: { spread: { home: -5, away: 5 } },
      },
      odds_data: {
        spread: { home: { line: -5, odds: -110 }, away: { line: 5, odds: -110 } },
        total: { over: { line: 220, odds: -110 }, under: { line: 220, odds: -110 } },
      },
    });

    expect(result).not.toBeNull();
    expect(result?.forecastType).toBe('spread');
    expect(result?.grade).toBe('W');
    expect(result?.accuracyBucket).toBe('A+');
    expect(result?.benchmarkForecast).toBe(-5);
  });

  it('extracts and grades totals from forecast data', async () => {
    const { gradeTotalFromForecastData } = await loadGradingService();

    const result = gradeTotalFromForecastData({
      id: 'fc-total',
      event_id: 'evt-total',
      league: 'nba',
      home_team: 'Lakers',
      away_team: 'Celtics',
      confidence_score: '0.71',
      forecast_data: {
        winner_pick: 'Lakers',
        total_side: 'over',
        projected_total_points: 220,
      },
      odds_data: {
        total: {
          over: { line: 221, odds: -110 },
          under: { line: 221, odds: -110 },
        },
      },
    }, 112, 111);

    expect(result).not.toBeNull();
    expect(result?.forecastType).toBe('total');
    expect(result?.predictedTotal).toBe(220);
    expect(result?.benchmarkForecast).toBe(220);
    expect(result?.grade).toBe('W');
  });

  it('preserves legacy winner fallback when total grading flag is off', async () => {
    process.env.GRADE_TOTALS_ENABLED = 'false';
    const { gradeForcast } = await loadGradingService();

    query.mockResolvedValueOnce({
      rows: [{ homeTeam: 'Lakers', awayTeam: 'Celtics', homeScore: 110, awayScore: 108, status: 'final' }],
    } as any);

    const result = await gradeForcast({
      id: 'fc-flag-off',
      event_id: 'evt-flag-off',
      league: 'nba',
      starts_at: '2026-03-27T19:00:00.000Z',
      home_team: 'Lakers',
      away_team: 'Celtics',
      confidence_score: '0.60',
      forecast_data: {
        winner_pick: 'Lakers',
        total_side: 'over',
        projected_total_points: 215,
      },
      odds_data: {
        total: {
          over: { line: 214, odds: -110 },
          under: { line: 214, odds: -110 },
        },
      },
    });

    expect(result).not.toBeNull();
    expect(result?.forecastType).toBe('spread');
    expect(result?.grade).toBe('W');
    expect(result?.benchmarkForecast).toBeNull();
  });

  it('uses total grading when the flag is on and no spread data exists', async () => {
    process.env.GRADE_TOTALS_ENABLED = 'true';
    const { gradeForcast } = await loadGradingService();

    query.mockResolvedValueOnce({
      rows: [{ homeTeam: 'Lakers', awayTeam: 'Celtics', homeScore: 112, awayScore: 111, status: 'final' }],
    } as any);

    const result = await gradeForcast({
      id: 'fc-flag-on',
      event_id: 'evt-flag-on',
      league: 'nba',
      starts_at: '2026-03-27T19:00:00.000Z',
      home_team: 'Lakers',
      away_team: 'Celtics',
      confidence_score: '0.60',
      forecast_data: {
        winner_pick: 'Lakers',
        total_side: 'over',
        projected_total_points: 220,
      },
      odds_data: {
        total: {
          over: { line: 221, odds: -110 },
          under: { line: 221, odds: -110 },
        },
      },
    });

    expect(result).not.toBeNull();
    expect(result?.forecastType).toBe('total');
    expect(result?.grade).toBe('W');
    expect(result?.predictedTotal).toBe(220);
    expect(result?.predictedSpread).toBeNull();
  });

  it('handles soccer abbreviation matching through the game lookup', async () => {
    const { gradeForcast } = await loadGradingService();

    query.mockResolvedValueOnce({
      rows: [{ homeTeam: 'ARS', awayTeam: 'CHE', homeScore: 2, awayScore: 1, status: 'final' }],
    } as any);

    const result = await gradeForcast({
      id: 'fc2',
      event_id: 'evt2',
      league: 'epl',
      starts_at: '2026-03-27T19:00:00.000Z',
      home_team: 'Arsenal',
      away_team: 'Chelsea',
      confidence_score: '0.64',
      forecast_data: { winner_pick: 'Arsenal' },
      odds_data: null,
    });

    expect(result?.actualWinner).toBe('Arsenal');
    expect(result?.grade).toBe('W');
  });

  it('returns null when no final game is found', async () => {
    const { gradeForcast } = await loadGradingService();

    query.mockResolvedValueOnce({ rows: [] } as any);
    vi.mocked(fetch).mockResolvedValue({
      ok: true,
      json: async () => ({ events: [] }),
    } as any);

    const result = await gradeForcast({
      id: 'fc3',
      event_id: 'evt3',
      league: 'nba',
      starts_at: '2026-03-27T19:00:00.000Z',
      home_team: 'Lakers',
      away_team: 'Celtics',
      confidence_score: '0.50',
      forecast_data: { winner_pick: 'Lakers' },
      odds_data: null,
    });

    expect(result).toBeNull();
  });

  it('falls back to ESPN athlete names for MMA results', async () => {
    const { gradeForcast } = await loadGradingService();

    query.mockResolvedValueOnce({ rows: [] } as any);
    vi.mocked(fetch).mockResolvedValue({
      ok: true,
      json: async () => ({
        events: [{
          competitions: [{
            status: { type: { name: 'STATUS_FINAL' } },
            competitors: [
              {
                athlete: { displayName: 'Valter Walker' },
                winner: false,
              },
              {
                athlete: { displayName: 'Marcin Tybura' },
                winner: true,
              },
            ],
          }],
        }],
      }),
    } as any);

    const result = await gradeForcast({
      id: 'mma-1',
      event_id: 'mma-vwa-mty-20260328',
      league: 'mma',
      starts_at: '2026-03-28T21:00:00.000Z',
      home_team: 'Marcin Tybura',
      away_team: 'Valter Walker',
      confidence_score: '0.60',
      forecast_data: { winner_pick: 'Marcin Tybura' },
      odds_data: null,
    });

    expect(result).not.toBeNull();
    expect(result?.actualWinner).toBe('Marcin Tybura');
    expect(result?.homeScore).toBe(1);
    expect(result?.awayScore).toBe(0);
  });

  it('mirrors resolved rows into rm_forecast_accuracy_v2', async () => {
    const { persistGradingResult } = await loadGradingService();

    query
      .mockResolvedValueOnce({ rows: [{ event_date: '2026-03-27' }] } as any)
      .mockResolvedValueOnce({ rowCount: 1, rows: [{ id: 'acc-v2' }] } as any);

    await persistGradingResult({
      forecastId: 'fc-v2',
      eventId: 'evt-v2',
      league: 'nba',
      forecastType: 'spread',
      predictedWinner: 'Lakers',
      actualWinner: 'Lakers',
      grade: 'W',
      homeScore: 111,
      awayScore: 102,
      actualSpread: 9,
      actualTotal: 213,
      predictedSpread: -4.5,
      predictedTotal: 220.5,
      accuracyPct: 75,
      accuracyBucket: 'A',
      originalForecast: -5,
      benchmarkForecast: -4.5,
      closingMarket: -4.5,
      benchmarkSource: 'closing',
      gradingPolicy: 'best_valid_number',
    });

    expect(query).toHaveBeenCalledTimes(2);
    expect(query.mock.calls.some(([sql]) => String(sql).includes('INSERT INTO rm_forecast_accuracy_v2'))).toBe(true);
  });

  it('updates v2 rows by forecast_id without the broad event OR match', async () => {
    const { upsertAccuracyV2FromRow } = await loadGradingService();

    query.mockResolvedValueOnce({ rowCount: 1, rows: [{ id: 'acc-1' }] } as any);

    await upsertAccuracyV2FromRow({
      forecast_id: 'fc-1',
      event_id: 'evt-1',
      league: 'nba',
      predicted_winner: 'Lakers',
      actual_winner: 'Lakers',
      final_grade: 'W',
    });

    expect(query).toHaveBeenCalledTimes(1);
    const sql = String(query.mock.calls[0]?.[0]);
    expect(sql).toContain('pg_advisory_xact_lock');
    expect(sql).toContain('WHERE $1::uuid IS NOT NULL');
    expect(sql).not.toContain('OR event_id = $2');
  });

  it('guards v2 inserts atomically when no matching forecast row exists yet', async () => {
    const { upsertAccuracyV2FromRow } = await loadGradingService();

    query.mockResolvedValueOnce({ rowCount: 0, rows: [] } as any);

    await upsertAccuracyV2FromRow({
      forecast_id: 'fc-2',
      event_id: 'evt-2',
      league: 'nba',
      predicted_winner: 'Celtics',
      actual_winner: 'Celtics',
      final_grade: 'W',
    });

    expect(query).toHaveBeenCalledTimes(1);
    const sql = String(query.mock.calls[0]?.[0]);
    expect(sql).toContain('INSERT INTO rm_forecast_accuracy_v2');
    expect(sql).toContain('WHERE NOT EXISTS');
    expect(sql).toContain('existing.forecast_id = $1::uuid');
    expect(sql).toContain('existing.event_id = $2');
  });
});
