const fs = require('fs');
const path = require('path');

// ─────────────────────────────────────────────────────────────
// VYNDR — Supplement Intelligence Systems
// Pure logic tests: all constants and formulas inlined
// ─────────────────────────────────────────────────────────────

describe('Supplement Intelligence Systems', () => {

  // ═══════════════════════════════════════════════════════════
  // System 1 — Coaching Tendencies
  // ═══════════════════════════════════════════════════════════

  describe('Coaching Tendencies', () => {

    const NBA_COACHING_FIELDS = [
      'pace_preference', 'rotation_depth', 'closing_lineup_consistency',
      'garbage_time_threshold', 'challenge_frequency', 'blitz_frequency',
      'zone_pct', 'rest_pattern', 'matchup_hunting_rate',
      'dnp_volatility', 'second_unit_usage', 'timeout_tendency',
    ];

    const MLB_COACHING_FIELDS = [
      'starter_hook_tendency', 'bullpen_deployment', 'platoon_aggressiveness',
      'sacrifice_bunt_rate', 'hit_and_run_rate', 'steal_aggressiveness',
      'defensive_shift_rate', 'lineup_consistency', 'rest_day_pattern',
      'challenge_aggressiveness',
    ];

    test('NBA coaching fields include all 12 expected keys', () => {
      expect(NBA_COACHING_FIELDS).toHaveLength(12);
      expect(NBA_COACHING_FIELDS).toContain('pace_preference');
      expect(NBA_COACHING_FIELDS).toContain('timeout_tendency');
    });

    test('MLB coaching fields include all 10 expected keys', () => {
      expect(MLB_COACHING_FIELDS).toHaveLength(10);
      expect(MLB_COACHING_FIELDS).toContain('starter_hook_tendency');
      expect(MLB_COACHING_FIELDS).toContain('challenge_aggressiveness');
    });

    test('parse_nba_coaching_decisions extracts rotation_depth from players with 10+ min', () => {
      const players = [
        { minutes: 32 }, { minutes: 28 }, { minutes: 24 },
        { minutes: 20 }, { minutes: 18 }, { minutes: 14 },
        { minutes: 12 }, { minutes: 11 }, { minutes: 8 },
        { minutes: 5 }, { minutes: 3 },
      ];
      const rotation_depth = players.filter(p => p.minutes >= 10).length;
      expect(rotation_depth).toBe(8);
    });

    test('rotation_depth of 8 from 8 players with 10+ minutes', () => {
      const playerMinutes = [35, 30, 28, 22, 18, 15, 12, 10, 7, 4];
      const rotation_depth = playerMinutes.filter(m => m >= 10).length;
      expect(rotation_depth).toBe(8);
    });

    test('parse_mlb_coaching_decisions extracts starter_hook_tendency', () => {
      // Hook tendency = avg innings before pull across recent starts
      const starterInnings = [5.2, 6.0, 5.1, 4.2, 6.1];
      const avgHook = starterInnings.reduce((s, v) => s + v, 0) / starterInnings.length;
      expect(avgHook).toBeCloseTo(5.32, 1);
    });

    test('shift detection: 15% threshold for flagging', () => {
      const SHIFT_THRESHOLD = 0.15;
      expect(SHIFT_THRESHOLD).toBe(0.15);
    });

    test('shift detection: 10% change does NOT flag', () => {
      const SHIFT_THRESHOLD = 0.15;
      const baseline = 0.50;
      const current = 0.55;
      const change = Math.abs(current - baseline) / baseline;
      expect(change).toBeCloseTo(0.10, 4);
      expect(change < SHIFT_THRESHOLD).toBe(true);
    });

    test('shift detection: 20% change DOES flag', () => {
      const SHIFT_THRESHOLD = 0.15;
      const baseline = 0.50;
      const current = 0.60;
      const change = Math.abs(current - baseline) / baseline;
      expect(change).toBeCloseTo(0.20, 4);
      expect(change >= SHIFT_THRESHOLD).toBe(true);
    });

    test('shift detection returns direction increased or decreased', () => {
      const baseline = 0.50;
      const currentUp = 0.65;
      const currentDown = 0.35;
      const dirUp = currentUp > baseline ? 'increased' : 'decreased';
      const dirDown = currentDown > baseline ? 'increased' : 'decreased';
      expect(dirUp).toBe('increased');
      expect(dirDown).toBe('decreased');
    });

    test('season baseline includes all numeric fields', () => {
      const seasonBaseline = {
        pace_preference: 98.5,
        rotation_depth: 9,
        closing_lineup_consistency: 0.72,
        zone_pct: 0.15,
        rest_pattern: 3.2,
      };
      for (const val of Object.values(seasonBaseline)) {
        expect(typeof val).toBe('number');
      }
    });

    test('recent tendencies calculated from last 15 games', () => {
      const RECENT_WINDOW = 15;
      const allGames = Array.from({ length: 40 }, (_, i) => ({ pace: 95 + (i % 10) }));
      const recentGames = allGames.slice(-RECENT_WINDOW);
      expect(recentGames).toHaveLength(15);
    });

    test('coaching fields are sport-specific (NBA != MLB)', () => {
      const overlap = NBA_COACHING_FIELDS.filter(f => MLB_COACHING_FIELDS.includes(f));
      expect(overlap).toHaveLength(0);
    });
  });

  // ═══════════════════════════════════════════════════════════
  // System 2 — Redistribution Engine
  // ═══════════════════════════════════════════════════════════

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

    function classify_absorption_tier(boost, confidence) {
      if (boost >= 0.20 && confidence >= 0.75) return 'primary';
      if (boost >= 0.10 && confidence >= 0.60) return 'secondary';
      if (boost >= 0.05) return 'tertiary';
      return 'minimal';
    }

    test('classify_absorption_tier: primary when boost>=0.20 AND confidence>=0.75', () => {
      expect(classify_absorption_tier(0.25, 0.80)).toBe('primary');
      expect(classify_absorption_tier(0.20, 0.75)).toBe('primary');
    });

    test('classify_absorption_tier: secondary when boost>=0.10 AND confidence>=0.60', () => {
      expect(classify_absorption_tier(0.15, 0.65)).toBe('secondary');
      expect(classify_absorption_tier(0.10, 0.60)).toBe('secondary');
    });

    test('classify_absorption_tier: tertiary when boost>=0.05', () => {
      expect(classify_absorption_tier(0.07, 0.40)).toBe('tertiary');
      expect(classify_absorption_tier(0.05, 0.10)).toBe('tertiary');
    });

    test('classify_absorption_tier: minimal when boost<0.05', () => {
      expect(classify_absorption_tier(0.04, 0.90)).toBe('minimal');
      expect(classify_absorption_tier(0.01, 0.50)).toBe('minimal');
    });

    test('concentrated coach (7-man): backup gets 70% of freed minutes', () => {
      const rotation_depth = 7;
      const minutes_freed = 30;
      const backup_share = rotation_depth <= 7 ? minutes_freed * 0.70 : minutes_freed / 4;
      expect(backup_share).toBe(21);
    });

    test('distributed coach (10-man): minutes spread across 3-4 players', () => {
      const rotation_depth = 10;
      const minutes_freed = 30;
      const per_player_share = rotation_depth <= 7 ? minutes_freed * 0.70 : minutes_freed / 4;
      expect(per_player_share).toBe(7.5);
      // 4 players share equally
      expect(minutes_freed / 4).toBe(7.5);
    });

    test('usage-efficiency tradeoff: -1.5% TS per +5% usage applied correctly', () => {
      // Formula: penalty = raw_boost * (-0.015 / 0.05)
      const raw_boost = 0.10;
      const penalty = raw_boost * (-0.015 / 0.05);
      expect(penalty).toBeCloseTo(-0.03, 4);
    });

    test('net boost = raw_boost + efficiency_penalty', () => {
      const raw_boost = 0.10;
      const penalty = raw_boost * (-0.015 / 0.05);
      const net_boost = raw_boost + penalty;
      expect(net_boost).toBeCloseTo(0.07, 4);
    });

    test('system change: primary_scorer out -> secondary_creator gets +0.08', () => {
      const SYSTEM_SHIFTS = {
        primary_scorer: { secondary_creator: 0.08, tertiary_scorer: 0.05 },
        primary_playmaker: { secondary_creator: 0.06 },
        interior_big: { stretch_big: 0.07 },
      };
      expect(SYSTEM_SHIFTS.primary_scorer.secondary_creator).toBe(0.08);
    });

    test('system change: primary_playmaker out -> secondary_creator gets +0.06', () => {
      const SYSTEM_SHIFTS = {
        primary_scorer: { secondary_creator: 0.08 },
        primary_playmaker: { secondary_creator: 0.06 },
        interior_big: { stretch_big: 0.07 },
      };
      expect(SYSTEM_SHIFTS.primary_playmaker.secondary_creator).toBe(0.06);
    });

    test('system change: interior_big out -> stretch_big gets +0.07', () => {
      const SYSTEM_SHIFTS = {
        primary_scorer: { secondary_creator: 0.08 },
        primary_playmaker: { secondary_creator: 0.06 },
        interior_big: { stretch_big: 0.07 },
      };
      expect(SYSTEM_SHIFTS.interior_big.stretch_big).toBe(0.07);
    });

    test('auto-grade threshold: 15%+ boost AND 0.65+ confidence', () => {
      const AUTO_BOOST_THRESHOLD = 0.15;
      const AUTO_CONFIDENCE_THRESHOLD = 0.65;
      const should_auto_grade = (boost, conf) =>
        boost >= AUTO_BOOST_THRESHOLD && conf >= AUTO_CONFIDENCE_THRESHOLD;
      expect(should_auto_grade(0.18, 0.70)).toBe(true);
      expect(should_auto_grade(0.15, 0.65)).toBe(true);
    });

    test('auto-grade: 14% boost does NOT trigger auto-grade', () => {
      const AUTO_BOOST_THRESHOLD = 0.15;
      const AUTO_CONFIDENCE_THRESHOLD = 0.65;
      const should_auto_grade = (boost, conf) =>
        boost >= AUTO_BOOST_THRESHOLD && conf >= AUTO_CONFIDENCE_THRESHOLD;
      expect(should_auto_grade(0.14, 0.90)).toBe(false);
    });

    test('absorption alert format includes is OUT and is underpriced', () => {
      const playerOut = 'LeBron James';
      const beneficiary = 'Anthony Davis';
      const alert = `${playerOut} is OUT — ${beneficiary} is underpriced at current line`;
      expect(alert).toContain('is OUT');
      expect(alert).toContain('is underpriced');
    });

    test('coach-specific redistribution_profile overrides generic system shifts', () => {
      const generic_boost = 0.08;
      const coach_profile = { secondary_creator_boost: 0.12 };
      const effective_boost = coach_profile.secondary_creator_boost || generic_boost;
      expect(effective_boost).toBe(0.12);
      expect(effective_boost).not.toBe(generic_boost);
    });
  });

  // ═══════════════════════════════════════════════════════════
  // System 3 — Alt Line Scanner
  // ═══════════════════════════════════════════════════════════

  describe('Alt Line Scanner', () => {

    const A_GRADES = ['A+', 'A', 'A-'];

    function isEligibleForAltScan(grade) {
      return A_GRADES.includes(grade);
    }

    test('only runs on A-grade props (A+, A, A-)', () => {
      expect(isEligibleForAltScan('A+')).toBe(true);
      expect(isEligibleForAltScan('A')).toBe(true);
      expect(isEligibleForAltScan('A-')).toBe(true);
    });

    test('returns eligible=false for B+ grade', () => {
      expect(isEligibleForAltScan('B+')).toBe(false);
      expect(isEligibleForAltScan('B')).toBe(false);
      expect(isEligibleForAltScan('C')).toBe(false);
    });

    test('edge improvement threshold is 3% (0.03)', () => {
      const EDGE_IMPROVEMENT_THRESHOLD = 0.03;
      expect(EDGE_IMPROVEMENT_THRESHOLD).toBe(0.03);
    });

    test('recommends alt when edge_vs_standard >= 0.03', () => {
      const EDGE_IMPROVEMENT_THRESHOLD = 0.03;
      const edge_vs_standard = 0.05;
      const recommend = edge_vs_standard >= EDGE_IMPROVEMENT_THRESHOLD;
      expect(recommend).toBe(true);
    });

    test('does NOT recommend when edge_vs_standard < 0.03', () => {
      const EDGE_IMPROVEMENT_THRESHOLD = 0.03;
      const edge_vs_standard = 0.02;
      const recommend = edge_vs_standard >= EDGE_IMPROVEMENT_THRESHOLD;
      expect(recommend).toBe(false);
    });

    test('alt lines sorted by ev_per_dollar descending', () => {
      const altLines = [
        { line: 22.5, ev_per_dollar: 0.04 },
        { line: 24.5, ev_per_dollar: 0.12 },
        { line: 27.5, ev_per_dollar: 0.08 },
      ];
      const sorted = [...altLines].sort((a, b) => b.ev_per_dollar - a.ev_per_dollar);
      expect(sorted[0].ev_per_dollar).toBe(0.12);
      expect(sorted[1].ev_per_dollar).toBe(0.08);
      expect(sorted[2].ev_per_dollar).toBe(0.04);
    });

    test('returns top 5 positive EV alts only', () => {
      const altLines = [
        { line: 20.5, ev_per_dollar: 0.15 },
        { line: 21.5, ev_per_dollar: 0.12 },
        { line: 22.5, ev_per_dollar: 0.09 },
        { line: 23.5, ev_per_dollar: 0.06 },
        { line: 24.5, ev_per_dollar: 0.03 },
        { line: 25.5, ev_per_dollar: 0.01 },
        { line: 26.5, ev_per_dollar: -0.02 },
      ];
      const positiveEV = altLines
        .filter(a => a.ev_per_dollar > 0)
        .sort((a, b) => b.ev_per_dollar - a.ev_per_dollar)
        .slice(0, 5);
      expect(positiveEV).toHaveLength(5);
      expect(positiveEV.every(a => a.ev_per_dollar > 0)).toBe(true);
    });

    test('model probability calculation: over = 1 - CDF, under = CDF', () => {
      // Inline normalCDF approximation
      function normalCDF(x, mean, stddev) {
        const z = (x - mean) / stddev;
        const t = 1 / (1 + 0.2316419 * Math.abs(z));
        const d = 0.3989422804014327;
        const p = d * Math.exp(-z * z / 2) *
          (t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))));
        return z > 0 ? 1 - p : p;
      }
      const mean = 25, stddev = 5, line = 25;
      const prob_over = 1 - normalCDF(line, mean, stddev);
      const prob_under = normalCDF(line, mean, stddev);
      expect(prob_over).toBeCloseTo(0.5, 1);
      expect(prob_under).toBeCloseTo(0.5, 1);
    });

    test('optimal alt is first element after sorting', () => {
      const sorted = [
        { line: 24.5, ev_per_dollar: 0.12 },
        { line: 22.5, ev_per_dollar: 0.08 },
        { line: 27.5, ev_per_dollar: 0.04 },
      ];
      const optimal = sorted[0];
      expect(optimal.line).toBe(24.5);
      expect(optimal.ev_per_dollar).toBe(0.12);
    });

    test('alt line includes bookmaker field', () => {
      const altLine = { line: 22.5, odds: -130, book: 'draftkings', ev_per_dollar: 0.08 };
      expect(altLine).toHaveProperty('book');
      expect(altLine.book).toBe('draftkings');
    });
  });

  // ═══════════════════════════════════════════════════════════
  // System 4 — Unconventional Data Pipeline
  // ═══════════════════════════════════════════════════════════

  describe('Unconventional Data Pipeline', () => {

    const MIN_INSTANCES = 500;
    const MIN_PEARSON_R = 0.15;
    const BASE_ALPHA = 0.05;

    function validateFactor(instances, r, p_value, num_tests) {
      if (instances < MIN_INSTANCES) return { validated: false, reason: 'insufficient_instances' };
      if (Math.abs(r) < MIN_PEARSON_R) return { validated: false, reason: 'weak_correlation' };
      const corrected_alpha = BASE_ALPHA / num_tests;
      if (p_value >= corrected_alpha) return { validated: false, reason: 'not_significant' };
      return { validated: true, corrected_alpha };
    }

    const UNCONVENTIONAL_FACTORS = [
      { name: 'travel_distance', validated: true },
      { name: 'altitude_adjustment', validated: false },
      { name: 'circadian_rhythm', validated: false },
      { name: 'back_to_back_fatigue', validated: true },
      { name: 'timezone_crossing', validated: false },
    ];

    test('validation requires minimum 500 instances', () => {
      expect(MIN_INSTANCES).toBe(500);
    });

    test('fails with 499 instances', () => {
      const result = validateFactor(499, 0.20, 0.001, 4);
      expect(result.validated).toBe(false);
      expect(result.reason).toBe('insufficient_instances');
    });

    test('passes with 500+ instances (if r and p pass)', () => {
      const result = validateFactor(600, 0.25, 0.001, 4);
      expect(result.validated).toBe(true);
    });

    test('minimum Pearson r is 0.15', () => {
      expect(MIN_PEARSON_R).toBe(0.15);
    });

    test('fails when r < 0.15', () => {
      const result = validateFactor(600, 0.10, 0.001, 4);
      expect(result.validated).toBe(false);
      expect(result.reason).toBe('weak_correlation');
    });

    test('Bonferroni correction: alpha = 0.05 / number_of_active_tests', () => {
      const num_tests = 4;
      const corrected = BASE_ALPHA / num_tests;
      expect(corrected).toBe(0.0125);
    });

    test('with 4 unvalidated factors, corrected alpha = 0.0125', () => {
      const unvalidated = UNCONVENTIONAL_FACTORS.filter(f => !f.validated);
      expect(unvalidated).toHaveLength(3);
      // When testing 4 factors simultaneously
      const corrected = BASE_ALPHA / 4;
      expect(corrected).toBe(0.0125);
    });

    test('with 1 unvalidated factor, corrected alpha = 0.05', () => {
      const corrected = BASE_ALPHA / 1;
      expect(corrected).toBe(0.05);
    });

    test('travel_distance starts as validated=True', () => {
      const travel = UNCONVENTIONAL_FACTORS.find(f => f.name === 'travel_distance');
      expect(travel.validated).toBe(true);
    });

    test('altitude_adjustment starts as validated=False', () => {
      const altitude = UNCONVENTIONAL_FACTORS.find(f => f.name === 'altitude_adjustment');
      expect(altitude.validated).toBe(false);
    });

    test('factor only enters grading engine AFTER validation (validated=True check)', () => {
      const activeFactors = UNCONVENTIONAL_FACTORS.filter(f => f.validated);
      expect(activeFactors.every(f => f.validated === true)).toBe(true);
      expect(activeFactors.map(f => f.name)).toContain('travel_distance');
      expect(activeFactors.map(f => f.name)).not.toContain('altitude_adjustment');
    });

    test('status endpoint returns all 5 factor names', () => {
      expect(UNCONVENTIONAL_FACTORS).toHaveLength(5);
      const names = UNCONVENTIONAL_FACTORS.map(f => f.name);
      expect(names).toContain('travel_distance');
      expect(names).toContain('altitude_adjustment');
      expect(names).toContain('circadian_rhythm');
      expect(names).toContain('back_to_back_fatigue');
      expect(names).toContain('timezone_crossing');
    });
  });

  // ═══════════════════════════════════════════════════════════
  // System 5 — Evolution Alerting
  // ═══════════════════════════════════════════════════════════

  describe('Evolution Alerting', () => {

    const MIN_GAMES = 15;
    const CHANGE_THRESHOLD = 0.10;
    const MIN_INFLECTIONS = 2;

    const NBA_METRICS = ['usage_rate', 'assist_rate', 'three_pa_rate', 'fg_pct', 'minutes'];
    const MLB_METRICS = ['k_rate', 'bb_rate', 'exit_velocity', 'hard_hit_pct', 'fb_velo'];

    function detectEvolution(gameCount, inflections) {
      if (gameCount < MIN_GAMES) return { evolution_detected: false, reason: 'insufficient_games' };
      if (inflections.length < MIN_INFLECTIONS) return { evolution_detected: false, reason: 'insufficient_inflections' };
      return {
        evolution_detected: true,
        detection_date: new Date().toISOString().split('T')[0],
        metrics: inflections,
      };
    }

    function isInflection(baseline, current) {
      const change = Math.abs(current - baseline) / baseline;
      return change >= CHANGE_THRESHOLD;
    }

    test('evolution detected when 2+ metrics show concurrent inflection', () => {
      const inflections = ['usage_rate', 'assist_rate'];
      const result = detectEvolution(20, inflections);
      expect(result.evolution_detected).toBe(true);
    });

    test('evolution NOT detected with only 1 inflection', () => {
      const inflections = ['usage_rate'];
      const result = detectEvolution(20, inflections);
      expect(result.evolution_detected).toBe(false);
      expect(result.reason).toBe('insufficient_inflections');
    });

    test('minimum 15 games required', () => {
      expect(MIN_GAMES).toBe(15);
    });

    test('returns evolution_detected=false with 14 games', () => {
      const result = detectEvolution(14, ['usage_rate', 'assist_rate']);
      expect(result.evolution_detected).toBe(false);
      expect(result.reason).toBe('insufficient_games');
    });

    test('change threshold is 10% (0.10)', () => {
      expect(CHANGE_THRESHOLD).toBe(0.10);
    });

    test('9% change does NOT qualify as inflection', () => {
      const baseline = 0.20;
      const current = 0.218; // 9% change
      expect(isInflection(baseline, current)).toBe(false);
    });

    test('11% change DOES qualify as inflection', () => {
      const baseline = 0.20;
      const current = 0.222; // 11% change
      expect(isInflection(baseline, current)).toBe(true);
    });

    test('NBA metrics: usage_rate, assist_rate, three_pa_rate, fg_pct, minutes', () => {
      expect(NBA_METRICS).toEqual(['usage_rate', 'assist_rate', 'three_pa_rate', 'fg_pct', 'minutes']);
      expect(NBA_METRICS).toHaveLength(5);
    });

    test('MLB metrics: k_rate, bb_rate, exit_velocity, hard_hit_pct, fb_velo', () => {
      expect(MLB_METRICS).toEqual(['k_rate', 'bb_rate', 'exit_velocity', 'hard_hit_pct', 'fb_velo']);
      expect(MLB_METRICS).toHaveLength(5);
    });

    test('evolution record includes detection_date and metrics', () => {
      const inflections = ['usage_rate', 'three_pa_rate'];
      const result = detectEvolution(20, inflections);
      expect(result).toHaveProperty('detection_date');
      expect(result).toHaveProperty('metrics');
      expect(result.metrics).toEqual(inflections);
    });

    test('Evolution Watch post format includes Evolution Watch', () => {
      const playerName = 'Jalen Brunson';
      const metrics = ['usage_rate', 'assist_rate'];
      const post = `Evolution Watch: ${playerName} — ${metrics.join(', ')} trending. The market hasn't priced it yet.`;
      expect(post).toContain('Evolution Watch');
    });

    test('Evolution Watch post includes market hasn\'t priced it yet', () => {
      const post = `Evolution Watch: Player X — usage_rate trending. The market hasn't priced it yet.`;
      expect(post).toContain("market hasn't priced it yet");
    });
  });

  // ═══════════════════════════════════════════════════════════
  // Migration 008 — Table Definitions
  // ═══════════════════════════════════════════════════════════

  describe('Migration 008', () => {

    const sql = fs.readFileSync(
      path.join(__dirname, '..', '..', 'supabase', 'migrations', '008_supplement_tables.sql'),
      'utf-8'
    );

    test('creates coaching_tendencies table with UNIQUE constraint', () => {
      expect(sql).toContain('CREATE TABLE IF NOT EXISTS coaching_tendencies');
      expect(sql).toContain('UNIQUE(coach_id, team_id, sport, season)');
    });

    test('creates player_out_history table', () => {
      expect(sql).toContain('CREATE TABLE IF NOT EXISTS player_out_history');
      expect(sql).toContain('player_out_id TEXT NOT NULL');
      expect(sql).toContain('beneficiary_stats JSONB NOT NULL');
    });

    test('creates evolution_detections table', () => {
      expect(sql).toContain('CREATE TABLE IF NOT EXISTS evolution_detections');
      expect(sql).toContain('detection_date DATE NOT NULL');
      expect(sql).toContain('metrics JSONB NOT NULL');
    });

    test('creates unconventional_validations with RLS', () => {
      expect(sql).toContain('CREATE TABLE IF NOT EXISTS unconventional_validations');
      expect(sql).toContain('ALTER TABLE unconventional_validations ENABLE ROW LEVEL SECURITY');
      expect(sql).toContain('factor_name TEXT NOT NULL');
    });
  });

});
