use crate::errors::PolicyEngineError::*;
use crate::errors::TelemetryError::TelemetryFieldSerializationError;
use crate::errors::{PolicyEngineError, TelemetryError};
use crate::match_utils::all_conditions_match::all_conditions_match;
use crate::match_utils::any_condition_matches::any_condition_matches;
use crate::match_utils::get_value_from_scalar_property_matcher::get_expected_value_from_scalar_property_matcher_or_error;
use crate::match_utils::list_condition_matches::list_property_matcher_matches;
use crate::match_utils::scalar_condition_matches::property_scalar_matcher_matches;
use crate::otel::effect_reason::{
    effect_reason_for_allow, effect_reason_for_deny, effect_reason_for_modify,
};
use crate::otel::otel_manager::OtelManager;
use crate::policy_types::*;
use crate::regex_matcher::RegexMatcher;
use crate::telemetry_constants as telemetry;
use crate::telemetry_constants::{
    ATTRIBUTE_NAME_AGENT_STATE_REDACTED_PATHS_JSON, ATTRIBUTE_NAME_TELEMETRY_ERRORS,
    ATTRIBUTE_NAME_TOOL_ARGS_REDACTED_PATHS_JSON, SPAN_NAME_DECIDE_TOOL_CALL,
    SPAN_NAME_EVALUATE_POLICY,
};
use crate::value_utils::get_value_at_path::get_value_at_path;
use crate::value_utils::redact_value::{RedactOption, redact_value};
// bring trait into scope for logger()
// bring trait into scope for meter()
#[cfg(not(feature = "otel"))]
use crate::otel::SpanExt;
use regex::Regex;
use serde::Serialize;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tracing::{Level, Span, span};
#[cfg(feature = "otel")]
use tracing_opentelemetry::OpenTelemetrySpanExt;

// === Actions ===
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "decision", rename_all = "snake_case")]
pub enum ProcessToolCallResult {
    Allow {
        rules_matched: Vec<ToolPolicyRule>,
        reason: String,
    },
    Deny {
        rules_matched: Vec<ToolPolicyRule>,
        reason: String,
    },
}

pub fn get_decision_name(action: &ProcessToolCallResult) -> &'static str {
    match action {
        ProcessToolCallResult::Allow { .. } => "allow",
        ProcessToolCallResult::Deny { .. } => "deny",
    }
}

#[derive(Debug, Clone)]
pub struct PolicyEngine {
    pub policies: Vec<ToolPolicyRecord>,
    #[allow(dead_code)]
    otel_manager: Option<Arc<OtelManager>>,
    regex_cache: Arc<Mutex<HashMap<String, Regex>>>,
}

impl Default for PolicyEngine {
    fn default() -> Self {
        Self {
            policies: Vec::new(),
            otel_manager: None,
            regex_cache: Arc::new(Mutex::new(HashMap::new())),
        }
    }
}

pub struct PolicyMatchContext<'a> {
    pub tool_name: &'a str,
    pub tool_args: &'a Value,
    pub agent_state: &'a Value,
    regex_matcher: RegexMatcher,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct SkippedPolicyKey {
    pub policy_id: String,
    pub policy_version_id: String,
    pub policy_status: ToolPolicyStatus,
}

#[derive(Debug, Clone, Default)]
pub struct ProcessToolCallOptions {
    pub redact_tool_args: Option<RedactOption>,
    pub redact_agent_state: Option<RedactOption>,
}

