"""Tests for safeconfig.core.crypto."""

from __future__ import annotations

import pytest

from pysafeconfigx.core.crypto import (
    decrypt_value,
    encrypt_value,
    generate_master_key,
    is_encrypted_blob,
    load_master_key,
)
from pysafeconfigx.core.exceptions import DecryptionError, EncryptionError, MasterKeyError


class TestGenerateMasterKey:
    def test_returns_string(self) -> None:
        key = generate_master_key()
        assert isinstance(key, str)

    def test_is_valid_base64(self) -> None:
        key = generate_master_key()
        # Should not raise
        raw = load_master_key(key)
        assert len(raw) == 32

    def test_keys_are_unique(self) -> None:
        keys = {generate_master_key() for _ in range(10)}
        assert len(keys) == 10


class TestLoadMasterKey:
    def test_valid_key(self, master_key: str) -> None:
        raw = load_master_key(master_key)
        assert len(raw) == 32

    def test_invalid_base64_raises(self) -> None:
        with pytest.raises(MasterKeyError, match="valid base64"):
            load_master_key("not-valid-base64!!!")

    def test_wrong_length_raises(self) -> None:
        import base64
        short = base64.urlsafe_b64encode(b"tooshort").decode()
        with pytest.raises(MasterKeyError, match="32 bytes"):
            load_master_key(short)


class TestEncryptDecrypt:
    def test_round_trip(self, master_key: str) -> None:
        plaintext = "super secret value"
        blob = encrypt_value(plaintext, master_key)
        recovered = decrypt_value(blob, master_key)
        assert recovered == plaintext

    def test_different_blobs_same_plaintext(self, master_key: str) -> None:
        """Each encryption uses a random salt/nonce, so blobs must differ."""
        blob1 = encrypt_value("same", master_key)
        blob2 = encrypt_value("same", master_key)
        assert blob1 != blob2

    def test_decryption_with_wrong_key_raises(self, master_key: str) -> None:
        blob = encrypt_value("secret", master_key)
        wrong_key = generate_master_key()
        with pytest.raises(DecryptionError):
            decrypt_value(blob, wrong_key)

    def test_tampered_blob_raises(self, master_key: str) -> None:
        blob = encrypt_value("secret", master_key)
        # Flip some bytes in the middle of the blob
        blob_bytes = bytearray(blob.encode())
        blob_bytes[20] ^= 0xFF
        tampered = blob_bytes.decode(errors="replace")
        with pytest.raises((DecryptionError, Exception)):
            decrypt_value(tampered, master_key)

    def test_empty_string(self, master_key: str) -> None:
        blob = encrypt_value("", master_key)
        assert decrypt_value(blob, master_key) == ""

    def test_unicode_value(self, master_key: str) -> None:
        plaintext = "مرحبا 🔑 ñoño"
        blob = encrypt_value(plaintext, master_key)
        assert decrypt_value(blob, master_key) == plaintext

    def test_long_value(self, master_key: str) -> None:
        plaintext = "x" * 10_000
        blob = encrypt_value(plaintext, master_key)
        assert decrypt_value(blob, master_key) == plaintext

    def test_bad_blob_too_short(self, master_key: str) -> None:
        with pytest.raises(DecryptionError, match="too short"):
            decrypt_value("AAAA", master_key)

    def test_config_key_in_error_message(self, master_key: str) -> None:
        """Decryption errors should mention the config key."""
        wrong_key = generate_master_key()
        blob = encrypt_value("secret", master_key)
        with pytest.raises(DecryptionError) as exc_info:
            decrypt_value(blob, wrong_key, config_key="MY_SECRET")
        assert "MY_SECRET" in str(exc_info.value)


class TestIsEncryptedBlob:
    def test_recognises_valid_blob(self, master_key: str) -> None:
        blob = encrypt_value("hello", master_key)
        assert is_encrypted_blob(blob) is True

    def test_rejects_plain_text(self) -> None:
        assert is_encrypted_blob("plain text value") is False

    def test_rejects_empty_string(self) -> None:
        assert is_encrypted_blob("") is False

    def test_rejects_short_base64(self) -> None:
        import base64
        short = base64.urlsafe_b64encode(b"short").decode()
        assert is_encrypted_blob(short) is False
