#!/usr/bin/env node
/**
 * Automated Testing Script for DashCaddy Security Fixes
 *
 * Tests all implemented security improvements:
 * 1. Path traversal protection
 * 2. Request size limits
 * 3. Startup validation
 * 4. Port locking
 * 5. Session management (LRU cache)
 * 6. Enhanced error logging
 * 7. Hardcoded secrets removal
 */

const http = require('http');
const https = require('https');
const crypto = require('crypto');

const API_BASE = process.env.API_BASE || 'http://localhost:3001';
const TEST_RESULTS = [];

// Color codes for terminal output
const colors = {
  reset: '\x1b[0m',
  green: '\x1b[32m',
  red: '\x1b[31m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  cyan: '\x1b[36m'
};

function log(message, color = 'reset') {
  console.log(`${colors[color]}${message}${colors.reset}`);
}

function logTest(name) {
  console.log(`\n${colors.cyan}━━━ Testing: ${name} ━━━${colors.reset}`);
}

function logResult(passed, message) {
  const icon = passed ? '✓' : '✗';
  const color = passed ? 'green' : 'red';
  log(`  ${icon} ${message}`, color);
  TEST_RESULTS.push({ passed, message });
}

async function makeRequest(path, options = {}) {
  return new Promise((resolve, reject) => {
    const url = new URL(path, API_BASE);
    const isHttps = url.protocol === 'https:';
    const client = isHttps ? https : http;

    const requestOptions = {
      hostname: url.hostname,
      port: url.port || (isHttps ? 443 : 80),
      path: url.pathname + url.search,
      method: options.method || 'GET',
      headers: options.headers || {},
      ...options
    };

    const req = client.request(requestOptions, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        resolve({
          statusCode: res.statusCode,
          headers: res.headers,
          body: data,
          data: data ? (data.startsWith('{') || data.startsWith('[') ? JSON.parse(data) : data) : null
        });
      });
    });

    req.on('error', reject);

    if (options.body) {
      req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
    }

    req.end();
  });
}

// Test 1: Path Traversal Protection
async function testPathTraversal() {
  logTest('Path Traversal Protection');

  const attacks = [
    { path: '/api/browse/directories?path=../../../../../../etc/passwd', desc: 'Unix path traversal' },
    { path: '/api/browse/directories?path=..\\..\\..\\Windows\\System32', desc: 'Windows path traversal' },
    { path: '/api/browse/directories?path=%2e%2e%2f%2e%2e%2fetc%2fpasswd', desc: 'URL-encoded traversal' },
    { path: '/api/browse/directories?path=/allowed/media/../../../secrets', desc: 'Mixed path traversal' }
  ];

  for (const attack of attacks) {
    try {
      const response = await makeRequest(attack.path);
      if (response.statusCode === 403 || response.statusCode === 400) {
        logResult(true, `Blocked: ${attack.desc}`);
      } else {
        logResult(false, `NOT BLOCKED (${response.statusCode}): ${attack.desc}`);
      }
    } catch (error) {
      logResult(false, `Error testing ${attack.desc}: ${error.message}`);
    }
  }
}