impl PolicyEngine {
    pub fn new(policies: Vec<ToolPolicyRecord>, otel_manager: Option<Arc<OtelManager>>) -> Self {
        Self {
            policies,
            otel_manager,
            regex_cache: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    pub fn set_policies(&mut self, policies: Vec<ToolPolicyRecord>) {
        self.policies = policies;
    }

    pub fn process_tool_call(
        &self,
        tool_name: &str,
        tool_call_id: &str,
        tool_args: &Value,
        agent_state: &Value,
        options: Option<ProcessToolCallOptions>,
    ) -> ProcessToolCallResult {
        #[cfg(feature = "otel")]
        let _start = Instant::now();
        let top_level_span = Span::current();

        let decide_tool_call_span = Self::start_decide_tool_call_span(
            &top_level_span,
            tool_call_id,
            tool_name,
            tool_args,
            agent_state,
            options,
        );
        let _guard_enter = decide_tool_call_span.enter();

        let mut matched_allows: Vec<ToolPolicyRule> = Vec::new();
        let mut policies_evaluated: bool = false;
        let mut allow_rules_evaluated: bool = false;
        let mut early_deny_decision: Option<ProcessToolCallResult> = None;

        let policy_match_context = PolicyMatchContext::new(
            tool_name,
            tool_args,
            agent_state,
            RegexMatcher::new(self.regex_cache.clone()),
        );

        let policies_for_tool: Vec<&ToolPolicyRecord> = self
            .policies
            .iter()
            .filter(|p| p.policy.tool_name == tool_name)
            .collect();
        let mut maybe_skipped: HashSet<SkippedPolicyKey> = policies_for_tool
            .iter()
            .map(|p| SkippedPolicyKey {
                policy_id: p.id.clone(),
                policy_version_id: p.current_version_id.clone(),
                policy_status: p.status,
            })
            .collect();

        let monitor_policies: Vec<&ToolPolicyRecord> = policies_for_tool
            .iter()
            .copied()
            .filter(|p| p.status == ToolPolicyStatus::MONITOR)
            .collect();

        let allow_policies: Vec<&ToolPolicyRecord> = policies_for_tool
            .iter()
            .copied()
            .filter(|p| {
                matches!(p.policy.rule, ToolPolicyRule::Allow { .. })
                    && p.status == ToolPolicyStatus::ACTIVE
            })
            .collect();

        let deny_policies: Vec<&ToolPolicyRecord> = policies_for_tool
            .iter()
            .copied()
            .filter(|p| {
                matches!(p.policy.rule, ToolPolicyRule::Deny { .. })
                    && p.status == ToolPolicyStatus::ACTIVE
            })
            .collect();

        for policy_record in monitor_policies
            .iter()
            .chain(deny_policies.iter())
            .chain(allow_policies.iter())
        {
            let policy = &policy_record.policy;

            let policy_span =
                Self::create_policy_span(&decide_tool_call_span, tool_call_id, policy_record);
            let _guard_policy = policy_span.enter();
            let is_active_policy = policy_record.status == ToolPolicyStatus::ACTIVE;

            if is_active_policy {
                policies_evaluated = true;
            }

            let key = SkippedPolicyKey {
                policy_id: policy_record.id.clone(),
                policy_version_id: policy_record.current_version_id.clone(),
                policy_status: policy_record.status,
            };

            maybe_skipped.remove(&key);
            let rule = &policy.rule;
            match rule {
                ToolPolicyRule::Allow { when } => {
                    let does_condition_match: ConditionEvaluationResult =
                        policy_match_context.evaluate_condition(when);

                    add_condition_evaluation_result_to_span(&policy_span, &does_condition_match);

                    let (effect, reason) = effect_reason_for_allow(&does_condition_match);
                    policy_span.set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_EFFECT, effect);
                    policy_span
                        .set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_EFFECT_REASON, reason);

                    if is_active_policy {
                        allow_rules_evaluated = true;
                        match does_condition_match {
                            ConditionEvaluationResult::Pass => {
                                matched_allows.push(rule.clone());
                            }
                            ConditionEvaluationResult::Fail => {
                                early_deny_decision = Some(ProcessToolCallResult::Deny {
                                    rules_matched: Vec::new(),
                                    reason: "Denied by policy".to_string(),
                                });
                                break;
                            }
                            ConditionEvaluationResult::Unknown { .. } => {}
                        }
                    }
                }

                ToolPolicyRule::Deny { when } => {
                    let does_condition_match: ConditionEvaluationResult =
                        policy_match_context.evaluate_condition(when);

                    add_condition_evaluation_result_to_span(&policy_span, &does_condition_match);

                    let (effect, reason) = effect_reason_for_deny(&does_condition_match);
                    policy_span.set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_EFFECT, effect);
                    policy_span
                        .set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_EFFECT_REASON, reason);

                    if is_active_policy
                        && let ConditionEvaluationResult::Pass = does_condition_match
                    {
                        early_deny_decision = Some(ProcessToolCallResult::Deny {
                            rules_matched: vec![rule.clone()],
                            reason: "Denied by policy".into(),
                        });
                        break;
                    }
                }

                ToolPolicyRule::Modify { when, .. } => {
                    let does_condition_match = policy_match_context.evaluate_condition(when);

                    add_condition_evaluation_result_to_span(&policy_span, &does_condition_match);

                    let (effect, reason) = effect_reason_for_modify(&does_condition_match);
                    policy_span.set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_EFFECT, effect);
                    if !reason.is_empty() {
                        policy_span
                            .set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_EFFECT_REASON, reason);
                    }

