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

const mocked = vi.hoisted(() => ({
  authMiddleware: vi.fn((req: any, _res: any, next: any) => { req.user = { userId: 'user-1' }; next(); }),
  findUserById: vi.fn(),
  poolQuery: vi.fn(),
  fetchOperationalCronSummaries: vi.fn(),
  fetchOperationalCronSummary: vi.fn(),
  fetchOperationalCronRuns: vi.fn(),
  fetchStoredMlbMarketRows: vi.fn(),
  fetchMlbMarketCompletionInsights: vi.fn(),
  summarizeStoredMlbMarketRows: vi.fn(),
  getMlbAlertAssets: vi.fn(async () => []),
  getMlbIssueHistory: vi.fn(async () => ({ rows: [] })),
  getStoredMlbZeroCandidateIssues: vi.fn(async () => []),
  getMlbZeroCandidateIssues: vi.fn(async () => []),
  summarizeMlbAlertAssets: vi.fn(() => ({ total: 0 })),
  summarizeMlbZeroCandidateIssues: vi.fn(() => ({ total: 0 })),
  describeForecastAsset: vi.fn(() => ({
    assetLabel: 'Asset',
    marketFamily: 'family',
    marketOrigin: 'origin',
    sourceBacked: true,
    legacyBundle: false,
    playerRole: null,
  })),
  summarizeForecastCountRows: vi.fn(() => ({ total: 0, byType: {}, byFamily: {} })),
}));

vi.mock('../../middleware/auth', () => ({ authMiddleware: mocked.authMiddleware }));
vi.mock('../../models/user', () => ({ findUserById: mocked.findUserById }));
vi.mock('../../db', () => ({ default: { query: mocked.poolQuery } }));
vi.mock('../../services/mlb-market-reporter', () => ({
  fetchOperationalCronSummaries: mocked.fetchOperationalCronSummaries,
  fetchOperationalCronSummary: mocked.fetchOperationalCronSummary,
  fetchOperationalCronRuns: mocked.fetchOperationalCronRuns,
  fetchStoredMlbMarketRows: mocked.fetchStoredMlbMarketRows,
  fetchMlbMarketCompletionInsights: mocked.fetchMlbMarketCompletionInsights,
  summarizeStoredMlbMarketRows: mocked.summarizeStoredMlbMarketRows,
}));
vi.mock('../../services/mlb-alerts', () => ({
  getMlbAlertAssets: mocked.getMlbAlertAssets,
  getMlbIssueHistory: mocked.getMlbIssueHistory,
  getStoredMlbZeroCandidateIssues: mocked.getStoredMlbZeroCandidateIssues,
  getMlbZeroCandidateIssues: mocked.getMlbZeroCandidateIssues,
  summarizeMlbAlertAssets: mocked.summarizeMlbAlertAssets,
  summarizeMlbZeroCandidateIssues: mocked.summarizeMlbZeroCandidateIssues,
}));
vi.mock('../../services/forecast-asset-taxonomy', () => ({
  describeForecastAsset: mocked.describeForecastAsset,
  summarizeForecastCountRows: mocked.summarizeForecastCountRows,
}));

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

