"""Unit tests for queue encryption functionality.

Tests the QueueEncryption class and integration with TelemetryQueue
for encryption at rest of disk-persisted queue data.
"""

import base64
import os
import tempfile
from pathlib import Path

import pytest
from cryptography.fernet import Fernet, InvalidToken

from mca_sdk.security.encryption import QueueEncryption
from mca_sdk.buffering.queue import TelemetryQueue
from mca_sdk.utils.exceptions import BufferingError


class TestQueueEncryption:
    """Test QueueEncryption class."""

    def test_encrypt_decrypt_round_trip(self):
        """Test encryption and decryption round trip."""
        # Given: QueueEncryption with a generated key
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])
        data = b"sensitive telemetry data"

        # When: Encrypting and then decrypting
        encrypted = encryption.encrypt(data)
        decrypted = encryption.decrypt(encrypted)

        # Then: Data should match original
        assert decrypted == data
        assert encrypted != data  # Encrypted should be different

    def test_encrypt_different_output_each_time(self):
        """Test that encryption produces different output each time (due to IV)."""
        # Given: QueueEncryption with same key
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])
        data = b"test data"

        # When: Encrypting same data twice
        encrypted1 = encryption.encrypt(data)
        encrypted2 = encryption.encrypt(data)

        # Then: Encrypted values should be different (due to random IV)
        assert encrypted1 != encrypted2
        # But both decrypt to same value
        assert encryption.decrypt(encrypted1) == data
        assert encryption.decrypt(encrypted2) == data

    def test_decrypt_with_wrong_key_fails(self):
        """Test that decryption with wrong key fails."""
        # Given: Two different keys
        key1 = Fernet.generate_key()
        key2 = Fernet.generate_key()
        encryption1 = QueueEncryption(encryption_keys=[key1])
        encryption2 = QueueEncryption(encryption_keys=[key2])
        data = b"secret"

        # When: Encrypting with key1 and trying to decrypt with key2
        encrypted = encryption1.encrypt(data)

        # Then: Should raise InvalidToken
        with pytest.raises(InvalidToken):
            encryption2.decrypt(encrypted)

    def test_key_rotation_decrypt_with_old_key(self):
        """Test that key rotation allows decryption with old keys."""
        # Given: Encryption with old key, then rotated to new key
        old_key = Fernet.generate_key()
        new_key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[old_key])
        data = b"data encrypted with old key"

        # When: Encrypt with old key
        encrypted_with_old = encryption.encrypt(data)

        # Then: Rotate to new key (old key becomes secondary)
        encryption.rotate_keys(new_key)

        # And: Should still decrypt data encrypted with old key
        decrypted = encryption.decrypt(encrypted_with_old)
        assert decrypted == data

        # And: New encryptions use new key
        encrypted_with_new = encryption.encrypt(b"new data")
        decrypted_new = encryption.decrypt(encrypted_with_new)
        assert decrypted_new == b"new data"

    def test_key_rotation_encrypts_with_primary_key(self):
        """Test that after rotation, encryption uses primary (first) key."""
        # Given: Encryption with multiple keys after rotation
        key1 = Fernet.generate_key()
        key2 = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key1])
        encryption.rotate_keys(key2)  # key2 is now primary

        # When: Encrypting data
        data = b"test"
        encrypted = encryption.encrypt(data)

        # Then: Should be decryptable with key2 only
        encryption_key2_only = QueueEncryption(encryption_keys=[key2])
        assert encryption_key2_only.decrypt(encrypted) == data

    def test_initialize_with_no_keys_raises_error(self):
        """Test that initializing with empty key list raises ConfigurationError."""
        # Given: Empty key list
        # When: Trying to create QueueEncryption with no keys
        # Then: Should raise ConfigurationError (NEVER auto-generate silently)
        from mca_sdk.utils.exceptions import ConfigurationError

        with pytest.raises(ConfigurationError) as exc_info:
            QueueEncryption(encryption_keys=[])

        assert "No encryption keys provided" in str(exc_info.value)
        assert "QueueEncryption requires at least one valid key" in str(exc_info.value)

    def test_from_secret_manager_integration(self):
        """Test creating encryption from secret manager (mocked)."""
        # This will be tested with actual secret manager integration
        # For now, test the basic pattern
        pytest.skip("Requires secret manager implementation (Story 3.9)")

    def test_max_keys_limit_enforced(self):
        """Test that max_keys limit is enforced during rotation."""
        # Given: QueueEncryption with max_keys=2
        key1 = Fernet.generate_key()
        key2 = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key1, key2], max_keys=2)

        # When: Rotating to a third key
        key3 = Fernet.generate_key()
        encryption.rotate_keys(key3)

        # Then: Should only have 2 keys (oldest pruned)
        # We can't directly check internal state, but we can verify behavior
        # Encrypt with new key (key3)
        data = b"test data"
        encrypted = encryption.encrypt(data)

        # Should decrypt (key3 or key2 still available)
        decrypted = encryption.decrypt(encrypted)
        assert decrypted == data

    def test_key_validation_on_init(self):
        """Test that invalid keys are rejected on initialization."""
        from mca_sdk.utils.exceptions import ConfigurationError

        # Invalid: wrong length
        with pytest.raises(ConfigurationError) as exc_info:
            QueueEncryption(encryption_keys=[b"too_short"])
        assert "invalid length" in str(exc_info.value).lower()

        # Invalid: not base64
        with pytest.raises(ConfigurationError) as exc_info:
            QueueEncryption(encryption_keys=[b"x" * 44])
        assert "invalid" in str(exc_info.value).lower()

    def test_decrypt_with_ttl(self):
        """Test TTL-based decryption."""
        # Given: Encryption with default TTL
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key], default_ttl=2)
        data = b"sensitive data"

        # When: Encrypting and decrypting immediately
        encrypted = encryption.encrypt(data)
        decrypted = encryption.decrypt(encrypted)

        # Then: Should succeed (within TTL)
        assert decrypted == data

        # When: Waiting beyond TTL
        import time

        time.sleep(3)

        # Then: Should fail with InvalidToken
        with pytest.raises(InvalidToken):
            encryption.decrypt(encrypted)

    def test_encrypted_data_is_authenticated(self):
        """Test that encrypted data uses Fernet authenticated encryption."""
        # Given: QueueEncryption
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])
        data = b"test"

        # When: Encrypting
        encrypted = encryption.encrypt(data)

        # Then: Should be valid Fernet token (authenticated, can decrypt)
        decrypted = encryption.decrypt(encrypted)
        assert decrypted == data

        # And: Tampering should be detected
        tampered = encrypted[:-1] + b"X"
        with pytest.raises(InvalidToken):
            encryption.decrypt(tampered)

    def test_decrypt_without_magic_header_backward_compat(self):
        """Test backward compatibility with data encrypted without magic header."""
        # Given: Data encrypted with raw Fernet (no magic header)
        key = Fernet.generate_key()
        raw_fernet = Fernet(key)
        data = b"old data"
        encrypted_old = raw_fernet.encrypt(data)

        # When: Decrypting with QueueEncryption
        encryption = QueueEncryption(encryption_keys=[key])
        decrypted = encryption.decrypt(encrypted_old)

        # Then: Should still work (backward compatibility)
        assert decrypted == data