                    if is_active_policy
                        && let ConditionEvaluationResult::Pass = does_condition_match
                    {
                        matched_allows.push(rule.clone());
                        policy_span.set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_MATCHED, true);
                    }
                }
            }
        }

        let decision = if let Some(d) = early_deny_decision {
            d
        } else if !policies_evaluated {
            ProcessToolCallResult::Allow {
                rules_matched: Vec::new(),
                reason: "No active policies for tool".into(),
            }
        } else if !allow_rules_evaluated || !matched_allows.is_empty() {
            ProcessToolCallResult::Allow {
                rules_matched: matched_allows,
                reason: "Not explicitly denied by any active policies".into(),
            }
        } else {
            ProcessToolCallResult::Deny {
                rules_matched: Vec::new(),
                reason: "Not explicitly allowed by any active policies".into(),
            }
        };

        let matched_len = match &decision {
            ProcessToolCallResult::Allow { rules_matched, .. } => rules_matched.len(),
            ProcessToolCallResult::Deny { rules_matched, .. } => rules_matched.len(),
        };

        let matched_reason = match &decision {
            ProcessToolCallResult::Deny { reason, .. } => reason.clone(),
            ProcessToolCallResult::Allow { reason, .. } => reason.clone(),
        };

        decide_tool_call_span.set_attribute(
            telemetry::ATTRIBUTE_NAME_POLICIES_SKIPPED,
            serde_json::to_string(&maybe_skipped)
                .unwrap_or_else(|_v| telemetry::POLICIES_SKIPPED_SERIALIZATION_ERROR.to_string()),
        );

        decide_tool_call_span.set_attribute(
            telemetry::ATTRIBUTE_NAME_DECISION_OUTCOME,
            get_decision_name(&decision).to_string(),
        );
        decide_tool_call_span
            .set_attribute(telemetry::ATTRIBUTE_NAME_DECISION_REASON, matched_reason);
        decide_tool_call_span.set_attribute(
            telemetry::ATTRIBUTE_NAME_MATCHED_RULE_COUNT,
            matched_len.to_string(),
        );

        decision
    }

    fn start_decide_tool_call_span(
        parent: &Span,
        tool_call_id: &str,
        tool_name: &str,
        tool_args: &Value,
        agent_state: &Value,
        options: Option<ProcessToolCallOptions>,
    ) -> Span {
        let mut errors: Vec<TelemetryError> = Vec::new();

        let span = span!(parent: parent, Level::INFO, SPAN_NAME_DECIDE_TOOL_CALL);
        span.set_attribute(telemetry::ATTRIBUTE_NAME_TOOL_NAME, tool_name.to_string());
        span.set_attribute(
            telemetry::ATTRIBUTE_NAME_TOOL_CALL_ID,
            tool_call_id.to_string(),
        );

        let redact_tool_args = options
            .as_ref()
            .and_then(|o| o.redact_tool_args.clone())
            .unwrap_or(RedactOption::Paths(vec![]));

        let (tool_args_redacted, tool_args_redacted_fields) =
            redact_value(tool_args, &redact_tool_args);

        span.set_attribute(
            telemetry::ATTRIBUTE_NAME_TOOL_ARGS_JSON,
            tool_args_redacted.to_string(),
        );
        let tool_args_redacted_fields_result = serde_json::to_string(&tool_args_redacted_fields);
        match tool_args_redacted_fields_result {
            Ok(attribute_value) => {
                span.set_attribute(
                    telemetry::ATTRIBUTE_NAME_TOOL_ARGS_REDACTED_PATHS_JSON,
                    attribute_value,
                );
            }
            Err(_e) => errors.push(TelemetryFieldSerializationError {
                field: ATTRIBUTE_NAME_TOOL_ARGS_REDACTED_PATHS_JSON.into(),
            }),
        }

        let redact_agent_state = options
            .as_ref()
            .and_then(|o| o.redact_agent_state.clone())
            .unwrap_or(RedactOption::Paths(vec![]));

        let (agent_state_redacted, agent_state_redacted_fields) =
            redact_value(agent_state, &redact_agent_state);

        span.set_attribute(
            telemetry::ATTRIBUTE_NAME_AGENT_STATE_JSON,
            agent_state_redacted.to_string(),
        );

        let agent_state_redacted_fields_result =
            serde_json::to_string(&agent_state_redacted_fields);
        match agent_state_redacted_fields_result {
            Ok(attribute_value) => {
                span.set_attribute(
                    telemetry::ATTRIBUTE_NAME_AGENT_STATE_REDACTED_PATHS_JSON,
                    attribute_value,
                );
            }
            Err(_e) => errors.push(TelemetryFieldSerializationError {
                field: ATTRIBUTE_NAME_AGENT_STATE_REDACTED_PATHS_JSON.into(),
            }),
        }

        if !errors.is_empty() {
            let json_string =
                serde_json::to_string(&errors.iter().map(ToString::to_string).collect::<Vec<_>>())
                    .unwrap_or_else(|_v| telemetry::TELEMETRY_SERIALIZATION_ERROR.to_string());
            span.set_attribute(ATTRIBUTE_NAME_TELEMETRY_ERRORS, json_string);
        }

        span
    }

    fn create_policy_span(
        parent: &Span,
        tool_call_id: &str,
        record: &ToolPolicyRecord,
    ) -> span::Span {
        let policy_span = span!(parent: parent, Level::INFO, SPAN_NAME_EVALUATE_POLICY);
        policy_span.set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_ID, record.id.clone());
        policy_span.set_attribute(
            telemetry::ATTRIBUTE_NAME_POLICY_STATUS,
            record.status.to_string(),
        );
        policy_span.set_attribute(
            telemetry::ATTRIBUTE_NAME_POLICY_VERSION_ID,
            record.current_version_id.clone(),
        );
        policy_span.set_attribute(
            telemetry::ATTRIBUTE_NAME_TOOL_CALL_ID,
            tool_call_id.to_string(),
        );
        policy_span
    }
}

