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(),
  getPickBalance: vi.fn(),
  ensureDailyGrant: vi.fn(),
  isInGracePeriod: vi.fn(() => false),
  recordLedgerEntry: vi.fn(),
  deductPick: vi.fn(),
  poolQuery: vi.fn(),
}));

const MLB_ASSET_ID = '550e8400-e29b-41d4-a716-446655440001';
const MLB_ASSET_ID_DISABLED = '550e8400-e29b-41d4-a716-446655440002';
const NBA_ASSET_ID = '550e8400-e29b-41d4-a716-446655440003';
const RACE_ASSET_ID = '550e8400-e29b-41d4-a716-446655440004';

vi.mock('../../middleware/auth', () => ({ authMiddleware: mocked.authMiddleware }));
vi.mock('../../models/user', () => ({
  findUserById: mocked.findUserById,
  deductPick: mocked.deductPick,
  getPickBalance: mocked.getPickBalance,
  ensureDailyGrant: mocked.ensureDailyGrant,
  isInGracePeriod: mocked.isInGracePeriod,
}));
vi.mock('../../models/ledger', () => ({ recordLedgerEntry: mocked.recordLedgerEntry }));
vi.mock('../../db', () => ({ default: { query: mocked.poolQuery } }));

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

describe('/forecast-assets contract', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    process.env.FEATURE_PLAYER_PROPS_PER_PLAYER_UNLOCK = 'true';
    delete process.env.MLB_PROP_CONTEXT_V2;
    mocked.findUserById.mockResolvedValue({ id: 'user-1', is_weatherman: true, email_verified: true });
    mocked.getPickBalance.mockResolvedValue({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.deductPick.mockResolvedValue({ success: true, source: 'single_pick' });
  });

  it('returns additive mlbPropContext when unlocking an mlb player prop asset by default', async () => {
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [{
          id: MLB_ASSET_ID,
          event_id: 'evt-1',
          forecast_type: 'PLAYER_PROP',
          team_side: 'home',
          player_name: 'Shohei Ohtani',
          confidence_score: 0.77,
          league: 'mlb',
          forecast_payload: {
            recommendation: 'over',
            model_context: {
              k_rank: 4,
              park_factor: 108,
              weather_impact: 'positive',
              handedness_split: 'favorable',
              lineup_certainty: 'confirmed',
            },
          },
        }],
      })
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).post(`/${MLB_ASSET_ID}/unlock`);

    expect(res.status).toBe(200);
    expect(res.body.mlbPropContext).toEqual({
      kRank: 4,
      parkFactor: 108,
      weatherImpact: 'positive',
      handednessSplit: 'favorable',
      lineupCertainty: 'confirmed',
    });
  });

  it('omits mlbPropContext only when explicitly disabled', async () => {
    process.env.MLB_PROP_CONTEXT_V2 = 'false';
    const router = await loadRouter();

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [{
          id: MLB_ASSET_ID_DISABLED,
          event_id: 'evt-2',
          forecast_type: 'PLAYER_PROP',
          team_side: 'away',
          player_name: 'Elly De La Cruz',
          confidence_score: 0.71,
          league: 'mlb',
          forecast_payload: {
            recommendation: 'over',
            model_context: {
              k_rank: 3,
              park_factor: 102,
              weather_impact: 'neutral',
              handedness_split: 'switch vs RHP',
              lineup_certainty: 'confirmed',
            },
          },
        }],
      })
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).post(`/${MLB_ASSET_ID_DISABLED}/unlock`);

    expect(res.status).toBe(200);
    expect(res.body).not.toHaveProperty('mlbPropContext');
  });

  it('returns canonical and deprecated balance fields for player prop unlocks', async () => {
    const router = await loadRouter();

    mocked.getPickBalance
      .mockResolvedValueOnce({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 })
      .mockResolvedValueOnce({ single_picks: 0, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 })
      .mockResolvedValueOnce({ single_picks: 0, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 1 });
    mocked.findUserById.mockResolvedValue({ id: 'user-1', is_weatherman: false, email_verified: true });

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [{
          id: NBA_ASSET_ID,
          event_id: 'evt-3',
          forecast_type: 'PLAYER_PROP',
          team_side: 'home',
          player_name: 'Jayson Tatum',
          confidence_score: 0.79,
          league: 'nba',
          forecast_payload: {
            recommendation: 'over',
          },
        }],
      })
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).post(`/${NBA_ASSET_ID}/unlock`);

    expect(res.status).toBe(200);
    expect(res.body.forecastBalance).toBe(1);
    expect(res.body.forecast_balance).toBe(1);
  });

  it('does not double-charge when a concurrent unlock already claimed the asset row', async () => {
    const router = await loadRouter();

    mocked.findUserById.mockResolvedValue({ id: 'user-1', is_weatherman: false, email_verified: true });
    mocked.getPickBalance
      .mockResolvedValueOnce({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 })
      .mockResolvedValueOnce({ single_picks: 1, daily_pass_picks: 0, daily_pass_valid: false, daily_free_forecasts: 0 });

    mocked.poolQuery
      .mockResolvedValueOnce({
        rows: [{
          id: RACE_ASSET_ID,
          event_id: 'evt-race',
          forecast_type: 'PLAYER_PROP',
          team_side: 'away',
          player_name: 'Nikola Jokic',
          confidence_score: 0.81,
          league: 'nba',
          forecast_payload: { recommendation: 'over' },
        }],
      })
      .mockResolvedValueOnce({ rows: [] })
      .mockResolvedValueOnce({ rowCount: 0, rows: [] });

    const app = express();
    app.use('/', router);
    const res = await request(app).post(`/${RACE_ASSET_ID}/unlock`);

    expect(res.status).toBe(200);
    expect(mocked.deductPick).not.toHaveBeenCalled();
    expect(mocked.recordLedgerEntry).not.toHaveBeenCalled();
    expect(res.body.locked).toBe(false);
  });

  it('rejects invalid asset ids before hitting the database', async () => {
    const router = await loadRouter();

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

    const res = await request(app).post('/fake-id/unlock');

    expect(res.status).toBe(400);
    expect(res.body).toEqual({ error: 'Invalid player prop asset id' });
    expect(mocked.poolQuery).not.toHaveBeenCalled();
  });
});
