const { SIMILARITY_WEIGHTS, calculateSimilarityScore, findSimilarGames, getPosteriorDistribution } = require('../../src/services/similarityEngine');
const { detectChangepoints, checkMultiSignalEvolution } = require('../../src/services/evolutionEngine');
const { detectDiscrepancy, detectSteamMove, getReliabilityScore } = require('../../src/services/lineDiscrepancyDetector');
const { scanAltLines, calculateModelProbability, compareToBookImplied, normalCDF: altNormalCDF } = require('../../src/services/altLineScanner');
const { DISTRIBUTION_SHAPES, getDistributionShape, calculateProbability, normalCDF, poissonCDF, negativeBinomialCDF } = require('../../src/services/bayesianEngine');
const { walkForwardValidate, calculateCLV, checkDrift, applyLearningRateCap } = require('../../src/services/modelTrainer');
const { phiCoefficient, hasMinimumObservations, calculateJuiceAdjustedEV } = require('../../src/services/correlationMath');

// Mock axios for evolution engine tests
jest.mock('axios', () => ({
  post: jest.fn(),
}));
const axios = require('axios');

describe('Intelligence Engine', () => {

  // --- Similarity Engine ---

  describe('Similarity Engine', () => {
    test('similarity score returns value between 0 and 1', () => {
      const gameA = { functional_role_match: 0.8, pace: 100, rest_days: 2 };
      const gameB = { functional_role_match: 0.7, pace: 98, rest_days: 1 };
      const score = calculateSimilarityScore(gameA, gameB);
      expect(score).toBeGreaterThanOrEqual(0);
      expect(score).toBeLessThanOrEqual(1);
    });

    test('similarity weights sum to 1.0', () => {
      const sum = Object.values(SIMILARITY_WEIGHTS).reduce((s, v) => s + v, 0);
      expect(Math.round(sum * 100) / 100).toBe(1.0);
    });

    test('identical games return score of 1', () => {
      const game = { functional_role_match: 0.8, pace: 100, rest_days: 2, opponent_defensive_rating: 110 };
      const score = calculateSimilarityScore(game, game);
      expect(score).toBe(1);
    });

    test('findSimilarGames returns LOW confidence when below minInstances', () => {
      const target = { pace: 100 };
      const historical = Array.from({ length: 5 }, (_, i) => ({ pace: 95 + i, statValue: 20 + i }));
      const result = findSimilarGames(target, historical, 15);
      expect(result.confidence).toBe('LOW');
      expect(result.usedSeasonAvg).toBe(true);
    });

    test('findSimilarGames returns HIGH confidence with enough instances', () => {
      const target = { pace: 100 };
      const historical = Array.from({ length: 30 }, (_, i) => ({ pace: 95 + (i % 10), statValue: 20 + i }));
      const result = findSimilarGames(target, historical, 15);
      expect(result.confidence).toBe('HIGH');
      expect(result.usedSeasonAvg).toBe(false);
    });

    test('getPosteriorDistribution returns correct structure', () => {
      const games = Array.from({ length: 20 }, (_, i) => ({ statValue: 20 + Math.random() * 10 }));
      const dist = getPosteriorDistribution(games);
      expect(dist).toHaveProperty('mean');
      expect(dist).toHaveProperty('stddev');
      expect(dist).toHaveProperty('ci_low');
      expect(dist).toHaveProperty('ci_high');
      expect(dist).toHaveProperty('n');
      expect(dist.n).toBe(20);
      expect(dist.ci_low).toBeLessThan(dist.ci_high);
    });
  });

  // --- Evolution Engine ---

  describe('Evolution Engine', () => {
    test('graceful degradation on HTTP failure', async () => {
      axios.post.mockRejectedValue(new Error('Connection refused'));
      const result = await detectChangepoints('player1', 'usage_rate', [0.2, 0.3], ['2026-01-01', '2026-01-02']);
      expect(result.evolution_detected).toBe(false);
      expect(result.error).toBe('Connection refused');
    });

    test('graceful degradation on timeout', async () => {
      const timeoutError = new Error('timeout');
      timeoutError.code = 'ECONNABORTED';
      axios.post.mockRejectedValue(timeoutError);
      const result = await detectChangepoints('player1', 'usage_rate', [0.2, 0.3], ['2026-01-01', '2026-01-02']);
      expect(result.evolution_detected).toBe(false);
      expect(result.error).toBe('timeout');
    });
  });

  // --- Line Discrepancy Detector ---

  describe('Line Discrepancy Detector', () => {
    test('detects discrepancy when gap > 0.5', () => {
      const lines = [
        { book: 'pinnacle', line: 24.5 },
        { book: 'circa', line: 24.5 },
        { book: 'draftkings', line: 25.5 },
        { book: 'fanduel', line: 25.5 },
        { book: 'betmgm', line: 25.5 },
      ];
      const result = detectDiscrepancy(lines);
      expect(result.discrepancy).toBe(true);
      expect(result.gap).toBe(1);
    });

    test('no discrepancy when gap <= 0.5', () => {
      const lines = [
        { book: 'pinnacle', line: 25.0 },
        { book: 'draftkings', line: 25.5 },
        { book: 'fanduel', line: 25.0 },
      ];
      const result = detectDiscrepancy(lines);
      expect(result.discrepancy).toBe(false);
    });

    test('detects steam move with 3+ books moving 0.5+ in 10 min', () => {
      const now = new Date();
      const movements = [
        { book: 'draftkings', line: 0.5, timestamp: now.toISOString() },
        { book: 'fanduel', line: 0.5, timestamp: new Date(now.getTime() + 60000).toISOString() },
        { book: 'betmgm', line: 0.5, timestamp: new Date(now.getTime() + 120000).toISOString() },
        { book: 'caesars', line: 0.5, timestamp: new Date(now.getTime() + 180000).toISOString() },
      ];
      const result = detectSteamMove(movements);
      expect(result.steam_move).toBe(true);
      expect(result.books_moved).toBeGreaterThanOrEqual(3);
    });

    test('no steam move with insufficient books', () => {
      const now = new Date();
      const movements = [
        { book: 'draftkings', line: 0.5, timestamp: now.toISOString() },
        { book: 'fanduel', line: 0.5, timestamp: new Date(now.getTime() + 60000).toISOString() },
      ];
      const result = detectSteamMove(movements);
      expect(result.steam_move).toBe(false);
    });

    test('reliability score returns valid range', () => {
      const score = getReliabilityScore('points', 'nba');
      expect(score).toBeGreaterThan(0);
      expect(score).toBeLessThanOrEqual(1);
    });
  });

  // --- Alt Line Scanner ---

  describe('Alt Line Scanner', () => {
    test('calculates model probability correctly', () => {
      // Mean 25, stddev 5, line 25 => P(over) should be ~0.5
      const prob = calculateModelProbability(25, 5, 25, 'over');
      expect(prob).toBeCloseTo(0.5, 1);
    });

    test('compareToBookImplied detects value', () => {
      const result = compareToBookImplied(0.60, -110);
      expect(result.model_prob).toBe(0.6);
      expect(result.book_implied).toBeCloseTo(0.524, 2);
      expect(result.value_detected).toBe(true);
      expect(result.edge).toBeGreaterThan(0);
    });

    test('scanAltLines returns optimal line with edge', () => {
      const prop = { projected_mean: 25, projected_stddev: 5, direction: 'over' };
      const odds = [
        { line: 22.5, odds: -130, book: 'draftkings' },
        { line: 24.5, odds: -110, book: 'draftkings' },
        { line: 27.5, odds: +120, book: 'draftkings' },
      ];
      const result = scanAltLines(prop, odds);
      expect(result).not.toBeNull();
      expect(result.optimal_line).toBeDefined();
      expect(result.edge).toBeGreaterThan(0);
    });
  });

  // --- Bayesian Engine ---

  describe('Bayesian Engine', () => {
    test('normalCDF returns ~0.5 at the mean', () => {
      const result = normalCDF(10, 10, 3);
      expect(result).toBeCloseTo(0.5, 2);
    });

    test('normalCDF returns known values', () => {
      // P(X <= 1) for N(0,1) should be ~0.8413
      const result = normalCDF(1, 0, 1);
      expect(result).toBeCloseTo(0.8413, 2);
    });

    test('poissonCDF correctness', () => {
      // P(X <= 2) for Poisson(1) = e^-1 * (1 + 1 + 0.5) = 0.9197
      const result = poissonCDF(2, 1);
      expect(result).toBeCloseTo(0.9197, 2);
    });

    test('distribution shape mapping returns correct shapes', () => {
      expect(getDistributionShape('points')).toBe('normal');
      expect(getDistributionShape('walks')).toBe('poisson');
      expect(getDistributionShape('home_runs')).toBe('negative_binomial');
      expect(getDistributionShape('pitcher_strikeouts')).toBe('bimodal_mixture');
      expect(getDistributionShape('unknown_stat')).toBe('normal');
    });

    test('calculateProbability works for poisson over', () => {
      const prob = calculateProbability('poisson', { lambda: 5 }, 4, 'over');
      // P(X > 4) for Poisson(5)
      expect(prob).toBeGreaterThan(0.4);
      expect(prob).toBeLessThan(0.8);
    });
  });

  // --- Model Trainer ---

  describe('Model Trainer', () => {
    test('walkForwardValidate returns accuracy metrics', () => {
      const predictions = [
        { predicted: 25, timestamp: '2026-01-01' },
        { predicted: 22, timestamp: '2026-01-02' },
        { predicted: 30, timestamp: '2026-01-03' },
      ];
      const actuals = [
        { actual: 24, timestamp: '2026-01-01' },
        { actual: 23, timestamp: '2026-01-02' },
        { actual: 28, timestamp: '2026-01-03' },
      ];
      const result = walkForwardValidate(predictions, actuals);
      expect(result).toHaveProperty('accuracy');
      expect(result).toHaveProperty('mae');
      expect(result).toHaveProperty('rmse');
      expect(result.n).toBe(3);
    });

    test('CLV calculation returns correct structure', () => {
      const clv = calculateCLV(24.5, 25.0, 25.5);
      expect(clv.clv_at_prediction).toBe(1.0);
      expect(clv.clv_at_24hr).toBe(0.5);
      expect(clv.clv_at_tip).toBe(0);
    });

    test('drift detection after 10 consecutive negative CLV', () => {
      const history = [1, 2, 0.5, -1, -2, -0.5, -1, -0.3, -2, -1.5, -0.8, -0.2, -1];
      const result = checkDrift(history);
      expect(result.drift_detected).toBe(true);
      expect(result.consecutive_negative).toBe(10);
      expect(result.alert).toBe(true);
    });

    test('no drift with mixed CLV history', () => {
      const history = [1, -1, 2, -2, 0.5, -0.5, 1, -1, 0.3];
      const result = checkDrift(history);
      expect(result.drift_detected).toBe(false);
    });

    test('learning rate cap enforcement (max 0.05 delta)', () => {
      expect(applyLearningRateCap(0.20, 0.30)).toBe(0.25);
      expect(applyLearningRateCap(0.20, 0.10)).toBe(0.15);
      expect(applyLearningRateCap(0.20, 0.22)).toBe(0.22);
      expect(applyLearningRateCap(0.20, 0.20)).toBe(0.20);
    });
  });

  // --- Correlation Math ---

  describe('Correlation Math', () => {
    test('phi coefficient calculation for joint outcomes', () => {
      // Perfect positive correlation
      const phi = phiCoefficient(50, 0, 0, 50);
      expect(phi).toBe(1);
    });

    test('phi coefficient returns 0 for no correlation', () => {
      const phi = phiCoefficient(25, 25, 25, 25);
      expect(phi).toBe(0);
    });

    test('phi coefficient handles zero denominator', () => {
      const phi = phiCoefficient(0, 0, 0, 0);
      expect(phi).toBe(0);
    });

    test('hasMinimumObservations checks threshold', () => {
      expect(hasMinimumObservations(100)).toBe(true);
      expect(hasMinimumObservations(99)).toBe(false);
      expect(hasMinimumObservations(50, 50)).toBe(true);
    });

    test('calculateJuiceAdjustedEV returns correct EV', () => {
      // 60% win prob, -110 stake: 0.6*100 - 0.4*110 = 60 - 44 = 16
      const ev = calculateJuiceAdjustedEV(0.6, 110);
      expect(ev).toBeCloseTo(16, 0);
    });

    test('calculateJuiceAdjustedEV negative EV on bad bet', () => {
      // 40% win prob: 0.4*100 - 0.6*110 = 40 - 66 = -26
      const ev = calculateJuiceAdjustedEV(0.4, 110);
      expect(ev).toBeLessThan(0);
    });
  });

});
