"""Tests for the FIPS-aware crypto module (security/fips.py).

Validates that all FIPS wrapper functions produce correct results in
both standard mode (stdlib fallback) and — when GM_FIPS_MODE=1 — via
the cryptography library's OpenSSL backend.
"""

import hashlib
import hmac
import os
import secrets
from unittest import mock

import pytest

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


# =========================================================================
# Random bytes
# =========================================================================

class TestFipsRandomBytes:
    def test_returns_requested_length(self):
        assert len(fips_random_bytes(32)) == 32
        assert len(fips_random_bytes(1)) == 1
        assert len(fips_random_bytes(64)) == 64

    def test_returns_bytes(self):
        result = fips_random_bytes(16)
        assert isinstance(result, bytes)

    def test_different_calls_produce_different_output(self):
        a = fips_random_bytes(32)
        b = fips_random_bytes(32)
        assert a != b  # Vanishingly unlikely to be equal

    def test_zero_bytes(self):
        assert fips_random_bytes(0) == b""


# =========================================================================
# SHA-256
# =========================================================================

class TestFipsSha256:
    def test_known_vector(self):
        """SHA-256 of empty string is well-known."""
        expected = hashlib.sha256(b"").digest()
        assert fips_sha256(b"") == expected

    def test_matches_hashlib(self):
        data = b"GreyMatter FIPS compliance test"
        assert fips_sha256(data) == hashlib.sha256(data).digest()

    def test_returns_32_bytes(self):
        assert len(fips_sha256(b"test")) == 32

    def test_hex_output(self):
        """Callers can use .hex() for hex-encoded digest."""
        data = b"test"
        assert fips_sha256(data).hex() == hashlib.sha256(data).hexdigest()

    def test_deterministic(self):
        data = b"same input"
        assert fips_sha256(data) == fips_sha256(data)

    def test_different_inputs_different_outputs(self):
        assert fips_sha256(b"a") != fips_sha256(b"b")


# =========================================================================
# HMAC-SHA256
# =========================================================================

class TestFipsHmacSha256:
    KEY = b"test-key-32-bytes-for-hmac!!!!"

    def test_matches_stdlib(self):
        data = b"payload"
        expected = hmac.new(self.KEY, data, hashlib.sha256).digest()
        assert fips_hmac_sha256(self.KEY, data) == expected

    def test_returns_32_bytes(self):
        assert len(fips_hmac_sha256(self.KEY, b"data")) == 32

    def test_hex_output(self):
        data = b"payload"
        expected = hmac.new(self.KEY, data, hashlib.sha256).hexdigest()
        assert fips_hmac_sha256(self.KEY, data).hex() == expected

    def test_different_keys_different_macs(self):
        data = b"same-data"
        mac1 = fips_hmac_sha256(b"key1", data)
        mac2 = fips_hmac_sha256(b"key2", data)
        assert mac1 != mac2

    def test_different_data_different_macs(self):
        mac1 = fips_hmac_sha256(self.KEY, b"data1")
        mac2 = fips_hmac_sha256(self.KEY, b"data2")
        assert mac1 != mac2

    def test_deterministic(self):
        data = b"payload"
        assert fips_hmac_sha256(self.KEY, data) == fips_hmac_sha256(self.KEY, data)


# =========================================================================
# PBKDF2
# =========================================================================

class TestFipsPbkdf2:
    def test_matches_hashlib(self):
        password = b"my-password"
        salt = b"fixed-salt-16bytes"
        expected = hashlib.pbkdf2_hmac("sha256", password, salt, 100_000)
        assert fips_pbkdf2(password, salt, iterations=100_000) == expected

    def test_custom_dklen(self):
        result = fips_pbkdf2(b"pw", b"salt", iterations=1000, dklen=16)
        assert len(result) == 16

    def test_default_dklen(self):
        result = fips_pbkdf2(b"pw", b"salt", iterations=1000)
        assert len(result) == 32

    def test_different_salts_different_keys(self):
        k1 = fips_pbkdf2(b"pw", b"salt1", iterations=1000)
        k2 = fips_pbkdf2(b"pw", b"salt2", iterations=1000)
        assert k1 != k2

    def test_different_passwords_different_keys(self):
        k1 = fips_pbkdf2(b"pw1", b"salt", iterations=1000)
        k2 = fips_pbkdf2(b"pw2", b"salt", iterations=1000)
        assert k1 != k2

    def test_deterministic(self):
        args = (b"password", b"salt", 1000, 32)
        assert fips_pbkdf2(*args) == fips_pbkdf2(*args)


# =========================================================================
# Token helpers
# =========================================================================

