# #region Test.Encryption [C:3] [TYPE Module] [SEMANTICS test,encryption,fernet,key,crypto]
# @BRIEF Verify EncryptionManager encrypt/decrypt and ensure_encryption_key resolution contracts.
# @RELATION BINDS_TO -> [EncryptionCore]
# @RELATION BINDS_TO -> [EncryptionKeyModule]
# @TEST_EDGE: missing_key -> RuntimeError when ENCRYPTION_KEY absent
# @TEST_EDGE: invalid_key -> RuntimeError when ENCRYPTION_KEY is not valid Fernet format
# @TEST_EDGE: external_fail -> decrypt of garbage data raises Exception
# @TEST_INVARIANT: symmetric_encryption -> VERIFIED_BY: encrypt_decrypt_cycle, empty_string_encryption

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

import pytest

# Hardcoded valid Fernet key — generated once, frozen as fixture.
# 32 url-safe base64 bytes. Never recompute at test time (anti-tautology).
_VALID_FERNET_KEY = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0="


# ── _require_fernet_key ─────────────────────────────────────────────────────

class TestRequireFernetKey:
    """_require_fernet_key — loads and validates Fernet key from environment."""

    # #region test_require_key_valid [C:2] [TYPE Function]
    # @BRIEF Valid ENCRYPTION_KEY returns key bytes without error.
    def test_require_key_valid(self, monkeypatch):
        from src.core.encryption import _require_fernet_key

        monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
        result = _require_fernet_key()
        assert result == _VALID_FERNET_KEY.encode()
    # #endregion test_require_key_valid

    # #region test_require_key_missing [C:2] [TYPE Function]
    # @BRIEF Missing ENCRYPTION_KEY raises RuntimeError with startup guidance.
    def test_require_key_missing(self, monkeypatch):
        from src.core.encryption import _require_fernet_key

        monkeypatch.delenv("ENCRYPTION_KEY", raising=False)
        with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"):
            _require_fernet_key()
    # #endregion test_require_key_missing

    # #region test_require_key_invalid [C:2] [TYPE Function]
    # @BRIEF Malformed ENCRYPTION_KEY raises RuntimeError about valid Fernet key.
    def test_require_key_invalid(self, monkeypatch):
        from src.core.encryption import _require_fernet_key

        monkeypatch.setenv("ENCRYPTION_KEY", "not-a-valid-fernet-key")
        with pytest.raises(RuntimeError, match="valid Fernet key"):
            _require_fernet_key()
    # #endregion test_require_key_invalid

    # #region test_require_key_whitespace_only [C:2] [TYPE Function]
    # @BRIEF Whitespace-only ENCRYPTION_KEY is treated as missing.
    def test_require_key_whitespace_only(self, monkeypatch):
        from src.core.encryption import _require_fernet_key

        monkeypatch.setenv("ENCRYPTION_KEY", "   ")
        with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"):
            _require_fernet_key()
    # #endregion test_require_key_whitespace_only


# ── EncryptionManager ───────────────────────────────────────────────────────

class TestEncryptionManager:
    """EncryptionManager — encrypt/decrypt operations with validated Fernet key."""

    # #region test_encrypt_decrypt_cycle [C:2] [TYPE Function]
    # @BRIEF Encrypt then decrypt returns original plaintext (symmetric invariant).
    def test_encrypt_decrypt_cycle(self, monkeypatch):
        from src.core.encryption import EncryptionManager

        monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
        mgr = EncryptionManager()
        # Hardcoded fixture — plaintext declared before assertion, not computed.
        plaintext = "my_secret_key"
        encrypted = mgr.encrypt(plaintext)
        assert encrypted != plaintext
        assert mgr.decrypt(encrypted) == plaintext
    # #endregion test_encrypt_decrypt_cycle

    # #region test_empty_string_encryption [C:2] [TYPE Function]
    # @BRIEF Empty string survives encrypt/decrypt cycle.
    def test_empty_string_encryption(self, monkeypatch):
        from src.core.encryption import EncryptionManager

        monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
        mgr = EncryptionManager()
        encrypted = mgr.encrypt("")
        assert mgr.decrypt(encrypted) == ""
    # #endregion test_empty_string_encryption

    # #region test_decrypt_garbage_raises [C:2] [TYPE Function]
    # @BRIEF Decrypting non-Fernet data raises an exception (external_fail edge).
    def test_decrypt_garbage_raises(self, monkeypatch):
        from src.core.encryption import EncryptionManager

        monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
        mgr = EncryptionManager()
        with pytest.raises(Exception):
            mgr.decrypt("not-encrypted-data")
    # #endregion test_decrypt_garbage_raises

    # #region test_init_missing_key_raises [C:2] [TYPE Function]
    # @BRIEF EncryptionManager() without ENCRYPTION_KEY raises RuntimeError at init.
    def test_init_missing_key_raises(self, monkeypatch):
        from src.core.encryption import EncryptionManager

        monkeypatch.delenv("ENCRYPTION_KEY", raising=False)
        with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"):
            EncryptionManager()
    # #endregion test_init_missing_key_raises


