"""Property-based tests for privacy mechanisms.

Tests differential privacy noise, k-anonymity enforcement, and hashing.
"""

from __future__ import annotations

import hypothesis.strategies as st
from hypothesis import assume, given, settings

from autodebug.learning.privacy import (
    DifferentialPrivacy,
    KAnonymityChecker,
    PrivacyAuditor,
    PrivacyConfig,
    hash_code_pattern,
    hash_identifier,
)

# --- Differential Privacy ---


class TestDifferentialPrivacyProperties:
    """Property tests for DP noise mechanisms."""

    @given(st.integers(min_value=0, max_value=10000))
    @settings(max_examples=50)
    def test_noisy_count_preserves_magnitude(self, true_count):
        """Noisy count should be within reasonable bounds of true count."""
        config = PrivacyConfig(epsilon=1.0, sensitivity=1.0)
        dp = DifferentialPrivacy(config)
        noisy = dp.add_noise_to_count(true_count)

        # With epsilon=1.0, noise scale = sensitivity/epsilon = 1.0
        # 99.9% of Laplace samples are within 7*scale = 7
        # Use generous bound to avoid flaky test
        bound = max(50, true_count * 0.5)
        assert abs(noisy - true_count) < bound, (
            f"Noisy count {noisy} too far from true count {true_count}"
        )

    @given(st.floats(min_value=0.0, max_value=1.0))
    @settings(max_examples=50)
    def test_noisy_rate_bounded_zero_one(self, true_rate):
        """Noisy rate should always be clamped to [0, 1]."""
        config = PrivacyConfig(epsilon=1.0)
        dp = DifferentialPrivacy(config)
        noisy = dp.add_noise_to_rate(true_rate, sample_size=100)
        assert 0.0 <= noisy <= 1.0, f"Rate out of bounds: {noisy}"

    def test_budget_tracking(self):
        """Privacy budget should be trackable."""
        config = PrivacyConfig(epsilon=1.0, total_budget=10.0)
        dp = DifferentialPrivacy(config)
        initial = dp.get_remaining_budget()
        # Budget starts at total_budget or infinity depending on impl
        assert initial > 0

    def test_budget_check(self):
        """check_budget() should return True when budget remains."""
        config = PrivacyConfig(epsilon=1.0, total_budget=10.0)
        dp = DifferentialPrivacy(config)
        assert dp.check_budget() is True


# --- K-Anonymity ---


class TestKAnonymityProperties:
    """Property tests for k-anonymity enforcement."""

    def test_suppresses_small_groups(self):
        """Groups with fewer than k records should not be k-anonymous."""
        checker = KAnonymityChecker(k=3)
        records = [
            {"type": "ValueError", "org": "org_1"},
            {"type": "ValueError", "org": "org_2"},
            # Only 2 records for this type, k=3 requires at least 3
        ]
        result = checker.is_k_anonymous(records, ["type"])
        # Dataset has only 2 records in one equivalence class, k=3 -> not anonymous
        assert result is False

    def test_large_groups_are_anonymous(self):
        """Groups with k or more records should be k-anonymous."""
        checker = KAnonymityChecker(k=3)
        records = [
            {"type": "ValueError", "org": "org_1"},
            {"type": "ValueError", "org": "org_2"},
            {"type": "ValueError", "org": "org_3"},
            {"type": "KeyError", "org": "org_4"},
            {"type": "KeyError", "org": "org_5"},
            {"type": "KeyError", "org": "org_6"},
        ]
        result = checker.is_k_anonymous(records, ["type"])
        assert result is True

    def test_mixed_group_sizes(self):
        """If any group has fewer than k records, dataset is not k-anonymous."""
        checker = KAnonymityChecker(k=3)
        records = [
            {"type": "ValueError", "org": "org_1"},
            {"type": "ValueError", "org": "org_2"},
            {"type": "ValueError", "org": "org_3"},
            {"type": "KeyError", "org": "org_4"},  # Only 1 KeyError
        ]
        result = checker.is_k_anonymous(records, ["type"])
        assert result is False

    def test_equivalence_class_sizes(self):
        """get_equivalence_class_sizes should return correct counts."""
        checker = KAnonymityChecker(k=3)
        records = [
            {"type": "ValueError", "org": "org_1"},
            {"type": "ValueError", "org": "org_2"},
            {"type": "KeyError", "org": "org_3"},
        ]
        sizes = checker.get_equivalence_class_sizes(records, ["type"])
        assert len(sizes) == 2
        total = sum(sizes.values())
        assert total == 3

    def test_find_small_classes(self):
        """find_small_classes should identify classes below k threshold."""
        checker = KAnonymityChecker(k=3)
        records = [
            {"type": "ValueError", "org": "org_1"},
            {"type": "ValueError", "org": "org_2"},
            {"type": "ValueError", "org": "org_3"},
            {"type": "KeyError", "org": "org_4"},
        ]
        small = checker.find_small_classes(records, ["type"])
        assert len(small) >= 1  # KeyError group has only 1 record


