"""Tests for credential encryption utilities."""

from __future__ import annotations

from unittest.mock import patch

import pytest

from core.utils.encryption import (
    _derive_fernet_key,
    decrypt_value,
    encrypt_value,
    is_encrypted,
)


class TestEncryptDecrypt:
    """Tests for encrypt_value and decrypt_value functions."""

    def test_encrypt_decrypt_roundtrip(self):
        """Encrypted values should decrypt to original."""
        original = "ghs_test_token_abc123"
        encrypted = encrypt_value(original)
        decrypted = decrypt_value(encrypted)

        assert decrypted == original

    def test_encrypted_value_differs_from_original(self):
        """Encrypted value should not contain the original plaintext."""
        original = "my-secret-api-key"
        encrypted = encrypt_value(original)

        assert encrypted != original
        assert original not in encrypted

    def test_encryption_is_not_deterministic(self):
        """Same plaintext should produce different ciphertext each time."""
        original = "same-value"
        encrypted1 = encrypt_value(original)
        encrypted2 = encrypt_value(original)

        # Fernet includes timestamp and random IV, so outputs differ
        assert encrypted1 != encrypted2

        # Both should decrypt to the same value
        assert decrypt_value(encrypted1) == original
        assert decrypt_value(encrypted2) == original

    def test_encrypt_empty_raises(self):
        """Encrypting empty string should raise ValueError."""
        with pytest.raises(ValueError, match="Cannot encrypt empty value"):
            encrypt_value("")

    def test_decrypt_empty_raises(self):
        """Decrypting empty string should raise ValueError."""
        with pytest.raises(ValueError, match="Cannot decrypt empty value"):
            decrypt_value("")

    def test_decrypt_invalid_ciphertext_raises(self):
        """Decrypting invalid ciphertext should raise ValueError."""
        with pytest.raises(ValueError, match="Failed to decrypt"):
            decrypt_value("not-a-valid-encrypted-value")

    def test_unicode_values(self):
        """Should handle unicode characters correctly."""
        original = "token-with-unicode-\u00e9\u00e8\u00ea-\u4e2d\u6587"
        encrypted = encrypt_value(original)
        decrypted = decrypt_value(encrypted)

        assert decrypted == original

    def test_long_values(self):
        """Should handle long values correctly."""
        original = "x" * 10000
        encrypted = encrypt_value(original)
        decrypted = decrypt_value(encrypted)

        assert decrypted == original


class TestKeyRotation:
    """Tests for key rotation via SECRET_KEY_FALLBACKS."""

    def test_decrypt_with_fallback_key(self, settings):
        """Values encrypted with old key should decrypt using fallbacks."""
        old_key = "old-secret-key-for-testing-rotation-1234567890"
        new_key = "new-secret-key-after-rotation-0987654321"

        # Encrypt with old key
        with patch.object(settings, "SECRET_KEY", old_key):
            encrypted = encrypt_value("my-secret")

        # Decrypt with new key + old key in fallbacks
        with patch.object(settings, "SECRET_KEY", new_key):
            with patch.object(settings, "SECRET_KEY_FALLBACKS", [old_key]):
                decrypted = decrypt_value(encrypted)

        assert decrypted == "my-secret"

    def test_decrypt_prefers_current_key(self, settings):
        """Decryption should try current key before fallbacks."""
        current_key = "current-secret-key-1234567890123456"

        with patch.object(settings, "SECRET_KEY", current_key):
            encrypted = encrypt_value("my-secret")
            # Should decrypt without needing fallbacks
            decrypted = decrypt_value(encrypted)

        assert decrypted == "my-secret"

    def test_decrypt_fails_without_correct_key(self, settings):
        """Decryption should fail if correct key not in current or fallbacks."""
        original_key = "original-key-used-for-encryption-123"
        wrong_key = "completely-different-wrong-key-456"

        # Encrypt with original key
        with patch.object(settings, "SECRET_KEY", original_key):
            encrypted = encrypt_value("secret-data")

        # Try to decrypt with wrong key and no fallbacks
        with patch.object(settings, "SECRET_KEY", wrong_key):
            with patch.object(settings, "SECRET_KEY_FALLBACKS", []):
                with pytest.raises(ValueError, match="Failed to decrypt"):
                    decrypt_value(encrypted)


class TestKeyDerivation:
    """Tests for the key derivation function."""

    def test_key_derivation_is_deterministic(self):
        """Same input should produce same derived key."""
        secret = "my-django-secret-key"
        key1 = _derive_fernet_key(secret)
        key2 = _derive_fernet_key(secret)

        assert key1 == key2

    def test_different_secrets_produce_different_keys(self):
        """Different secrets should produce different keys."""
        key1 = _derive_fernet_key("secret-one")
        key2 = _derive_fernet_key("secret-two")

        assert key1 != key2

    def test_derived_key_is_valid_fernet_key(self):
        """Derived key should be valid for Fernet."""
        from cryptography.fernet import Fernet

        key = _derive_fernet_key("test-secret-key")

        # Should not raise
        fernet = Fernet(key)
        assert fernet is not None


class TestIsEncrypted:
    """Tests for the is_encrypted heuristic."""

    def test_encrypted_value_detected(self):
        """Encrypted values should be detected."""
        encrypted = encrypt_value("test-token")
        assert is_encrypted(encrypted) is True

    def test_plaintext_not_detected(self):
        """Plaintext values should not be detected as encrypted."""
        assert is_encrypted("ghs_plaintext_token") is False
        assert is_encrypted("some-api-key") is False
        assert is_encrypted("") is False

    def test_short_values_not_detected(self):
        """Short values should not be detected as encrypted."""
        assert is_encrypted("gAAAAA") is False
        assert is_encrypted("gAAAAAshort") is False
