"""Tests for PermissionGateway."""

import pytest

from cyberbrain26.security_plane.actions import (
    ACTION_SCOPE_REQUIREMENTS,
    KNOWN_ACTIONS,
    KNOWN_SAFE_ACTIONS,
    SENSITIVE_ACTIONS,
)
from cyberbrain26.security_plane.audit import AuditEventType, AuditLog, AuditQuery
from cyberbrain26.security_plane.identity import (
    AgentIdentity,
    PermissionScope,
    TrustLevel,
)
from cyberbrain26.security_plane.permission import (
    PermissionDecision,
    PermissionGateway,
    PermissionRequest,
    PermissionResult,
)
from cyberbrain26.security_plane.policy import (
    PolicyDecision,
    PolicyEngine,
    PolicyMode,
    PolicyRule,
)


@pytest.fixture
def trusted_agent() -> AgentIdentity:
    return AgentIdentity(
        agent_id="agent-1",
        role="coder",
        trust_level=TrustLevel.TRUSTED,
        scopes={PermissionScope.READ_FILE, PermissionScope.SHELL},
    )


@pytest.fixture
def untrusted_agent() -> AgentIdentity:
    return AgentIdentity(
        agent_id="agent-2",
        role="coder",
        trust_level=TrustLevel.UNTRUSTED,
    )


@pytest.fixture
def gateway() -> PermissionGateway:
    return PermissionGateway(PolicyEngine(mode=PolicyMode.SAFE))


class TestPermissionGatewayInit:
    def test_default_policy_engine(self) -> None:
        gateway = PermissionGateway()
        assert gateway.policy_engine is not None

    def test_custom_policy_engine(self) -> None:
        engine = PolicyEngine(mode=PolicyMode.FULL)
        gateway = PermissionGateway(engine)
        assert gateway.policy_engine is engine

    def test_action_scope_map(self) -> None:
        assert PermissionGateway.ACTION_SCOPE_MAP["read_file"] == "read_file"
        assert PermissionGateway.ACTION_SCOPE_MAP["execute_shell"] == "shell"
        assert PermissionGateway.ACTION_SCOPE_MAP["access_secrets"] == "secrets"


