"""Tests for Gemini review fixes to encryption implementation.

Tests validate fixes for:
1. from_password() salt storage
2. _unsafe_from_secret_manager_single_key() renamed
3. decrypt() race condition retry
4. encrypt() lock optimization
5. enqueue() non-blocking persistence
"""

import threading
import time
from cryptography.fernet import Fernet
import pytest

from mca_sdk.security.encryption import EncryptionManager, QueueEncryption


class TestFromPasswordSaltStorage:
    """Test Fix #1: from_password() stores salt for key recovery."""

    def test_salt_stored_on_instance(self):
        """Test that salt is stored on EncryptionManager instance."""
        manager = EncryptionManager.from_password("test-password")

        assert manager.salt is not None, "Salt should be stored"
        assert isinstance(manager.salt, bytes), "Salt should be bytes"
        assert len(manager.salt) == 16, "Salt should be 16 bytes"

    def test_same_password_with_salt_recreates_key(self):
        """Test that same password + salt regenerates same encryption key."""
        password = "my-secret-password"

        # Create first manager and encrypt data
        manager1 = EncryptionManager.from_password(password)
        saved_salt = manager1.salt
        plaintext = b"sensitive data"
        encrypted = manager1.encrypt(plaintext)

        # Simulate restart: recreate manager with same password + saved salt
        manager2 = EncryptionManager.from_password(password, salt=saved_salt)

        # Should decrypt successfully
        decrypted = manager2.decrypt(encrypted)
        assert decrypted == plaintext

    def test_different_salt_generates_different_key(self):
        """Test that different salts generate different keys (cannot decrypt)."""
        password = "my-secret-password"

        # Create first manager
        manager1 = EncryptionManager.from_password(password)
        plaintext = b"sensitive data"
        encrypted = manager1.encrypt(plaintext)

        # Create second manager with same password but NEW salt (default behavior)
        manager2 = EncryptionManager.from_password(password)

        # Should have different salt
        assert manager2.salt != manager1.salt

        # Should FAIL to decrypt (different key)
        from cryptography.fernet import InvalidToken

        with pytest.raises(InvalidToken):
            manager2.decrypt(encrypted)

    def test_custom_salt_provided(self):
        """Test that custom salt is stored correctly."""
        password = "test-password"
        custom_salt = b"1234567890123456"  # 16 bytes

        manager = EncryptionManager.from_password(password, salt=custom_salt)

        assert manager.salt == custom_salt


class TestFromSecretManagerRenamed:
    """Test Fix #2: from_secret_manager() renamed to prevent production misuse."""

    def test_method_renamed_to_unsafe(self):
        """Test that unsafe method name makes production risk obvious."""
        # The method should now be named _unsafe_from_secret_manager_single_key
        assert hasattr(QueueEncryption, "_unsafe_from_secret_manager_single_key")

        # Old name should not exist (prevent accidental use)
        assert not hasattr(QueueEncryption, "from_secret_manager")


class TestDecryptRaceConditionRetry:
    """Test Fix #3: decrypt() implements double-check locking retry."""

    def test_decrypt_retries_on_key_rotation_race(self):
        """Test that decrypt retries with updated keys after rotation race."""
        # Create initial encryption with key v1
        key1 = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key1])

        plaintext = b"test data"
        encrypted = encryption.encrypt(plaintext)

        # Rotate to key v2 (v2 becomes primary, v1 kept for backward compat)
        key2 = Fernet.generate_key()
        encryption.rotate_keys(key2)

        # Decrypt should succeed with either v1 or v2
        # (data encrypted with v1, current keys are [v2, v1])
        decrypted = encryption.decrypt(encrypted)
        assert decrypted == plaintext

    def test_decrypt_concurrent_rotation_no_data_loss(self):
        """Test that concurrent rotation during decryption doesn't lose data."""
        key1 = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key1])

        # Encrypt with key1
        encrypted = encryption.encrypt(b"important data")

        # Simulate race: rotate keys while decrypting
        key2 = Fernet.generate_key()

        def rotate_keys_delayed():
            time.sleep(0.01)  # Small delay to create race
            encryption.rotate_keys(key2)

        rotation_thread = threading.Thread(target=rotate_keys_delayed)
        rotation_thread.start()

        # Decrypt should succeed despite concurrent rotation
        decrypted = encryption.decrypt(encrypted)
        assert decrypted == b"important data"

        rotation_thread.join()