class TestTelemetryQueueEncryption:
    """Test TelemetryQueue with encryption enabled."""

    def test_queue_persists_encrypted_data_to_disk(self, tmp_path):
        """Test that queue data is encrypted when written to disk."""
        # Given: Queue with encryption enabled
        queue_file = tmp_path / "queue.json"
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])

        queue = TelemetryQueue(
            max_size=10,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption,
            encryption_enabled=True,
        )

        # When: Enqueuing items to cause persistence (overflow)
        for i in range(11):  # Exceed max_size to trigger persistence
            queue.enqueue({"item": f"data-{i}"})

        queue.shutdown(timeout=2.0)

        # Then: File should exist and contain encrypted data
        assert queue_file.exists()

        # And: Reading raw file should show encrypted data (not plaintext)
        raw_content = queue_file.read_bytes()
        assert b"data-10" not in raw_content  # Should not contain plaintext
        assert b"item" not in raw_content  # Should not contain field names

    def test_queue_loads_encrypted_data_from_disk(self, tmp_path):
        """Test that queue can load and decrypt persisted data."""
        # Given: Queue with encryption that persists data
        queue_file = tmp_path / "queue.json"
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])

        # First queue: persist data
        queue1 = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption,
            encryption_enabled=True,
        )
        for i in range(6):  # Overflow to persist
            queue1.enqueue({"metric": "test", "value": i})
        queue1.shutdown(timeout=2.0)

        # When: Creating new queue with same encryption key
        encryption2 = QueueEncryption(encryption_keys=[key])
        queue2 = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption2,
            encryption_enabled=True,
        )

        # Then: Should have recovered items from disk
        stats = queue2.buffer_stats()
        assert stats["persistence"]["items_recovered_on_startup"] > 0

        queue2.shutdown(timeout=2.0)

    def test_queue_without_encryption_stores_plaintext(self, tmp_path):
        """Test that queue without encryption stores plaintext (baseline)."""
        # Given: Queue without encryption
        queue_file = tmp_path / "queue.json"
        queue = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=None,
            encryption_enabled=False,
        )

        # When: Enqueuing data
        for i in range(6):  # Overflow to persist
            queue.enqueue({"metric": "test_metric", "value": i})
        queue.shutdown(timeout=2.0)

        # Then: File should contain plaintext JSON
        raw_content = queue_file.read_text()
        assert "test_metric" in raw_content  # Should contain plaintext

    def test_unauthorized_access_cannot_read_encrypted_queue(self, tmp_path):
        """Test that encrypted queue file is unusable without key (AC #4)."""
        # Given: Queue with encryption that persists data
        queue_file = tmp_path / "queue.json"
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])

        queue1 = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption,
            encryption_enabled=True,
        )
        queue1.enqueue({"sensitive": "patient_data", "value": 123})
        for i in range(5):  # Overflow to persist
            queue1.enqueue({"item": i})
        queue1.shutdown(timeout=2.0)

        # When: Attempting to load with wrong key
        wrong_key = Fernet.generate_key()
        wrong_encryption = QueueEncryption(encryption_keys=[wrong_key])

        queue2 = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=wrong_encryption,
            encryption_enabled=True,
        )

        # Then: Should fail to load encrypted data (start with empty queue)
        stats = queue2.buffer_stats()
        assert stats["persistence"]["items_recovered_on_startup"] == 0
        assert stats["persistence"]["corruptions_detected"] >= 1

        queue2.shutdown(timeout=2.0)

    def test_queue_with_encryption_disabled_flag_skips_encryption(self, tmp_path):
        """Test that encryption_enabled=False skips encryption even if key provided."""
        # Given: Queue with encryption key but encryption disabled
        queue_file = tmp_path / "queue.json"
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])

        queue = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption,
            encryption_enabled=False,  # Disabled
        )

        # When: Enqueuing data
        for i in range(6):  # Overflow
            queue.enqueue({"data": f"item-{i}"})
        queue.shutdown(timeout=2.0)

        # Then: File should contain plaintext (encryption was disabled)
        raw_content = queue_file.read_text()
        assert "item-" in raw_content  # Plaintext visible

    def test_encryption_configuration_from_environment(self, tmp_path, monkeypatch):
        """Test loading encryption key from environment variable."""
        # Given: Encryption key in environment
        key = Fernet.generate_key()
        monkeypatch.setenv("MCA_QUEUE_ENCRYPTION_KEY", key.decode("ascii"))

        queue_file = tmp_path / "queue.json"
        queue = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=None,  # Should load from env
            encryption_enabled=True,
        )

        # When: Enqueuing data
        for i in range(6):
            queue.enqueue({"test": i})
        queue.shutdown(timeout=2.0)

        # Then: File should be encrypted
        raw_content = queue_file.read_bytes()
        assert b"test" not in raw_content

    def test_queue_encryption_with_key_rotation(self, tmp_path):
        """Test queue persistence with key rotation."""
        # Given: Queue with old key
        queue_file = tmp_path / "queue.json"
        old_key = Fernet.generate_key()
        encryption1 = QueueEncryption(encryption_keys=[old_key])

        queue1 = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption1,
            encryption_enabled=True,
        )
        for i in range(6):
            queue1.enqueue({"old_data": i})
        queue1.shutdown(timeout=2.0)

        # When: Rotating to new key
        new_key = Fernet.generate_key()
        encryption2 = QueueEncryption(encryption_keys=[old_key])
        encryption2.rotate_keys(new_key)  # new_key is primary, old_key is fallback

        queue2 = TelemetryQueue(
            max_size=5,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption2,
            encryption_enabled=True,
        )

        # Then: Should load old data (encrypted with old key)
        stats = queue2.buffer_stats()
        assert stats["persistence"]["items_recovered_on_startup"] > 0

        # And: New persisted data uses new key
        for i in range(6):
            queue2.enqueue({"new_data": i})
        queue2.shutdown(timeout=2.0)

        # And: File should be re-encrypted with new key
        assert queue_file.exists()