fn add_condition_evaluation_result_to_span(span: &Span, result: &ConditionEvaluationResult) {
    match result {
        ConditionEvaluationResult::Pass => {
            span.set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_MATCHED, true)
        }
        ConditionEvaluationResult::Fail => {
            span.set_attribute(telemetry::ATTRIBUTE_NAME_POLICY_MATCHED, false)
        }
        ConditionEvaluationResult::Unknown { reason } => {
            span.set_attribute(
                telemetry::ATTRIBUTE_NAME_POLICY_MATCHED,
                telemetry::EFFECT_UNKNOWN,
            );
            span.set_attribute(
                telemetry::ATTRIBUTE_NAME_POLICY_EVALUATION_ERRORS_JSON,
                serde_json::to_string(&vec![reason.to_string()])
                    .unwrap_or_else(|_v| telemetry::TELEMETRY_SERIALIZATION_ERROR.to_string()),
            );
        }
    }
}

impl<'a> PolicyMatchContext<'a> {
    pub fn new(
        tool_name: &'a str,
        tool_args: &'a Value,
        agent_state: &'a Value,
        regex_matcher: RegexMatcher,
    ) -> Self {
        Self {
            tool_name,
            tool_args,
            agent_state,
            regex_matcher,
        }
    }

    fn evaluate_condition(&self, condition: &ToolCondition) -> ConditionEvaluationResult {
        match condition {
            ToolCondition::Atomic(a) => match self.atomic_condition_matches(a) {
                Err(e) => ConditionEvaluationResult::Unknown { reason: e },
                Ok(b) => match b {
                    true => ConditionEvaluationResult::Pass,
                    false => ConditionEvaluationResult::Fail,
                },
            },

            ToolCondition::Not { not } => {
                let result_to_negate = self.evaluate_condition(not);
                negate_condition_evaluation_result(result_to_negate)
            }

            ToolCondition::Boolean(b) => match b.op {
                BooleanOperator::AnyOf => {
                    any_condition_matches(&b.conditions, |c| self.evaluate_condition(c))
                }
                BooleanOperator::AllOf => {
                    all_conditions_match(&b.conditions, |c| self.evaluate_condition(c))
                }
            },
        }
    }

    fn atomic_condition_matches(
        &self,
        atomic: &AtomicToolCondition,
    ) -> Result<bool, PolicyEngineError> {
        match atomic.source {
            SourceTag::Args => {
                self.property_matcher_matches(&atomic.matches, self.tool_args, &atomic.source)
            }
            SourceTag::State => {
                self.property_matcher_matches(&atomic.matches, self.agent_state, &atomic.source)
            }
        }
    }

    fn property_matcher_matches(
        &self,
        matcher: &PropertyMatcher,
        source: &Value,
        source_tag: &SourceTag,
    ) -> Result<bool, PolicyEngineError> {
        let actual_value_key = match matcher {
            PropertyMatcher::Scalar(scalar_matcher) => &scalar_matcher.key,
            PropertyMatcher::List(list_matcher) => &list_matcher.key,
        };

        let actual_value_option = get_value_at_path(source, actual_value_key);
        let Some(actual_value) = actual_value_option else {
            return Err(MissingActualValueAtPath {
                source_tag: source_tag.to_string(),
                path: actual_value_key.into(),
            });
        };

        let result = match matcher {
            PropertyMatcher::Scalar(scalar_matcher) => {
                self.scalar_property_matcher_matches(scalar_matcher, actual_value)
            }

            PropertyMatcher::List(list_matcher) => {
                list_property_matcher_matches(list_matcher, actual_value)
            }
        };

        match result {
            Ok(_) => result,
            Err(ComparisonError {
                required_type,
                source_type,
            }) => Err(MismatchedValueAtPath {
                source_tag: source_tag.to_string(),
                path: actual_value_key.to_string(),
                expected: required_type,
                actual: source_type,
            }),
            Err(_) => result,
        }
    }

    fn scalar_property_matcher_matches(
        &self,
        matcher: &PropertyScalarMatcher,
        actual_value: &Value,
    ) -> Result<bool, PolicyEngineError> {
        // expected literal or reference-resolved value
        let expected_value = get_expected_value_from_scalar_property_matcher_or_error(
            matcher,
            self.tool_args,
            self.agent_state,
        )?;

        property_scalar_matcher_matches(matcher, actual_value, &expected_value, &self.regex_matcher)
    }
}