class TestEncryptCorrectness:
    """Test Fix #4 REVERTED: encrypt() holds lock to prevent data loss race."""

    def test_encrypt_prevents_key_pruning_race(self):
        """Test that encryption cannot use pruned keys (data loss prevention)."""
        key1 = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key1], max_keys=1)

        # Encrypt with key1
        encrypted = encryption.encrypt(b"test data")

        # Should decrypt successfully with key1
        decrypted = encryption.decrypt(encrypted)
        assert decrypted == b"test data"

    def test_encrypt_concurrent_operations(self):
        """Test that multiple threads can encrypt (with lock held)."""
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])

        results = []

        def encrypt_data(data: bytes):
            encrypted = encryption.encrypt(data)
            results.append(encrypted)

        # Create 10 threads encrypting concurrently
        threads = []
        for i in range(10):
            t = threading.Thread(target=encrypt_data, args=(f"data-{i}".encode(),))
            threads.append(t)
            t.start()

        # Wait for all threads
        for t in threads:
            t.join()

        # All encryptions should succeed
        assert len(results) == 10

        # All encrypted data should be different (unique IVs)
        assert len(set(results)) == 10

    def test_encrypt_with_rotation_all_decryptable(self):
        """Test that all encrypted data remains decryptable after rotation."""
        key1 = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key1])

        encrypted_items = []

        def encrypt_loop():
            for i in range(5):
                enc = encryption.encrypt(f"data-{i}".encode())
                encrypted_items.append(enc)
                time.sleep(0.01)

        def rotate_keys():
            time.sleep(0.02)
            key2 = Fernet.generate_key()
            encryption.rotate_keys(key2)

        # Start encryption and rotation concurrently
        encrypt_thread = threading.Thread(target=encrypt_loop)
        rotate_thread = threading.Thread(target=rotate_keys)

        encrypt_thread.start()
        rotate_thread.start()

        encrypt_thread.join()
        rotate_thread.join()

        # All encryptions should succeed
        assert len(encrypted_items) == 5

        # CRITICAL: All should decrypt successfully (no data loss from race)
        for encrypted in encrypted_items:
            decrypted = encryption.decrypt(encrypted)
            assert b"data-" in decrypted


class TestEnqueueNonBlocking:
    """Test Fix #5: enqueue() doesn't block on disk I/O."""

    def test_enqueue_releases_lock_before_persistence(self):
        """Test that enqueue releases lock before triggering persistence."""
        from mca_sdk.buffering.queue import TelemetryQueue
        import tempfile

        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as f:
            persist_path = f.name

        try:
            # Create small queue that will fill quickly
            queue = TelemetryQueue(max_size=2, persist=True, persist_path=persist_path)

            # Fill queue to trigger persistence
            queue.enqueue({"item": 1})
            queue.enqueue({"item": 2})  # Queue full
            queue.enqueue({"item": 3})  # Should trigger persistence

            # Enqueue should return immediately (not block on disk I/O)
            # If it blocks, this test would be very slow

            # Verify queue still functional
            assert queue.size() == 2  # maxlen=2, oldest evicted

        finally:
            import os

            if os.path.exists(persist_path):
                os.unlink(persist_path)

    def test_enqueue_concurrent_no_deadlock(self):
        """Test that multiple threads can enqueue without deadlock."""
        from mca_sdk.buffering.queue import TelemetryQueue
        import tempfile

        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as f:
            persist_path = f.name

        try:
            queue = TelemetryQueue(max_size=10, persist=True, persist_path=persist_path)

            def enqueue_items():
                for i in range(20):
                    queue.enqueue({"thread": threading.current_thread().name, "item": i})

            # Create 5 threads enqueueing concurrently
            threads = []
            for i in range(5):
                t = threading.Thread(target=enqueue_items, name=f"Thread-{i}")
                threads.append(t)
                t.start()

            # Wait for all threads (should not deadlock)
            for t in threads:
                t.join(timeout=5.0)
                assert not t.is_alive(), "Thread deadlocked"

            # Queue should have items from all threads
            assert queue.size() > 0

        finally:
            import os

            if os.path.exists(persist_path):
                os.unlink(persist_path)
