"""Extended integration tests for FIPS Phase C crypto wrapper.

Covers gaps not addressed in test_fips_integration.py:
- FIPS runtime fallback paths (OpenSSL failure → stdlib graceful fallback)
- FIPS operational logging verification
- teams/auth.py integration (JWT, MFA, session tokens via FIPS crypto)
- wallet/storage.py integration (seed encryption via FIPS crypto)
- Import fallback shim correctness
- Performance bounds for crypto operations
- Structural safety checks (timing-safe comparisons)
"""

from __future__ import annotations

import base64
import hashlib
import hmac as _hmac
import json
import logging
import os
import time
from unittest import mock

import pytest

from greymatter_core.security.fips import (
    _FIPS_MODE,
    _HAS_CRYPTO,
    fips_choice,
    fips_hmac_sha256,
    fips_pbkdf2,
    fips_random_bytes,
    fips_sha256,
    fips_token_bytes,
    fips_token_urlsafe,
    is_fips_mode,
)


# =========================================================================
# FIPS runtime fallback: OpenSSL failure → stdlib graceful degradation
# =========================================================================

class TestFipsFallbackOnException:
    """When FIPS mode is active but an OpenSSL call fails at runtime,
    each function should fall back to stdlib transparently."""

    def _make_fips_active(self):
        """Return patches that make FIPS mode appear active."""
        return {
            "greymatter_core.security.fips._FIPS_MODE": True,
            "greymatter_core.security.fips._HAS_CRYPTO": True,
        }

    def test_random_bytes_falls_back_on_openssl_error(self):
        """If RAND_bytes raises, fips_random_bytes should fall back to os.urandom."""
        # Create a mock backend whose RAND_bytes raises
        mock_backend = mock.MagicMock()
        mock_backend._lib.RAND_bytes.side_effect = RuntimeError("FIPS DRBG failure")

        patches = self._make_fips_active()
        patches["greymatter_core.security.fips._ossl_backend"] = mock_backend

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_ossl_backend": mock_backend,
        }):
            result = fips_random_bytes(32)
            assert isinstance(result, bytes)
            assert len(result) == 32

    def test_sha256_falls_back_on_crypto_error(self):
        """If cryptography Hash raises, fips_sha256 should fall back to hashlib."""
        mock_hashes = mock.MagicMock()
        mock_hashes.Hash.side_effect = RuntimeError("FIPS Hash init failure")
        mock_hashes.SHA256.return_value = mock.MagicMock()

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_crypto_hashes": mock_hashes,
        }):
            data = b"test data for sha256 fallback"
            result = fips_sha256(data)
            expected = hashlib.sha256(data).digest()
            assert result == expected

    def test_hmac_sha256_falls_back_on_crypto_error(self):
        """If cryptography HMAC raises, fips_hmac_sha256 should fall back to hmac.new."""
        mock_hmac_cls = mock.MagicMock(side_effect=RuntimeError("FIPS HMAC init failure"))

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_CryptoHMAC": mock_hmac_cls,
            "_crypto_hashes": mock.MagicMock(),
        }):
            key = b"my-secret-key"
            data = b"test payload"
            result = fips_hmac_sha256(key, data)
            expected = _hmac.new(key, data, hashlib.sha256).digest()
            assert result == expected

    def test_pbkdf2_falls_back_on_crypto_error(self):
        """If cryptography PBKDF2HMAC raises, fips_pbkdf2 should fall back to hashlib."""
        mock_pbkdf2_cls = mock.MagicMock(side_effect=RuntimeError("FIPS KDF failure"))

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_PBKDF2HMAC": mock_pbkdf2_cls,
            "_crypto_hashes": mock.MagicMock(),
        }):
            pw, salt, iters = b"password", b"salt-value", 1000
            result = fips_pbkdf2(pw, salt, iterations=iters)
            expected = hashlib.pbkdf2_hmac("sha256", pw, salt, iters, dklen=32)
            assert result == expected

    def test_fallback_logs_warning(self, caplog):
        """Fallback should emit a warning log so operators can detect degraded mode."""
        mock_hashes = mock.MagicMock()
        mock_hashes.Hash.side_effect = RuntimeError("Simulated FIPS failure")
        mock_hashes.SHA256.return_value = mock.MagicMock()

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_crypto_hashes": mock_hashes,
        }):
            with caplog.at_level(logging.WARNING, logger="greymatter_core.security.fips"):
                fips_sha256(b"trigger fallback")

            assert any("falling back" in rec.message.lower() for rec in caplog.records), \
                f"Expected fallback warning, got: {[r.message for r in caplog.records]}"

    def test_all_functions_produce_correct_output_after_fallback(self):
        """Full-stack: even when all FIPS paths fail, output is crypto-correct."""
        mock_backend = mock.MagicMock()
        mock_backend._lib.RAND_bytes.side_effect = RuntimeError("FIPS RNG failure")

        mock_hashes = mock.MagicMock()
        mock_hashes.Hash.side_effect = RuntimeError("SHA failure")
        mock_hashes.SHA256.return_value = mock.MagicMock()

        mock_hmac_cls = mock.MagicMock(side_effect=RuntimeError("HMAC failure"))

        mock_pbkdf2_cls = mock.MagicMock(side_effect=RuntimeError("KDF failure"))

        with mock.patch.multiple("greymatter_core.security.fips", **{
            "_FIPS_MODE": True,
            "_HAS_CRYPTO": True,
            "_ossl_backend": mock_backend,
            "_crypto_hashes": mock_hashes,
            "_CryptoHMAC": mock_hmac_cls,
            "_PBKDF2HMAC": mock_pbkdf2_cls,
        }):
            # All should work via stdlib fallback
            rb = fips_random_bytes(16)
            assert len(rb) == 16

            sha = fips_sha256(b"test")
            assert sha == hashlib.sha256(b"test").digest()

            mac = fips_hmac_sha256(b"key", b"msg")
            assert mac == _hmac.new(b"key", b"msg", hashlib.sha256).digest()

            kdf = fips_pbkdf2(b"pw", b"salt", iterations=100)
            assert kdf == hashlib.pbkdf2_hmac("sha256", b"pw", b"salt", 100)

            # Token helpers use fips_random_bytes under the hood
            token = fips_token_urlsafe(16)
            assert isinstance(token, str) and len(token) > 0

            tb = fips_token_bytes(16)
            assert isinstance(tb, bytes) and len(tb) == 16


