/**
 * Grading pipeline end-to-end integration.
 *
 *   1. Mock SharpAPI returning 3 player props (one A-tier shaped, one
 *      neutral, one trap-heavy that should downgrade).
 *   2. Mock feature cache returning features.
 *   3. Mock trap detection — return high composite for the trap prop.
 *   4. Run the orchestrator.
 *   5. Assert: all 3 props get a grade_history insert.
 *   6. Assert: A-tier prop queued for Engine 2, D/C-tier not queued.
 *   7. Assert: trap composite > 0.5 downgrades the trap prop's grade.
 */

process.env.ENGINE2_ENABLED = 'true';

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),
}));

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

// Feature vectors per player — vary to drive different grades.
const mockFeatures = { current: new Map() };
jest.mock('../../src/services/intelligence/featureCache', () => ({
  getFeatures: async ({ playerName }) => {
    const f = mockFeatures.current.get(playerName) || {};
    return { features: f, meta: { features_available: Object.keys(f), features_missing: [] } };
  },
}));

const mockTrapScores = { current: new Map() };
jest.mock('../../src/services/intelligence/trapDetection', () => ({
  getTrapScore: async ({ playerName }) => mockTrapScores.current.get(playerName) || {
    composite: 0, signals: {}, active_count: 0, recommendation: 'proceed',
  },
  normalizeName: (n) => n,
  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 }, { points: 32 }, { points: 27 }, { points: 31 },
  ],
  getCareerPlayoffGames: async () => 50,
  getWithWithoutStats: async () => null,
}));

const mockInserts = [];
const mockUpdates = [];
jest.mock('../../src/utils/supabase', () => ({
  getSupabaseServiceClient: () => ({
    from(table) {
      const proxy = {
        insert(row) {
          mockInserts.push({ table, row });
          return {
            select() {
              return { single: () => Promise.resolve({ data: { id: `gid-${mockInserts.length}` }, error: null }) };
            },
          };
        },
        update(patch) {
          mockUpdates.push({ table, patch });
          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;
  mockUpdates.length = 0;
  mockFeatures.current.clear();
  mockTrapScores.current.clear();
  engine2.clearQueue();
});

describe('grading pipeline — end-to-end through orchestrator', () => {
  test('three props grade through, A-tier queued, trap-heavy downgraded', async () => {
    // ESPN scoreboard with one game.
    mockAxiosGet.mockResolvedValue({
      status: 200,
      data: {
        events: [{
          id: 'ev-mix',
          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' } },
            ],
          }],
        }],
      },
    });

    // Three props:
    //   A-tier: hot recent form, weak D, rested, home — should grade A or A-
    //   neutral: average everything — should land C-ish
    //   trap-heavy: hot recent form BUT trap composite 0.7 → downgrade
    mockGetPlayerProps.mockResolvedValue([
      { player: 'A-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
      { player: 'C-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
      { player: 'Trap-Player', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
    ]);

    mockFeatures.current.set('A-Player', {
      l5_avg: 32, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2,
    });
    mockFeatures.current.set('C-Player', { l5_avg: 25, l20_avg: 25.5 });
    mockFeatures.current.set('Trap-Player', {
      l5_avg: 32, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0, rest_days: 2,
    });

    // Only the trap-heavy prop has a high trap composite.
    mockTrapScores.current.set('Trap-Player', { composite: 0.7, signals: {}, active_count: 4, recommendation: 'avoid' });

    const summary = await orchestrator.runPipeline('nba', { skipEngine2: true });

    expect(summary.props_graded).toBe(3);
    expect(mockInserts.length).toBe(3);

    // All inserts went to grade_history.
    expect(mockInserts.every((i) => i.table === 'grade_history')).toBe(true);

    // factors column is stored as an array.
    for (const i of mockInserts) {
      expect(Array.isArray(i.row.factors) || i.row.factors === null).toBe(true);
    }

    // A-Player grade is in A range; Trap-Player is downgraded BELOW A.
    const aRow = mockInserts.find((i) => i.row.player_name === 'A-Player');
    const cRow = mockInserts.find((i) => i.row.player_name === 'C-Player');
    const trapRow = mockInserts.find((i) => i.row.player_name === 'Trap-Player');
    const GRADE_ORDER = ['F','D','C-','C','C+','B-','B','B+','A-','A','A+'];
    const idx = (g) => GRADE_ORDER.indexOf(g);

    expect(idx(aRow.row.grade)).toBeGreaterThan(idx('B+'));
    expect(idx(trapRow.row.grade)).toBeLessThan(idx(aRow.row.grade));
    expect(idx(cRow.row.grade)).toBeLessThan(idx(aRow.row.grade));

    // Engine 2 queue: A-Player qualifies (A range); Trap-Player downgraded
    // out of A/B range so should not be queued; C-Player C-tier excluded.
    const queued = engine2.getQueueSize();
    expect(queued).toBeGreaterThanOrEqual(1);
  });

  test('p_over and factors get persisted on the row', async () => {
    mockAxiosGet.mockResolvedValue({
      status: 200,
      data: {
        events: [{
          id: 'ev-store',
          competitions: [{ competitors: [
            { homeAway: 'home', id: '1', team: { abbreviation: 'NYK' } },
            { homeAway: 'away', id: '2', team: { abbreviation: 'BOS' } },
          ] }],
        }],
      },
    });
    mockGetPlayerProps.mockResolvedValue([
      { player: 'P', team: 'NYK', statType: 'points', line: 25.5, direction: 'over' },
    ]);
    mockFeatures.current.set('P', {
      l5_avg: 30, l20_avg: 28, opp_rank_stat: 0.85, home_away: 1.0,
    });
    await orchestrator.runPipeline('nba', { skipEngine2: true });
    const row = mockInserts[0].row;
    expect(row.factors).not.toBeNull();
    expect(Array.isArray(row.factors)).toBe(true);
    expect(row.factors.length).toBeGreaterThan(0);
    expect(Number.isFinite(row.p_over)).toBe(true);
  });
});