fn negate_condition_evaluation_result(
    result: ConditionEvaluationResult,
) -> ConditionEvaluationResult {
    match result {
        ConditionEvaluationResult::Pass => ConditionEvaluationResult::Fail,
        ConditionEvaluationResult::Fail => ConditionEvaluationResult::Pass,
        ConditionEvaluationResult::Unknown { reason } => {
            ConditionEvaluationResult::Unknown { reason }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::otel::effect_reason::{
        effect_reason_for_allow, effect_reason_for_deny, effect_reason_for_modify,
    };
    use crate::policy_loader::load_many_from_reader;
    use crate::telemetry_constants as telemetry;
    use serde_json::json;

    fn wrap(records: Vec<ToolPolicy>, status: ToolPolicyStatus) -> Vec<ToolPolicyRecord> {
        records
            .into_iter()
            .enumerate()
            .map(|(i, p)| ToolPolicyRecord {
                id: format!("rec-{}", i),
                name: format!("policy-{}", p.tool_name),
                current_version_id: "v1".into(),
                policy: p,
                status,
            })
            .collect()
    }

    fn engine(policies: Vec<ToolPolicyRecord>) -> PolicyEngine {
        PolicyEngine::new(policies, None)
    }

    #[test]
    fn deny_high_loan_amount() {
        let policies = wrap(
            vec![
                ToolPolicy {
                    tool_name: "grant_loan".into(),
                    rule: ToolPolicyRule::Deny {
                        when: ToolCondition::Atomic(AtomicToolCondition {
                            source: SourceTag::Args,
                            matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                                key: "loanAmount".into(),
                                op: ValueScalarOperator::Gt,
                                value: ValueOrReference::Value(Value::Number(
                                    serde_json::Number::from_f64(10000.0).unwrap(),
                                )),
                            }),
                        }),
                    },
                },
                ToolPolicy {
                    tool_name: "grant_loan".into(),
                    rule: ToolPolicyRule::Allow {
                        when: ToolCondition::Atomic(AtomicToolCondition {
                            source: SourceTag::State,
                            matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                                key: "user.isAdmin".into(),
                                op: ValueScalarOperator::Eq,
                                value: ValueOrReference::Value(Value::Bool(true)),
                            }),
                        }),
                    },
                },
            ],
            ToolPolicyStatus::ACTIVE,
        );
        let eng = engine(policies);

        // Get references to expected matched rules inside engine
        let deny_rule_ref = match &eng.policies[0].policy.rule {
            r @ ToolPolicyRule::Deny { .. } => r,
            _ => unreachable!(),
        };
        let allow_rule_ref = match &eng.policies[1].policy.rule {
            r @ ToolPolicyRule::Allow { .. } => r,
            _ => unreachable!(),
        };

        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-1",
            &json!({"loanAmount": 20000}),
            &json!({"user": {"isAdmin": false}}),
            None,
        );
        match action {
            ProcessToolCallResult::Deny {
                rules_matched,
                reason,
            } => {
                assert_eq!(reason, "Denied by policy");
                assert_eq!(rules_matched.len(), 1);
                assert_eq!(rules_matched[0], *deny_rule_ref);
                assert_ne!(rules_matched[0], *allow_rule_ref);
            }
            _ => panic!("expected deny"),
        }
    }

    #[test]
    fn monitor_policies_do_not_trigger_deny() {
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "grant_loan".into(),
                rule: ToolPolicyRule::Deny {
                    when: ToolCondition::Atomic(AtomicToolCondition {
                        source: SourceTag::State,
                        matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                            key: "allow".into(),
                            op: ValueScalarOperator::Eq,
                            value: ValueOrReference::Value(Value::Bool(false)),
                        }),
                    }),
                },
            }],
            ToolPolicyStatus::MONITOR,
        );
        let eng = engine(policies);

        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-1",
            &json!({}),
            &json!({"allow": false}),
            None,
        );

        match action {
            ProcessToolCallResult::Allow {
                rules_matched,
                reason,
            } => {
                assert_eq!(reason, "No active policies for tool");
                assert_eq!(rules_matched.len(), 0);
            }
            _ => panic!("expected allow"),
        }
    }

    #[test]
    fn monitor_policies_do_not_trigger_allow() {
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "grant_loan".into(),
                rule: ToolPolicyRule::Allow {
                    when: ToolCondition::Atomic(AtomicToolCondition {
                        source: SourceTag::State,
                        matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                            key: "allow".into(),
                            op: ValueScalarOperator::Eq,
                            value: ValueOrReference::Value(Value::Bool(true)),
                        }),
                    }),
                },
            }],
            ToolPolicyStatus::MONITOR,
        );
        let eng = engine(policies);

        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-1",
            &json!({}),
            &json!({"allow": true }),
            None,
        );

        match action {
            ProcessToolCallResult::Allow {
                rules_matched,
                reason,
            } => {
                assert_eq!(reason, "No active policies for tool");
                assert_eq!(rules_matched.len(), 0);
            }
            _ => panic!("expected allow"),
        }
    }

    #[test]
    fn internal_variable_references() {
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "view_account_details".into(),
                rule: ToolPolicyRule::Allow {
                    when: ToolCondition::Atomic(AtomicToolCondition {
                        source: SourceTag::Args,
                        matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                            key: "user_id".into(),
                            op: ValueScalarOperator::Eq,
                            value: ValueOrReference::Reference(ValueReference {
                                source: SourceTag::State,
                                path: "user.id".into(),
                            }),
                        }),
                    }),
                },
            }],
            ToolPolicyStatus::ACTIVE,
        );

        let policy_engine = PolicyEngine::new(policies, None);
        let allow_rule_ref = match &policy_engine.policies[0].policy.rule {
            r @ ToolPolicyRule::Allow { .. } => r,
            _ => unreachable!(),
        };

        let expected_allowed_action = policy_engine.process_tool_call(
            "view_account_details",
            "test-call-2",
            &json!({"user_id": 123}),
            &json!({"user": {"id": 123}}),
            None,
        );

        match expected_allowed_action {
            ProcessToolCallResult::Allow { rules_matched, .. } => {
                assert_eq!(rules_matched.len(), 1);
                assert_eq!(rules_matched[0], *allow_rule_ref);
            }
            _ => panic!(
                "Operation should be allowed because tool_args.user_id === agent_state.user.id"
            ),
        }

        let expected_denied_action = policy_engine.process_tool_call(
            "view_account_details",
            "test-call-3",
            &json!({"user_id": 123}),
            &json!({"user": {"id": 456}}),
            None,
        );

        match expected_denied_action {
            ProcessToolCallResult::Deny {
                rules_matched,
                reason,
            } => {
                assert_eq!(reason, "Denied by policy");
                assert_eq!(rules_matched.len(), 0);
            }
            _ => panic!(
                "Operation should not be allowed because tool_args.user_id !== agent_state.user.id"
            ),
        }
    }

    #[test]
    fn allow_admin_overrides() {
        let policies = wrap(
            vec![
                ToolPolicy {
                    tool_name: "grant_loan".into(),
                    rule: ToolPolicyRule::Deny {
                        when: ToolCondition::Atomic(AtomicToolCondition {
                            source: SourceTag::Args,
                            matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                                key: "loanAmount".into(),
                                op: ValueScalarOperator::Gt,
                                value: ValueOrReference::Value(Value::Number(
                                    serde_json::Number::from_f64(10000.0).unwrap(),
                                )),
                            }),
                        }),
                    },
                },
                ToolPolicy {
                    tool_name: "grant_loan".into(),
                    rule: ToolPolicyRule::Allow {
                        when: ToolCondition::Atomic(AtomicToolCondition {
                            source: SourceTag::State,
                            matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                                key: "user.isAdmin".into(),
                                op: ValueScalarOperator::Eq,
                                value: ValueOrReference::Value(Value::Bool(true)),
                            }),
                        }),
                    },
                },
            ],
            ToolPolicyStatus::ACTIVE,
        );
        let eng = engine(policies);
        let deny_rule_ref = match &eng.policies[0].policy.rule {
            r @ ToolPolicyRule::Deny { .. } => r,
            _ => unreachable!(),
        };
        let allow_rule_ref = match &eng.policies[1].policy.rule {
            r @ ToolPolicyRule::Allow { .. } => r,
            _ => unreachable!(),
        };
        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-4",
            &json!({"loanAmount": 9000}),
            &json!({"user": {"isAdmin": true}}),
            None,
        );

        match action {
            ProcessToolCallResult::Allow { rules_matched, .. } => {
                assert_eq!(rules_matched.len(), 1);
                assert_eq!(rules_matched[0], *allow_rule_ref);
                assert_ne!(rules_matched[0], *deny_rule_ref);
            }
            _ => panic!("expected allow"),
        }
    }

    #[test]
    fn allow_if_no_deny() {
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "grant_loan".into(),
                rule: ToolPolicyRule::Deny {
                    when: ToolCondition::Atomic(AtomicToolCondition {
                        source: SourceTag::Args,
                        matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                            key: "loanAmount".into(),
                            op: ValueScalarOperator::Gt,
                            value: ValueOrReference::Value(Value::Number(
                                serde_json::Number::from_f64(10000.0).unwrap(),
                            )),
                        }),
                    }),
                },
            }],
            ToolPolicyStatus::ACTIVE,
        );
        let eng = engine(policies);
        let _deny_rule_ref = match &eng.policies[0].policy.rule {
            r @ ToolPolicyRule::Deny { .. } => r,
            _ => unreachable!(),
        };
        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-5",
            &json!({"loanAmount": 5000}),
            &json!({"user": {"isAdmin": false}}),
            None,
        );
        match action {
            ProcessToolCallResult::Allow { rules_matched, .. } => {
                assert_eq!(rules_matched.len(), 0);
            }
            _ => panic!("expected deny"),
        }
    }

    #[test]
    fn allow_if_name_in_list() {
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "greet".into(),
                rule: ToolPolicyRule::Allow {
                    when: ToolCondition::Atomic(AtomicToolCondition {
                        source: SourceTag::State,
                        matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                            key: "name".into(),
                            op: ValueScalarOperator::In,
                            value: ValueOrReference::Value(Value::Array(vec![Value::String(
                                "Jonathan".into(),
                            )])),
                        }),
                    }),
                },
            }],
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);

        let expected_allow_action = eng.process_tool_call(
            "greet",
            "test-call-6",
            &json!({}),
            &json!({"name": "Jonathan"}),
            None,
        );

        match expected_allow_action {
            ProcessToolCallResult::Allow { rules_matched, .. } => {
                assert_eq!(rules_matched.len(), 1);
            }
            _ => panic!("expected allow"),
        }

        let expected_deny_action = eng.process_tool_call(
            "greet",
            "test-call-7",
            &json!({}),
            &json!({"name": "Not Jonathan"}),
            None,
        );
        match expected_deny_action {
            ProcessToolCallResult::Allow { .. } => {
                panic!("Expected deny")
            }
            _ => {}
        }
    }

    #[test]
    fn list_contains_all() {
        use serde_json::json;
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "check_ops".into(),
                rule: ToolPolicyRule::Allow {
                    when: ToolCondition::Atomic(AtomicToolCondition {
                        source: SourceTag::Args,
                        matches: PropertyMatcher::List(PropertyListMatcher {
                            key: "ops".into(),
                            op: ValueListOperator::ContainsAll,
                            values: vec![Value::String("eq".into()), Value::String("lt".into())],
                        }),
                    }),
                },
            }],
            ToolPolicyStatus::ACTIVE,
        );
        let eng = PolicyEngine::new(policies, None);
        let allow_rule_ref = match &eng.policies[0].policy.rule {
            r @ ToolPolicyRule::Allow { .. } => r,
            _ => unreachable!(),
        };
        let action = eng.process_tool_call(
            "check_ops",
            "test-call-8",
            &json!({"ops": ["eq", "lt", "gt"]}),
            &json!({}),
            None,
        );

        match action {
            ProcessToolCallResult::Allow { rules_matched, .. } => {
                assert_eq!(rules_matched.len(), 1);
                assert_eq!(rules_matched[0], *allow_rule_ref);
            }
            _ => panic!("expected allow"),
        }
    }

    #[test]
    fn weather_in_narnia() {
        let policies = wrap(
            load_many_from_reader(
                r#"[
              {
                "tool_name": "get_weather_in_narnia",
                "rule": {
                  "id": "deny_if_allow_false",
                  "action": "deny",
                  "when": {
                    "op": "any_of",
                    "conditions": [
                      {
                        "source": "state",
                        "matches": {
                          "key": "allow",
                          "op": "eq",
                          "value": false
                        }
                      }
                    ]
                  }
                }
              }
            ]
            "#
                .as_bytes(),
            )
            .unwrap(),
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);
        let action = eng.process_tool_call(
            "get_weather_in_narnia",
            "test-call-9",
            &json!({}),
            &json!({"allow": false}),
            None,
        );

        match action {
            ProcessToolCallResult::Deny {
                rules_matched,
                reason,
            } => {
                assert_eq!(reason, "Denied by policy");
                assert_eq!(rules_matched.len(), 1);
            }
            _ => panic!("expected deny"),
        }
    }

    #[test]
    fn handle_missing_values() {
        let policies = wrap(
            load_many_from_reader(
                r#"[
              {
                "tool_name": "grant_loan",
                "rule": {
                  "id": "approve_if_not_jonathan",
                  "action": "deny",
                  "when": {
                    "op": "any_of",
                    "conditions": [
                      {
                        "source": "state",
                        "matches": {
                          "key": "user.name",
                          "op": "eq",
                          "value": "Jonathan"
                        }
                      }
                    ]
                  }
                }
              }
            ]
            "#
                .as_bytes(),
            )
            .unwrap(),
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);
        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-10",
            &json!({}),
            &json!({}), // state is empty, no name field.
            None,
        );

        if let ProcessToolCallResult::Deny { .. } = action {
            panic!("Expected allow");
        }
    }

    #[test]
    fn handle_null_values() {
        let policies = wrap(
            load_many_from_reader(
                r#"[
              {
                "tool_name": "grant_loan",
                "rule": {
                  "id": "approve_if_not_jonathan",
                  "action": "deny",
                  "when": {
                    "op": "any_of",
                    "conditions": [
                      {
                        "source": "state",
                        "matches": {
                          "key": "name",
                          "op": "eq",
                          "value": "Jonathan"
                        }
                      }
                    ]
                  }
                }
              }
            ]
            "#
                .as_bytes(),
            )
            .unwrap(),
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);
        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-11",
            &json!({}),
            &json!({"user": { "name": null }}), // user.name explicitly set to null.
            None,
        );

        if let ProcessToolCallResult::Deny { .. } = action {
            panic!("Expected allow");
        }
    }

    #[test]
    fn handle_not_equals_when_state_value_is_null() {
        // This policy says "only allow the loan if the user's name is not Jonathan.
        // Since the user's name is explicitly set to null, the matcher should not match, and the loan should be allowed.

        let policies = wrap(
            load_many_from_reader(
                r#"[
              {
                "tool_name": "grant_loan",
                "rule": {
                  "id": "allow_if_not_jonathan",
                  "action": "allow",
                  "when": {
                    "source": "state",
                    "matches": {
                      "key": "name",
                      "op": "ne",
                      "value": "Jonathan"
                    }
                  }
                }
              }
            ]
            "#
                .as_bytes(),
            )
            .unwrap(),
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);
        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-12",
            &json!({}),
            &json!({"user": { "name": null }}), // user.name is explicitly set to null.
            None,
        );

        if let ProcessToolCallResult::Allow { .. } = action {
            panic!("Expected deny");
        }
    }

    #[test]
    fn handle_value_must_equal_null() {
        let policies = wrap(
            load_many_from_reader(
                r#"[
              {
                "tool_name": "grant_loan",
                "rule": {
                  "id": "approve_if_not_banned",
                  "action": "allow",
                  "when": {
                    "op": "any_of",
                    "conditions": [
                      {
                        "source": "state",
                        "matches": {
                          "key": "banned",
                          "op": "eq",
                          "value": null
                        }
                      }
                    ]
                  }
                }
              }
            ]
            "#
                .as_bytes(),
            )
            .unwrap(),
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);
        let action = eng.process_tool_call(
            "grant_loan",
            "test-call-13",
            &json!({}),
            &json!({"banned": null}), // state has `banned` explicitly set to null.
            None,
        );

        if let ProcessToolCallResult::Deny { .. } = action {
            panic!("Expected allow");
        }
    }

    #[test]
    fn allow_if_regex_matches() {
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "greet".into(),
                rule: ToolPolicyRule::Allow {
                    when: ToolCondition::Atomic(AtomicToolCondition {
                        source: SourceTag::State,
                        matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                            key: "name".into(),
                            op: ValueScalarOperator::Regex,
                            value: ValueOrReference::Value(Value::String("^Jon.*".into())),
                        }),
                    }),
                },
            }],
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);
        let allow_action = eng.process_tool_call(
            "greet",
            "test-call-14",
            &json!({}),
            &json!({"name": "Jonathan"}),
            None,
        );

        match allow_action {
            ProcessToolCallResult::Allow { rules_matched, .. } => {
                assert_eq!(rules_matched.len(), 1);
            }
            _ => panic!("expected allow for matching regex"),
        }

        let deny_action = eng.process_tool_call(
            "greet",
            "test-call-15",
            &json!({}),
            &json!({"name": "Alice"}),
            None,
        );

        match deny_action {
            ProcessToolCallResult::Deny { .. } => {}
            _ => panic!("expected deny for non-matching regex"),
        }
    }

    #[test]
    fn evaluate_negated_condition() {
        let policies = wrap(
            vec![ToolPolicy {
                tool_name: "login_user".into(),
                rule: ToolPolicyRule::Allow {
                    when: ToolCondition::Not {
                        not: Box::new(ToolCondition::Atomic(AtomicToolCondition {
                            source: SourceTag::State,
                            matches: PropertyMatcher::Scalar(PropertyScalarMatcher {
                                key: "user.isBanned".into(),
                                op: ValueScalarOperator::Eq,
                                value: ValueOrReference::Value(Value::Bool(true)),
                            }),
                        })),
                    },
                },
            }],
            ToolPolicyStatus::ACTIVE,
        );

        let eng = engine(policies);
        let deny_action = eng.process_tool_call(
            "login_user",
            "test-call-15",
            &json!({}),
            &json!({"user": {"isBanned": true}}),
            None,
        );

        match deny_action {
            ProcessToolCallResult::Deny { .. } => {}
            _ => panic!("expected deny for matching negated condition."),
        }
    }
}
