<?php

declare(strict_types=1);

/**
 * State Encryption Integration Tests
 *
 * Integration tests for state encryption with security layers.
 * Tests the complete encryption flow from serialization to cache storage.
 */

use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheItem;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Cryptography\CryptographicUtilities;
use App\Framework\LiveComponents\Serialization\EncryptedStateSerializer;
use App\Framework\LiveComponents\Serialization\StateEncryptor;
use App\Framework\LiveComponents\Exceptions\StateEncryptionException;
use App\Framework\Random\SecureRandomGenerator;
use App\Framework\StateManagement\SerializableState;

// Test State with Sensitive Data
final readonly class PaymentState implements SerializableState
{
    public function __construct(
        public string $paymentId,
        public string $cardNumber,      // Sensitive - PCI data
        public string $cvv,              // Sensitive - must be encrypted
        public float $amount,
        public string $currency
    ) {}

    public function toArray(): array
    {
        return [
            'payment_id' => $this->paymentId,
            'card_number' => $this->cardNumber,
            'cvv' => $this->cvv,
            'amount' => $this->amount,
            'currency' => $this->currency,
        ];
    }

    public static function fromArray(array $data): self
    {
        return new self(
            paymentId: $data['payment_id'],
            cardNumber: $data['card_number'],
            cvv: $data['cvv'],
            amount: $data['amount'],
            currency: $data['currency']
        );
    }
}