# =========================================================================
# FIPS operational logging
# =========================================================================

class TestFipsLogging:
    """Verify FIPS mode emits correct operational log messages at import time."""

    def test_fips_mode_without_crypto_logs_warning(self, caplog):
        """GM_FIPS_MODE=1 without cryptography library should warn."""
        # We can't easily test import-time logging without reimporting,
        # but we can test the conditional logic that drives the logging.
        # The module sets _FIPS_MODE and _HAS_CRYPTO at import time;
        # when _FIPS_MODE is True and _HAS_CRYPTO is False, a warning is logged.
        assert isinstance(_FIPS_MODE, bool)
        assert isinstance(_HAS_CRYPTO, bool)
        # Structural check: is_fips_mode requires BOTH flags
        assert is_fips_mode() == (_FIPS_MODE and _HAS_CRYPTO)

    def test_fips_module_globals_are_consistent(self):
        """Module-level globals should be self-consistent."""
        from greymatter_core.security import fips
        # _FIPS_MODE is determined by env var at import time
        assert hasattr(fips, "_FIPS_MODE")
        assert hasattr(fips, "_HAS_CRYPTO")
        assert hasattr(fips, "_FIPS_PROVIDER_AVAILABLE")
        # is_fips_mode returns bool
        assert isinstance(fips.is_fips_mode(), bool)
        # If FIPS provider is available, _HAS_CRYPTO must be True
        if fips._FIPS_PROVIDER_AVAILABLE:
            assert fips._HAS_CRYPTO is True


# =========================================================================
# teams/auth.py integration — JWT, MFA, session tokens via FIPS crypto
# =========================================================================

