<?php

declare(strict_types=1);

use App\Framework\LiveComponents\Serialization\EncryptedStateSerializer;
use App\Framework\LiveComponents\Serialization\StateEncryptor;
use App\Framework\LiveComponents\Exceptions\StateEncryptionException;
use App\Framework\StateManagement\SerializableState;
use App\Framework\Cryptography\CryptographicUtilities;
use App\Framework\Random\SecureRandomGenerator;

// Test State Value Object
final readonly class TestEncryptableState implements SerializableState
{
    public function __construct(
        public int $count,
        public string $message,
        public ?string $optional = null
    ) {
    }

    public function toArray(): array
    {
        return [
            'count' => $this->count,
            'message' => $this->message,
            'optional' => $this->optional,
        ];
    }

    public static function fromArray(array $data): self
    {
        return new self(
            count: $data['count'] ?? 0,
            message: $data['message'] ?? '',
            optional: $data['optional'] ?? null
        );
    }
}

describe('EncryptedStateSerializer', function () {
    beforeEach(function () {
        // Use framework's RandomGenerator for cryptographically secure key
        $this->random = new SecureRandomGenerator();
        $this->crypto = new CryptographicUtilities($this->random);

        // Generate valid 32-byte encryption key using framework's RandomGenerator
        $this->encryptionKey = $this->random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);

        $this->encryptor = new StateEncryptor(
            $this->encryptionKey,
            $this->crypto,
            $this->random
        );

        $this->serializer = new EncryptedStateSerializer($this->encryptor);

        $this->testState = new TestEncryptableState(
            count: 42,
            message: 'Hello, encrypted world!',
            optional: 'optional data'
        );
    });

    it('serializes and encrypts state to string', function () {
        $encrypted = $this->serializer->serialize($this->testState);

        expect($encrypted)->toBeString();
        expect($encrypted)->not->toBeEmpty();

        // Encrypted data should be base64-encoded
        $decoded = base64_decode($encrypted, strict: true);
        expect($decoded)->not->toBeFalse();

        // Should be longer than original due to nonce and MAC
        expect(strlen($encrypted))->toBeGreaterThan(50);
    });

    it('deserializes and decrypts state correctly', function () {
        $encrypted = $this->serializer->serialize($this->testState);

        $decrypted = $this->serializer->deserialize($encrypted, TestEncryptableState::class);

        expect($decrypted)->toBeInstanceOf(TestEncryptableState::class);
        expect($decrypted->count)->toBe(42);
        expect($decrypted->message)->toBe('Hello, encrypted world!');
        expect($decrypted->optional)->toBe('optional data');
    });

    it('produces different ciphertext for same state due to unique nonces', function () {
        $encrypted1 = $this->serializer->serialize($this->testState);
        $encrypted2 = $this->serializer->serialize($this->testState);

        // Same plaintext should produce different ciphertext (unique nonces)
        expect($encrypted1)->not->toBe($encrypted2);

        // But both should decrypt to same state
        $decrypted1 = $this->serializer->deserialize($encrypted1, TestEncryptableState::class);
        $decrypted2 = $this->serializer->deserialize($encrypted2, TestEncryptableState::class);

        expect($decrypted1->count)->toBe($decrypted2->count);
        expect($decrypted1->message)->toBe($decrypted2->message);
    });

    it('handles state with null optional field', function () {
        $state = new TestEncryptableState(
            count: 10,
            message: 'test',
            optional: null
        );

        $encrypted = $this->serializer->serialize($state);
        $decrypted = $this->serializer->deserialize($encrypted, TestEncryptableState::class);

        expect($decrypted->optional)->toBeNull();
    });

    it('handles state with special characters', function () {
        $state = new TestEncryptableState(
            count: 123,
            message: "Special chars: 🔐 <script>alert('xss')</script> '; DROP TABLE users;--",
            optional: "Unicode: 你好世界 مرحبا العالم"
        );

        $encrypted = $this->serializer->serialize($state);
        $decrypted = $this->serializer->deserialize($encrypted, TestEncryptableState::class);

        expect($decrypted->message)->toBe($state->message);
        expect($decrypted->optional)->toBe($state->optional);
    });

    it('throws exception for invalid encrypted data', function () {
        $this->serializer->deserialize('invalid-base64-data', TestEncryptableState::class);
    })->throws(StateEncryptionException::class);

    it('throws exception for tampered ciphertext', function () {
        $encrypted = $this->serializer->serialize($this->testState);

        // Tamper with encrypted data
        $tamperedEncrypted = substr($encrypted, 0, -5) . 'XXXXX';

        $this->serializer->deserialize($tamperedEncrypted, TestEncryptableState::class);
    })->throws(StateEncryptionException::class);

    it('throws exception for invalid state class', function () {
        $encrypted = $this->serializer->serialize($this->testState);

        // Try to deserialize to wrong class
        $this->serializer->deserialize($encrypted, \stdClass::class);
    })->throws(StateEncryptionException::class, 'must implement fromArray');

    it('throws exception for state without toArray method', function () {
        $invalidState = new class {
            public int $value = 42;
        };

        $this->serializer->serialize($invalidState);
    })->throws(StateEncryptionException::class, 'must implement toArray');

    it('throws exception for corrupted JSON in ciphertext', function () {
        // Create manually corrupted encrypted data with valid structure but invalid JSON
        $nonce = random_bytes(24);
        $invalidJson = '{invalid json}';
        $ciphertext = sodium_crypto_secretbox($invalidJson, $nonce, $this->encryptionKey);

        $versionByte = chr(1);
        $encrypted = base64_encode($versionByte . $nonce . $ciphertext);

        $this->serializer->deserialize($encrypted, TestEncryptableState::class);
    })->throws(StateEncryptionException::class);

    it('preserves data types through encryption cycle', function () {
        $state = new TestEncryptableState(
            count: 0,  // Zero value
            message: '',  // Empty string
            optional: null
        );

        $encrypted = $this->serializer->serialize($state);
        $decrypted = $this->serializer->deserialize($encrypted, TestEncryptableState::class);

        expect($decrypted->count)->toBe(0);
        expect($decrypted->count)->toBeInt();
        expect($decrypted->message)->toBe('');
        expect($decrypted->message)->toBeString();
        expect($decrypted->optional)->toBeNull();
    });

    it('handles large state objects', function () {
        $largeMessage = str_repeat('A', 10000);  // 10KB string

        $state = new TestEncryptableState(
            count: 999999,
            message: $largeMessage,
            optional: str_repeat('B', 5000)
        );

        $encrypted = $this->serializer->serialize($state);
        $decrypted = $this->serializer->deserialize($encrypted, TestEncryptableState::class);

        expect($decrypted->message)->toBe($largeMessage);
        expect(strlen($decrypted->message))->toBe(10000);
    });
});