# --- Hashing ---


class TestHashingProperties:
    """Property tests for privacy-preserving hashing."""

    @given(st.text(min_size=1, max_size=100))
    @settings(max_examples=50)
    def test_hash_identifier_deterministic(self, identifier):
        """Same identifier always produces same hash."""
        h1 = hash_identifier(identifier)
        h2 = hash_identifier(identifier)
        assert h1 == h2

    @given(st.text(min_size=1, max_size=100))
    @settings(max_examples=30)
    def test_hash_identifier_is_hex(self, identifier):
        """Hash should be a hex string."""
        h = hash_identifier(identifier)
        assert all(c in "0123456789abcdef" for c in h)
        assert len(h) == 32

    @given(
        st.text(min_size=1, max_size=50, alphabet="abcdefghij"),
        st.text(min_size=1, max_size=50, alphabet="abcdefghij"),
    )
    @settings(max_examples=50)
    def test_different_identifiers_different_hashes(self, id1, id2):
        """Different identifiers should (usually) produce different hashes."""
        assume(id1 != id2)
        h1 = hash_identifier(id1)
        h2 = hash_identifier(id2)
        # With 32 hex chars (128 bits), collision probability is negligible
        assert h1 != h2, f"Hash collision: {id1!r} and {id2!r} both hash to {h1}"

    @given(st.text(min_size=1, max_size=200))
    @settings(max_examples=30)
    def test_hash_code_pattern_deterministic(self, code):
        """Same code always produces same pattern hash."""
        h1 = hash_code_pattern(code)
        h2 = hash_code_pattern(code)
        assert h1 == h2

    @given(st.text(min_size=1, max_size=200))
    @settings(max_examples=30)
    def test_hash_code_pattern_is_hex(self, code):
        """Code pattern hash should be a hex string."""
        h = hash_code_pattern(code)
        assert all(c in "0123456789abcdef" for c in h)
        assert len(h) == 16


# --- Privacy Auditor ---


class TestPrivacyAuditorProperties:
    """Tests for the privacy auditor."""

    def test_flags_file_paths(self):
        """Patterns containing file paths should be flagged."""
        auditor = PrivacyAuditor()
        result = auditor.audit_pattern({
            "exception_type": "FileNotFoundError",
            "message": "No such file: /home/user/secret/data.csv",
        })
        assert isinstance(result, dict)
        assert "safe_to_share" in result

    def test_flags_credentials(self):
        """Patterns containing credential-like strings should be flagged."""
        auditor = PrivacyAuditor()
        result = auditor.audit_pattern({
            "exception_type": "AuthError",
            "message": "Invalid token: sk-abc123secret",
        })
        assert isinstance(result, dict)
        assert "safe_to_share" in result

    def test_safe_pattern_allowed(self):
        """Clean patterns without PII should be allowed."""
        auditor = PrivacyAuditor()
        result = auditor.audit_pattern({
            "exception_type": "ValueError",
            "message": "Invalid integer format",
        })
        assert result["safe_to_share"] is True

    def test_metrics_tracking(self):
        """Auditor should track metrics correctly."""
        auditor = PrivacyAuditor()
        auditor.audit_pattern({"exception_type": "ValueError", "message": "bad value"})
        metrics = auditor.get_metrics()
        assert metrics.total_patterns_shared >= 0