// Test 2: Request Size Limits
async function testRequestSizeLimits() {
  logTest('Request Size Limits');

  // Test 1: Small payload (should work)
  try {
    const smallPayload = { data: 'a'.repeat(100) };
    const response = await makeRequest('/api/services', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(smallPayload)
    });
    logResult(true, 'Small payload accepted (100 bytes)');
  } catch (error) {
    logResult(false, `Small payload rejected: ${error.message}`);
  }

  // Test 2: Large payload on general endpoint (should fail)
  try {
    const largePayload = { data: 'a'.repeat(2 * 1024 * 1024) }; // 2MB
    const response = await makeRequest('/api/services', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(largePayload)
    });
    if (response.statusCode === 413 || response.statusCode === 400) {
      logResult(true, 'Large payload rejected on general endpoint (2MB)');
    } else {
      logResult(false, `Large payload NOT rejected (status: ${response.statusCode})`);
    }
  } catch (error) {
    if (error.message.includes('413') || error.message.includes('ECONNRESET')) {
      logResult(true, 'Large payload rejected (connection reset)');
    } else {
      logResult(false, `Unexpected error: ${error.message}`);
    }
  }

  // Test 3: Large payload on logo endpoint (should work)
  try {
    const largeImage = 'a'.repeat(5 * 1024 * 1024); // 5MB
    const response = await makeRequest('/api/logo', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ logo: largeImage })
    });
    if (response.statusCode !== 413) {
      logResult(true, 'Large payload accepted on logo endpoint (5MB)');
    } else {
      logResult(false, 'Large payload rejected on logo endpoint');
    }
  } catch (error) {
    // May fail for other reasons (auth, validation), but not size
    if (!error.message.includes('413')) {
      logResult(true, 'Logo endpoint accepts large payloads (failed for non-size reason)');
    } else {
      logResult(false, `Logo endpoint rejected large payload: ${error.message}`);
    }
  }
}

// Test 3: Startup Validation
async function testStartupValidation() {
  logTest('Startup Validation');

  // Check if server is running (implies validation passed)
  try {
    const response = await makeRequest('/health');
    if (response.statusCode === 200) {
      logResult(true, 'Server started successfully (validation passed)');
    } else {
      logResult(false, `Server health check failed: ${response.statusCode}`);
    }
  } catch (error) {
    logResult(false, `Cannot reach server: ${error.message}`);
  }

  // Check for validation logs (requires access to logs)
  log('  → Check Docker logs for: "✓ Startup configuration validation passed"', 'yellow');
}

// Test 4: Enhanced Error Logging (Request ID)
async function testEnhancedLogging() {
  logTest('Enhanced Error Logging');

  try {
    // Make a request that will be logged
    const response = await makeRequest('/api/services');

    // Check if X-Request-ID header is present
    if (response.headers['x-request-id']) {
      const requestId = response.headers['x-request-id'];
      const isValidUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(requestId);

      if (isValidUUID) {
        logResult(true, `Request ID header present and valid: ${requestId.substring(0, 8)}...`);
      } else {
        logResult(false, `Request ID present but invalid format: ${requestId}`);
      }
    } else {
      logResult(false, 'Request ID header not present');
    }
  } catch (error) {
    logResult(false, `Error testing logging: ${error.message}`);
  }
}

// Test 5: Session Management (LRU Cache)
async function testSessionManagement() {
  logTest('Session Management (LRU Cache)');

  log('  → This test requires code inspection (cannot test cache behavior externally)', 'yellow');
  log('  → Manual verification: Check server.js for LRUCache usage', 'yellow');

  // We can test that sessions still work
  try {
    const response = await makeRequest('/api/totp/setup', { method: 'POST' });
    if (response.statusCode === 200 || response.statusCode === 401) {
      logResult(true, 'Session-based endpoints still functional');
    } else {
      logResult(false, `Unexpected response from session endpoint: ${response.statusCode}`);
    }
  } catch (error) {
    logResult(false, `Error testing session endpoints: ${error.message}`);
  }
}