describe('StateEncryptor', function () {
    beforeEach(function () {
        $this->random = new SecureRandomGenerator();
        $this->crypto = new CryptographicUtilities($this->random);
        $this->encryptionKey = $this->random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);

        $this->encryptor = new StateEncryptor(
            $this->encryptionKey,
            $this->crypto,
            $this->random
        );
    });

    it('encrypts and decrypts plaintext correctly', function () {
        $plaintext = 'Secret message';

        $encrypted = $this->encryptor->encrypt($plaintext);
        $decrypted = $this->encryptor->decrypt($encrypted);

        expect($decrypted)->toBe($plaintext);
    });

    it('produces different ciphertext for same plaintext', function () {
        $plaintext = 'Same message';

        $encrypted1 = $this->encryptor->encrypt($plaintext);
        $encrypted2 = $this->encryptor->encrypt($plaintext);

        expect($encrypted1)->not->toBe($encrypted2);

        expect($this->encryptor->decrypt($encrypted1))->toBe($plaintext);
        expect($this->encryptor->decrypt($encrypted2))->toBe($plaintext);
    });

    it('throws exception for invalid encryption key length', function () {
        new StateEncryptor(
            'too-short-key',  // Invalid key length
            $this->crypto,
            $this->random
        );
    })->throws(\InvalidArgumentException::class, 'Encryption key must be');

    it('throws exception for weak encryption key', function () {
        $random = new SecureRandomGenerator();
        $crypto = new CryptographicUtilities($random);

        // All zeros - weak key (insufficient entropy)
        $weakKey = str_repeat("\0", SODIUM_CRYPTO_SECRETBOX_KEYBYTES);

        new StateEncryptor(
            $weakKey,
            $crypto,
            $random
        );
    })->throws(\InvalidArgumentException::class, 'insufficient entropy');

    it('detects MAC tampering', function () {
        $encrypted = $this->encryptor->encrypt('original message');

        // Tamper with MAC by modifying last bytes
        $decoded = base64_decode($encrypted, strict: true);
        $tampered = substr($decoded, 0, -5) . 'XXXXX';
        $tamperedEncrypted = base64_encode($tampered);

        $this->encryptor->decrypt($tamperedEncrypted);
    })->throws(StateEncryptionException::class, 'MAC verification failed');

    it('throws exception for corrupted base64', function () {
        $this->encryptor->decrypt('!!!invalid-base64!!!');
    })->throws(StateEncryptionException::class);

    it('throws exception for invalid encryption version', function () {
        // Create encrypted data with invalid version byte
        $nonce = random_bytes(24);
        $plaintext = 'test';
        $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->encryptionKey);

        $invalidVersion = chr(99);  // Invalid version
        $encrypted = base64_encode($invalidVersion . $nonce . $ciphertext);

        $this->encryptor->decrypt($encrypted);
    })->throws(StateEncryptionException::class, 'Unsupported encryption version');

    it('handles empty plaintext', function () {
        $encrypted = $this->encryptor->encrypt('');
        $decrypted = $this->encryptor->decrypt($encrypted);

        expect($decrypted)->toBe('');
    });

    it('handles unicode plaintext', function () {
        $plaintext = '🔐 Encrypted Unicode: 你好世界 مرحبا العالم';

        $encrypted = $this->encryptor->encrypt($plaintext);
        $decrypted = $this->encryptor->decrypt($encrypted);

        expect($decrypted)->toBe($plaintext);
    });

    it('validates encrypted data format', function () {
        $encrypted = $this->encryptor->encrypt('test');

        expect($this->encryptor->isEncrypted($encrypted))->toBeTrue();
        expect($this->encryptor->isEncrypted('not-encrypted'))->toBeFalse();
        expect($this->encryptor->isEncrypted(''))->toBeFalse();
    });
});

