"""Property-based tests for the EgressGate with hypothesis.

Every property is a fail-closed guarantee: for ANY combination of
provider, classifications and destination, the gate never grants egress
that the policy does not explicitly allow.
"""

from __future__ import annotations

import tempfile
from pathlib import Path

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

from cyberbrain26.data_plane import SQLiteDatabase
from cyberbrain26.security_plane import (
    AuditLog,
    DataClassification,
    EgressGate,
    EgressPolicy,
    EgressRequest,
    SecurityError,
)
from cyberbrain26.security_plane.extension_points import EgressPermissionGate

# Hypothesis tuned to find failures quickly.
MAX_EXAMPLES = 300


def egress_requests_for_policy(policy: EgressPolicy):
    """Strategy that can emit any realistic request for the given policy."""
    provider_names = list(policy.allowed_remote_providers) or list(policy.local_provider_ids)
    if not provider_names:
        # If neither allowlist is populated, we must still emit requests to
        # prove the gate denies unknown providers and empty classification.
        provider_names = ["unknown_provider", "remote"]
    providers = st.sampled_from(provider_names)
    classifications = st.lists(st.sampled_from(list(DataClassification)), unique=True)
    destinations = st.sampled_from(
        ["http://localhost", "https://example.com", "http://127.0.0.1:9000", "https://example.com?q=1#frag"]
    )
    return st.tuples(providers, classifications, destinations)


