/**
 * Descomplicar® Crescimento Digital
 * https://descomplicar.pt
 */

<?php

declare(strict_types=1);

namespace DeskMoloni\Tests\Security;

use PHPUnit\Framework\TestCase;

/**
 * Security Test: Encryption and Data Protection
 * 
 * This test MUST FAIL initially as part of TDD methodology.
 * Tests encryption implementation and security vulnerabilities.
 * 
 * @group security
 * @group encryption
 */
class EncryptionSecurityTest extends TestCase
{
    private array $testConfig;
    private \DeskMoloni\Encryption $encryption;

    protected function setUp(): void
    {
        global $testConfig;
        $this->testConfig = $testConfig;
        
        // This will fail initially until Encryption class is implemented
        $this->encryption = new \DeskMoloni\Encryption($testConfig['encryption']);
    }

    /**
     * Test OAuth token encryption and decryption
     * This test will initially fail until encryption implementation exists
     */
    public function testOAuthTokenEncryption(): void
    {
        $originalToken = 'oauth_access_token_example_12345';
        
        // Test encryption
        $encrypted = $this->encryption->encrypt($originalToken);
        
        $this->assertIsString($encrypted);
        $this->assertNotEquals($originalToken, $encrypted);
        $this->assertGreaterThan(strlen($originalToken), strlen($encrypted));
        
        // Test decryption
        $decrypted = $this->encryption->decrypt($encrypted);
        
        $this->assertEquals($originalToken, $decrypted);
    }

    /**
     * Test encryption key rotation security
     */
    public function testEncryptionKeyRotation(): void
    {
        $sensitiveData = 'client_secret_sensitive_data';
        
        // Encrypt with current key
        $encrypted1 = $this->encryption->encrypt($sensitiveData);
        
        // Simulate key rotation
        $newKey = bin2hex(random_bytes(32));
        $this->encryption->rotateKey($newKey);
        
        // Should still be able to decrypt old data
        $decrypted1 = $this->encryption->decrypt($encrypted1);
        $this->assertEquals($sensitiveData, $decrypted1);
        
        // New encryptions should use new key
        $encrypted2 = $this->encryption->encrypt($sensitiveData);
        $this->assertNotEquals($encrypted1, $encrypted2);
        
        // Both should decrypt to same value
        $decrypted2 = $this->encryption->decrypt($encrypted2);
        $this->assertEquals($sensitiveData, $decrypted2);
    }

    /**
     * Test encryption algorithm security (AES-256-GCM)
     */
    public function testEncryptionAlgorithmSecurity(): void
    {
        $testData = 'sensitive_api_credentials';
        
        // Test multiple encryptions produce different results (due to IV)
        $encrypted1 = $this->encryption->encrypt($testData);
        $encrypted2 = $this->encryption->encrypt($testData);
        
        $this->assertNotEquals($encrypted1, $encrypted2, 'Same plaintext should produce different ciphertext due to random IV');
        
        // Both should decrypt to same value
        $this->assertEquals($testData, $this->encryption->decrypt($encrypted1));
        $this->assertEquals($testData, $this->encryption->decrypt($encrypted2));
        
        // Test encryption metadata
        $metadata = $this->encryption->getEncryptionMetadata($encrypted1);
        
        $this->assertIsArray($metadata);
        $this->assertArrayHasKey('algorithm', $metadata);
        $this->assertArrayHasKey('iv_length', $metadata);
        $this->assertArrayHasKey('tag_length', $metadata);
        $this->assertEquals('AES-256-GCM', $metadata['algorithm']);
        $this->assertEquals(12, $metadata['iv_length']); // GCM standard IV length
        $this->assertEquals(16, $metadata['tag_length']); // GCM tag length
    }

    /**
     * Test encryption key strength requirements
     */
    public function testEncryptionKeyStrength(): void
    {
        // Test weak key rejection
        $weakKeys = [
            'weak',
            '12345678',
            str_repeat('a', 16),
            'password123'
        ];
        
        foreach ($weakKeys as $weakKey) {
            $this->expectException(\InvalidArgumentException::class);
            $this->expectExceptionMessage('Encryption key does not meet security requirements');
            
            new \DeskMoloni\Encryption(['key' => $weakKey, 'cipher' => 'AES-256-GCM']);
        }
        
        // Test strong key acceptance
        $strongKey = bin2hex(random_bytes(32)); // 256-bit key
        $strongEncryption = new \DeskMoloni\Encryption(['key' => $strongKey, 'cipher' => 'AES-256-GCM']);
        
        $this->assertInstanceOf(\DeskMoloni\Encryption::class, $strongEncryption);
    }

