/**
 * Ship Infrastructure Tests — VYNDR v5.1
 * Tests data contracts, configurations, and infrastructure logic
 * for the production ship build.
 */

// ── Inline constants (JS-side contracts for Python service configs) ──

const RETRY_CONFIG = {
  maxRetries: 3,
  baseDelayMs: 1000,
  backoffMultiplier: 2,
};

const DATA_FRESHNESS = {
  odds:          { default_ttl: 0.25,  game_day_ttl: 0.083 },
  weather:       { default_ttl: 1.0,   game_day_ttl: 0.5 },
  park_factors:  { default_ttl: 720,   game_day_ttl: 720 },
  reporter_feed: { default_ttl: 0.017, game_day_ttl: 0.017 },
};

const CONTEXT_FACTORS = [
  'park_factor', 'weather_wind', 'weather_temp', 'weather_humidity',
  'platoon_split', 'bullpen_fatigue', 'umpire_tendency', 'lineup_confirmed',
  'rest_days', 'travel_distance', 'rivalry_flag', 'injury_report',
  'recent_form', 'season_avg', 'home_away_split',
];

const RATE_LIMITS = {
  default: { windowMs: 60000, max: 60 },
  grade:   { windowMs: 60000, max: 20 },
};

const HEALTH_RESPONSE_FIELDS = ['status', 'version', 'services', 'timestamp'];

const BOOT_SEQUENCE = [
  'database',
  'park_factors',
  'archetypes',
  'reporter_seed',
  'api_server',
];

const FAILURE_MONITOR = {
  threshold: 3,
  windowMinutes: 30,
};

const CORS_CONFIG = {
  pattern: '/api/*',
  enabled: true,
};

const FLASK_DOCS = {
  version: '5.1',
  endpointKeys: [
    'scan', 'grade', 'health', 'props', 'stats',
    'tracker', 'waitlist', 'auth', 'payments', 'docs',
  ],
};

// ── Helpers (inline logic under test) ──

function retryWithBackoff(fn, config = RETRY_CONFIG) {
  let attempts = 0;
  const delays = [];
  return {
    async execute() {
      while (attempts < config.maxRetries) {
        try {
          return await fn();
        } catch (e) {
          attempts++;
          const delay = config.baseDelayMs * Math.pow(config.backoffMultiplier, attempts - 1);
          delays.push(delay);
        }
      }
      return null;
    },
    getDelays() { return delays; },
    getAttempts() { return attempts; },
  };
}

function isFresh(lastFetched, ttlHours) {
  const ageHours = (Date.now() - lastFetched) / (1000 * 60 * 60);
  return ageHours < ttlHours;
}

function getTtl(dataType, isGameDay) {
  const entry = DATA_FRESHNESS[dataType];
  if (!entry) return null;
  return isGameDay ? entry.game_day_ttl : entry.default_ttl;
}

function aggregateContext(factors) {
  if (!factors || Object.keys(factors).length === 0) return 0;
  return CONTEXT_FACTORS.reduce((sum, key) => sum + (factors[key] || 0), 0);
}

function buildHealthResponse(serviceStatuses) {
  const degraded = Object.values(serviceStatuses).some(s => s !== 'ok');
  return {
    status: degraded ? 'degraded' : 'ok',
    version: '5.1',
    services: serviceStatuses,
    timestamp: new Date().toISOString(),
  };
}

function checkFailureAlert(failures, windowMinutes) {
  const cutoff = Date.now() - windowMinutes * 60 * 1000;
  const recentFailures = failures.filter(ts => ts > cutoff);
  return {
    count: recentFailures.length,
    alert: recentFailures.length >= FAILURE_MONITOR.threshold,
  };
}

// ── Tests ──

describe('Retry Logic', () => {
  test('retries up to 3 times before giving up', async () => {
    let callCount = 0;
    const failing = () => { callCount++; throw new Error('fail'); };
    const runner = retryWithBackoff(failing);
    const result = await runner.execute();
    expect(callCount).toBe(3);
    expect(result).toBeNull();
  });

  test('exponential backoff doubles each delay', async () => {
    const failing = () => { throw new Error('fail'); };
    const runner = retryWithBackoff(failing);
    await runner.execute();
    const delays = runner.getDelays();
    expect(delays).toEqual([1000, 2000, 4000]);
  });

  test('returns null after all retries exhausted', async () => {
    const failing = () => { throw new Error('fail'); };
    const runner = retryWithBackoff(failing);
    const result = await runner.execute();
    expect(result).toBeNull();
  });
});

