// Soccer-branch tests for trapDetection. NBA path is covered by the
// existing trapDetection.test.js; we only need to verify the soccer
// signals fire on the right conditions and that the sport dispatch
// keeps the two trap sets isolated.
//
// Soccer signals are synchronous and pure over `input.features` — no
// Redis or DB mocks required.

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

function soccerInput(features = {}, statType = 'goals') {
  return {
    sport: 'soccer',
    statType,
    features: { stat_type: statType, ...features },
    odds: { playerLine: 0.5, consensus: null },
  };
}

describe('trapDetection — soccer branch', () => {
  describe('getTrapScore dispatches on sport', () => {
    test('soccer input runs ONLY the soccer signals (no NBA signals fire)', async () => {
      const r = await trap.getTrapScore(soccerInput({ goals_per_90: 0.5 }));
      const names = Object.keys(r.signals);
      // The NBA names should be absent; the soccer names should be present.
      expect(names).not.toContain('reverse_line_movement');
      expect(names).not.toContain('historical_hit_rate_paradox');
      expect(names).toContain('xg_regression');
      expect(names).toContain('altitude_risk');
      expect(names).toContain('rotation_risk');
      expect(names).toContain('minute_discount');
      expect(names).toContain('referee_card_bias');
      expect(names).toContain('strong_defense');
    });

    test('nba input still runs the NBA signal set unchanged', async () => {
      const r = await trap.getTrapScore({
        sport: 'nba',
        gameId: 'gX', playerName: 'A', statType: 'points',
        odds: { playerLine: 25.5 },
      });
      const names = Object.keys(r.signals);
      expect(names).toContain('reverse_line_movement');
      expect(names).toContain('historical_hit_rate_paradox');
      expect(names).not.toContain('xg_regression');
    });
  });

  describe('signalXgRegression', () => {
    test('fires when xg_delta > 0.3', () => {
      const result = trap.__internals.signalXgRegression(
        soccerInput({ xg_delta: 0.6 })
      );
      expect(result.active).toBe(true);
      expect(result.score).toBeGreaterThan(0);
      expect(result.explanation).toMatch(/above expected goals/);
    });
    test('does NOT fire when xg_delta is near zero', () => {
      const result = trap.__internals.signalXgRegression(
        soccerInput({ xg_delta: 0.05 })
      );
      expect(result.active).toBe(true);
      expect(result.score).toBe(0);
    });
    test('inactive when xg_delta is null', () => {
      const result = trap.__internals.signalXgRegression(soccerInput({ xg_delta: null }));
      expect(result.active).toBe(false);
    });
  });

  describe('signalAltitudeRisk', () => {
    test('fires for non-host-continent team at high altitude', () => {
      const r = trap.__internals.signalAltitudeRisk(
        soccerInput({ altitude_impact: 'high', home_continent: false, venue_altitude_ft: 7349 })
      );
      expect(r.active).toBe(true);
      expect(r.score).toBeGreaterThan(0);
      expect(r.explanation).toMatch(/altitude/);
    });
    test('host-continent team gets a pass (acclimated)', () => {
      const r = trap.__internals.signalAltitudeRisk(
        soccerInput({ altitude_impact: 'high', home_continent: true })
      );
      expect(r.active).toBe(false);
    });
    test('moderate or no altitude → inactive', () => {
      expect(trap.__internals.signalAltitudeRisk(
        soccerInput({ altitude_impact: 'moderate', home_continent: false })
      ).active).toBe(false);
      expect(trap.__internals.signalAltitudeRisk(
        soccerInput({ altitude_impact: 'none' })
      ).active).toBe(false);
    });
  });

  describe('signalRotationRisk', () => {
    test('fires for low start_rate + short rest', () => {
      const r = trap.__internals.signalRotationRisk(
        soccerInput({ start_rate: 0.5, rest_days: 1 })
      );
      expect(r.active).toBe(true);
      expect(r.score).toBeGreaterThan(0);
    });
    test('does NOT fire when start_rate is high', () => {
      const r = trap.__internals.signalRotationRisk(
        soccerInput({ start_rate: 0.95, rest_days: 1 })
      );
      expect(r.active).toBe(true);
      expect(r.score).toBe(0);
    });
    test('does NOT fire when rest_days is sufficient', () => {
      const r = trap.__internals.signalRotationRisk(
        soccerInput({ start_rate: 0.5, rest_days: 5 })
      );
      expect(r.active).toBe(true);
      expect(r.score).toBe(0);
    });
    test('inactive when fields missing', () => {
      const r = trap.__internals.signalRotationRisk(soccerInput({}));
      expect(r.active).toBe(false);
    });
  });

  describe('signalMinuteDiscount', () => {
    test('fires when minutes_per_game < 70', () => {
      const r = trap.__internals.signalMinuteDiscount(soccerInput({ minutes_per_game: 55 }));
      expect(r.active).toBe(true);
      expect(r.score).toBeGreaterThan(0);
    });
    test('does NOT fire when minutes_per_game >= 70', () => {
      const r = trap.__internals.signalMinuteDiscount(soccerInput({ minutes_per_game: 88 }));
      expect(r.active).toBe(true);
      expect(r.score).toBe(0);
    });
  });

  describe('signalRefereeCardBias (POSITIVE)', () => {
    test('marks positive signal for card-heavy ref on a CARDS prop', () => {
      const r = trap.__internals.signalRefereeCardBias(
        soccerInput({ referee_cards_per_game: 6.2, referee_name: 'Anthony Taylor' }, 'cards')
      );
      expect(r.positive).toBe(true);
      expect(r.active).toBe(false); // explicit: positive signals do NOT count active
      expect(r.score).toBe(0);
      expect(r.explanation).toMatch(/favorable for card over/);
    });

    test('does NOT mark positive for a non-cards stat type', () => {
      const r = trap.__internals.signalRefereeCardBias(
        soccerInput({ referee_cards_per_game: 6.2 }, 'goals')
      );
      expect(r.positive).not.toBe(true);
      expect(r.active).toBe(false);
    });

    test('composite EXCLUDES positive signals even when many fire', async () => {
      // Drop one trap + one positive — composite should reflect ONLY the trap.
      const r = await trap.getTrapScore(soccerInput(
        {
          xg_delta: 0.6, // trap fires
          referee_cards_per_game: 6.5, // positive fires
        },
        'cards', // makes the positive applicable
      ));
      expect(r.active_count).toBe(1); // only the xG regression counts
      expect(r.composite).toBeGreaterThan(0);
    });
  });

  describe('signalStrongDefense', () => {
    test('fires for top-5 defense on goals over', () => {
      const r = trap.__internals.signalStrongDefense(
        soccerInput({ opp_defensive_rank: 3 }, 'goals')
      );
      expect(r.active).toBe(true);
      expect(r.score).toBeGreaterThan(0);
    });
    test('fires for top-5 defense on shots_on_target', () => {
      const r = trap.__internals.signalStrongDefense(
        soccerInput({ opp_defensive_rank: 5 }, 'shots_on_target')
      );
      expect(r.active).toBe(true);
    });
    test('inactive for non-scoring stat types', () => {
      const r = trap.__internals.signalStrongDefense(
        soccerInput({ opp_defensive_rank: 3 }, 'cards')
      );
      expect(r.active).toBe(false);
    });
    test('does NOT fire when defense is mid-table', () => {
      const r = trap.__internals.signalStrongDefense(
        soccerInput({ opp_defensive_rank: 18 }, 'goals')
      );
      expect(r.active).toBe(true);
      expect(r.score).toBe(0);
    });
  });

  describe('composite scoring', () => {
    test('multiple soccer traps → composite >= 0.5 → avoid', async () => {
      const r = await trap.getTrapScore(soccerInput(
        {
          xg_delta: 0.5,
          altitude_impact: 'high',
          home_continent: false,
          start_rate: 0.5,
          rest_days: 1,
          opp_defensive_rank: 3,
        },
        'goals',
      ));
      expect(r.composite).toBeGreaterThanOrEqual(0.5);
      expect(r.recommendation).toBe('avoid');
    });

    test('no soccer traps → composite 0 → proceed', async () => {
      const r = await trap.getTrapScore(soccerInput({
        xg_delta: 0.05,
        altitude_impact: 'none',
        start_rate: 0.95,
        rest_days: 5,
        minutes_per_game: 85,
        opp_defensive_rank: 20,
      }, 'goals'));
      expect(r.composite).toBe(0);
      expect(r.recommendation).toBe('proceed');
    });
  });
});