class TestEgressGateProperty:
    """Fail-closed guarantees for ANY request, not just pre-selected cases."""

    @settings(max_examples=MAX_EXAMPLES, deadline=5000)
    @given(egress_requests_for_policy(EgressPolicy()))
    def test_no_bypass_when_no_allowlist(
        self, params: tuple[str, list[DataClassification], str]
    ) -> None:
        """A gate with no provider allowlist denies EVERY request, always."""
        provider, classifications, destination = params
        with tempfile.TemporaryDirectory() as tmp_dir:
            audit = AuditLog(database=SQLiteDatabase(Path(tmp_dir) / "egress.sqlite3"))
            policy = EgressPolicy()
            gate = EgressGate(policy, audit_log=audit, base_decision=True)
            with pytest.raises(SecurityError):
                gate.authorize(
                    EgressRequest(
                        agent_id="agent-0",
                        provider_id=provider,
                        payload_sha256="0" * 64,
                        classifications=frozenset(classifications),
                        destination=destination,
                    )
                )

    @settings(max_examples=MAX_EXAMPLES, deadline=5000)
    @given(st.sampled_from(list(DataClassification)))
    def test_classification_boundary_is_enforced(self, cls: DataClassification) -> None:
        """For ANY classification, it is accepted iff the policy explicitly allows it."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            allowed = frozenset({DataClassification.PUBLIC, DataClassification.PERSONAL})
            audit = AuditLog(database=SQLiteDatabase(Path(tmp_dir) / "classification_boundary.db"))
            policy = EgressPolicy(
                allowed_remote_providers=frozenset({"remote"}),
                allowed_remote_classifications=allowed,
            )
            gate = EgressGate(policy, audit_log=audit, base_decision=True)
            if cls in allowed:
                gate.authorize(
                    EgressRequest(
                        agent_id="agent-0",
                        provider_id="remote",
                        payload_sha256="0" * 64,
                        classifications=frozenset({cls}),
                        destination="http://localhost",
                    )
                )
            else:
                with pytest.raises(SecurityError):
                    gate.authorize(
                        EgressRequest(
                            agent_id="agent-0",
                            provider_id="remote",
                            payload_sha256="0" * 64,
                            classifications=frozenset({cls}),
                            destination="http://localhost",
                        )
                    )

    @settings(max_examples=MAX_EXAMPLES, deadline=5000)
    @given(st.lists(st.sampled_from(list(DataClassification)), unique=True, min_size=1, max_size=4))
    def test_disallowed_classification_combinations_are_rejected(
        self, classes: list[DataClassification]
    ) -> None:
        """For ANY combination of classification sets, a disallowed class is rejected."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            audit = AuditLog(database=SQLiteDatabase(Path(tmp_dir) / "disallowed_combinations.db"))
            policy = EgressPolicy(
                allowed_remote_providers=frozenset({"remote"}),
                allowed_remote_classifications=frozenset({DataClassification.PUBLIC}),
            )
            gate = EgressGate(policy, audit_log=audit, base_decision=True)
            if all(c in {DataClassification.PUBLIC} for c in classes):
                gate.authorize(
                    EgressRequest(
                        agent_id="agent-0",
                        provider_id="remote",
                        payload_sha256="0" * 64,
                        classifications=frozenset(classes),
                        destination="http://localhost",
                    )
                )
            else:
                with pytest.raises(SecurityError, match="remote classifications denied"):
                    gate.authorize(
                        EgressRequest(
                            agent_id="agent-0",
                            provider_id="remote",
                            payload_sha256="0" * 64,
                            classifications=frozenset(classes),
                            destination="http://localhost",
                        )
                    )

    @settings(max_examples=MAX_EXAMPLES, deadline=5000)
    @given(st.sampled_from([True, False]), st.lists(st.sampled_from(list(DataClassification)), unique=True, min_size=1, max_size=4))
    def test_fail_closed_overrides_all_classifications(
        self, base_decision: bool, classes: list[DataClassification]
    ) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            audit = AuditLog(database=SQLiteDatabase(Path(tmp_dir) / "fail_closed_overrides.db"))
            policy = EgressPolicy(
                allowed_remote_providers=frozenset({"remote"}),
                allowed_remote_classifications=frozenset({DataClassification.PUBLIC}),
                local_provider_ids=frozenset({"local"}),
                allowed_local_classifications=frozenset(
                    {DataClassification.PUBLIC, DataClassification.PERSONAL, DataClassification.SENSITIVE}
                ),
            )
            gate = EgressGate(policy, audit_log=audit)
            gate.egress_permission_gate = EgressPermissionGate(base_decision=base_decision)
            if base_decision:
                allowed_local = {
                    DataClassification.PUBLIC,
                    DataClassification.PERSONAL,
                    DataClassification.SENSITIVE,
                }
                if all(c in allowed_local for c in classes):
                    gate.authorize(
                        EgressRequest(
                            agent_id="agent-0",
                            provider_id="local",
                            payload_sha256="0" * 64,
                            classifications=frozenset(classes),
                            destination="http://localhost",
                        )
                    )
                else:
                    with pytest.raises(SecurityError, match="local classifications denied"):
                        gate.authorize(
                            EgressRequest(
                                agent_id="agent-0",
                                provider_id="local",
                                payload_sha256="0" * 64,
                                classifications=frozenset(classes),
                                destination="http://localhost",
                            )
                        )
            else:
                with pytest.raises(SecurityError, match="egress permission denied"):
                    gate.authorize(
                        EgressRequest(
                            agent_id="agent-0",
                            provider_id="local",
                            payload_sha256="0" * 64,
                            classifications=frozenset(classes),
                            destination="http://localhost",
                        )
                    )

    @settings(max_examples=MAX_EXAMPLES, deadline=5000)
    @given(st.sampled_from(list(DataClassification)))
    def test_local_classification_gate_independent_from_remote(self, cls: DataClassification) -> None:
        """Local and remote classification gates are evaluated independently."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            audit = AuditLog(database=SQLiteDatabase(Path(tmp_dir) / "local_vs_remote.db"))
            local_allowed = frozenset({DataClassification.PUBLIC})
            gate = EgressGate(
                EgressPolicy(
                    local_provider_ids=frozenset({"local"}),
                    allowed_local_classifications=local_allowed,
                ),
                audit_log=audit,
                base_decision=True,
            )
            if cls == DataClassification.PUBLIC:
                gate.authorize(
                    EgressRequest(
                        agent_id="agent-0",
                        provider_id="local",
                        payload_sha256="0" * 64,
                        classifications=frozenset({cls}),
                        destination="http://localhost",
                    )
                )
            else:
                with pytest.raises(SecurityError):
                    gate.authorize(
                        EgressRequest(
                            agent_id="agent-0",
                            provider_id="local",
                            payload_sha256="0" * 64,
                            classifications=frozenset({cls}),
                            destination="http://localhost",
                        )
                    )
