// Mock the two services trap detection talks to externally.

const mockLM = { reverse: null, juice: null, lm: null };
jest.mock('../../src/services/intelligence/lineMovement', () => ({
  reverseLineMovement: async () => mockLM.reverse,
  juiceDegradation: async () => mockLM.juice,
  getLineMovement: async () => mockLM.lm,
}));

const mockResolutions = { current: [] };
jest.mock('../../src/utils/supabase', () => ({
  getSupabaseServiceClient: () => ({
    from() {
      const proxy = {
        select() { return proxy; },
        eq() { return proxy; },
        then(resolve) { return resolve({ data: mockResolutions.current, error: null }); },
      };
      return proxy;
    },
  }),
}));

const trap = require('../../src/services/intelligence/trapDetection');

beforeEach(() => {
  mockLM.reverse = null;
  mockLM.juice = null;
  mockLM.lm = null;
  mockResolutions.current = [];
});

describe('trap signals (individual)', () => {
  test('reverse_line_movement: inactive without snapshots', async () => {
    mockLM.reverse = null;
    const r = await trap.__internals.signalReverseLineMovement({ gameId: 'g', playerName: 'P', statType: 'points' });
    expect(r.active).toBe(false);
  });

  test('reverse_line_movement: active and scoring when RLM detected', async () => {
    mockLM.reverse = { isReverse: true, score: 0.7, publicSide: 'over', lineDirection: 'under', movement: -1 };
    const r = await trap.__internals.signalReverseLineMovement({ gameId: 'g', playerName: 'P', statType: 'points' });
    expect(r.active).toBe(true);
    expect(r.score).toBe(0.7);
  });

  test('new_context_trap: scales with flag count', () => {
    const noFlags = trap.__internals.signalNewContextTrap({ gameContext: {} });
    expect(noFlags.active).toBe(false);
    const oneFlag = trap.__internals.signalNewContextTrap({ gameContext: { game_in_series: 1 } });
    expect(oneFlag.score).toBeCloseTo(0.25, 5);
    const allFlags = trap.__internals.signalNewContextTrap({
      gameContext: { game_in_series: 1, first_playoff_game: true, new_opponent_in_series: true, new_venue: true },
    });
    expect(allFlags.score).toBe(1);
  });

  test('recency_inflation: scores when L5 hotter than L20', () => {
    const r = trap.__internals.signalRecencyInflation({ features: { l5_avg: 30, l20_avg: 22 } });
    expect(r.active).toBe(true);
    expect(r.score).toBeCloseTo((30 - 22) / 22, 5);
  });

  test('recency_inflation: inactive without L5/L20', () => {
    const r = trap.__internals.signalRecencyInflation({ features: { l5_avg: 25 } });
    expect(r.active).toBe(false);
  });

  test('juice_degradation: passes through lineMovement signal', async () => {
    mockLM.juice = { applicable: true, score: 0.4, worstSide: 'over' };
    const r = await trap.__internals.signalJuiceDegradation({ gameId: 'g', playerName: 'P', statType: 'points' });
    expect(r.score).toBe(0.4);
  });

  test('teammate_return_trap: scales with returning usage', () => {
    const r = trap.__internals.signalTeammateReturnTrap({ gameContext: { returning_teammate_usage_rate: 0.32 } });
    expect(r.active).toBe(true);
    expect(r.score).toBeCloseTo(0.16, 5);
  });

  test('line_consensus_divergence: scores from |line - median| / stddev', () => {
    const r = trap.__internals.signalLineConsensusDivergence({
      odds: { playerLine: 26.5, consensus: { median: 24.5, stddev: 1.0 } },
    });
    expect(r.active).toBe(true);
    expect(r.score).toBe(1.0); // |26.5-24.5|/1 = 2.0 capped to 1.0
  });

  test('historical_hit_rate_paradox: inactive with thin history', async () => {
    mockResolutions.current = [{ result: 'hit', direction: 'over', line: 25.5 }];
    const r = await trap.__internals.signalHistoricalHitRateParadox({
      playerName: 'P', statType: 'points', sport: 'nba', gameId: 'g',
    });
    expect(r.active).toBe(false);
  });

  test('historical_hit_rate_paradox: active when line moves AGAINST a high hit-rate', async () => {
    mockResolutions.current = Array.from({ length: 25 }, (_, i) => ({
      result: i < 18 ? 'hit' : 'miss',
      direction: 'over',
      line: 25.5,
    }));
    mockLM.lm = { movement: -1.0 }; // moved DOWN while player usually OVER
    const r = await trap.__internals.signalHistoricalHitRateParadox({
      playerName: 'P', statType: 'points', sport: 'nba', gameId: 'g',
    });
    expect(r.active).toBe(true);
    expect(r.score).toBeGreaterThan(0);
  });
});

describe('getTrapScore (composite)', () => {
  test('only ACTIVE signals average — null signals do not dilute', async () => {
    // Two active signals: new_context_trap (0.25 = 1 flag) + recency_inflation
    // (~0.36). The other five inactive.
    const result = await trap.getTrapScore({
      features: { l5_avg: 30, l20_avg: 22 },
      gameContext: { game_in_series: 1 },
    });
    expect(result.active_count).toBe(2);
    expect(result.composite).toBeCloseTo((0.25 + (30 - 22) / 22) / 2, 5);
    expect(result.recommendation).toBe('caution');
  });

  test('recommendation thresholds', () => {
    expect(trap.__internals.recommend(0.1)).toBe('proceed');
    expect(trap.__internals.recommend(0.3)).toBe('caution');
    expect(trap.__internals.recommend(0.6)).toBe('avoid');
  });

  test('KAT-scenario: G1 of Finals + recency inflation → avoid/caution', async () => {
    // High recency (L5 32 vs L20 22 → 0.45) + 2 context flags (0.5).
    // Composite = (0.45 + 0.5) / 2 = 0.475 → caution.
    const result = await trap.getTrapScore({
      features: { l5_avg: 32, l20_avg: 22 },
      gameContext: { game_in_series: 1, first_playoff_game: true },
    });
    expect(result.active_count).toBe(2);
    expect(['caution', 'avoid']).toContain(result.recommendation);
  });
});