// Test 6: Hardcoded Secrets Removal
async function testSecretsRemoval() {
  logTest('Hardcoded Secrets Removal');

  try {
    // Read app-templates.js and check for "changeme123"
    const fs = require('fs');
    const templatesPath = require('path').join(__dirname, 'app-templates.js');
    const content = fs.readFileSync(templatesPath, 'utf8');

    const matches = content.match(/changeme123/g);
    if (!matches || matches.length === 0) {
      logResult(true, 'No hardcoded "changeme123" passwords found');
    } else {
      logResult(false, `Found ${matches.length} instances of "changeme123" still in templates`);
    }

    // Check for secrets arrays
    const secretsMatches = content.match(/secrets:\s*\[/g);
    if (secretsMatches && secretsMatches.length >= 10) {
      logResult(true, `Found ${secretsMatches.length} secrets configurations`);
    } else {
      logResult(false, `Only found ${secretsMatches?.length || 0} secrets configurations (expected 14+)`);
    }
  } catch (error) {
    logResult(false, `Error reading templates: ${error.message}`);
  }
}

// Test 7: Port Locking Mechanism
async function testPortLocking() {
  logTest('Port Locking Mechanism');

  try {
    // Check if .port-locks directory exists
    const fs = require('fs');
    const path = require('path');
    const locksDir = path.join(__dirname, '.port-locks');

    if (fs.existsSync(locksDir)) {
      logResult(true, 'Port locks directory exists');

      // Check if it's writable
      try {
        const testFile = path.join(locksDir, 'test-write');
        fs.writeFileSync(testFile, 'test');
        fs.unlinkSync(testFile);
        logResult(true, 'Port locks directory is writable');
      } catch (error) {
        logResult(false, `Port locks directory not writable: ${error.message}`);
      }
    } else {
      logResult(false, 'Port locks directory does not exist');
    }

    // Check if PortLockManager module exists
    const portLockPath = path.join(__dirname, 'port-lock-manager.js');
    if (fs.existsSync(portLockPath)) {
      logResult(true, 'PortLockManager module exists');
    } else {
      logResult(false, 'PortLockManager module not found');
    }
  } catch (error) {
    logResult(false, `Error testing port locking: ${error.message}`);
  }
}

// Test 8: Docker Security Module
async function testDockerSecurity() {
  logTest('Docker Image Verification');

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

    // Check if docker-security.js exists
    const securityPath = path.join(__dirname, 'docker-security.js');
    if (fs.existsSync(securityPath)) {
      logResult(true, 'DockerSecurity module exists');
    } else {
      logResult(false, 'DockerSecurity module not found');
    }

    // Check if config file exists
    const configPath = path.join(__dirname, 'docker-security-config.json');
    if (fs.existsSync(configPath)) {
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      logResult(true, `Security config exists (mode: ${config.verificationMode || 'not set'})`);
    } else {
      log('  → Security config will be created on first use', 'yellow');
      logResult(true, 'Config will be auto-created');
    }
  } catch (error) {
    logResult(false, `Error testing Docker security: ${error.message}`);
  }
}

// Main test runner
async function runTests() {
  log('\n╔════════════════════════════════════════════════════╗', 'cyan');
  log('║  DashCaddy Security Fixes - Test Suite           ║', 'cyan');
  log('╚════════════════════════════════════════════════════╝', 'cyan');

  log(`\nAPI Base URL: ${API_BASE}`, 'blue');
  log('Starting tests...\n', 'blue');

  // Run all tests
  await testStartupValidation();
  await testPathTraversal();
  await testRequestSizeLimits();
  await testEnhancedLogging();
  await testSessionManagement();
  await testSecretsRemoval();
  await testPortLocking();
  await testDockerSecurity();

  // Summary
  log('\n╔════════════════════════════════════════════════════╗', 'cyan');
  log('║  Test Summary                                     ║', 'cyan');
  log('╚════════════════════════════════════════════════════╝', 'cyan');

  const passed = TEST_RESULTS.filter(r => r.passed).length;
  const failed = TEST_RESULTS.filter(r => !r.passed).length;
  const total = TEST_RESULTS.length;

  log(`\nTotal Tests: ${total}`, 'blue');
  log(`Passed: ${passed}`, 'green');
  log(`Failed: ${failed}`, failed > 0 ? 'red' : 'green');
  log(`Success Rate: ${((passed / total) * 100).toFixed(1)}%\n`, failed === 0 ? 'green' : 'yellow');

  if (failed > 0) {
    log('Failed tests:', 'red');
    TEST_RESULTS.filter(r => !r.passed).forEach(r => {
      log(`  ✗ ${r.message}`, 'red');
    });
  }

  process.exit(failed > 0 ? 1 : 0);
}

// Run tests if executed directly
if (require.main === module) {
  runTests().catch(error => {
    log(`\nFatal error: ${error.message}`, 'red');
    console.error(error);
    process.exit(1);
  });
}

module.exports = { runTests };