class TestTeamsJWTIntegration:
    """JWT tokens (create + verify) use fips_hmac_sha256 under the hood."""

    def test_jwt_create_and_verify_roundtrip(self):
        from teams.auth import JWTManager
        mgr = JWTManager(secret="fips-test-jwt-secret-32chars!!")
        token = mgr.create_token("member-1", "solo-1", role="admin")
        claims = mgr.verify_token(token)
        assert claims is not None
        assert claims.member_id == "member-1"
        assert claims.solo_id == "solo-1"
        assert claims.role == "admin"

    def test_jwt_wrong_secret_rejected(self):
        from teams.auth import JWTManager
        mgr1 = JWTManager(secret="correct-secret")
        mgr2 = JWTManager(secret="wrong-secret")
        token = mgr1.create_token("member-1", "solo-1")
        claims = mgr2.verify_token(token)
        assert claims is None

    def test_jwt_tampered_payload_rejected(self):
        from teams.auth import JWTManager
        mgr = JWTManager(secret="fips-jwt-secret")
        token = mgr.create_token("member-1", "solo-1")
        parts = token.split(".")
        assert len(parts) == 3
        # Tamper with the payload (middle part)
        import base64
        payload_bytes = base64.urlsafe_b64decode(parts[1] + "==")
        payload_dict = json.loads(payload_bytes)
        payload_dict["role"] = "admin"  # Attempt privilege escalation
        tampered_b64 = base64.urlsafe_b64encode(
            json.dumps(payload_dict, separators=(",", ":")).encode()
        ).rstrip(b"=").decode("ascii")
        tampered_token = f"{parts[0]}.{tampered_b64}.{parts[2]}"
        claims = mgr.verify_token(tampered_token)
        assert claims is None

    def test_jwt_expired_token_rejected(self):
        from teams.auth import JWTManager
        mgr = JWTManager(secret="fips-jwt-secret")
        # Create token that expires immediately
        token = mgr.create_token("member-1", "solo-1", expires_in=0.001)
        time.sleep(0.01)
        claims = mgr.verify_token(token)
        assert claims is None

    def test_jwt_no_expiry_stays_valid(self):
        from teams.auth import JWTManager
        mgr = JWTManager(secret="fips-jwt-secret")
        token = mgr.create_token("member-1", "solo-1", expires_in=0)
        claims = mgr.verify_token(token)
        assert claims is not None
        assert claims.member_id == "member-1"

    def test_jwt_format_is_3_part_dot_separated(self):
        from teams.auth import JWTManager
        mgr = JWTManager(secret="fips-jwt-secret")
        token = mgr.create_token("member-1", "solo-1")
        parts = token.split(".")
        assert len(parts) == 3
        # Header should decode to {"alg":"HS256","typ":"JWT"}
        header = json.loads(base64.urlsafe_b64decode(parts[0] + "=="))
        assert header["alg"] == "HS256"
        assert header["typ"] == "JWT"

    def test_jwt_malformed_token_rejected(self):
        from teams.auth import JWTManager
        mgr = JWTManager(secret="fips-jwt-secret")
        assert mgr.verify_token("") is None
        assert mgr.verify_token("only.two") is None
        assert mgr.verify_token("a.b.c.d") is None
        assert mgr.verify_token("not-a-jwt") is None

    def test_jwt_deterministic_signature(self):
        """Same secret + same payload → same HMAC signature."""
        from teams.auth import JWTManager, _b64url_encode
        mgr = JWTManager(secret="deterministic-secret")
        # Manually construct identical payloads to verify HMAC determinism
        header_b64 = _b64url_encode(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
        payload_b64 = _b64url_encode(json.dumps({"member_id": "m1", "solo_id": "s1"}, separators=(",", ":")).encode())
        signing_input = f"{header_b64}.{payload_b64}"
        sig1 = fips_hmac_sha256("deterministic-secret".encode(), signing_input.encode())
        sig2 = fips_hmac_sha256("deterministic-secret".encode(), signing_input.encode())
        assert sig1 == sig2


class TestTeamsMFAIntegration:
    """MFA challenge flow uses FIPS token generation."""

    def test_issue_challenge_generates_unique_ids(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        c1 = svc.issue_challenge("solo-A")
        c2 = svc.issue_challenge("solo-B")
        assert c1.challenge_id != c2.challenge_id
        assert c1.solo_id == "solo-A"
        assert c2.solo_id == "solo-B"

    def test_challenge_expiry(self):
        from teams.auth import TeamAuthService, MFAChallenge
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        # Create an expired challenge
        c = MFAChallenge(
            challenge_id="expired-1",
            method="totp",
            solo_id="solo-X",
            expires_at=time.time() - 10,  # 10 seconds ago
        )
        assert c.is_expired is True

    def test_verify_challenge_accepts_first_time_join(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        challenge = svc.issue_challenge("solo-new")
        # Without TOTP secret, first-time join accepts any non-empty response
        result = svc.verify_challenge(challenge.challenge_id, "123456")
        assert result is True

    def test_verify_challenge_rejects_empty_response(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        challenge = svc.issue_challenge("solo-new")
        result = svc.verify_challenge(challenge.challenge_id, "")
        assert result is False

    def test_verify_unknown_challenge_returns_false(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        result = svc.verify_challenge("nonexistent-id", "123456")
        assert result is False


class TestTeamsSessionTokenIntegration:
    """Session tokens use fips_token_urlsafe + fips_sha256."""

    def test_issue_and_verify_session_token(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        raw_token = svc.issue_session_token("member-A", "solo-A")
        assert isinstance(raw_token, str)
        assert len(raw_token) > 40  # urlsafe token has good entropy

        session = svc.verify_token(raw_token)
        assert session is not None
        assert session.member_id == "member-A"
        assert session.solo_id == "solo-A"

    def test_wrong_token_rejected(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        svc.issue_session_token("member-A", "solo-A")
        session = svc.verify_token("fake-token-value")
        assert session is None

    def test_revoked_token_invalid(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        raw_token = svc.issue_session_token("member-A", "solo-A")
        # Verify it works
        assert svc.verify_token(raw_token) is not None
        # Revoke
        svc.revoke_token("member-A")
        # Should be invalid now
        assert svc.verify_token(raw_token) is None

    def test_multiple_session_tokens_unique(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        tokens = [svc.issue_session_token(f"member-{i}", "solo-1") for i in range(10)]
        assert len(set(tokens)) == 10

    def test_session_token_hash_uses_fips_sha256(self):
        """Verify the stored hash matches fips_sha256 of the raw token."""
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        raw_token = svc.issue_session_token("member-X", "solo-X")

        expected_hash = fips_sha256(raw_token.encode()).hex()
        # The service stores the hash internally
        assert expected_hash in svc._active_sessions

    def test_expired_session_token_rejected(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="test-jwt-secret")
        raw_token = svc.issue_session_token("member-A", "solo-A", expires_in=0.001)
        time.sleep(0.01)
        assert svc.verify_token(raw_token) is None


class TestTeamsAuthFullFlow:
    """End-to-end: MFA → session token → JWT — all using FIPS crypto."""

    def test_full_auth_flow(self):
        from teams.auth import TeamAuthService
        svc = TeamAuthService(jwt_secret="e2e-test-secret")

        # Step 1: Issue MFA challenge
        challenge = svc.issue_challenge("solo-onboard")
        assert not challenge.is_expired

        # Step 2: Verify MFA (first-time join)
        verified = svc.verify_challenge(challenge.challenge_id, "accept")
        assert verified is True

        # Step 3: Issue session token
        session_token = svc.issue_session_token("member-new", "solo-onboard")
        session = svc.verify_token(session_token)
        assert session is not None
        assert session.member_id == "member-new"

        # Step 4: Issue JWT
        jwt = svc.issue_jwt("member-new", "solo-onboard", role="member")
        claims = svc.verify_jwt(jwt)
        assert claims is not None
        assert claims.member_id == "member-new"
        assert claims.role == "member"

        # Step 5: Revoke on check-out
        svc.revoke_token("member-new")
        assert svc.verify_token(session_token) is None
        # JWT still valid (stateless — not revoked by session revocation)
        assert svc.verify_jwt(jwt) is not None


# =========================================================================
# wallet/storage.py integration — seed encryption via FIPS crypto
# =========================================================================

class TestWalletSeedStorageIntegration:
    """SeedStorage uses fips_random_bytes (salt) and fips_sha256 (checksum)."""

    @pytest.fixture
    def storage(self, tmp_path):
        try:
            from wallet.storage import SeedStorage
            return SeedStorage(storage_dir=tmp_path)
        except ImportError:
            pytest.skip("cryptography package required for SeedStorage")

    def test_store_and_load_roundtrip(self, storage):
        mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        storage.store(mnemonic, "strong-password-123!")
        loaded = storage.load("strong-password-123!")
        assert loaded == mnemonic

    def test_wrong_password_fails(self, storage):
        mnemonic = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
        storage.store(mnemonic, "correct-password")
        with pytest.raises(ValueError, match="wrong password|corrupted"):
            storage.load("wrong-password")

    def test_checksum_uses_fips_sha256(self, storage, tmp_path):
        """Stored checksum should match fips_sha256 of the mnemonic."""
        mnemonic = "test mnemonic phrase for checksum verification"
        storage.store(mnemonic, "pw")
        data = json.loads(storage.storage_path.read_text())
        expected_checksum = fips_sha256(mnemonic.encode("utf-8")).hex()
        assert data["checksum"] == expected_checksum

    def test_salt_is_unique_per_store(self, tmp_path):
        """Each store() call generates a unique random salt via fips_random_bytes."""
        try:
            from wallet.storage import SeedStorage
        except ImportError:
            pytest.skip("cryptography package required")

        s1 = SeedStorage(storage_dir=tmp_path / "s1")
        s1.store("mnemonic one two three four five six seven eight nine ten eleven twelve", "pw")
        data1 = json.loads(s1.storage_path.read_text())

        s2 = SeedStorage(storage_dir=tmp_path / "s2")
        s2.store("mnemonic one two three four five six seven eight nine ten eleven twelve", "pw")
        data2 = json.loads(s2.storage_path.read_text())

        assert data1["salt"] != data2["salt"]

    def test_empty_mnemonic_rejected(self, storage):
        with pytest.raises(ValueError, match="empty"):
            storage.store("", "password")

    def test_empty_password_rejected(self, storage):
        with pytest.raises(ValueError, match="empty"):
            storage.store("valid mnemonic phrase here one two three four five six seven eight nine", "")

    def test_duplicate_store_raises(self, storage):
        mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        storage.store(mnemonic, "pw")
        with pytest.raises(FileExistsError):
            storage.store(mnemonic, "pw")

    def test_overwrite_replaces_existing(self, storage):
        m1 = "first mnemonic one two three four five six seven eight nine ten eleven"
        m2 = "second mnemonic one two three four five six seven eight nine ten eleven"
        storage.store(m1, "pw")
        storage.overwrite(m2, "pw2")
        loaded = storage.load("pw2")
        assert loaded == m2

    def test_verify_password_correct(self, storage):
        mnemonic = "test phrase one two three four five six seven eight nine ten eleven twelve"
        storage.store(mnemonic, "my-password")
        assert storage.verify_password("my-password") is True
        assert storage.verify_password("wrong-password") is False

    def test_metadata_without_decryption(self, storage):
        mnemonic = "word " * 24
        mnemonic = mnemonic.strip()
        storage.store(mnemonic, "pw")
        meta = storage.get_metadata()
        assert meta is not None
        assert meta["version"] == 1
        assert meta["word_count"] == 24
        assert "created_at" in meta

    def test_destroy_securely_deletes(self, storage):
        mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        storage.store(mnemonic, "pw")
        assert storage.exists()
        result = storage.destroy()
        assert result is True
        assert not storage.exists()

    def test_destroy_nonexistent_returns_false(self, storage):
        assert storage.destroy() is False

    def test_storage_file_permissions(self, storage):
        """Encrypted seed file should have owner-only permissions (0o600)."""
        mnemonic = "test phrase one two three four five six seven eight nine ten eleven twelve"
        path = storage.store(mnemonic, "pw")
        mode = oct(path.stat().st_mode & 0o777)
        assert mode == "0o600", f"Expected 0o600, got {mode}"


# =========================================================================
# Import fallback shim correctness
# =========================================================================

class TestImportFallbackShims:
    """When greymatter_core is not importable, dependent modules define
    local fallback shim functions. Verify those shims are crypto-correct."""

    def test_teams_auth_shim_hmac_matches_fips(self):
        """The teams/auth.py fallback fips_hmac_sha256 should match real one."""
        # The shim is: hmac.new(key, data, hashlib.sha256).digest()
        key = b"shim-test-key"
        data = b"shim-test-data"
        shim_result = _hmac.new(key, data, hashlib.sha256).digest()
        real_result = fips_hmac_sha256(key, data)
        assert shim_result == real_result

    def test_teams_auth_shim_sha256_matches_fips(self):
        """The teams/auth.py fallback fips_sha256 should match real one."""
        data = b"shim sha256 test"
        shim_result = hashlib.sha256(data).digest()
        real_result = fips_sha256(data)
        assert shim_result == real_result

    def test_wallet_storage_shim_random_bytes_type(self):
        """The wallet/storage.py fallback fips_random_bytes (os.urandom) should
        return bytes of the correct length."""
        # The shim is: fips_random_bytes = os.urandom
        result = os.urandom(32)
        assert isinstance(result, bytes)
        assert len(result) == 32

    def test_wallet_storage_shim_sha256_matches_fips(self):
        """The wallet/storage.py fallback fips_sha256 should match real one."""
        data = b"wallet checksum test"
        shim_result = hashlib.sha256(data).digest()
        real_result = fips_sha256(data)
        assert shim_result == real_result

    def test_shim_and_real_produce_identical_hmac_for_varied_inputs(self):
        """Cross-validate shim vs real FIPS for diverse inputs."""
        cases = [
            (b"", b""),
            (b"k", b"d"),
            (b"k" * 128, b"d" * 4096),
            (os.urandom(64), os.urandom(256)),
        ]
        for key, data in cases:
            shim = _hmac.new(key, data, hashlib.sha256).digest()
            real = fips_hmac_sha256(key, data)
            assert shim == real, f"Mismatch for key len={len(key)}, data len={len(data)}"


# =========================================================================
# Timing-safe comparison structural checks
# =========================================================================

class TestTimingSafeStructural:
    """Verify that security-critical code paths use hmac.compare_digest
    instead of == for signature comparisons."""

    def test_token_verify_uses_compare_digest(self):
        """tokens.py verify_node_token should use hmac.compare_digest."""
        import inspect
        from greymatter_core.security.tokens import verify_node_token
        source = inspect.getsource(verify_node_token)
        assert "compare_digest" in source, \
            "verify_node_token must use hmac.compare_digest for timing-safe comparison"

    def test_encryption_decrypt_uses_compare_digest(self):
        """encryption.py decrypt_sync_payload should use hmac.compare_digest."""
        import inspect
        from greymatter_core.security.encryption import decrypt_sync_payload
        source = inspect.getsource(decrypt_sync_payload)
        assert "compare_digest" in source, \
            "decrypt_sync_payload must use hmac.compare_digest for timing-safe comparison"

    def test_jwt_verify_uses_compare_digest(self):
        """teams/auth.py JWTManager.verify_token should use hmac.compare_digest."""
        import inspect
        from teams.auth import JWTManager
        source = inspect.getsource(JWTManager.verify_token)
        assert "compare_digest" in source, \
            "JWTManager.verify_token must use hmac.compare_digest for timing-safe comparison"


# =========================================================================
# Performance bounds
# =========================================================================

class TestPerformanceBounds:
    """Basic perf checks: crypto ops should complete within reasonable time."""

    def test_random_bytes_performance(self):
        """1000 × fips_random_bytes(32) should complete in under 1 second."""
        start = time.monotonic()
        for _ in range(1000):
            fips_random_bytes(32)
        elapsed = time.monotonic() - start
        assert elapsed < 1.0, f"Too slow: {elapsed:.3f}s for 1000 random_bytes calls"

    def test_sha256_performance(self):
        """1000 × fips_sha256(1KB) should complete in under 1 second."""
        data = os.urandom(1024)
        start = time.monotonic()
        for _ in range(1000):
            fips_sha256(data)
        elapsed = time.monotonic() - start
        assert elapsed < 1.0, f"Too slow: {elapsed:.3f}s for 1000 sha256 calls"

    def test_hmac_performance(self):
        """1000 × fips_hmac_sha256 should complete in under 1 second."""
        key = os.urandom(32)
        data = os.urandom(1024)
        start = time.monotonic()
        for _ in range(1000):
            fips_hmac_sha256(key, data)
        elapsed = time.monotonic() - start
        assert elapsed < 1.0, f"Too slow: {elapsed:.3f}s for 1000 hmac calls"

    def test_pbkdf2_performance(self):
        """fips_pbkdf2 with 1000 iters should complete in under 0.5s."""
        start = time.monotonic()
        fips_pbkdf2(b"password", b"salt", iterations=1000)
        elapsed = time.monotonic() - start
        assert elapsed < 0.5, f"Too slow: {elapsed:.3f}s for pbkdf2(1000 iters)"

    def test_token_generation_performance(self):
        """1000 token generations should complete in under 2 seconds."""
        from greymatter_core.security.tokens import generate_node_token
        secret = b"perf-test-secret"
        start = time.monotonic()
        for i in range(1000):
            generate_node_token(f"node-{i}", secret=secret)
        elapsed = time.monotonic() - start
        assert elapsed < 2.0, f"Too slow: {elapsed:.3f}s for 1000 token generations"

    def test_id_generation_performance(self):
        """10000 ID generations should complete in under 2 seconds."""
        from greymatter_core.ids import generate_pattern_id
        start = time.monotonic()
        for _ in range(10_000):
            generate_pattern_id()
        elapsed = time.monotonic() - start
        assert elapsed < 2.0, f"Too slow: {elapsed:.3f}s for 10000 ID generations"

    def test_encrypt_decrypt_performance(self):
        """100 encrypt+decrypt roundtrips (1KB payload) under 2 seconds."""
        from greymatter_core.security.encryption import (
            encrypt_sync_payload,
            decrypt_sync_payload,
        )
        payload = {"data": "x" * 1024}
        secret = b"perf-test-secret-32-bytes-long!!"
        start = time.monotonic()
        for _ in range(100):
            enc = encrypt_sync_payload(payload, secret=secret)
            decrypt_sync_payload(enc, secret=secret)
        elapsed = time.monotonic() - start
        assert elapsed < 2.0, f"Too slow: {elapsed:.3f}s for 100 encrypt/decrypt roundtrips"


# =========================================================================
# Additional error cases for decrypt_sync_payload
# =========================================================================

class TestDecryptErrorCases:
    """Error handling in decrypt_sync_payload for malformed inputs."""

    SECRET = b"error-case-test-secret-32-bytes!"

    def test_missing_ct_field_raises(self):
        from greymatter_core.security.encryption import decrypt_sync_payload
        with pytest.raises(Exception):
            decrypt_sync_payload({"v": 1, "hmac": "a" * 64, "nonce": "AA=="}, secret=self.SECRET)

    def test_missing_hmac_field_raises(self):
        from greymatter_core.security.encryption import decrypt_sync_payload
        with pytest.raises(Exception):
            decrypt_sync_payload({"v": 1, "ct": "AA==", "nonce": "AA=="}, secret=self.SECRET)

    def test_empty_encrypted_dict_raises(self):
        from greymatter_core.security.encryption import decrypt_sync_payload
        with pytest.raises(Exception):
            decrypt_sync_payload({}, secret=self.SECRET)

    def test_negative_version_raises(self):
        from greymatter_core.security.encryption import decrypt_sync_payload
        with pytest.raises(ValueError, match="Unknown encryption version"):
            decrypt_sync_payload({"v": -1, "ct": "", "hmac": ""}, secret=self.SECRET)

    def test_string_version_handled(self):
        """Non-int version should raise a clear error."""
        from greymatter_core.security.encryption import decrypt_sync_payload
        with pytest.raises(Exception):
            decrypt_sync_payload({"v": "invalid", "ct": "", "hmac": ""}, secret=self.SECRET)

    def test_base64_invalid_ct_raises(self):
        """Invalid base64 in ciphertext should raise."""
        from greymatter_core.security.encryption import decrypt_sync_payload
        with pytest.raises(Exception):
            decrypt_sync_payload(
                {"v": 1, "ct": "!!!not-base64!!!", "nonce": "AA==", "hmac": "a" * 64},
                secret=self.SECRET,
            )


# =========================================================================
# FIPS mode env var behavior
# =========================================================================

class TestFipsModeEnvVar:
    """Verify FIPS mode is driven by GM_FIPS_MODE environment variable."""

    def test_env_var_determines_fips_mode(self):
        """The module reads GM_FIPS_MODE at import time."""
        from greymatter_core.security import fips
        # _FIPS_MODE is set from env var
        expected = os.environ.get("GM_FIPS_MODE", "0") == "1"
        assert fips._FIPS_MODE == expected

    def test_is_fips_mode_false_by_default(self):
        """Without GM_FIPS_MODE=1, is_fips_mode() should be False."""
        # In CI/dev, GM_FIPS_MODE is typically not set
        if os.environ.get("GM_FIPS_MODE") != "1":
            assert is_fips_mode() is False

    def test_functions_work_without_fips_mode(self):
        """All FIPS functions should work correctly in non-FIPS mode."""
        # These all use stdlib fallback when FIPS is off
        assert len(fips_random_bytes(32)) == 32
        assert len(fips_sha256(b"test")) == 32
        assert len(fips_hmac_sha256(b"key", b"data")) == 32
        assert len(fips_pbkdf2(b"pw", b"salt", iterations=100)) == 32
        assert isinstance(fips_token_urlsafe(), str)
        assert isinstance(fips_token_bytes(), bytes)
        assert fips_choice("abc") in "abc"


# =========================================================================
# Cross-module: FIPS functions in encryption match standalone calls
# =========================================================================

class TestCrossModuleFIPSConsistency:
    """Verify that FIPS functions called from encryption.py and tokens.py
    produce the same results as calling them directly."""

    SECRET = b"cross-module-consistency-secret!"

    def test_token_hmac_matches_direct_fips_call(self):
        """The HMAC in a generated token should match a direct fips_hmac_sha256 call."""
        from greymatter_core.security.tokens import generate_node_token
        token = generate_node_token("test-node", secret=self.SECRET)
        parts = token.split(".")
        node_id, timestamp, nonce, sig = parts

        # Reconstruct the HMAC manually
        payload = f"{node_id}.{timestamp}.{nonce}"
        expected_sig = fips_hmac_sha256(self.SECRET, payload.encode()).hex()
        assert sig == expected_sig

    def test_derive_key_matches_direct_pbkdf2(self):
        """_derive_key should produce identical output to direct fips_pbkdf2."""
        from greymatter_core.security.encryption import _derive_key
        key_via_derive = _derive_key(b"secret", b"salt", length=32)
        key_via_pbkdf2 = fips_pbkdf2(b"secret", b"salt", iterations=100_000, dklen=32)
        assert key_via_derive == key_via_pbkdf2

    def test_sqlcipher_key_derivation_chain(self):
        """get_sqlcipher_key → _derive_key → fips_pbkdf2 should be traceable."""
        from greymatter_core.security.encryption import get_sqlcipher_key
        key = get_sqlcipher_key("my-node", secret=self.SECRET)
        # Manually derive the same key
        salt = b"gm-sqlcipher-my-node"
        manual_key = fips_pbkdf2(self.SECRET, salt, iterations=100_000, dklen=32).hex()
        assert key == manual_key

    def test_db_derive_key_chain(self):
        """derive_key_from_password → fips_pbkdf2 with 256,000 iterations."""
        from greymatter_core.db import derive_key_from_password
        hex_key, salt = derive_key_from_password("my-password")
        # Re-derive manually
        manual_key = fips_pbkdf2(
            b"my-password", salt, iterations=256_000, dklen=32
        ).hex()
        assert hex_key == manual_key