# ── ensure_encryption_key ───────────────────────────────────────────────────

class TestEnsureEncryptionKey:
    """ensure_encryption_key — resolve key from env or .env file, crash-early if absent."""

    # #region test_ensure_key_from_env [C:2] [TYPE Function]
    # @BRIEF Returns key from process environment when ENCRYPTION_KEY is set.
    def test_ensure_key_from_env(self, monkeypatch):
        from src.core.encryption_key import ensure_encryption_key

        monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
        result = ensure_encryption_key()
        assert result == _VALID_FERNET_KEY
    # #endregion test_ensure_key_from_env

    # #region test_ensure_key_from_dotenv_file [C:2] [TYPE Function]
    # @BRIEF Loads key from .env file when not in process environment; sets os.environ.
    def test_ensure_key_from_dotenv_file(self, monkeypatch, tmp_path):
        from src.core.encryption_key import ensure_encryption_key

        monkeypatch.delenv("ENCRYPTION_KEY", raising=False)
        env_file = tmp_path / ".env"
        env_file.write_text(f"ENCRYPTION_KEY={_VALID_FERNET_KEY}\nOTHER=value\n")
        result = ensure_encryption_key(env_file)
        assert result == _VALID_FERNET_KEY
        # Side-effect: key must be propagated to process environment.
        import os
        assert os.environ.get("ENCRYPTION_KEY") == _VALID_FERNET_KEY
    # #endregion test_ensure_key_from_dotenv_file

    # #region test_ensure_key_no_file_raises [C:2] [TYPE Function]
    # @BRIEF Crash-early when no .env file exists and env var is unset.
    def test_ensure_key_no_file_raises(self, monkeypatch):
        from src.core.encryption_key import ensure_encryption_key

        monkeypatch.delenv("ENCRYPTION_KEY", raising=False)
        with pytest.raises(RuntimeError, match="ENCRYPTION_KEY is not set"):
            ensure_encryption_key(Path("/nonexistent/.env"))
    # #endregion test_ensure_key_no_file_raises

    # #region test_ensure_key_file_without_key_raises [C:2] [TYPE Function]
    # @BRIEF Crash-early when .env file exists but contains no ENCRYPTION_KEY line.
    def test_ensure_key_file_without_key_raises(self, monkeypatch, tmp_path):
        from src.core.encryption_key import ensure_encryption_key

        monkeypatch.delenv("ENCRYPTION_KEY", raising=False)
        env_file = tmp_path / ".env"
        env_file.write_text("OTHER=value\nUNRELATED=stuff\n")
        with pytest.raises(RuntimeError, match="ENCRYPTION_KEY is not set"):
            ensure_encryption_key(env_file)
    # #endregion test_ensure_key_file_without_key_raises


# ── get_encryption_manager singleton ────────────────────────────────────────

class TestGetEncryptionManager:
    """get_encryption_manager() — process-wide singleton."""

    # #region test_singleton_returns_same_instance [C:2] [TYPE Function] [SEMANTICS test,singleton]
    # @BRIEF Two calls to get_encryption_manager return the identical object.
    def test_singleton_returns_same_instance(self, monkeypatch):
        from src.core.encryption import get_encryption_manager

        monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
        # Reset module-level singleton
        import src.core.encryption as enc_mod
        enc_mod._encryption_manager = None

        mgr1 = get_encryption_manager()
        mgr2 = get_encryption_manager()
        assert mgr1 is mgr2
    # #endregion test_singleton_returns_same_instance

    # #region test_singleton_after_exception [C:2] [TYPE Function] [SEMANTICS test,singleton,recovery]
    # @BRIEF If first creation fails (no ENCRYPTION_KEY), singleton resets
    #        properly — subsequent call with valid key succeeds and is cached.
    def test_singleton_after_exception(self, monkeypatch):
        from src.core.encryption import get_encryption_manager

        # Ensure no key — first call raises
        monkeypatch.delenv("ENCRYPTION_KEY", raising=False)
        import src.core.encryption as enc_mod
        enc_mod._encryption_manager = None

        with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"):
            get_encryption_manager()
        # Singleton remains None after exception
        assert enc_mod._encryption_manager is None

        # Now set valid key — second call succeeds
        monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
        mgr = get_encryption_manager()
        assert mgr is not None
        # And subsequent call returns same instance
        assert get_encryption_manager() is mgr
    # #endregion test_singleton_after_exception

# #endregion Test.Encryption