class TestQueueEncryptionPerformance:
    """Performance benchmarks for encryption overhead."""

    def test_encryption_overhead_1000_items(self, tmp_path):
        """Benchmark encryption performance with 1000-item queue."""
        import time

        # Given: Large queue with encryption
        queue_file = tmp_path / "large_queue.json"
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])

        queue = TelemetryQueue(
            max_size=1000,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption,
            encryption_enabled=True,
        )

        # When: Enqueuing 1001 items to trigger overflow persistence
        start_time = time.time()
        for i in range(1001):
            queue.enqueue(
                {
                    "metric": "test.metric",
                    "value": i,
                    "timestamp": time.time(),
                    "attributes": {"index": i, "batch": i // 100},
                }
            )

        # Wait for background writer to process
        queue.shutdown(timeout=5.0)
        elapsed = time.time() - start_time

        # Then: Should complete within reasonable time (< 2 seconds for 1000 items)
        assert elapsed < 2.0, f"Encryption overhead too high: {elapsed:.2f}s for 1000 items"

        # And: File should exist and be encrypted (Fernet token, not plaintext)
        assert queue_file.exists()
        raw_content = queue_file.read_bytes()
        # Verify encrypted (no plaintext metric names visible)
        assert b"test.metric" not in raw_content
        # Verify it's a Fernet token by attempting to decrypt
        decrypted = encryption.decrypt(raw_content)
        assert b"test.metric" in decrypted

    def test_decryption_overhead_1000_items(self, tmp_path):
        """Benchmark decryption performance when loading large queue."""
        import time

        # Given: Encrypted queue with 1000 items
        queue_file = tmp_path / "large_queue.json"
        key = Fernet.generate_key()
        encryption = QueueEncryption(encryption_keys=[key])

        # Create and persist large queue
        queue1 = TelemetryQueue(
            max_size=1000,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption,
            encryption_enabled=True,
        )
        for i in range(1001):  # Trigger overflow
            queue1.enqueue({"metric": "test", "value": i})
        queue1.shutdown(timeout=5.0)

        # When: Loading encrypted queue (measures decryption overhead)
        start_time = time.time()
        queue2 = TelemetryQueue(
            max_size=1000,
            persist=True,
            persist_path=str(queue_file),
            encryption=encryption,
            encryption_enabled=True,
        )
        elapsed = time.time() - start_time

        # Then: Decryption should be fast (< 1 second for 1000 items)
        assert elapsed < 1.0, f"Decryption overhead too high: {elapsed:.2f}s for 1000 items"

        # And: Items should be recovered
        stats = queue2.buffer_stats()
        assert stats["persistence"]["items_recovered_on_startup"] > 0

        queue2.shutdown(timeout=2.0)

    def test_key_rotation_overhead(self):
        """Benchmark key rotation operation performance."""
        import time

        # Given: QueueEncryption with multiple keys
        keys = [Fernet.generate_key() for _ in range(3)]
        encryption = QueueEncryption(encryption_keys=keys, max_keys=10)

        # When: Performing 100 rotations
        start_time = time.time()
        for _ in range(100):
            new_key = Fernet.generate_key()
            encryption.rotate_keys(new_key)
        elapsed = time.time() - start_time

        # Then: Should complete quickly (< 0.5 seconds for 100 rotations)
        assert elapsed < 0.5, f"Key rotation too slow: {elapsed:.2f}s for 100 rotations"