    /**
     * Test encryption tampering detection
     */
    public function testEncryptionTamperingDetection(): void
    {
        $originalData = 'tamper_test_data';
        $encrypted = $this->encryption->encrypt($originalData);
        
        // Test various tampering scenarios
        $tamperedVersions = [
            substr($encrypted, 0, -1), // Remove last character
            $encrypted . 'x', // Add character
            substr($encrypted, 1), // Remove first character
            str_replace('A', 'B', $encrypted, 1) // Change one character
        ];
        
        foreach ($tamperedVersions as $tampered) {
            $this->expectException(\DeskMoloni\Exceptions\EncryptionException::class);
            $this->expectExceptionMessage('Data integrity verification failed');
            
            $this->encryption->decrypt($tampered);
        }
    }

    /**
     * Test secure configuration storage
     */
    public function testSecureConfigurationStorage(): void
    {
        $configManager = new \DeskMoloni\ConfigManager($this->encryption);
        
        // Test storing sensitive configuration
        $sensitiveConfig = [
            'moloni_client_secret' => 'very_secret_value',
            'oauth_refresh_token' => 'refresh_token_12345',
            'webhook_secret' => 'webhook_secret_key'
        ];
        
        foreach ($sensitiveConfig as $key => $value) {
            $configManager->set($key, $value, true); // true = encrypt
        }
        
        // Verify data is encrypted in storage
        $pdo = new \PDO(
            "mysql:host={$this->testConfig['database']['hostname']};dbname={$this->testConfig['database']['database']}",
            $this->testConfig['database']['username'],
            $this->testConfig['database']['password'],
            [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
        );
        
        foreach ($sensitiveConfig as $key => $originalValue) {
            $stmt = $pdo->prepare("SELECT setting_value, encrypted FROM tbl_desk_moloni_config WHERE setting_key = ?");
            $stmt->execute([$key]);
            $stored = $stmt->fetch();
            
            $this->assertNotFalse($stored, "Config {$key} should be stored");
            $this->assertEquals(1, $stored['encrypted'], "Config {$key} should be marked as encrypted");
            $this->assertNotEquals($originalValue, $stored['setting_value'], "Config {$key} should not be stored in plaintext");
            
            // Verify retrieval works
            $retrieved = $configManager->get($key);
            $this->assertEquals($originalValue, $retrieved, "Config {$key} should decrypt correctly");
        }
    }

    /**
     * Test password/token validation and security
     */
    public function testPasswordTokenValidation(): void
    {
        $validator = new \DeskMoloni\SecurityValidator();
        
        // Test OAuth token validation
        $validTokens = [
            'valid_oauth_token_123456789',
            'Bearer_token_abcdef123456',
            str_repeat('a', 64) // Long alphanumeric token
        ];
        
        $invalidTokens = [
            '', // Empty
            'short', // Too short
            'token with spaces', // Contains spaces
            'token<script>', // Contains dangerous characters
            str_repeat('a', 1000) // Too long
        ];
        
        foreach ($validTokens as $token) {
            $this->assertTrue($validator->isValidOAuthToken($token), "Token should be valid: {$token}");
        }
        
        foreach ($invalidTokens as $token) {
            $this->assertFalse($validator->isValidOAuthToken($token), "Token should be invalid: {$token}");
        }
    }

    /**
     * Test SQL injection prevention in encrypted data
     */
    public function testSqlInjectionPrevention(): void
    {
        $maliciousInputs = [
            "'; DROP TABLE tbl_desk_moloni_config; --",
            "1' OR '1'='1",
            "'; UPDATE tbl_desk_moloni_config SET setting_value='hacked'; --",
            "<script>alert('xss')</script>",
            "../../etc/passwd"
        ];
        
        $configManager = new \DeskMoloni\ConfigManager($this->encryption);
        
        foreach ($maliciousInputs as $maliciousInput) {
            // Should be able to store and retrieve malicious input safely
            $configManager->set('test_malicious', $maliciousInput, true);
            $retrieved = $configManager->get('test_malicious');
            
            $this->assertEquals($maliciousInput, $retrieved, 'Malicious input should be safely stored and retrieved');
            
            // Verify no SQL injection occurred
            $pdo = new \PDO(
                "mysql:host={$this->testConfig['database']['hostname']};dbname={$this->testConfig['database']['database']}",
                $this->testConfig['database']['username'],
                $this->testConfig['database']['password'],
                [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
            );
            
            $stmt = $pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_config");
            $count = $stmt->fetch();
            
            $this->assertGreaterThan(0, $count['count'], 'Table should not be dropped or corrupted');
        }
    }

    /**
     * Test timing attack resistance
     */
    public function testTimingAttackResistance(): void
    {
        $correctPassword = 'correct_password_123';
        $incorrectPasswords = [
            'wrong_password_123',
            'correct_password_124', // Very similar
            'x', // Very different length
            str_repeat('x', strlen($correctPassword)) // Same length, different content
        ];
        
        $validator = new \DeskMoloni\SecurityValidator();
        
        // Measure timing for correct password
        $startTime = microtime(true);
        $validator->verifyPassword($correctPassword, password_hash($correctPassword, PASSWORD_ARGON2ID));
        $correctTime = microtime(true) - $startTime;
        
        // Measure timing for incorrect passwords
        $incorrectTimes = [];
        foreach ($incorrectPasswords as $incorrectPassword) {
            $startTime = microtime(true);
            $validator->verifyPassword($incorrectPassword, password_hash($correctPassword, PASSWORD_ARGON2ID));
            $incorrectTimes[] = microtime(true) - $startTime;
        }
        
        // Calculate timing variance
        $avgIncorrectTime = array_sum($incorrectTimes) / count($incorrectTimes);
        $timingDifference = abs($correctTime - $avgIncorrectTime);
        $maxAllowedDifference = 0.01; // 10ms tolerance
        
        $this->assertLessThan(
            $maxAllowedDifference,
            $timingDifference,
            'Password verification should be resistant to timing attacks'
        );
    }

    /**
     * Test secure random generation
     */
    public function testSecureRandomGeneration(): void
    {
        $randomGenerator = new \DeskMoloni\SecureRandom();
        
        // Test token generation
        $tokens = [];
        for ($i = 0; $i < 100; $i++) {
            $token = $randomGenerator->generateToken(32);
            
            $this->assertEquals(32, strlen($token));
            $this->assertNotContains($token, $tokens, 'Generated tokens should be unique');
            $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]+$/', $token, 'Token should be alphanumeric');
            
            $tokens[] = $token;
        }
        
        // Test cryptographic randomness quality
        $randomBytes = $randomGenerator->generateBytes(1000);
        $this->assertEquals(1000, strlen($randomBytes));
        
        // Basic entropy test - should not have long runs of same byte
        $maxRun = 0;
        $currentRun = 1;
        $prevByte = ord($randomBytes[0]);
        
        for ($i = 1; $i < strlen($randomBytes); $i++) {
            $currentByte = ord($randomBytes[$i]);
            if ($currentByte === $prevByte) {
                $currentRun++;
                $maxRun = max($maxRun, $currentRun);
            } else {
                $currentRun = 1;
            }
            $prevByte = $currentByte;
        }
        
        $this->assertLessThan(10, $maxRun, 'Random data should not have long runs of identical bytes');
    }

    /**
     * Test data sanitization and validation
     */
    public function testDataSanitizationAndValidation(): void
    {
        $sanitizer = new \DeskMoloni\DataSanitizer();
        
        $testCases = [
            // [input, expected_output, description]
            ['<script>alert("xss")</script>', 'alert("xss")', 'Should remove script tags'],
            ['SELECT * FROM users', 'SELECT * FROM users', 'Should allow safe SQL in strings'],
            ['user@example.com', 'user@example.com', 'Should preserve valid email'],
            ['user@<script>evil</script>.com', 'user@evil.com', 'Should sanitize email with XSS'],
            ['+351910000000', '+351910000000', 'Should preserve valid phone'],
            ['javascript:alert(1)', 'alert(1)', 'Should remove javascript protocol'],
            ["'; DROP TABLE users; --", "'; DROP TABLE users; --", 'Should escape SQL injection attempts']
        ];
        
        foreach ($testCases as [$input, $expectedOutput, $description]) {
            $sanitized = $sanitizer->sanitizeString($input);
            $this->assertEquals($expectedOutput, $sanitized, $description);
        }
        
        // Test specific field validation
        $this->assertTrue($sanitizer->isValidEmail('test@example.com'));
        $this->assertFalse($sanitizer->isValidEmail('invalid-email'));
        
        $this->assertTrue($sanitizer->isValidPhone('+351910000000'));
        $this->assertFalse($sanitizer->isValidPhone('not-a-phone'));
        
        $this->assertTrue($sanitizer->isValidVAT('123456789'));
        $this->assertFalse($sanitizer->isValidVAT('invalid-vat'));
    }

    protected function tearDown(): void
    {
        // Clean up test configuration
        $pdo = new \PDO(
            "mysql:host={$this->testConfig['database']['hostname']};dbname={$this->testConfig['database']['database']}",
            $this->testConfig['database']['username'],
            $this->testConfig['database']['password'],
            [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
        );
        
        $pdo->exec("DELETE FROM tbl_desk_moloni_config WHERE setting_key LIKE 'test_%' OR setting_key IN ('moloni_client_secret', 'oauth_refresh_token', 'webhook_secret')");
    }
}