/**
 * VYNDR Security Audit Tests
 * Pure logic tests — inline constants and validation logic.
 * 45 tests across JWT, input validation, image validation, parlay,
 * security headers, error handling, real IP, CORS, env check,
 * security logger, digest, source scan, and migration.
 */

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

// ---------------------------------------------------------------------------
// Inline helpers (mirrors production logic without importing app code)
// ---------------------------------------------------------------------------

const JWT_SECRET = 'test-secret-256bit-minimum-length-ok';
const INTERNAL_KEY = 'VYNDR_INTERNAL_abc123';

function parseJwt(token) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
    return payload;
  } catch {
    return null;
  }
}

function makeJwt(payload, expiresInSec = 3600) {
  const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
  const now = Math.floor(Date.now() / 1000);
  const body = Buffer.from(JSON.stringify({
    ...payload,
    iat: now,
    exp: now + expiresInSec,
  })).toString('base64url');
  const sig = Buffer.from('fake-sig').toString('base64url');
  return `${header}.${body}.${sig}`;
}

function extractBearer(authHeader) {
  if (!authHeader) return { status: 401, error: 'Missing Authorization header' };
  if (!authHeader.startsWith('Bearer ')) return { status: 401, error: 'Malformed Bearer token' };
  const token = authHeader.slice(7).trim();
  if (!token) return { status: 401, error: 'Empty token' };
  const payload = parseJwt(token);
  if (!payload) return { status: 401, error: 'Invalid token' };
  if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null;
  return payload;
}

function verifyIssuer(payload, expectedIssuer) {
  if (!payload || payload.iss !== expectedIssuer) return null;
  return payload;
}

function verifyInternalKey(key) {
  return key === INTERNAL_KEY;
}

const VALID_STAT_TYPES = [
  'points', 'rebounds', 'assists', 'threes', 'steals', 'blocks',
  'turnovers', 'pts_rebs_asts', 'pts_rebs', 'pts_asts', 'rebs_asts',
  'strikeouts', 'hits', 'home_runs', 'rbi', 'bases', 'outs',
];

