process.env.ENGINE2_ENABLED = 'false';  // queue but don't drain in unit tests

const mockAxiosGet = jest.fn();
jest.mock('axios', () => ({ get: (...args) => mockAxiosGet(...args), post: jest.fn() }));

const mockGetPlayerProps = jest.fn();
jest.mock('../../src/services/adapters/sharpApiAdapter', () => ({
  configured: () => true,
  getPlayerProps: (...args) => mockGetPlayerProps(...args),
  getGameOdds: async () => null,
  getConsensusLine: async () => null,
}));

jest.mock('../../src/services/adapters/openRouterAdapter', () => ({
  configured: () => false,
  analyze: async () => null,
  getUsage: () => ({ requestsToday: 0, requestsRemaining: 1000 }),
}));

// Feature/trap/consistency stubs — orchestrator owns the wiring; we
// verify that wiring with deterministic returns rather than re-testing
// every sub-service.
jest.mock('../../src/services/intelligence/featureCache', () => ({
  getFeatures: async () => ({
    features: { l5_avg: 30, l20_avg: 26, home_away: 1.0, opp_rank_stat: 0.85, rest_days: 2 },
    meta: { features_available: [], features_missing: [] },
  }),
}));

jest.mock('../../src/services/intelligence/trapDetection', () => ({
  getTrapScore: async () => ({
    composite: 0.1,
    signals: {},
    active_count: 0,
    recommendation: 'proceed',
  }),
  normalizeName: (n) => String(n || '').toLowerCase(),
  detectReturningTeammate: () => null,
}));

jest.mock('../../src/services/intelligence/consistencyScore', () => ({
  getConsistency: async () => ({ consistency: 'reliable', cv: 0.18, score: 0.7, games: 20 }),
}));

jest.mock('../../src/services/intelligence/gameLogService', () => ({
  getGameLogs: async () => [{ points: 30 }, { points: 28 }],
  getCareerPlayoffGames: async () => null,
  getWithWithoutStats: async () => null,
}));

const mockInserts = [];
jest.mock('../../src/utils/supabase', () => ({
  getSupabaseServiceClient: () => ({
    from() {
      const proxy = {
        insert(row) {
          mockInserts.push(row);
          const inserted = { id: `gid-${mockInserts.length}` };
          return {
            select() { return { single: () => Promise.resolve({ data: inserted, error: null }) }; },
          };
        },
        update() {
          return { eq() { return Promise.resolve({ error: null }); } };
        },
      };
      return proxy;
    },
  }),
}));

const orchestrator = require('../../src/services/intelligence/gradingOrchestrator');
const engine2 = require('../../src/services/intelligence/engine2');

beforeEach(() => {
  mockAxiosGet.mockReset();
  mockGetPlayerProps.mockReset();
  mockInserts.length = 0;
  engine2.clearQueue();
});