describe('/weather-report MLB market completion routes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mocked.findUserById.mockResolvedValue({
      id: 'user-1',
      email: 'admin@rainmakersports.app',
      is_weatherman: true,
      email_verified: true,
    });
  });

  it('returns the operational cron registry for admins', async () => {
    mocked.fetchOperationalCronSummaries.mockResolvedValue([
      {
        slug: 'mlb_market_completion',
        name: 'MLB Market Completion Engine',
        schedule: '* * * * *',
        description: 'Detects incomplete markets',
        category: 'Market Integrity',
        priority: 'HIGH',
        status: 'ACTIVE',
        safe_mode: true,
        non_blocking: true,
        last_run: '2026-03-28T15:00:00.000Z',
        avg_runtime_ms: 12000,
        error_rate: 0,
        notes: ['Supports multi-source completion'],
        metrics: { total_markets: 4054, multi_source_complete: 153 },
      },
    ]);
    const router = await loadRouter();
    const app = express();
    app.use('/', router);

    const res = await request(app).get('/crons');

    expect(res.status).toBe(200);
    expect(res.body).toEqual({
      crons: [{
        slug: 'mlb_market_completion',
        name: 'MLB Market Completion Engine',
        schedule: '* * * * *',
        description: 'Detects incomplete markets',
        category: 'Market Integrity',
        priority: 'HIGH',
        status: 'ACTIVE',
        safeMode: true,
        nonBlocking: true,
        lastRun: '2026-03-28T15:00:00.000Z',
        avgRuntimeMs: 12000,
        errorRate: 0,
        notes: ['Supports multi-source completion'],
        metrics: { totalMarkets: 4054, multiSourceComplete: 153 },
      }],
    });
  });

  it('returns cron run history for a known slug and 404 for an unknown slug', async () => {
    mocked.fetchOperationalCronSummary
      .mockResolvedValueOnce({
        slug: 'mlb_market_completion',
        name: 'MLB Market Completion Engine',
        schedule: '* * * * *',
        description: 'Detects incomplete markets',
        category: 'Market Integrity',
        priority: 'HIGH',
        status: 'ACTIVE',
        safe_mode: true,
        non_blocking: true,
        last_run: '2026-03-28T15:00:00.000Z',
        avg_runtime_ms: 12000,
        error_rate: 0,
        notes: [],
        metrics: { total_markets: 4054 },
      })
      .mockResolvedValueOnce(null);
    mocked.fetchOperationalCronRuns.mockResolvedValue([
      { id: 'run-1', cron_slug: 'mlb_market_completion', status: 'SUCCESS', started_at: '2026-03-28T15:00:00.000Z', completed_at: '2026-03-28T15:00:01.000Z', duration_ms: 1000, metrics: { total_markets: 4054 }, error_message: null },
    ]);
    const router = await loadRouter();
    const app = express();
    app.use('/', router);

    const ok = await request(app).get('/crons/mlb_market_completion/runs?limit=5');
    expect(ok.status).toBe(200);
    expect(ok.body).toEqual({
      slug: 'mlb_market_completion',
      cron: {
        slug: 'mlb_market_completion',
        name: 'MLB Market Completion Engine',
        schedule: '* * * * *',
        description: 'Detects incomplete markets',
        category: 'Market Integrity',
        priority: 'HIGH',
        status: 'ACTIVE',
        safeMode: true,
        nonBlocking: true,
        lastRun: '2026-03-28T15:00:00.000Z',
        avgRuntimeMs: 12000,
        errorRate: 0,
        notes: [],
        metrics: { totalMarkets: 4054 },
      },
      runs: [{
        id: 'run-1',
        cronSlug: 'mlb_market_completion',
        startedAt: '2026-03-28T15:00:00.000Z',
        completedAt: '2026-03-28T15:00:01.000Z',
        status: 'SUCCESS',
        durationMs: 1000,
        metrics: { totalMarkets: 4054 },
        errorMessage: null,
      }],
    });

    const missing = await request(app).get('/crons/missing/runs');
    expect(missing.status).toBe(404);
    expect(missing.body).toEqual({ error: 'Cron not found' });
  });

  it('returns normalized market rows with the expected completion fields and handles empty state', async () => {
    mocked.fetchStoredMlbMarketRows
      .mockResolvedValueOnce([
        {
          event_id: 'evt-1',
          starts_at: '2026-03-28T19:00:00.000Z',
          home_team: 'LAD',
          away_team: 'SEA',
          player_id: 'SHOHEI_OHTANI_1_MLB',
          player_name: 'Shohei Ohtani',
          team_short: 'LAD',
          stat_type: 'batting_hits',
          normalized_stat_type: 'batting_hits',
          line: 1.5,
          market_name: 'Shohei Ohtani Hits Over/Under',
          primary_source: 'fanduel',
          completion_method: 'multi_source',
          completeness_status: 'multi_source_complete',
          is_gap_filled: true,
          available_sides: ['over', 'under'],
          over_payload: { odds: -110, source: 'fanduel' },
          under_payload: { odds: -105, source: 'draftkings' },
          source_map: [{ side: 'over', source: 'fanduel', odds: -110 }],
          raw_market_count: 2,
          updated_at: '2026-03-28T18:58:00.000Z',
        },
      ])
      .mockResolvedValueOnce([
        {
          event_id: 'evt-1',
          starts_at: '2026-03-28T19:00:00.000Z',
          home_team: 'LAD',
          away_team: 'SEA',
          player_id: 'SHOHEI_OHTANI_1_MLB',
          player_name: 'Shohei Ohtani',
          team_short: 'LAD',
          stat_type: 'batting_hits',
          normalized_stat_type: 'batting_hits',
          line: 1.5,
          market_name: 'Shohei Ohtani Hits Over/Under',
          primary_source: 'fanduel',
          completion_method: 'multi_source',
          completeness_status: 'multi_source_complete',
          is_gap_filled: true,
          available_sides: ['over', 'under'],
          over_payload: { odds: -110, source: 'fanduel' },
          under_payload: { odds: -105, source: 'draftkings' },
          source_map: [{ side: 'over', source: 'fanduel', odds: -110 }],
          raw_market_count: 2,
          updated_at: '2026-03-28T18:58:00.000Z',
        },
      ])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([]);
    mocked.fetchMlbMarketCompletionInsights
      .mockResolvedValueOnce({
        incompleteWithAlternateLines: 12,
        incompleteWithoutAlternateLines: 88,
        exactSideBreakdown: {
          underOnly: 95,
          overOnly: 1,
          bothExactSidesPresent: 0,
        },
      })
      .mockResolvedValueOnce({
        incompleteWithAlternateLines: 0,
        incompleteWithoutAlternateLines: 0,
        exactSideBreakdown: {
          underOnly: 0,
          overOnly: 0,
          bothExactSidesPresent: 0,
        },
      });
    mocked.summarizeStoredMlbMarketRows
      .mockReturnValueOnce({
        totalMarkets: 1,
        sourceComplete: 0,
        multiSourceComplete: 1,
        incomplete: 0,
        gapFilled: 1,
        underOnlyMarkets: 0,
        overOnlyMarkets: 0,
        twoWayMarkets: 1,
        zeroGapFillEvents: 0,
        byStatType: { batting_hits: 1 },
        byAvailableSides: { 'over|under': 1 },
        bySource: { fanduel: 1, draftkings: 1 },
        bySourceCount: { '1': 1 },
        eventBreakdown: [{
          eventId: 'evt-1',
          startsAt: '2026-03-28T19:00:00.000Z',
          homeTeam: 'LAD',
          awayTeam: 'SEA',
          totalMarkets: 1,
          sourceComplete: 0,
          multiSourceComplete: 1,
          incomplete: 0,
          gapFilledMarkets: 1,
          underOnlyMarkets: 0,
          overOnlyMarkets: 0,
          twoWayMarkets: 1,
          zeroGapFillEvent: false,
          completionRate: 1,
        }],
      })
      .mockReturnValueOnce({
        totalMarkets: 0,
        sourceComplete: 0,
        multiSourceComplete: 0,
        incomplete: 0,
        gapFilled: 0,
        underOnlyMarkets: 0,
        overOnlyMarkets: 0,
        twoWayMarkets: 0,
        zeroGapFillEvents: 0,
        byStatType: {},
        byAvailableSides: {},
        bySource: {},
        bySourceCount: {},
        eventBreakdown: [],
      });
    const router = await loadRouter();
    const app = express();
    app.use('/', router);

    const res = await request(app).get('/mlb-market-completion?eventId=evt-1&completenessStatus=multi_source_complete');
    expect(res.status).toBe(200);
    expect(res.body).toMatchObject({
      totalCount: 1,
      count: 1,
      appliedFilters: {
        eventId: 'evt-1',
        completenessStatus: 'multi_source_complete',
      },
      summary: {
        totalMarkets: 1,
        multiSourceComplete: 1,
        completed: 1,
        completionRate: 1,
        completionRatePct: 100,
        gapFillAttempts: 1,
        gapFillSuccesses: 1,
        gapFillSuccessRate: 1,
        gapFillSuccessRatePct: 100,
        underOnlyMarkets: 0,
        overOnlyMarkets: 0,
        twoWayMarkets: 1,
        zeroGapFillEvents: 0,
        sourceDistribution: { fanduel: 1, draftkings: 1 },
        sourceBuckets: { fanduel: 1, draftkings: 1, other: 0 },
        eventBreakdown: [{
          eventId: 'evt-1',
          startsAt: '2026-03-28T19:00:00.000Z',
          homeTeam: 'LAD',
          awayTeam: 'SEA',
          totalMarkets: 1,
          sourceComplete: 0,
          multiSourceComplete: 1,
          incomplete: 0,
          gapFilledMarkets: 1,
          underOnlyMarkets: 0,
          overOnlyMarkets: 0,
          twoWayMarkets: 1,
          zeroGapFillEvent: false,
          completionRate: 1,
        }],
        incompleteWithAlternateLines: 12,
        incompleteWithoutAlternateLines: 88,
        exactSideBreakdown: {
          underOnly: 95,
          overOnly: 1,
          bothExactSidesPresent: 0,
        },
      },
      markets: [
        {
          eventId: 'evt-1',
          playerId: 'SHOHEI_OHTANI_1_MLB',
          completionMethod: 'multi_source',
          completenessStatus: 'multi_source_complete',
          isGapFilled: true,
          over: { odds: -110, source: 'fanduel' },
          under: { odds: -105, source: 'draftkings' },
          sourceMap: [{ side: 'over', source: 'fanduel', odds: -110 }],
        },
      ],
    });

    const empty = await request(app).get('/mlb-market-completion');
    expect(empty.status).toBe(200);
    expect(empty.body).toEqual({
      summary: {
        totalMarkets: 0,
        sourceComplete: 0,
        multiSourceComplete: 0,
        incomplete: 0,
        gapFilled: 0,
        underOnlyMarkets: 0,
        overOnlyMarkets: 0,
        twoWayMarkets: 0,
        zeroGapFillEvents: 0,
        byStatType: {},
        byAvailableSides: {},
        bySource: {},
        bySourceCount: {},
        eventBreakdown: [],
        completed: 0,
        completionRate: 0,
        completionRatePct: 0,
        gapFillAttempts: 0,
        gapFillSuccesses: 0,
        gapFillSuccessRate: 0,
        gapFillSuccessRatePct: 0,
        sourceDistribution: {},
        sourceBuckets: { fanduel: 0, draftkings: 0, other: 0 },
        incompleteWithAlternateLines: 0,
        incompleteWithAlternateLinesPct: 0,
        incompleteWithoutAlternateLines: 0,
        incompleteWithoutAlternateLinesPct: 0,
        exactSideBreakdown: {
          underOnly: 0,
          overOnly: 0,
          bothExactSidesPresent: 0,
        },
      },
      totalCount: 0,
      count: 0,
      appliedFilters: {
        eventId: null,
        completenessStatus: null,
      },
      markets: [],
    });
  });

  it('rejects non-admin access', async () => {
    mocked.findUserById.mockResolvedValueOnce({ id: 'user-1', is_weatherman: false });
    const router = await loadRouter();
    const app = express();
    app.use('/', router);

    const res = await request(app).get('/crons');

    expect(res.status).toBe(403);
    expect(res.body).toEqual({ error: 'Admin access required' });
  });
});