function sanitize(val) {
  return val.replace(/[;'"\\`]/g, '').replace(/<[^>]+>/g, '').trim().slice(0, 100);
}

function validateGradeRequest(body) {
  const errors = [];
  if (!body.player_name) errors.push('player_name is required');
  if (body.stat_type && !VALID_STAT_TYPES.includes(body.stat_type)) {
    errors.push(`Invalid stat_type: ${body.stat_type}`);
  }
  if (body.line !== undefined) {
    if (body.line > 500) errors.push('Line out of range (max 500)');
    if (body.line < 0) errors.push('Line cannot be negative');
  }
  if (body.over_under && !['over', 'under'].includes(body.over_under)) {
    errors.push('over_under must be "over" or "under"');
  }
  if (body.player_name && body.player_name.length > 100) {
    errors.push('player_name exceeds max length of 100');
  }
  return errors.length ? { valid: false, errors } : { valid: true };
}

function detectSqlInjection(input) {
  const patterns = [/drop\s+table/i, /;\s*delete/i, /union\s+select/i, /or\s+1\s*=\s*1/i];
  return patterns.some((p) => p.test(input));
}

function validateImageMagicBytes(buffer) {
  if (buffer.length < 4) return false;
  // PNG
  if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return true;
  // JPEG
  if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return true;
  return false;
}

function isExecutable(buffer) {
  if (buffer.length < 2) return false;
  return buffer[0] === 0x4d && buffer[1] === 0x5a; // MZ header
}

const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10 MB

function validateParlayLegs(legs) {
  if (!Array.isArray(legs) || legs.length < 2) return { valid: false, error: 'Minimum 2 legs required' };
  if (legs.length > 12) return { valid: false, error: 'Maximum 12 legs allowed' };
  for (let i = 0; i < legs.length; i++) {
    if (!legs[i].player_name || !legs[i].stat_type) {
      return { valid: false, error: `Leg ${i + 1} must have player_name and stat_type` };
    }
  }
  return { valid: true };
}

function getSecurityHeaders() {
  return {
    'X-Frame-Options': 'DENY',
    'X-Content-Type-Options': 'nosniff',
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    'Content-Security-Policy': "default-src 'self'",
  };
}

function productionErrorHandler(err) {
  return { status: err.status || 500, body: { error: 'Internal server error' } };
}

function notFoundHandler() {
  return { status: 404, body: { error: 'Endpoint not found' } };
}

function rateLimitHandler() {
  return { status: 429, body: { error: 'Rate limit exceeded' } };
}

function extractRealIp(headers) {
  if (headers['x-forwarded-for']) {
    return headers['x-forwarded-for'].split(',')[0].trim();
  }
  return headers.remote_addr || '127.0.0.1';
}

function parseAllowedOrigins(envVar) {
  if (!envVar) return ['http://localhost:3000'];
  return envVar.split(',').map((o) => o.trim());
}

function checkEnvVars(env, required, recommended) {
  const errors = [];
  const warnings = [];
  for (const v of required) {
    if (!env[v]) errors.push(`Missing required env var: ${v}`);
  }
  for (const v of recommended) {
    if (!env[v]) warnings.push(`Missing recommended env var: ${v}`);
  }
  return { errors, warnings, shouldExit: errors.length > 0 };
}

function detectSqlInjectionInBody(body) {
  const str = JSON.stringify(body);
  return /drop\s+table/i.test(str) || /;\s*delete/i.test(str) || /union\s+select/i.test(str);
}

const RATE_ABUSE_THRESHOLD = 100; // req per min
const RETENTION_DAYS = 90;

function buildSecurityDigest(events) {
  const ipCounts = {};
  const typeCounts = {};
  for (const e of events) {
    ipCounts[e.ip_address] = (ipCounts[e.ip_address] || 0) + 1;
    typeCounts[e.event_type] = (typeCounts[e.event_type] || 0) + 1;
  }
  const flaggedIps = Object.entries(ipCounts)
    .filter(([, c]) => c >= 50)
    .map(([ip]) => ip);
  return { flaggedIps, typeCounts };
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('JWT Auth', () => {
  test('1 — valid JWT with Bearer prefix returns payload', () => {
    const token = makeJwt({ sub: 'user-123', iss: 'vyndr' });
    const result = extractBearer(`Bearer ${token}`);
    expect(result).not.toBeNull();
    expect(result.sub).toBe('user-123');
  });

  test('2 — expired JWT returns null', () => {
    const token = makeJwt({ sub: 'user-123' }, -100); // expired 100s ago
    const result = extractBearer(`Bearer ${token}`);
    expect(result).toBeNull();
  });

  test('3 — missing Authorization header returns 401 shape', () => {
    const result = extractBearer(undefined);
    expect(result).toEqual({ status: 401, error: 'Missing Authorization header' });
  });

  test('4 — malformed Bearer token (no space) returns 401 shape', () => {
    const result = extractBearer('Bearertoken123');
    expect(result).toEqual({ status: 401, error: 'Malformed Bearer token' });
  });

  test('5 — empty token returns 401 shape', () => {
    const result = extractBearer('Bearer ');
    expect(result).toEqual({ status: 401, error: 'Empty token' });
  });

  test('6 — JWT issuer verification rejects wrong issuer', () => {
    const payload = { sub: 'user-123', iss: 'other-service' };
    const result = verifyIssuer(payload, 'vyndr');
    expect(result).toBeNull();
  });

  test('7 — internal key auth passes with valid key', () => {
    expect(verifyInternalKey(INTERNAL_KEY)).toBe(true);
    expect(verifyInternalKey('wrong-key')).toBe(false);
  });
});

describe('Input Validation', () => {
  test('8 — valid grade request passes validation', () => {
    const result = validateGradeRequest({
      player_name: 'LeBron James',
      stat_type: 'points',
      line: 25.5,
      over_under: 'over',
    });
    expect(result.valid).toBe(true);
  });

  test('9 — missing player_name returns error', () => {
    const result = validateGradeRequest({ stat_type: 'points', line: 25.5 });
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('player_name is required');
  });

  test('10 — invalid stat_type returns error', () => {
    const result = validateGradeRequest({ player_name: 'LeBron', stat_type: 'fake_stat' });
    expect(result.valid).toBe(false);
    expect(result.errors[0]).toMatch(/Invalid stat_type/);
  });

  test('11 — line out of range (>500) returns error', () => {
    const result = validateGradeRequest({ player_name: 'LeBron', line: 501 });
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Line out of range (max 500)');
  });

  test('12 — negative line returns error', () => {
    const result = validateGradeRequest({ player_name: 'LeBron', line: -5 });
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Line cannot be negative');
  });

  test('13 — over_under must be over or under', () => {
    const result = validateGradeRequest({ player_name: 'LeBron', over_under: 'sideways' });
    expect(result.valid).toBe(false);
    expect(result.errors[0]).toMatch(/over_under/);
  });

  test('14 — SQL injection chars stripped from player_name', () => {
    expect(sanitize("LeBron'; DROP TABLE users;--")).toBe('LeBron DROP TABLE users--');
  });

  test('15 — HTML tags stripped from input', () => {
    expect(sanitize('LeBron<script>alert(1)</script>James')).toBe('LeBronalert(1)James');
  });

  test('16 — player name max length 100 enforced', () => {
    const long = 'A'.repeat(200);
    expect(sanitize(long).length).toBe(100);
  });

  test('17 — SQL injection pattern "drop table" detected', () => {
    expect(detectSqlInjection('DROP TABLE users')).toBe(true);
    expect(detectSqlInjection('LeBron James')).toBe(false);
  });
});

describe('Image Validation', () => {
  test('18 — PNG magic bytes accepted', () => {
    const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
    expect(validateImageMagicBytes(buf)).toBe(true);
  });

  test('19 — JPEG magic bytes accepted', () => {
    const buf = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
    expect(validateImageMagicBytes(buf)).toBe(true);
  });

  test('20 — executable file (MZ header) rejected', () => {
    const buf = Buffer.from([0x4d, 0x5a, 0x90, 0x00]);
    expect(validateImageMagicBytes(buf)).toBe(false);
    expect(isExecutable(buf)).toBe(true);
  });

  test('21 — oversized file (>10MB) rejected', () => {
    const size = 11 * 1024 * 1024;
    expect(size > MAX_IMAGE_SIZE).toBe(true);
  });
});

describe('Parlay Validation', () => {
  test('22 — minimum 2 legs required', () => {
    const result = validateParlayLegs([{ player_name: 'LeBron', stat_type: 'points' }]);
    expect(result.valid).toBe(false);
    expect(result.error).toMatch(/Minimum 2 legs/);
  });

  test('23 — maximum 12 legs enforced', () => {
    const legs = Array.from({ length: 13 }, () => ({ player_name: 'X', stat_type: 'points' }));
    const result = validateParlayLegs(legs);
    expect(result.valid).toBe(false);
    expect(result.error).toMatch(/Maximum 12 legs/);
  });

  test('24 — each leg must have player_name and stat_type', () => {
    const legs = [
      { player_name: 'LeBron', stat_type: 'points' },
      { player_name: 'Steph' }, // missing stat_type
    ];
    const result = validateParlayLegs(legs);
    expect(result.valid).toBe(false);
    expect(result.error).toMatch(/Leg 2 must have/);
  });
});

describe('Security Headers', () => {
  const headers = getSecurityHeaders();

  test('25 — X-Frame-Options is DENY', () => {
    expect(headers['X-Frame-Options']).toBe('DENY');
  });

  test('26 — X-Content-Type-Options is nosniff', () => {
    expect(headers['X-Content-Type-Options']).toBe('nosniff');
  });

  test('27 — Strict-Transport-Security present', () => {
    expect(headers['Strict-Transport-Security']).toBeDefined();
    expect(headers['Strict-Transport-Security']).toMatch(/max-age=/);
  });

  test('28 — Content-Security-Policy present', () => {
    expect(headers['Content-Security-Policy']).toBeDefined();
    expect(headers['Content-Security-Policy']).toContain("default-src");
  });
});

describe('Error Handling', () => {
  test('29 — production error handler returns generic message (no stack trace)', () => {
    const err = new Error('secret DB password in stack');
    err.status = 500;
    const response = productionErrorHandler(err);
    expect(response.body.error).toBe('Internal server error');
    expect(JSON.stringify(response.body)).not.toContain('stack');
    expect(JSON.stringify(response.body)).not.toContain('secret DB password');
  });

  test('30 — 404 returns JSON with Endpoint not found', () => {
    const response = notFoundHandler();
    expect(response.status).toBe(404);
    expect(response.body.error).toBe('Endpoint not found');
  });

  test('31 — 429 returns JSON with Rate limit exceeded', () => {
    const response = rateLimitHandler();
    expect(response.status).toBe(429);
    expect(response.body.error).toBe('Rate limit exceeded');
  });
});

describe('Real IP Extraction', () => {
  test('32 — X-Forwarded-For first IP extracted correctly', () => {
    const ip = extractRealIp({ 'x-forwarded-for': '203.0.113.50, 70.41.3.18, 150.172.238.178' });
    expect(ip).toBe('203.0.113.50');
  });

  test('33 — falls back to remote_addr when no forwarded header', () => {
    const ip = extractRealIp({ remote_addr: '10.0.0.1' });
    expect(ip).toBe('10.0.0.1');
  });
});

describe('CORS', () => {
  test('34 — ALLOWED_ORIGINS parsed from comma-separated env var', () => {
    const origins = parseAllowedOrigins('https://vyndr.app, https://app.vyndr.app');
    expect(origins).toEqual(['https://vyndr.app', 'https://app.vyndr.app']);
  });

  test('35 — default origin is localhost:3000', () => {
    const origins = parseAllowedOrigins(undefined);
    expect(origins).toEqual(['http://localhost:3000']);
  });
});

describe('Environment Check', () => {
  test('36 — missing required var flagged', () => {
    const result = checkEnvVars({}, ['DATABASE_URL', 'JWT_SECRET'], []);
    expect(result.errors.length).toBe(2);
    expect(result.shouldExit).toBe(true);
  });

  test('37 — missing recommended var warns but does not exit', () => {
    const result = checkEnvVars(
      { DATABASE_URL: 'postgres://...' },
      ['DATABASE_URL'],
      ['SENTRY_DSN'],
    );
    expect(result.errors.length).toBe(0);
    expect(result.warnings.length).toBe(1);
    expect(result.shouldExit).toBe(false);
  });
});

describe('Security Logger', () => {
  test('38 — SQL injection pattern detected in request body', () => {
    const body = { player_name: "LeBron'; DROP TABLE users;--" };
    expect(detectSqlInjectionInBody(body)).toBe(true);
  });

  test('39 — rate abuse threshold is 100 req/min', () => {
    expect(RATE_ABUSE_THRESHOLD).toBe(100);
  });

  test('40 — security event cleanup uses 90-day retention', () => {
    expect(RETENTION_DAYS).toBe(90);
  });
});

describe('Security Digest', () => {
  test('41 — digest flags IPs with 50+ events', () => {
    const events = Array.from({ length: 55 }, (_, i) => ({
      ip_address: '203.0.113.50',
      event_type: 'sql_injection',
    }));
    events.push({ ip_address: '10.0.0.1', event_type: 'rate_limit' });
    const digest = buildSecurityDigest(events);
    expect(digest.flaggedIps).toContain('203.0.113.50');
    expect(digest.flaggedIps).not.toContain('10.0.0.1');
  });

  test('42 — digest counts events by type', () => {
    const events = [
      { ip_address: '1.1.1.1', event_type: 'sql_injection' },
      { ip_address: '1.1.1.1', event_type: 'sql_injection' },
      { ip_address: '2.2.2.2', event_type: 'rate_limit' },
    ];
    const digest = buildSecurityDigest(events);
    expect(digest.typeCounts.sql_injection).toBe(2);
    expect(digest.typeCounts.rate_limit).toBe(1);
  });
});

describe('Source Code Scan', () => {
  test('43 — no sk_live_ found in any source file', () => {
    function scan(dir) {
      const entries = fs.readdirSync(dir, { withFileTypes: true });
      for (const e of entries) {
        if (['node_modules', '.git', '__pycache__', '.next'].includes(e.name)) continue;
        const full = path.join(dir, e.name);
        if (e.isDirectory()) scan(full);
        else if (/\.(py|js|ts|json|yml)$/.test(e.name)) {
          const content = fs.readFileSync(full, 'utf8');
          expect(content).not.toContain('sk_live_');
        }
      }
    }
    scan(path.join(__dirname, '../../src'));
  });

  test('44 — .gitignore includes critical patterns', () => {
    const gitignore = fs.readFileSync(path.join(__dirname, '../../.gitignore'), 'utf8');
    expect(gitignore).toContain('.env');
    expect(gitignore).toContain('.env.local');
    expect(gitignore).toContain('.env.production');
    expect(gitignore).toContain('*.pem');
    expect(gitignore).toContain('*.key');
  });
});

describe('Migration 010', () => {
  test('45 — creates security_events table with RLS', () => {
    const sql = fs.readFileSync(
      path.join(__dirname, '../../supabase/migrations/010_security_events.sql'),
      'utf8',
    );
    expect(sql).toContain('CREATE TABLE');
    expect(sql).toContain('security_events');
    expect(sql).toContain('ENABLE ROW LEVEL SECURITY');
    expect(sql).toContain('event_type');
    expect(sql).toContain('ip_address');
    expect(sql).toContain('created_at');
  });
});