describe('State Encryption Integration', function () {
    beforeEach(function () {
        // Setup encryption infrastructure
        $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
        );

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

        // Test payment state
        $this->paymentState = new PaymentState(
            paymentId: 'pay_12345',
            cardNumber: '4111111111111111',
            cvv: '123',
            amount: 99.99,
            currency: 'USD'
        );
    });

    describe('End-to-End Encryption Flow', function () {
        it('encrypts state and stores in cache', function () {
            // 1. Encrypt state
            $encrypted = $this->serializer->serialize($this->paymentState);

            // 2. Store in cache
            $cacheKey = CacheKey::fromString('payment:session:abc123');
            $cacheItem = CacheItem::forSet(
                key: $cacheKey,
                value: $encrypted,
                ttl: Duration::fromMinutes(15)
            );

            $this->cache->set($cacheItem);

            // 3. Retrieve from cache
            $retrieved = $this->cache->get($cacheKey);

            expect($retrieved->isHit)->toBeTrue();
            expect($retrieved->value)->toBeString();

            // 4. Verify encrypted (no plaintext)
            expect($retrieved->value)->not->toContain('4111111111111111');
            expect($retrieved->value)->not->toContain('123');
        });

        it('decrypts state correctly from cache', function () {
            // Encrypt and store
            $encrypted = $this->serializer->serialize($this->paymentState);
            $cacheKey = CacheKey::fromString('payment:session:xyz789');
            $this->cache->set(CacheItem::forSet(
                key: $cacheKey,
                value: $encrypted
            ));

            // Retrieve and decrypt
            $cached = $this->cache->get($cacheKey);
            $decrypted = $this->serializer->deserialize(
                $cached->value,
                PaymentState::class
            );

            // Verify decryption
            expect($decrypted)->toBeInstanceOf(PaymentState::class);
            expect($decrypted->paymentId)->toBe('pay_12345');
            expect($decrypted->cardNumber)->toBe('4111111111111111');
            expect($decrypted->cvv)->toBe('123');
            expect($decrypted->amount)->toBe(99.99);
        });

        it('handles cache expiration correctly', function () {
            // Store with short TTL
            $encrypted = $this->serializer->serialize($this->paymentState);
            $cacheKey = CacheKey::fromString('payment:session:expired');

            // Manually expire by not setting (or setting expired)
            // InMemoryCache doesn't auto-expire, so we simulate by not storing

            $retrieved = $this->cache->get($cacheKey);
            expect($retrieved->isHit)->toBeFalse();
        });
    });

    describe('Security Properties', function () {
        it('prevents tampering with MAC verification', function () {
            // Encrypt and store
            $encrypted = $this->serializer->serialize($this->paymentState);
            $cacheKey = CacheKey::fromString('payment:tamper-test');
            $this->cache->set(CacheItem::forSet(
                key: $cacheKey,
                value: $encrypted
            ));

            // Retrieve and tamper
            $cached = $this->cache->get($cacheKey);
            $tampered = substr($cached->value, 0, -5) . 'XXXXX';

            // Store tampered data
            $this->cache->set(CacheItem::forSet(
                key: $cacheKey,
                value: $tampered
            ));

            // Attempt to decrypt should fail
            $tamperedData = $this->cache->get($cacheKey);

            expect(fn() => $this->serializer->deserialize(
                $tamperedData->value,
                PaymentState::class
            ))->toThrow(StateEncryptionException::class);
        });

        it('uses unique nonces for each encryption', function () {
            // Encrypt same state twice
            $encrypted1 = $this->serializer->serialize($this->paymentState);
            $encrypted2 = $this->serializer->serialize($this->paymentState);

            // Should be different due to unique nonces
            expect($encrypted1)->not->toBe($encrypted2);

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

            expect($decrypted1->cardNumber)->toBe($decrypted2->cardNumber);
        });

        it('does not leak sensitive data in cache storage', function () {
            // Store multiple encrypted states
            for ($i = 1; $i <= 5; $i++) {
                $state = new PaymentState(
                    paymentId: "pay_{$i}",
                    cardNumber: '4111111111111111',
                    cvv: '123',
                    amount: 100.00 * $i,
                    currency: 'USD'
                );

                $encrypted = $this->serializer->serialize($state);
                $cacheKey = CacheKey::fromString("payment:session:{$i}");
                $this->cache->set(CacheItem::forSet(
                    key: $cacheKey,
                    value: $encrypted
                ));
            }

            // Scan all cache entries
            $allKeys = $this->cache->scan('payment:session:*');

            foreach ($allKeys as $keyString) {
                $cacheKey = CacheKey::fromString($keyString);
                $item = $this->cache->get($cacheKey);

                // No plaintext card data should be visible
                expect($item->value)->not->toContain('4111111111111111');
                expect($item->value)->not->toContain('123');
            }
        });
    });

    describe('Performance Characteristics', function () {
        it('maintains acceptable encryption overhead', function () {
            $startTime = microtime(true);

            for ($i = 0; $i < 100; $i++) {
                $encrypted = $this->serializer->serialize($this->paymentState);
                $this->serializer->deserialize($encrypted, PaymentState::class);
            }

            $duration = (microtime(true) - $startTime) * 1000;
            $avgTimeMs = $duration / 100;

            // Should be < 2ms per encrypt+decrypt cycle (as per docs)
            expect($avgTimeMs)->toBeLessThan(2.0);
        });

        it('handles concurrent cache operations', function () {
            // Simulate concurrent writes
            $operations = [];

            for ($i = 0; $i < 10; $i++) {
                $state = new PaymentState(
                    paymentId: "concurrent_{$i}",
                    cardNumber: '4111111111111111',
                    cvv: '123',
                    amount: 50.00,
                    currency: 'USD'
                );

                $encrypted = $this->serializer->serialize($state);
                $cacheKey = CacheKey::fromString("payment:concurrent:{$i}");

                $this->cache->set(CacheItem::forSet(
                    key: $cacheKey,
                    value: $encrypted
                ));

                $operations[] = ['key' => $cacheKey, 'paymentId' => "concurrent_{$i}"];
            }

            // Verify all operations succeeded
            foreach ($operations as $op) {
                $cached = $this->cache->get($op['key']);
                $decrypted = $this->serializer->deserialize(
                    $cached->value,
                    PaymentState::class
                );

                expect($decrypted->paymentId)->toBe($op['paymentId']);
            }
        });
    });

    describe('Error Handling', function () {
        it('throws StateEncryptionException on decryption failure', function () {
            // Store corrupted data
            $cacheKey = CacheKey::fromString('payment:corrupted');
            $this->cache->set(CacheItem::forSet(
                key: $cacheKey,
                value: 'corrupted-encrypted-data'
            ));

            $cached = $this->cache->get($cacheKey);

            expect(fn() => $this->serializer->deserialize(
                $cached->value,
                PaymentState::class
            ))->toThrow(StateEncryptionException::class);
        });

        it('does not leak encryption key in error messages', function () {
            try {
                $this->serializer->deserialize(
                    'invalid-data',
                    PaymentState::class
                );
            } catch (StateEncryptionException $e) {
                // Error message should not contain encryption key
                expect($e->getMessage())->not->toContain(bin2hex($this->encryptionKey));
            }
        });

        it('handles invalid state class gracefully', function () {
            $encrypted = $this->serializer->serialize($this->paymentState);

            expect(fn() => $this->serializer->deserialize(
                $encrypted,
                \stdClass::class
            ))->toThrow(StateEncryptionException::class);
        });
    });

    describe('Multi-Component Isolation', function () {
        it('maintains separate encrypted states per component', function () {
            $component1Key = CacheKey::fromString('payment:component1');
            $component2Key = CacheKey::fromString('payment:component2');

            $state1 = new PaymentState(
                paymentId: 'pay_comp1',
                cardNumber: '4111111111111111',
                cvv: '111',
                amount: 50.00,
                currency: 'USD'
            );

            $state2 = new PaymentState(
                paymentId: 'pay_comp2',
                cardNumber: '5555555555554444',
                cvv: '222',
                amount: 75.00,
                currency: 'EUR'
            );

            // Encrypt and store both
            $encrypted1 = $this->serializer->serialize($state1);
            $encrypted2 = $this->serializer->serialize($state2);

            $this->cache->set(CacheItem::forSet(key: $component1Key, value: $encrypted1));
            $this->cache->set(CacheItem::forSet(key: $component2Key, value: $encrypted2));

            // Verify different encrypted data
            $cached1 = $this->cache->get($component1Key);
            $cached2 = $this->cache->get($component2Key);

            expect($cached1->value)->not->toBe($cached2->value);

            // Decrypt and verify correct isolation
            $decrypted1 = $this->serializer->deserialize($cached1->value, PaymentState::class);
            $decrypted2 = $this->serializer->deserialize($cached2->value, PaymentState::class);

            expect($decrypted1->paymentId)->toBe('pay_comp1');
            expect($decrypted1->currency)->toBe('USD');
            expect($decrypted2->paymentId)->toBe('pay_comp2');
            expect($decrypted2->currency)->toBe('EUR');
        });
    });
});