describe('StateEncryptor Security', function () {
    beforeEach(function () {
        $this->random = new SecureRandomGenerator();
        $this->crypto = new CryptographicUtilities($this->random);
        $this->encryptionKey = $this->random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);

        $this->encryptor = new StateEncryptor(
            $this->encryptionKey,
            $this->crypto,
            $this->random
        );
    });

    it('uses different nonce for each encryption', function () {
        $plaintext = 'test';

        $nonces = [];
        for ($i = 0; $i < 100; $i++) {
            $encrypted = $this->encryptor->encrypt($plaintext);
            $decoded = base64_decode($encrypted, strict: true);

            // Minimum length check: 1 (version) + 24 (nonce) + 16 (min ciphertext from sodium_crypto_secretbox)
            expect(strlen($decoded))->toBeGreaterThanOrEqual(41);

            // Extract nonce (after version byte, 24 bytes)
            $nonce = substr($decoded, 1, 24);

            expect(strlen($nonce))->toBe(24);
            expect($nonces)->not->toContain($nonce);

            $nonces[] = $nonce;
        }

        // All nonces should be unique
        expect(count($nonces))->toBe(100);
        expect(count(array_unique($nonces, SORT_REGULAR)))->toBe(100);
    });

    it('prevents key reuse across different encryptors', function () {
        $plaintext = 'secret';

        $random = new SecureRandomGenerator();
        $crypto = new CryptographicUtilities($random);

        $encryptor1 = new StateEncryptor(
            $this->encryptionKey,
            $crypto,
            $random
        );

        // Different key using framework's RandomGenerator
        $differentKey = $random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
        $encryptor2 = new StateEncryptor(
            $differentKey,
            $crypto,
            $random
        );

        $encrypted1 = $encryptor1->encrypt($plaintext);

        // Should fail with wrong key
        $encryptor2->decrypt($encrypted1);
    })->throws(StateEncryptionException::class);

    it('includes version byte for future compatibility', function () {
        $encrypted = $this->encryptor->encrypt('test');
        $decoded = base64_decode($encrypted, strict: true);

        // First byte should be version byte (value 1)
        $version = ord($decoded[0]);
        expect($version)->toBe(1);
    });
});
