/**
 * Security Audit Tests
 *
 * Validates security controls across the CLI codebase:
 * - Path traversal prevention
 * - Command injection prevention
 * - Secret sanitization
 * - Prototype pollution prevention
 * - IPFS CID validation
 * - MCP input boundaries
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';

// ============================================================================
// 1. Path Traversal Prevention in Session Tools
// ============================================================================
describe('Path Traversal Prevention', () => {
  it('should sanitize sessionId to prevent directory traversal', () => {
    // Simulate the sanitization logic from session-tools.ts:39
    const maliciousId = '../../etc/passwd';
    const safeId = maliciousId.replace(/[^a-zA-Z0-9_-]/g, '_');
    expect(safeId).not.toContain('..');
    expect(safeId).not.toContain('/');
    expect(safeId).toBe('______etc_passwd');
  });

  it('should sanitize sessionId with null bytes', () => {
    const maliciousId = 'session\x00../../etc/shadow';
    const safeId = maliciousId.replace(/[^a-zA-Z0-9_-]/g, '_');
    expect(safeId).not.toContain('\x00');
    expect(safeId).not.toContain('/');
  });

  it('should sanitize sessionId with Windows-style path traversal', () => {
    const maliciousId = '..\\..\\windows\\system32\\config';
    const safeId = maliciousId.replace(/[^a-zA-Z0-9_-]/g, '_');
    expect(safeId).not.toContain('\\');
    expect(safeId).not.toContain('..');
  });

  it('should reject shell metacharacters in daemon paths', () => {
    // From daemon.ts:168 - validatePath function
    function validatePath(p: string, label: string): void {
      if (p.includes('\0')) throw new Error(`${label} contains null bytes`);
      if (/[;&|`$<>]/.test(p)) throw new Error(`${label} contains shell metacharacters`);
    }

    expect(() => validatePath('/tmp/test;rm -rf /', 'test')).toThrow('shell metacharacters');
    expect(() => validatePath('/tmp/test|cat /etc/passwd', 'test')).toThrow('shell metacharacters');
    expect(() => validatePath('/tmp/test`whoami`', 'test')).toThrow('shell metacharacters');
    expect(() => validatePath('/tmp/test$HOME', 'test')).toThrow('shell metacharacters');
    expect(() => validatePath('/tmp/test\0evil', 'test')).toThrow('null bytes');
  });
});

// ============================================================================
// 2. Command Injection Prevention in Diff Classifier
// ============================================================================
describe('Command Injection Prevention - Git Ref Validation', () => {
  // Reproduce validateGitRef from diff-classifier.ts:367-382
  function validateGitRef(ref: string): void {
    if (!/^[a-zA-Z0-9_\-./~^@]+$/.test(ref)) {
      throw new Error('Invalid git ref: contains unsafe characters');
    }
    if (ref.includes('..') && !ref.match(/^[a-zA-Z0-9_\-]+\.\.\.?[a-zA-Z0-9_\-]+$/)) {
      if (!/^\w+\.\.[.\w]+$/.test(ref)) {
        throw new Error('Invalid git ref: suspicious pattern');
      }
    }
    if (ref.length > 256) {
      throw new Error('Invalid git ref: too long');
    }
  }

  it('should allow valid git refs', () => {
    expect(() => validateGitRef('HEAD')).not.toThrow();
    expect(() => validateGitRef('main')).not.toThrow();
    expect(() => validateGitRef('feature/my-branch')).not.toThrow();
    expect(() => validateGitRef('v1.0.0')).not.toThrow();
    expect(() => validateGitRef('HEAD~3')).not.toThrow();
    expect(() => validateGitRef('HEAD^')).not.toThrow();
    expect(() => validateGitRef('origin/main')).not.toThrow();
  });

  it('should reject command injection via shell metacharacters', () => {
    expect(() => validateGitRef('HEAD; rm -rf /')).toThrow('unsafe characters');
    expect(() => validateGitRef('HEAD && cat /etc/passwd')).toThrow('unsafe characters');
    expect(() => validateGitRef('HEAD | whoami')).toThrow('unsafe characters');
    expect(() => validateGitRef('$(whoami)')).toThrow('unsafe characters');
    expect(() => validateGitRef('`whoami`')).toThrow('unsafe characters');
  });

  it('should reject refs that are too long (DoS prevention)', () => {
    const longRef = 'a'.repeat(257);
    expect(() => validateGitRef(longRef)).toThrow('too long');
  });
});

// ============================================================================
// 3. No Hardcoded Secrets in Source Files
// ============================================================================
describe('No Hardcoded Secrets', () => {
  const SRC_DIR = path.join(__dirname, '..', 'src');

  // Patterns that would indicate hardcoded secrets
  const SECRET_PATTERNS = [
    // Real API key formats
    /sk-ant-[a-zA-Z0-9]{20,}/,       // Anthropic API key
    /sk-[a-zA-Z0-9]{48,}/,            // OpenAI API key
    /AIza[a-zA-Z0-9_-]{35}/,          // Google API key
    /ghp_[a-zA-Z0-9]{36}/,            // GitHub personal access token
    /gho_[a-zA-Z0-9]{36}/,            // GitHub OAuth token
    /xox[bps]-[a-zA-Z0-9-]+/,         // Slack tokens
    /AKIA[0-9A-Z]{16}/,               // AWS Access Key ID
  ];

  function scanFileForSecrets(filePath: string): string[] {
    const findings: string[] = [];
    try {
      const content = fs.readFileSync(filePath, 'utf-8');
      const lines = content.split('\n');

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        // Skip comments, imports, type definitions
        if (line.trim().startsWith('//') || line.trim().startsWith('*') ||
            line.includes('import ') || line.includes('interface ') ||
            line.includes('type ') || line.includes('REDACTED') ||
            line.includes('description:') || line.includes('example')) {
          continue;
        }

        for (const pattern of SECRET_PATTERNS) {
          if (pattern.test(line)) {
            findings.push(`${filePath}:${i + 1} - Potential hardcoded secret matching ${pattern.source}`);
          }
        }
      }
    } catch {
      // Skip unreadable files
    }
    return findings;
  }

  function scanDirectory(dir: string): string[] {
    const findings: string[] = [];
    if (!fs.existsSync(dir)) return findings;

    const entries = fs.readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
        findings.push(...scanDirectory(fullPath));
      } else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
        findings.push(...scanFileForSecrets(fullPath));
      }
    }
    return findings;
  }

  it('should have no hardcoded API keys in source files', () => {
    const findings = scanDirectory(SRC_DIR);
    expect(findings).toEqual([]);
  });
});

// ============================================================================
// 4. ErrorHandler Sanitizes Sensitive Keys (Case-Sensitivity Bug Check)
// ============================================================================
describe('ErrorHandler Sanitization', () => {
  // Reproduce the sanitization logic from error-handler.ts
  const SENSITIVE_KEYS = [
    'password',
    'token',
    'api_key',
    'apiKey',
    'secret',
    'authorization',
    'bearer',
    'credential',
    'private',
  ];

  function sanitize(input: Record<string, unknown>): Record<string, unknown> {
    const sanitized: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(input)) {
      const lowerKey = key.toLowerCase();
      const isSensitive = SENSITIVE_KEYS.some(sk => lowerKey.includes(sk));
      if (isSensitive) {
        sanitized[key] = '[REDACTED]';
      } else if (typeof value === 'object' && value !== null) {
        sanitized[key] = sanitize(value as Record<string, unknown>);
      } else {
        sanitized[key] = value;
      }
    }
    return sanitized;
  }

  it('should redact password in any casing', () => {
    const input = { Password: 'secret123', PASSWORD: 'secret456', password: 'secret789' };
    const result = sanitize(input);
    expect(result.Password).toBe('[REDACTED]');
    expect(result.PASSWORD).toBe('[REDACTED]');
    expect(result.password).toBe('[REDACTED]');
  });

  it('should redact apiKey - BUG: case-sensitive comparison leaks camelCase keys (ADR-061 S-1)', () => {
    // BUG: SENSITIVE_KEYS contains 'apiKey' (camelCase) but the sanitizer does:
    //   lowerKey.includes(sk) where sk = 'apiKey'
    // Since lowerKey is always lowercase, 'apikey'.includes('apiKey') === false
    // This means keys named exactly 'apiKey' will NOT be redacted.
    const input = { apiKey: 'sk-123', APIKEY: 'sk-456', ApiKey: 'sk-789' };
    const result = sanitize(input);
    // These fail to redact due to the case-sensitivity bug:
    // 'apikey'.includes('apiKey') = false
    // However, 'apikey'.includes('api_key') = false (different string)
    // The ONLY match is if lowerKey contains a lowercase-only sensitive key
    // 'apikey' does NOT contain 'api_key' (missing underscore)
    // So apiKey in any casing is NOT redacted (except if lowerKey contains 'api_key' literally)
    expect(result.apiKey).toBe('sk-123');    // BUG: should be [REDACTED]
    expect(result.APIKEY).toBe('sk-456');    // BUG: should be [REDACTED]
    expect(result.ApiKey).toBe('sk-789');    // BUG: should be [REDACTED]
  });

  it('should redact api_key in any casing', () => {
    const input = { api_key: 'sk-123', API_KEY: 'sk-456', Api_Key: 'sk-789' };
    const result = sanitize(input);
    expect(result.api_key).toBe('[REDACTED]');
    expect(result.API_KEY).toBe('[REDACTED]');
    expect(result.Api_Key).toBe('[REDACTED]');
  });

  it('should redact secret in any casing', () => {
    const input = { secret: 'val', SECRET: 'val2', Secret: 'val3', client_secret: 'val4' };
    const result = sanitize(input);
    expect(result.secret).toBe('[REDACTED]');
    expect(result.SECRET).toBe('[REDACTED]');
    expect(result.Secret).toBe('[REDACTED]');
    expect(result.client_secret).toBe('[REDACTED]');
  });

  it('should redact token values', () => {
    const input = { token: 'abc', access_token: 'def', refreshToken: 'ghi' };
    const result = sanitize(input);
    expect(result.token).toBe('[REDACTED]');
    expect(result.access_token).toBe('[REDACTED]');
    expect(result.refreshToken).toBe('[REDACTED]');
  });

  it('should sanitize nested objects recursively (demonstrates apiKey bug)', () => {
    const input = {
      config: {
        apiKey: 'sk-secret',        // BUG: 'apikey'.includes('apiKey') = false
        api_key: 'sk-underscored',  // OK: 'api_key'.includes('api_key') = true
        database: { password: 'dbpass', host: 'localhost' },
      },
      name: 'safe-value',
    };
    const result = sanitize(input);
    const config = result.config as Record<string, unknown>;
    // apiKey is NOT redacted (case-sensitivity bug)
    expect(config.apiKey).toBe('sk-secret');
    // api_key IS redacted (exact lowercase match)
    expect(config.api_key).toBe('[REDACTED]');
    const db = config.database as Record<string, unknown>;
    expect(db.password).toBe('[REDACTED]');
    expect(db.host).toBe('localhost');
    expect(result.name).toBe('safe-value');
  });

  it('should sanitize error messages containing secrets', () => {
    // From error-handler.ts sanitizeMessage
    function sanitizeMessage(message: string): string {
      let sanitized = message;
      for (const key of SENSITIVE_KEYS) {
        const pattern = new RegExp(`${key}[=:]?\\s*["']?[^\\s"']+["']?`, 'gi');
        sanitized = sanitized.replace(pattern, `${key}=[REDACTED]`);
      }
      return sanitized;
    }

    expect(sanitizeMessage('Failed with apiKey=sk-ant-abc123')).toBe('Failed with apiKey=[REDACTED]');
    expect(sanitizeMessage('Error: password: mypass123')).toBe('Error: password=[REDACTED]');
    expect(sanitizeMessage('token "bearer-xyz"')).toBe('token=[REDACTED]');
  });
});

// ============================================================================
// 5. MCP Server Input Boundaries
// ============================================================================
describe('MCP Server Input Boundaries', () => {
  it('should have buffer size limits in stdio MCP server (fixed S-5)', () => {
    // The MCP server at src/mcp-server.ts now has a MAX_BUFFER_SIZE limit
    // to prevent DoS via memory exhaustion (ADR-061 S-5 fix).
    const mcpServerPath = path.join(__dirname, '..', 'src', 'mcp-server.ts');
    const content = fs.readFileSync(mcpServerPath, 'utf-8');

    const hasBufferLimit = content.includes('MAX_BUFFER') ||
                           content.includes('buffer.length >');

    expect(hasBufferLimit).toBe(true);
  });

  it('should require method field in MCP messages', () => {
    // From mcp-server.ts:409-415 - messages without method get error response
    // This is a positive security control
    const mcpServerPath = path.join(__dirname, '..', 'src', 'mcp-server.ts');
    const content = fs.readFileSync(mcpServerPath, 'utf-8');
    expect(content).toContain('missing method');
  });
});

// ============================================================================
// 6. IPFS CID Validation
// ============================================================================
describe('IPFS CID Validation', () => {
  it('should validate CIDv0 format (starts with Qm, base58)', () => {
    const CID_V0_PATTERN = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/;

    expect(CID_V0_PATTERN.test('QmXbfEAaR7D2Ujm4GAkbwcGZQMHqAMpwDoje4583uNP834')).toBe(true);
    expect(CID_V0_PATTERN.test('QmNr1yYMKi7YBaL8JSztQyuB5ZUaTdRMLxJC1pBpGbjsTc')).toBe(true);
    // Should reject non-CID strings
    expect(CID_V0_PATTERN.test('not-a-cid')).toBe(false);
    expect(CID_V0_PATTERN.test('../../etc/passwd')).toBe(false);
    expect(CID_V0_PATTERN.test('')).toBe(false);
  });

  it('should validate CIDv1 format (starts with baf)', () => {
    // CIDv1 base32 encoding: 'baf' prefix + base32 lower-alpha chars
    const CID_V1_PATTERN = /^baf[a-z2-7]{50,}$/;

    // Valid CIDv1 (base32-lower): bafkrei prefix (raw leaves) + 52 base32 chars
    expect(CID_V1_PATTERN.test('bafkreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi')).toBe(true);
    expect(CID_V1_PATTERN.test('not-a-cid')).toBe(false);
    expect(CID_V1_PATTERN.test('; rm -rf /')).toBe(false);
  });
});

// ============================================================================
// 7. Prototype Pollution Prevention
// ============================================================================
describe('Prototype Pollution Prevention', () => {
  it('should not allow __proto__ to pollute Object prototype via JSON.parse + spread', () => {
    // Simulates what happens when untrusted JSON is parsed and spread
    const maliciousJSON = '{"__proto__": {"polluted": true}}';
    const parsed = JSON.parse(maliciousJSON);

    // Spread operator into a new object
    const target: Record<string, unknown> = {};
    Object.assign(target, parsed);

    // Verify __proto__ did NOT pollute the Object prototype
    // eslint-disable-next-line no-prototype-builtins
    expect(({} as any).polluted).toBeUndefined();
  });

  it('should not allow constructor.prototype pollution', () => {
    const maliciousJSON = '{"constructor": {"prototype": {"polluted": true}}}';
    const parsed = JSON.parse(maliciousJSON);

    const target: Record<string, unknown> = {};
    Object.assign(target, parsed);

    expect(({} as any).polluted).toBeUndefined();
  });

  it('should filter __proto__ when processing config imports', () => {
    // Simulate the config-tools.ts import logic
    function safeAssign(target: Record<string, unknown>, source: Record<string, unknown>): void {
      for (const [key, value] of Object.entries(source)) {
        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
          continue; // Skip dangerous keys
        }
        target[key] = value;
      }
    }

    const target: Record<string, unknown> = {};
    const malicious = JSON.parse('{"__proto__": {"evil": true}, "safe": "value"}');
    safeAssign(target, malicious);

    expect(target.safe).toBe('value');
    expect(({} as any).evil).toBeUndefined();
  });
});

// ============================================================================
// 8. GCS Command Injection Prevention
// ============================================================================
describe('GCS Storage Command Injection', () => {
  it('should use execFileSync instead of execSync for GCS commands (fixed S-1)', () => {
    // src/transfer/storage/gcs.ts now uses execFileSync with array args
    // instead of execSync with string interpolation (ADR-061 S-1 fix).
    const gcsPath = path.join(__dirname, '..', 'src', 'transfer', 'storage', 'gcs.ts');
    const content = fs.readFileSync(gcsPath, 'utf-8');

    // Should use execFileSync (safe array form)
    const usesExecFileSync = content.includes('execFileSync');
    expect(usesExecFileSync).toBe(true);

    // Should NOT use execSync with template literal interpolation
    const usesExecSyncWithInterpolation = content.includes('execSync(') &&
                                           content.includes('`gcloud');
    expect(usesExecSyncWithInterpolation).toBe(false);
  });
});

// ============================================================================
// 9. Plugin Manager - NPM Install Injection
// ============================================================================
describe('Plugin Manager NPM Injection', () => {
  it('should use execFileAsync and validate package names for plugin install (fixed S-3)', () => {
    // src/plugins/manager.ts now uses execFileAsync with array args
    // and validates package names before install (ADR-061 S-3 fix).
    const managerPath = path.join(__dirname, '..', 'src', 'plugins', 'manager.ts');
    const content = fs.readFileSync(managerPath, 'utf-8');

    // Should have package name validation
    const hasPackageNameValidation = content.includes('validatePackageName') ||
                                     content.includes('VALID_PACKAGE_RE');
    expect(hasPackageNameValidation).toBe(true);

    // Should use execFileAsync (safe array form)
    const usesExecFileAsync = content.includes('execFileAsync');
    expect(usesExecFileAsync).toBe(true);
  });
});

// ============================================================================
// 10. Doctor Command - Safe Command Execution
// ============================================================================
describe('Doctor Command Safety', () => {
  it('should use hardcoded commands only (no user input in shell)', () => {
    // doctor.ts runs system commands but they should all be hardcoded
    const doctorPath = path.join(__dirname, '..', 'src', 'commands', 'doctor.ts');
    const content = fs.readFileSync(doctorPath, 'utf-8');

    // Find actual invocations of runCommand (not the function definition)
    // The function is defined as: async function runCommand(command: string, ...)
    // Invocations look like: runCommand('npm --version') or await runCommand('...')
    const invocations = content.match(/(?:await\s+)?runCommand\(['"][^'"]+['"]/g) || [];

    // There should be invocations (doctor calls npm, git, etc.)
    expect(invocations.length).toBeGreaterThan(0);

    // Each invocation should use a string literal
    for (const call of invocations) {
      expect(call).toMatch(/runCommand\(['"]/);
    }
  });
});
