"""Tests for PolicyEngine — Default Deny policy evaluation."""

import ast

import pytest

from cyberbrain26.security_plane.audit import AuditEventType, AuditLog, AuditQuery
from cyberbrain26.security_plane.identity import (
    AgentIdentity,
    PermissionScope,
    TrustLevel,
)
from cyberbrain26.security_plane.policy import (
    MAX_CONDITION_LENGTH,
    MAX_CONDITION_NODES,
    PolicyConditionError,
    PolicyContext,
    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 engine() -> PolicyEngine:
    return PolicyEngine(mode=PolicyMode.SAFE)


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


class TestPolicyEngineInit:
    def test_default_mode_is_safe(self) -> None:
        engine = PolicyEngine()
        assert engine.mode == PolicyMode.SAFE

    def test_custom_mode(self) -> None:
        engine = PolicyEngine(mode=PolicyMode.FULL)
        assert engine.mode == PolicyMode.FULL

    def test_default_rules_loaded(self, engine: PolicyEngine) -> None:
        rules = engine.list_rules()
        assert len(rules) >= 1
        assert any(r.name == "default_deny" for r in rules)


class TestDefaultDeny:
    def test_unknown_action_denied_in_safe(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        # SAFE mode's allow rule covers the *known* non-sensitive vocabulary.
        # An action outside it is unregistered, so it is denied before any rule
        # is consulted rather than being allowed for merely not being sensitive.
        result = engine.evaluate(trusted_agent, "unknown_action")
        assert result.decision == PolicyDecision.DENY
        assert result.rule == "unregistered_action"

    @pytest.mark.parametrize(
        "variant", [" unknown_action ", "UNKNOWN_ACTION", "\nunknown_action"]
    )
    def test_unknown_action_variants_are_denied(
        self,
        engine: PolicyEngine,
        trusted_agent: AgentIdentity,
        variant: str,
    ) -> None:
        result = engine.evaluate(trusted_agent, variant)
        assert result.decision == PolicyDecision.DENY
        assert result.rule == "unregistered_action"

    @pytest.mark.parametrize("mode", list(PolicyMode))
    def test_unknown_action_denied_in_every_mode(
        self, trusted_agent: AgentIdentity, mode: PolicyMode
    ) -> None:
        # Non-SAFE modes install a default_allow rule; it must not turn an
        # unregistered action name into an allowed one.
        engine = PolicyEngine(mode=mode)
        result = engine.evaluate(trusted_agent, "unknown_action")
        assert result.decision == PolicyDecision.DENY
        assert result.rule == "unregistered_action"

    @pytest.mark.parametrize(
        "variant", [" read_file ", "READ_FILE", "\nread_file"]
    )
    def test_registered_action_variants_reach_one_verdict(
        self,
        engine: PolicyEngine,
        trusted_agent: AgentIdentity,
        variant: str,
    ) -> None:
        # Normalization must not change a known action's verdict either.
        result = engine.evaluate(trusted_agent, variant)
        assert result.decision == PolicyDecision.ALLOW

    def test_sensitive_action_variant_still_requires_approval(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        # is_sensitive() is reachable from policy conditions, so a variant must
        # not slip past the approval gate by looking non-sensitive.
        result = engine.evaluate(trusted_agent, " DELETE_FILE ")
        assert result.decision == PolicyDecision.REQUIRE_APPROVAL

    def test_sensitive_action_requires_approval_in_safe(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        result = engine.evaluate(trusted_agent, "delete_file")
        assert result.decision == PolicyDecision.REQUIRE_APPROVAL
        assert result.requires_approval is True

    def test_non_sensitive_action_allowed(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        # In SAFE mode, non-sensitive actions are allowed by default
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.decision == PolicyDecision.ALLOW


class TestUntrustedAgents:
    def test_untrusted_always_denied(
        self, engine: PolicyEngine, untrusted_agent: AgentIdentity
    ) -> None:
        result = engine.evaluate(untrusted_agent, "read_file")
        assert result.decision == PolicyDecision.DENY
        assert "UNTRUSTED" in result.reason

    def test_untrusted_denied_for_sensitive(
        self, engine: PolicyEngine, untrusted_agent: AgentIdentity
    ) -> None:
        result = engine.evaluate(untrusted_agent, "delete_file")
        assert result.decision == PolicyDecision.DENY


class TestCustomRules:
    def test_add_rule_allows_action(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        engine.add_rule(
            PolicyRule(
                name="allow_delete",
                action_pattern="delete_file",
                decision=PolicyDecision.ALLOW,
            )
        )
        result = engine.evaluate(trusted_agent, "delete_file")
        assert result.decision == PolicyDecision.ALLOW
        assert result.rule == "allow_delete"

    def test_add_rule_deny_overrides(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        engine.add_rule(
            PolicyRule(
                name="deny_read",
                action_pattern="read_file",
                decision=PolicyDecision.DENY,
            )
        )
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.decision == PolicyDecision.DENY

    def test_remove_rule(self, engine: PolicyEngine) -> None:
        engine.add_rule(
            PolicyRule(
                name="temp_rule",
                action_pattern="*",
                decision=PolicyDecision.DENY,
            )
        )
        assert engine.remove_rule("temp_rule") is True
        assert engine.remove_rule("temp_rule") is False

    def test_wildcard_pattern(self, engine: PolicyEngine, trusted_agent: AgentIdentity) -> None:
        engine.add_rule(
            PolicyRule(
                name="allow_all",
                action_pattern="*",
                decision=PolicyDecision.ALLOW,
            )
        )
        # A wildcard rule covers every *registered* action...
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.decision == PolicyDecision.ALLOW
        assert result.rule == "allow_all"
        # ...but not an unregistered name, which is denied before rules run.
        unregistered = engine.evaluate(trusted_agent, "anything")
        assert unregistered.decision == PolicyDecision.DENY
        assert unregistered.rule == "unregistered_action"


class TestPolicyContext:
    def test_default_context(self) -> None:
        ctx = PolicyContext(action="read_file")
        assert ctx.action == "read_file"
        assert ctx.resource is None
        assert ctx.risk_score == 0.0
        assert ctx.mode == PolicyMode.SAFE

    def test_custom_context(self) -> None:
        ctx = PolicyContext(
            action="delete_file",
            resource="/etc/passwd",
            risk_score=0.8,
            mode=PolicyMode.FULL,
        )
        assert ctx.resource == "/etc/passwd"
        assert ctx.risk_score == 0.8
        assert ctx.mode == PolicyMode.FULL


class TestSensitiveActions:
    def test_is_sensitive_delete(self, engine: PolicyEngine) -> None:
        assert engine.is_sensitive("delete_file") is True

    def test_is_sensitive_write(self, engine: PolicyEngine) -> None:
        assert engine.is_sensitive("write_file") is True

    def test_is_sensitive_shell(self, engine: PolicyEngine) -> None:
        assert engine.is_sensitive("execute_shell") is True

    def test_is_sensitive_secrets(self, engine: PolicyEngine) -> None:
        assert engine.is_sensitive("access_secrets") is True

    def test_is_not_sensitive_read(self, engine: PolicyEngine) -> None:
        assert engine.is_sensitive("read_file") is False

    def test_is_not_sensitive_unknown(self, engine: PolicyEngine) -> None:
        assert engine.is_sensitive("unknown_action") is False


class TestPolicyModes:
    def test_full_mode_allows_sensitive_without_approval(
        self, trusted_agent: AgentIdentity
    ) -> None:
        engine = PolicyEngine(mode=PolicyMode.FULL)
        # In FULL mode, sensitive actions are denied by default (no default_allow)
        result = engine.evaluate(trusted_agent, "delete_file")
        assert result.decision == PolicyDecision.DENY

    def test_research_mode(self, trusted_agent: AgentIdentity) -> None:
        engine = PolicyEngine(mode=PolicyMode.RESEARCH)
        # In RESEARCH mode, actions are denied by default (no default_allow)
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.decision == PolicyDecision.DENY

    def test_red_team_lab_mode(self, trusted_agent: AgentIdentity) -> None:
        engine = PolicyEngine(mode=PolicyMode.RED_TEAM_LAB)
        # In RED_TEAM_LAB mode, actions are denied by default (no default_allow)
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.decision == PolicyDecision.DENY


class TestPolicyEvaluation:
    def test_evaluation_has_context(self, engine: PolicyEngine, trusted_agent: AgentIdentity) -> None:
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.context is not None
        assert result.context.action == "read_file"

    def test_evaluation_reason(self, engine: PolicyEngine, trusted_agent: AgentIdentity) -> None:
        result = engine.evaluate(trusted_agent, "delete_file")
        assert result.reason  # non-empty
        assert "approval" in result.reason.lower() or "sensitive" in result.reason.lower()


class TestAuditLogging:
    def test_red_team_lab_logs_all_decisions(
        self, trusted_agent: AgentIdentity, audit_log: AuditLog
    ) -> None:
        engine = PolicyEngine(mode=PolicyMode.RED_TEAM_LAB, audit_log=audit_log)
        engine.evaluate(trusted_agent, "read_file")
        engine.evaluate(trusted_agent, "write_file")
        engine.evaluate(trusted_agent, "execute_shell")
        policy_decisions = audit_log.query(AuditQuery(event_type=AuditEventType.POLICY_DECISION))
        assert len(policy_decisions) == 3

    def test_red_team_lab_logs_deny_for_untrusted(
        self, untrusted_agent: AgentIdentity, audit_log: AuditLog
    ) -> None:
        engine = PolicyEngine(mode=PolicyMode.RED_TEAM_LAB, audit_log=audit_log)
        engine.evaluate(untrusted_agent, "read_file")
        policy_decisions = audit_log.query(AuditQuery(event_type=AuditEventType.POLICY_DECISION))
        assert len(policy_decisions) == 1
        assert policy_decisions[0].policy_decision == PolicyDecision.DENY.value

    def test_safe_mode_logs_deny_only(
        self, trusted_agent: AgentIdentity, audit_log: AuditLog
    ) -> None:
        engine = PolicyEngine(mode=PolicyMode.SAFE, audit_log=audit_log)
        engine.evaluate(trusted_agent, "read_file")
        engine.evaluate(trusted_agent, "delete_file")
        policy_decisions = audit_log.query(AuditQuery(event_type=AuditEventType.POLICY_DECISION))
        assert len(policy_decisions) == 1
        assert policy_decisions[0].policy_decision == PolicyDecision.REQUIRE_APPROVAL.value


class TestConditionSafety:
    """Bezpečný AST evaluátor policy podmínek — pozitivní a negativní případy."""

    @pytest.fixture
    def evaluate(self, engine: PolicyEngine, trusted_agent: AgentIdentity):
        def _evaluate(
            condition: str,
            action: str = "read_file",
            risk_score: float = 0.0,
            mode: PolicyMode = PolicyMode.SAFE,
        ) -> bool:
            context = PolicyContext(action=action, risk_score=risk_score, mode=mode)
            return engine._eval_condition(condition, action, trusted_agent, context)

        return _evaluate

    @pytest.fixture
    def expect_rejected(self, evaluate):
        def _reject(condition: str, **kwargs: object) -> None:
            with pytest.raises(PolicyConditionError):
                evaluate(condition, **kwargs)

        return _reject

    def test_is_sensitive_true(self, evaluate) -> None:
        assert evaluate("is_sensitive(action)", action="execute_shell") is True

    def test_is_sensitive_false(self, evaluate) -> None:
        assert evaluate("is_sensitive(action)", action="read_file") is False

    def test_not_is_sensitive(self, evaluate) -> None:
        assert evaluate("not is_sensitive(action)", action="read_file") is True

    def test_equality_match(self, evaluate) -> None:
        assert evaluate('action == "execute_python"', action="execute_python") is True

    def test_inequality_match(self, evaluate) -> None:
        assert evaluate('action != "read_file"', action="read_file") is False

    def test_risk_score_below_threshold(self, evaluate) -> None:
        assert evaluate("risk_score < 0.5", risk_score=0.1) is True

    def test_risk_score_above_threshold(self, evaluate) -> None:
        assert evaluate("risk_score >= 0.5", risk_score=0.9) is True

    def test_mode_equal_to_itself(self, evaluate) -> None:
        assert evaluate("mode == mode", mode=PolicyMode.FULL) is True

    def test_true_literal(self, evaluate) -> None:
        assert evaluate("True") is True

    def test_false_literal(self, evaluate) -> None:
        assert evaluate("False") is False

    def test_or_combination(self, evaluate) -> None:
        condition = "is_sensitive(action) or action == 'read_file'"
        assert evaluate(condition, action="read_file") is True
        assert evaluate(condition, action="write_file") is True

    def test_and_combination(self, evaluate) -> None:
        condition = "is_sensitive(action) and action == 'write_file'"
        assert evaluate(condition, action="write_file") is True
        assert evaluate(condition, action="delete_file") is False

    def test_long_but_valid_condition(self, evaluate) -> None:
        condition = " or ".join(["action == 'read_file'"] * 8)
        assert len(condition) < MAX_CONDITION_LENGTH
        assert evaluate(condition, action="read_file") is True
        assert evaluate(condition, action="write_file") is False

    def test_safe_mode_conditions_still_apply_end_to_end(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        allowed = engine.evaluate(trusted_agent, "read_file")
        assert allowed.decision == PolicyDecision.ALLOW
        blocked = engine.evaluate(trusted_agent, "execute_shell")
        assert blocked.decision == PolicyDecision.REQUIRE_APPROVAL
        assert blocked.rule == "safe_mode_sensitive_requires_approval"

    def test_import_os_system_rejected(self, expect_rejected) -> None:
        expect_rejected("__import__('os').system('id')")

    def test_class_mro_traversal_rejected(self, expect_rejected) -> None:
        expect_rejected("identity.__class__.__mro__[1].__subclasses__()")

    def test_tuple_class_traversal_rejected(self, expect_rejected) -> None:
        expect_rejected("().__class__.__bases__[0].__subclasses__()")

    def test_open_call_rejected_without_creating_file(
        self, expect_rejected, tmp_path
    ) -> None:
        target = tmp_path / "cb26_policy_should_not_exist"
        condition = f"open({str(target)!r}, 'w')"
        expect_rejected(condition)
        assert not target.exists()

    def test_frozenset_union_rejected(self, expect_rejected) -> None:
        expect_rejected("SENSITIVE_ACTIONS.union({'x'})")

    def test_globals_rejected(self, expect_rejected) -> None:
        expect_rejected("globals()")

    def test_eval_rejected(self, expect_rejected) -> None:
        expect_rejected("eval('1')")

    def test_exec_rejected(self, expect_rejected) -> None:
        expect_rejected("exec('1')")

    def test_lambda_rejected(self, expect_rejected) -> None:
        expect_rejected("lambda: 1")

    def test_list_comprehension_rejected(self, expect_rejected) -> None:
        expect_rejected("[i for i in range(10)]")

    def test_dict_comprehension_rejected(self, expect_rejected) -> None:
        expect_rejected("{k: k for k in range(3)}")

    def test_binop_and_ifexp_rejected(self, expect_rejected) -> None:
        expect_rejected("1 + 1 if action else 0")

    def test_power_bomb_rejected(self, expect_rejected) -> None:
        expect_rejected("10**10**10")

    def test_string_multiplication_bomb_rejected(self, expect_rejected) -> None:
        expect_rejected("'a' * 10**9")

    def test_list_multiplication_bomb_rejected(self, expect_rejected) -> None:
        expect_rejected("[0] * 10**9")

    def test_attribute_access_rejected(self, expect_rejected) -> None:
        expect_rejected("identity.trust_level")

    def test_subscript_rejected(self, expect_rejected) -> None:
        expect_rejected("SENSITIVE_ACTIONS[0]")

    def test_unknown_name_rejected(self, expect_rejected) -> None:
        expect_rejected("unknown_name == action")

    def test_non_bool_result_rejected(self, expect_rejected) -> None:
        expect_rejected("action")

    def test_string_injection_not_executed(self, evaluate) -> None:
        condition = """action == "x' or '1'=='1\""""
        payload = "x' or '1'=='1 or True"
        assert evaluate(condition, action=payload) is False

    def test_condition_too_long_rejected(self, expect_rejected) -> None:
        condition = " or ".join(["action == 'x'"] * 40)
        assert len(condition) > MAX_CONDITION_LENGTH
        expect_rejected(condition)

    def test_too_many_nodes_rejected(self, expect_rejected) -> None:
        condition = " and ".join(["1<1"] * 12)
        assert len(condition) < MAX_CONDITION_LENGTH
        assert sum(1 for _ in ast.walk(ast.parse(condition, mode="eval"))) > MAX_CONDITION_NODES
        expect_rejected(condition)

    def test_invalid_syntax_rejected(self, expect_rejected) -> None:
        expect_rejected("action ==")

    def test_call_without_arguments_fails_closed(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        rule = PolicyRule(
            name="bad_call",
            action_pattern="*",
            decision=PolicyDecision.ALLOW,
            condition="is_sensitive()",
        )
        engine.add_rule(rule)
        context = PolicyContext(action="read_file")
        assert engine._rule_matches(rule, "read_file", trusted_agent, context) is False

    def test_call_with_extra_arguments_fails_closed(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        rule = PolicyRule(
            name="bad_call_args",
            action_pattern="*",
            decision=PolicyDecision.ALLOW,
            condition="is_sensitive(action, 1)",
        )
        engine.add_rule(rule)
        context = PolicyContext(action="read_file")
        assert engine._rule_matches(rule, "read_file", trusted_agent, context) is False

    def test_in_operator_condition_true_for_sensitive(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        rule = PolicyRule(
            name="in_operator_sensitive",
            action_pattern="*",
            decision=PolicyDecision.ALLOW,
            condition="action in SENSITIVE_ACTIONS",
        )
        engine.add_rule(rule)
        context = PolicyContext(action="execute_shell")
        assert engine._rule_matches(rule, "execute_shell", trusted_agent, context) is True

    def test_in_operator_condition_false_for_non_sensitive(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        rule = PolicyRule(
            name="in_operator_non_sensitive",
            action_pattern="*",
            decision=PolicyDecision.ALLOW,
            condition="action in SENSITIVE_ACTIONS",
        )
        engine.add_rule(rule)
        context = PolicyContext(action="read_file")
        assert engine._rule_matches(rule, "read_file", trusted_agent, context) is False

    def test_not_in_operator_condition_true_for_non_sensitive(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        condition = "action not in SENSITIVE_ACTIONS"
        rule = PolicyRule(
            name="not_in_operator",
            action_pattern="*",
            decision=PolicyDecision.ALLOW,
            condition=condition,
        )
        engine.add_rule(rule)
        context = PolicyContext(action="read_file")
        assert engine._rule_matches(rule, "read_file", trusted_agent, context) is True

    def test_not_in_parentheses_true_for_non_sensitive(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        condition = "not (action in SENSITIVE_ACTIONS)"
        rule = PolicyRule(
            name="not_in_parentheses",
            action_pattern="*",
            decision=PolicyDecision.ALLOW,
            condition=condition,
        )
        engine.add_rule(rule)
        context = PolicyContext(action="read_file")
        assert engine._rule_matches(rule, "read_file", trusted_agent, context) is True

    def test_injection_rule_not_applied_end_to_end(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        engine.add_rule(
            PolicyRule(
                name="inj",
                action_pattern="*",
                decision=PolicyDecision.ALLOW,
                condition="__import__('os').system('id')",
            )
        )
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.rule != "inj"
        result_sensitive = engine.evaluate(trusted_agent, "delete_file")
        assert result_sensitive.rule != "inj"
        assert result_sensitive.decision == PolicyDecision.REQUIRE_APPROVAL

    def test_attribute_rule_not_applied_end_to_end(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        engine.add_rule(
            PolicyRule(
                name="inj_attr",
                action_pattern="*",
                decision=PolicyDecision.ALLOW,
                condition="identity.__class__",
            )
        )
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.rule != "inj_attr"

    def test_binop_rule_not_applied_end_to_end(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity
    ) -> None:
        engine.add_rule(
            PolicyRule(
                name="inj_binop",
                action_pattern="*",
                decision=PolicyDecision.ALLOW,
                condition="1 + 1",
            )
        )
        result = engine.evaluate(trusted_agent, "read_file")
        assert result.rule != "inj_binop"

    def test_injection_rule_has_no_side_effect(
        self, engine: PolicyEngine, trusted_agent: AgentIdentity, tmp_path
    ) -> None:
        target = tmp_path / "cb26_e2e_should_not_exist"
        engine.add_rule(
            PolicyRule(
                name="inj_open",
                action_pattern="*",
                decision=PolicyDecision.ALLOW,
                condition=f"open({str(target)!r}, 'w')",
            )
        )
        engine.evaluate(trusted_agent, "read_file")
        assert not target.exists()