describe('runPipeline', () => {
  test('empty slate → summary with zeros', async () => {
    mockAxiosGet.mockResolvedValue({ status: 200, data: { events: [] } });
    const summary = await orchestrator.runPipeline('nba');
    expect(summary).toMatchObject({
      sport: 'nba',
      games_processed: 0,
      props_graded: 0,
    });
  });

  test('one game with three props → 3 grades inserted', async () => {
    mockAxiosGet.mockResolvedValue({
      status: 200,
      data: {
        events: [{
          id: 'ev-1',
          date: '2026-06-08T23:30Z',
          status: { type: { state: 'pre' } },
          competitions: [{
            competitors: [
              { homeAway: 'home', id: '1', team: { abbreviation: 'NYK', displayName: 'Knicks' } },
              { homeAway: 'away', id: '2', team: { abbreviation: 'BOS', displayName: 'Celtics' } },
            ],
          }],
        }],
      },
    });
    mockGetPlayerProps.mockResolvedValue([
      { player: 'Jalen Brunson', team: 'NYK', statType: 'points', line: 26.5, direction: 'over' },
      { player: 'Jayson Tatum', team: 'BOS', statType: 'points', line: 28.5, direction: 'over' },
      { player: 'OG Anunoby', team: 'NYK', statType: 'rebounds', line: 6.5, direction: 'under' },
    ]);

    const summary = await orchestrator.runPipeline('nba', { skipEngine2: true });
    expect(summary.games_processed).toBe(1);
    expect(summary.props_graded).toBe(3);
    expect(mockInserts).toHaveLength(3);
  });

  test('SharpAPI failure on one game increments errors, other games continue', async () => {
    mockAxiosGet.mockResolvedValue({
      status: 200,
      data: {
        events: [
          { id: 'ev-a', competitions: [{ competitors: [{ homeAway: 'home', team: { abbreviation: 'A' } }, { homeAway: 'away', team: { abbreviation: 'B' } }] }] },
          { id: 'ev-b', competitions: [{ competitors: [{ homeAway: 'home', team: { abbreviation: 'C' } }, { homeAway: 'away', team: { abbreviation: 'D' } }] }] },
        ],
      },
    });
    mockGetPlayerProps
      .mockRejectedValueOnce(new Error('upstream down'))
      .mockResolvedValueOnce([{ player: 'X', team: 'C', statType: 'points', line: 10, direction: 'over' }]);
    const summary = await orchestrator.runPipeline('nba', { skipEngine2: true });
    expect(summary.errors).toBe(1);
    expect(summary.props_graded).toBe(1);
  });

  test('A-tier grades queued for engine2; C-tier not queued', async () => {
    process.env.ENGINE2_ENABLED = 'true';
    jest.resetModules();
    // Re-require with fresh ENGINE2_ENABLED.
    jest.doMock('axios', () => ({ get: (...args) => mockAxiosGet(...args), post: jest.fn() }));
    jest.doMock('../../src/services/adapters/sharpApiAdapter', () => ({
      configured: () => true, getPlayerProps: (...args) => mockGetPlayerProps(...args),
    }));
    jest.doMock('../../src/services/adapters/openRouterAdapter', () => ({
      configured: () => false, analyze: async () => null, getUsage: () => ({ requestsToday: 0, requestsRemaining: 1000 }),
    }));
    jest.doMock('../../src/services/intelligence/featureCache', () => ({
      getFeatures: async () => ({ features: { l5_avg: 32, l20_avg: 27, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2 }, meta: {} }),
    }));
    jest.doMock('../../src/services/intelligence/trapDetection', () => ({
      getTrapScore: async () => ({ composite: 0.05, signals: {} }),
      normalizeName: (n) => n,
    }));
    jest.doMock('../../src/services/intelligence/consistencyScore', () => ({
      getConsistency: async () => ({ consistency: 'elite', score: 1.0 }),
    }));
    jest.doMock('../../src/services/intelligence/gameLogService', () => ({
      getGameLogs: async () => null, getCareerPlayoffGames: async () => null,
    }));
    jest.doMock('../../src/utils/supabase', () => ({
      getSupabaseServiceClient: () => ({
        from() {
          const proxy = {
            insert(row) {
              mockInserts.push(row);
              return { select() { return { single: () => Promise.resolve({ data: { id: `gid-${mockInserts.length}` }, error: null }) }; } };
            },
            update() { return { eq() { return Promise.resolve({ error: null }); } }; },
          };
          return proxy;
        },
      }),
    }));

    const orch = require('../../src/services/intelligence/gradingOrchestrator');
    const e2 = require('../../src/services/intelligence/engine2');
    e2.clearQueue();
    mockInserts.length = 0;
    mockAxiosGet.mockResolvedValue({
      status: 200,
      data: {
        events: [{
          id: 'ev-q',
          competitions: [{ competitors: [
            { homeAway: 'home', team: { abbreviation: 'NYK' } },
            { homeAway: 'away', team: { abbreviation: 'BOS' } },
          ] }],
        }],
      },
    });
    mockGetPlayerProps.mockResolvedValue([
      { player: 'A-tier player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
    ]);

    const summary = await orch.runPipeline('nba', { skipEngine2: true });
    expect(summary.props_graded).toBe(1);
    // The grade should have been A/A-/A+ given those features, so engine2
    // queue should have grown.
    expect(summary.engine2_queued).toBe(1);
    process.env.ENGINE2_ENABLED = 'false';
  });
});

describe('getEngineStatus', () => {
  test('returns adapter configuration and queue size', () => {
    const status = orchestrator.getEngineStatus();
    expect(status.adapters_configured).toHaveProperty('sharp_api');
    expect(status.adapters_configured).toHaveProperty('open_router');
    expect(typeof status.engine2_queue_size).toBe('number');
  });
});