class TestFipsTokenUrlsafe:
    def test_returns_string(self):
        assert isinstance(fips_token_urlsafe(32), str)

    def test_different_calls_different_tokens(self):
        a = fips_token_urlsafe(32)
        b = fips_token_urlsafe(32)
        assert a != b

    def test_url_safe_characters(self):
        token = fips_token_urlsafe(48)
        # URL-safe base64 uses only A-Z, a-z, 0-9, -, _
        import re
        assert re.fullmatch(r"[A-Za-z0-9_-]+", token)

    def test_minimum_entropy(self):
        """32 bytes → at least 43 base64 characters."""
        token = fips_token_urlsafe(32)
        assert len(token) >= 40


class TestFipsTokenBytes:
    def test_returns_bytes(self):
        result = fips_token_bytes(32)
        assert isinstance(result, bytes)
        assert len(result) == 32

    def test_different_calls_different_values(self):
        a = fips_token_bytes(16)
        b = fips_token_bytes(16)
        assert a != b


class TestFipsChoice:
    def test_returns_element_from_sequence(self):
        seq = "abcdef"
        for _ in range(100):
            assert fips_choice(seq) in seq

    def test_empty_sequence_raises(self):
        with pytest.raises(IndexError, match="empty"):
            fips_choice([])

    def test_single_element(self):
        assert fips_choice([42]) == 42

    def test_reasonable_distribution(self):
        """All elements should appear at least once in 1000 draws from a 4-element set."""
        seq = "ABCD"
        seen = set()
        for _ in range(1000):
            seen.add(fips_choice(seq))
        assert seen == set("ABCD")


# =========================================================================
# FIPS mode detection
# =========================================================================

class TestFipsModeDetection:
    def test_is_fips_mode_returns_bool(self):
        assert isinstance(is_fips_mode(), bool)


# =========================================================================
# Integration: existing security module still works after FIPS migration
# =========================================================================

class TestSecurityModuleIntegration:
    """Verify that the security module's public API still works after
    migrating its internals to use FIPS wrappers."""

    SECRET = b"test-cluster-secret-for-fips-!!!"

    def test_token_roundtrip(self):
        from greymatter_core.security.tokens import (
            generate_node_token,
            verify_node_token,
        )
        token = generate_node_token("fips-node-1", secret=self.SECRET)
        valid, node_id = verify_node_token(token, secret=self.SECRET)
        assert valid is True
        assert node_id == "fips-node-1"

    def test_token_wrong_secret_rejected(self):
        from greymatter_core.security.tokens import (
            generate_node_token,
            verify_node_token,
        )
        token = generate_node_token("fips-node-1", secret=self.SECRET)
        valid, _ = verify_node_token(token, secret=b"wrong-secret-value")
        assert valid is False

    def test_sync_payload_roundtrip(self):
        from greymatter_core.security.encryption import (
            encrypt_sync_payload,
            decrypt_sync_payload,
        )
        payload = {"table": "test", "rows": [{"id": "1", "val": "fips"}]}
        encrypted = encrypt_sync_payload(payload, secret=self.SECRET)
        decrypted = decrypt_sync_payload(encrypted, secret=self.SECRET)
        assert decrypted == payload

    def test_sync_payload_tamper_detected(self):
        from greymatter_core.security.encryption import (
            encrypt_sync_payload,
            decrypt_sync_payload,
        )
        encrypted = encrypt_sync_payload({"a": 1}, secret=self.SECRET)
        encrypted["hmac"] = "0" * 64
        with pytest.raises(ValueError, match="HMAC verification failed"):
            decrypt_sync_payload(encrypted, secret=self.SECRET)

    def test_key_derivation(self):
        from greymatter_core.security.encryption import _derive_key
        key = _derive_key(b"secret", b"salt", length=32)
        assert len(key) == 32
        # Deterministic
        assert _derive_key(b"secret", b"salt", length=32) == key

    def test_db_key_derivation(self):
        from greymatter_core.db import derive_key_from_password
        hex_key, salt = derive_key_from_password("test-password")
        assert len(hex_key) == 64  # 32 bytes = 64 hex chars
        assert len(salt) == 16
        # Same password + salt = same key
        hex_key2, _ = derive_key_from_password("test-password", salt=salt)
        assert hex_key == hex_key2

    def test_id_generation(self):
        from greymatter_core.ids import generate_pattern_id, generate_knowledge_id
        pid = generate_pattern_id()
        kid = generate_knowledge_id()
        assert pid.startswith("pat-")
        assert kid.startswith("kn-")
        assert len(pid) == 12  # "pat-" + 8 chars
        assert len(kid) == 11  # "kn-" + 8 chars

    def test_fips_exports_from_security_init(self):
        """Verify FIPS functions are accessible from the security package."""
        from greymatter_core.security import (
            fips_random_bytes,
            fips_sha256,
            fips_hmac_sha256,
            fips_pbkdf2,
            is_fips_mode,
        )
        assert callable(fips_random_bytes)
        assert callable(fips_sha256)
        assert callable(fips_hmac_sha256)
        assert callable(fips_pbkdf2)
        assert callable(is_fips_mode)