describe('Data Warehouse + Game-Day TTL Override', () => {
  test('default TTL lookup returns non-game-day value', () => {
    expect(getTtl('odds', false)).toBe(0.25);
    expect(getTtl('weather', false)).toBe(1.0);
  });

  test('game-day TTL is shorter than default for weather', () => {
    const defaultTtl = getTtl('weather', false);
    const gameDayTtl = getTtl('weather', true);
    expect(gameDayTtl).toBeLessThan(defaultTtl);
  });

  test('cache freshness check passes for recent data', () => {
    const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
    expect(isFresh(fiveMinutesAgo, 0.25)).toBe(true); // 5min < 15min
  });

  test('stale data flagged when TTL exceeded', () => {
    const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
    expect(isFresh(twoHoursAgo, 0.25)).toBe(false); // 2hr > 15min
  });
});

describe('Health Check Endpoint Shape', () => {
  test('health response contains all required fields', () => {
    const response = buildHealthResponse({ db: 'ok', redis: 'ok', oddsApi: 'ok' });
    HEALTH_RESPONSE_FIELDS.forEach(field => {
      expect(response).toHaveProperty(field);
    });
  });

  test('status is degraded when any service is unavailable', () => {
    const response = buildHealthResponse({ db: 'ok', redis: 'down', oddsApi: 'ok' });
    expect(response.status).toBe('degraded');
  });
});

describe('Rate Limiting Config', () => {
  test('default rate limit is 60 requests per minute', () => {
    expect(RATE_LIMITS.default.max).toBe(60);
    expect(RATE_LIMITS.default.windowMs).toBe(60000);
  });

  test('grade endpoints limited to 20 requests per minute', () => {
    expect(RATE_LIMITS.grade.max).toBe(20);
    expect(RATE_LIMITS.grade.windowMs).toBe(60000);
  });
});

describe('Context Aggregator', () => {
  test('sums all 15 context factors correctly', () => {
    const factors = {};
    CONTEXT_FACTORS.forEach(key => { factors[key] = 1; });
    expect(aggregateContext(factors)).toBe(15);
  });

  test('missing factors default to 0', () => {
    const partial = { park_factor: 3, weather_wind: 2 };
    expect(aggregateContext(partial)).toBe(5);
  });

  test('empty factors object returns 0', () => {
    expect(aggregateContext({})).toBe(0);
  });
});

describe('Cold Start Boot Sequence Order', () => {
  test('park_factors loaded before archetypes', () => {
    const parkIdx = BOOT_SEQUENCE.indexOf('park_factors');
    const archIdx = BOOT_SEQUENCE.indexOf('archetypes');
    expect(parkIdx).toBeLessThan(archIdx);
    expect(parkIdx).not.toBe(-1);
  });

  test('reporter_seed happens after database is loaded', () => {
    const dbIdx = BOOT_SEQUENCE.indexOf('database');
    const reporterIdx = BOOT_SEQUENCE.indexOf('reporter_seed');
    expect(reporterIdx).toBeGreaterThan(dbIdx);
  });
});

describe('API Failure Monitoring', () => {
  test('failure count threshold is 3', () => {
    expect(FAILURE_MONITOR.threshold).toBe(3);
  });

  test('alert triggered at 3+ failures within 30 minutes', () => {
    const now = Date.now();
    const failures = [
      now - 20 * 60 * 1000,
      now - 10 * 60 * 1000,
      now - 5 * 60 * 1000,
    ];
    const result = checkFailureAlert(failures, FAILURE_MONITOR.windowMinutes);
    expect(result.alert).toBe(true);
    expect(result.count).toBe(3);
  });
});

describe('Data Freshness TTLs', () => {
  test('odds default TTL is 0.25 hours (15 minutes)', () => {
    expect(DATA_FRESHNESS.odds.default_ttl).toBe(0.25);
  });

  test('weather game-day TTL is 0.5 hours (30 minutes)', () => {
    expect(DATA_FRESHNESS.weather.game_day_ttl).toBe(0.5);
  });

  test('park_factors TTL is 720 hours (30 days)', () => {
    expect(DATA_FRESHNESS.park_factors.default_ttl).toBe(720);
  });

  test('reporter_feed TTL is 0.017 hours (~1 minute)', () => {
    expect(DATA_FRESHNESS.reporter_feed.default_ttl).toBe(0.017);
  });
});

describe('Flask App Docs Endpoint Shape', () => {
  test('/api/docs returns all expected endpoint keys', () => {
    FLASK_DOCS.endpointKeys.forEach(key => {
      expect(FLASK_DOCS.endpointKeys).toContain(key);
    });
    expect(FLASK_DOCS.endpointKeys.length).toBe(10);
  });

  test('version is 5.1', () => {
    expect(FLASK_DOCS.version).toBe('5.1');
  });
});

describe('CORS Config', () => {
  test('/api/* pattern is enabled', () => {
    expect(CORS_CONFIG.pattern).toBe('/api/*');
    expect(CORS_CONFIG.enabled).toBe(true);
  });
});