class TestScopeCheck:
    def test_allowed_action_with_scope(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        result = gateway.evaluate(trusted_agent, "read_file")
        assert result.decision == PermissionDecision.ALLOW

    def test_denied_action_without_scope(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        # Agent has READ_FILE and SHELL but not WRITE_FILE
        result = gateway.evaluate(trusted_agent, "write_file")
        assert result.decision == PermissionDecision.DENY
        assert "write_file" in result.reason

    def test_unmapped_action_is_denied(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        # An action outside the canonical vocabulary is denied, even though the
        # agent is trusted and holds scopes: the system has no statement to make
        # about the name, so it must not fall through every gate.
        result = gateway.evaluate(trusted_agent, "unknown_action")
        assert result.decision == PermissionDecision.DENY
        assert "not a registered action" in result.reason

    @pytest.mark.parametrize(
        "variant", [" execute_shell ", "EXECUTE_SHELL", "\texecute_shell\n", " Execute_Shell "]
    )
    def test_whitespace_and_case_variants_normalize_to_one_verdict(
        self,
        gateway: PermissionGateway,
        trusted_agent: AgentIdentity,
        variant: str,
    ) -> None:
        # A cosmetic difference must never select a weaker authorization path:
        # every variant resolves to "execute_shell" and reaches the same verdict.
        result = gateway.evaluate(trusted_agent, variant)
        assert result.decision == PermissionDecision.REQUIRE_APPROVAL
        assert result.request is not None
        assert result.request.action == "execute_shell"

    def test_unmapped_action_variant_is_denied(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        result = gateway.evaluate(trusted_agent, "  UNKNOWN_ACTION ")
        assert result.decision == PermissionDecision.DENY
        assert "not a registered action" in result.reason

    def test_allowlisted_safe_action_passes(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        # "plan" needs no identity scope and is on the explicit safe allowlist,
        # so the gateway does not deny it for being scope-less.
        result = gateway.evaluate(trusted_agent, "plan")
        assert result.decision == PermissionDecision.ALLOW

    def test_scope_lookup_normalizes_action(
        self, gateway: PermissionGateway
    ) -> None:
        assert gateway.require_scope(" EXECUTE_SHELL ") == "shell"
        assert gateway.has_scope_mapping(" Read_File ") is True

    def test_require_scope_returns_correct_scope(
        self, gateway: PermissionGateway
    ) -> None:
        assert gateway.require_scope("read_file") == "read_file"
        assert gateway.require_scope("execute_docker") == "docker"
        assert gateway.require_scope("nonexistent") is None

    def test_has_scope_mapping(self, gateway: PermissionGateway) -> None:
        assert gateway.has_scope_mapping("read_file") is True
        assert gateway.has_scope_mapping("nonexistent") is False


class TestUntrustedAgents:
    def test_untrusted_denied(
        self, gateway: PermissionGateway, untrusted_agent: AgentIdentity
    ) -> None:
        result = gateway.evaluate(untrusted_agent, "read_file")
        assert result.decision == PermissionDecision.DENY
        assert "UNTRUSTED" in result.reason

    def test_untrusted_denied_sensitive(
        self, gateway: PermissionGateway, untrusted_agent: AgentIdentity
    ) -> None:
        result = gateway.evaluate(untrusted_agent, "execute_shell")
        assert result.decision == PermissionDecision.DENY


class TestPolicyIntegration:
    def test_sensitive_action_requires_approval(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        # Agent has SHELL scope, but SAFE mode requires approval for execute_shell
        result = gateway.evaluate(trusted_agent, "execute_shell")
        assert result.decision == PermissionDecision.REQUIRE_APPROVAL
        assert result.approval_required is True

    def test_full_mode_allows_sensitive(
        self, trusted_agent: AgentIdentity
    ) -> None:
        gateway = PermissionGateway(PolicyEngine(mode=PolicyMode.FULL))
        result = gateway.evaluate(trusted_agent, "execute_shell")
        assert result.decision == PermissionDecision.DENY
        assert "default" in result.reason.lower() or "deny" in result.reason.lower()

    def test_policy_denied(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        # Add a deny rule
        gateway.policy_engine.add_rule(
            PolicyRule(
                name="deny_all",
                action_pattern="*",
                decision=PolicyDecision.DENY,
                reason="Deny all",
            )
        )
        result = gateway.evaluate(trusted_agent, "read_file")
        assert result.decision == PermissionDecision.DENY


class TestPermissionRequest:
    def test_create_request(self, trusted_agent: AgentIdentity) -> None:
        request = PermissionRequest(
            agent=trusted_agent,
            action="read_file",
            resource="/etc/passwd",
        )
        assert request.agent == trusted_agent
        assert request.action == "read_file"
        assert request.resource == "/etc/passwd"
        assert request.context == {}
        assert request.risk_score == 0.0

    def test_request_with_context(self, trusted_agent: AgentIdentity) -> None:
        request = PermissionRequest(
            agent=trusted_agent,
            action="execute_shell",
            resource="/bin/ls",
            context={"env": "production"},
            risk_score=0.8,
        )
        assert request.context == {"env": "production"}
        assert request.risk_score == 0.8


class TestPermissionResult:
    def test_allow_result(self, trusted_agent: AgentIdentity) -> None:
        gateway = PermissionGateway()
        result = gateway.evaluate(trusted_agent, "read_file")
        assert isinstance(result, PermissionResult)
        assert result.decision == PermissionDecision.ALLOW
        assert result.reason  # non-empty

    def test_deny_result_has_reason(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        result = gateway.evaluate(trusted_agent, "write_file")
        assert result.decision == PermissionDecision.DENY
        assert result.reason  # non-empty
        assert result.scope_required == "write_file"

    def test_approval_result_has_flag(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity
    ) -> None:
        result = gateway.evaluate(trusted_agent, "execute_shell")
        assert result.approval_required is True


@pytest.fixture
def audit_log() -> AuditLog:
    return AuditLog()


class TestPermissionGatewayAuditLogging:
    def test_trust_check_logged_as_identity_check(
        self, untrusted_agent: AgentIdentity, audit_log: AuditLog
    ) -> None:
        gateway = PermissionGateway(PolicyEngine(mode=PolicyMode.SAFE), audit_log=audit_log)
        result = gateway.evaluate(untrusted_agent, "read_file")
        assert result.decision == PermissionDecision.DENY
        identity_events = audit_log.query(AuditQuery(event_type=AuditEventType.IDENTITY_CHECK))
        assert len(identity_events) == 1
        assert identity_events[0].result == "denied"

    def test_scope_check_logged_as_permission_check(
        self, gateway: PermissionGateway, trusted_agent: AgentIdentity, audit_log: AuditLog
    ) -> None:
        gateway.audit_log = audit_log
        gateway.policy_engine.audit_log = audit_log
        result = gateway.evaluate(trusted_agent, "write_file")
        assert result.decision == PermissionDecision.DENY
        perm_events = audit_log.query(AuditQuery(event_type=AuditEventType.PERMISSION_CHECK))
        assert len(perm_events) == 1
        assert perm_events[0].result == "scope_check_failed"

    def test_approve_logged_as_permission_check(
        self, trusted_agent: AgentIdentity, audit_log: AuditLog
    ) -> None:
        gateway = PermissionGateway(PolicyEngine(mode=PolicyMode.SAFE), audit_log=audit_log)
        result = gateway.evaluate(trusted_agent, "execute_shell")
        assert result.decision == PermissionDecision.REQUIRE_APPROVAL
        perm_events = audit_log.query(AuditQuery(event_type=AuditEventType.PERMISSION_CHECK))
        assert len(perm_events) >= 1
        assert any(e.result == "approval_required" for e in perm_events)

    def test_allow_logged_as_permission_check(
        self, trusted_agent: AgentIdentity, audit_log: AuditLog
    ) -> None:
        gateway = PermissionGateway(PolicyEngine(mode=PolicyMode.SAFE), audit_log=audit_log)
        result = gateway.evaluate(trusted_agent, "read_file")
        assert result.decision == PermissionDecision.ALLOW
        perm_events = audit_log.query(AuditQuery(event_type=AuditEventType.PERMISSION_CHECK))
        assert any(e.result == "scope_check_passed" for e in perm_events)
        assert any(e.result == "allowed" for e in perm_events)

    def test_no_audit_when_no_log(self, gateway: PermissionGateway, trusted_agent: AgentIdentity) -> None:
        assert gateway.audit_log is None
        result = gateway.evaluate(trusted_agent, "read_file")
        assert result.decision == PermissionDecision.ALLOW

# --------------------------------------------------------------------------- #
# Sensitive actions must have a scope requirement (unreachable-vocabulary fix) #
# --------------------------------------------------------------------------- #

# The four actions SENSITIVE_ACTIONS names that previously had no entry in
# ACTION_SCOPE_REQUIREMENTS. Being unmapped they were denied by the gateway as
# unregistered and could never reach the approval gate prepared for them.
NEWLY_SCOPED_ACTIONS: dict[str, str] = {
    "publish_content": PermissionScope.NETWORK.value,
    "send_external_message": PermissionScope.NETWORK.value,
    "credential_access": PermissionScope.SECRETS.value,
    "data_upload": PermissionScope.NETWORK.value,
}


@pytest.fixture
def unscoped_sensitive_agent() -> AgentIdentity:
    """A trusted agent holding every scope EXCEPT the four required above."""
    required = set(NEWLY_SCOPED_ACTIONS.values())
    all_scopes = {scope.value for scope in PermissionScope}
    return AgentIdentity(
        agent_id="agent-scoped",
        role="coder",
        trust_level=TrustLevel.TRUSTED,
        scopes=all_scopes - required,
    )


class TestNewlyScopedSensitiveActions:
    @pytest.mark.parametrize(
        ("action", "expected_scope"), sorted(NEWLY_SCOPED_ACTIONS.items())
    )
    def test_action_reaches_scope_gate_with_required_scope(
        self, gateway: PermissionGateway, action: str, expected_scope: str
    ) -> None:
        assert gateway.has_scope_mapping(action) is True
        assert gateway.require_scope(action) == expected_scope
        assert ACTION_SCOPE_REQUIREMENTS[action] == expected_scope

    @pytest.mark.parametrize(
        ("action", "expected_scope"), sorted(NEWLY_SCOPED_ACTIONS.items())
    )
    def test_denied_when_agent_lacks_required_scope(
        self,
        gateway: PermissionGateway,
        unscoped_sensitive_agent: AgentIdentity,
        action: str,
        expected_scope: str,
    ) -> None:
        result = gateway.evaluate(unscoped_sensitive_agent, action)
        assert result.decision == PermissionDecision.DENY
        assert result.scope_required == expected_scope
        assert expected_scope in result.reason

    @pytest.mark.parametrize(
        ("action", "expected_scope"), sorted(NEWLY_SCOPED_ACTIONS.items())
    )
    def test_reaches_policy_and_approval_gate_when_scoped_held(
        self, gateway: PermissionGateway, action: str, expected_scope: str
    ) -> None:
        agent = AgentIdentity(
            agent_id="agent-with-scope",
            role="coder",
            trust_level=TrustLevel.TRUSTED,
            scopes={expected_scope},
        )
        # SAFE mode classifies all four as sensitive, so holding the scope moves
        # the decision past the scope gate to the human-approval gate instead of
        # an unconditional "unregistered" denial.
        result = gateway.evaluate(agent, action)
        assert result.decision == PermissionDecision.REQUIRE_APPROVAL
        assert result.approval_required is True
        assert result.scope_required == expected_scope

    @pytest.mark.parametrize("action", sorted(NEWLY_SCOPED_ACTIONS))
    def test_case_and_whitespace_variants_reach_same_scope(
        self, gateway: PermissionGateway, action: str
    ) -> None:
        assert gateway.require_scope(f"  {action.upper()} ") == NEWLY_SCOPED_ACTIONS[
            action
        ]


class TestVocabularyInvariants:
    def test_no_action_is_both_unregistered_and_allowed(self) -> None:
        """An action can never be reachable-but-unregistered: everything the
        permission gateway can pass step 1 for is either scope-mapped or
        explicitly named safe, and every sensitive action is scope-mapped."""
        unmapped = {
            action
            for action in KNOWN_ACTIONS
            if action not in ACTION_SCOPE_REQUIREMENTS and action not in KNOWN_SAFE_ACTIONS
        }
        assert unmapped == set()

    def test_every_sensitive_action_has_a_scope_requirement(self) -> None:
        missing = SENSITIVE_ACTIONS - set(ACTION_SCOPE_REQUIREMENTS)
        assert missing == set(), f"sensitive actions without a scope: {sorted(missing)}"

    def test_known_safe_actions_is_not_widened(self) -> None:
        # "code" authorises the LABEL only; the real code path re-authorizes as
        # "execute_python". The set must stay exactly the three labels.
        assert KNOWN_SAFE_ACTIONS == frozenset({"plan", "evaluate", "code"})
        assert KNOWN_SAFE_ACTIONS & SENSITIVE_ACTIONS == frozenset()

    def test_sensitive_action_without_scope_is_denied_with_clear_reason(self) -> None:
        """Documented, deliberate denial: if a sensitive action ever loses its
        scope mapping again, it is denied with a reason that says exactly that,
        instead of being reported as an unknown action name."""
        gateway = PermissionGateway(PolicyEngine(mode=PolicyMode.SAFE))
        agent = AgentIdentity(
            agent_id="agent-1",
            role="coder",
            trust_level=TrustLevel.TRUSTED,
            scopes={scope.value for scope in PermissionScope},
        )
        gateway.ACTION_SCOPE_MAP = {
            k: v for k, v in ACTION_SCOPE_REQUIREMENTS.items() if k != "credential_access"
        }
        result = gateway.evaluate(agent, "credential_access")
        assert result.decision == PermissionDecision.DENY
        assert result.scope_required is None
        assert "has no identity scope requirement" in result.reason

    def test_sensitive_unscoped_denial_is_audited(self) -> None:
        audit_log = AuditLog()
        gateway = PermissionGateway(PolicyEngine(mode=PolicyMode.SAFE), audit_log=audit_log)
        agent = AgentIdentity(
            agent_id="agent-1",
            role="coder",
            trust_level=TrustLevel.TRUSTED,
            scopes={PermissionScope.NETWORK.value},
        )
        gateway.ACTION_SCOPE_MAP = {
            k: v for k, v in ACTION_SCOPE_REQUIREMENTS.items() if k != "data_upload"
        }
        assert gateway.evaluate(agent, "data_upload").decision == PermissionDecision.DENY
        events = audit_log.query(AuditQuery(event_type=AuditEventType.PERMISSION_CHECK))
        assert any(e.result == "unscoped_sensitive_action" for e in events)
