//! End-to-end: an effectful tool wrapped by GatedTool is short-circuited
//! by an AllowlistGate that denies unknown names; an allowed tool flows
//! through.

use async_trait::async_trait;
use klieo_core::{Tool, ToolCtx, ToolError};
use klieo_ops::gates::{AllowlistGate, GateDecision, GatedTool};
use std::sync::Arc;

struct DummyEffectful;

#[async_trait]
impl Tool for DummyEffectful {
    fn name(&self) -> &str {
        "dummy_effect"
    }

    fn description(&self) -> &str {
        ""
    }

    fn json_schema(&self) -> &serde_json::Value {
        static S: std::sync::OnceLock<serde_json::Value> = std::sync::OnceLock::new();
        S.get_or_init(|| serde_json::json!({"type": "object"}))
    }

    fn is_effectful(&self) -> bool {
        true
    }

    async fn invoke(
        &self,
        args: serde_json::Value,
        _ctx: ToolCtx,
    ) -> Result<serde_json::Value, ToolError> {
        Ok(args)
    }
}

#[tokio::test]
async fn allowlist_gate_blocks_disallowed_effectful_tool() {
    let inner = Arc::new(DummyEffectful);
    let gate = Arc::new(AllowlistGate::new(&["something_else"]));
    let gated = GatedTool::new(inner, vec![gate]);
    let decision = gated
        .evaluate_for_test("dummy_effect", &serde_json::json!({}))
        .await;
    assert!(matches!(decision, GateDecision::Deny { .. }));
}

#[tokio::test]
async fn allowlist_gate_admits_allowed_effectful_tool() {
    let inner = Arc::new(DummyEffectful);
    let gate = Arc::new(AllowlistGate::new(&["dummy_effect"]));
    let gated = GatedTool::new(inner, vec![gate]);
    let decision = gated
        .evaluate_for_test("dummy_effect", &serde_json::json!({}))
        .await;
    assert!(matches!(decision, GateDecision::Allow));
}

struct DummyReadOnly;

#[async_trait]
impl Tool for DummyReadOnly {
    fn name(&self) -> &str {
        "dummy_read"
    }

    fn description(&self) -> &str {
        ""
    }

    fn json_schema(&self) -> &serde_json::Value {
        static S: std::sync::OnceLock<serde_json::Value> = std::sync::OnceLock::new();
        S.get_or_init(|| serde_json::json!({"type": "object"}))
    }

    // is_effectful defaults to false (no override).

    async fn invoke(
        &self,
        args: serde_json::Value,
        _ctx: ToolCtx,
    ) -> Result<serde_json::Value, ToolError> {
        Ok(args)
    }
}

#[tokio::test]
async fn gated_tool_runs_gates_regardless_of_is_effectful() {
    let inner = Arc::new(DummyReadOnly);
    let gate = Arc::new(AllowlistGate::new(&["something_else"]));
    let gated = GatedTool::new(inner, vec![gate]);
    let decision = gated
        .evaluate_for_test("dummy_read", &serde_json::json!({}))
        .await;
    assert!(
        matches!(decision, GateDecision::Deny { .. }),
        "GatedTool must gate ALL wrapped tools, not just is_effectful() == true"
    );
}

#[tokio::test]
async fn policy_gate_evaluates_cedar_policy() {
    use klieo_ops::gates::{Gate, GateRequest, PolicyGate};

    let policy_src = r#"
        permit(
            principal,
            action == Action::"invoke",
            resource
        ) when { context.amount < 5000 };
    "#;
    let gate = PolicyGate::from_cedar_source(policy_src).expect("parse ok");

    let allow_req = GateRequest::new("ClaimsPayoutTool", serde_json::json!({"amount": 1234}));
    let dec = gate.evaluate(allow_req).await;
    assert!(
        matches!(dec, klieo_ops::gates::GateDecision::Allow),
        "expected Allow, got {dec:?}"
    );

    let deny_req = GateRequest::new("ClaimsPayoutTool", serde_json::json!({"amount": 99999}));
    let dec = gate.evaluate(deny_req).await;
    assert!(
        matches!(dec, klieo_ops::gates::GateDecision::Deny { .. }),
        "expected Deny, got {dec:?}"
    );
}

#[tokio::test]
async fn policy_gate_accepts_nested_context() {
    use klieo_ops::gates::{Gate, GateRequest, PolicyGate};

    let policy_src = r#"
        permit(
            principal,
            action == Action::"invoke",
            resource
        ) when { context.customer.risk_score == decimal("0.5000") };
    "#;
    let gate = PolicyGate::from_cedar_source(policy_src).expect("parse ok");
    let req = GateRequest::new(
        "AssessCustomerTool",
        serde_json::json!({
            "customer": {"risk_score": 0.5}
        }),
    );
    let decision = gate.evaluate(req).await;
    assert!(
        matches!(decision, klieo_ops::gates::GateDecision::Allow),
        "nested context.customer.risk_score must lift; got {decision:?}"
    );
}

#[tokio::test]
async fn policy_gate_denies_unrepresentable_null() {
    use klieo_ops::gates::{Gate, GateRequest, PolicyGate};

    let policy_src = r#"
        permit(principal, action, resource);
    "#;
    let gate = PolicyGate::from_cedar_source(policy_src).expect("parse ok");
    let req = GateRequest::new("AnyTool", serde_json::json!({"weird": null}));
    let decision = gate.evaluate(req).await;
    assert!(
        matches!(
            decision,
            klieo_ops::gates::GateDecision::Deny { ref code, .. } if code == "cedar.context.unrepresentable"
        ),
        "null arg must deny with code `cedar.context.unrepresentable`; got {decision:?}"
    );
}

#[tokio::test]
async fn policy_gate_accepts_array_of_scalars() {
    use klieo_ops::gates::{Gate, GateRequest, PolicyGate};

    let policy_src = r#"
        permit(
            principal,
            action == Action::"invoke",
            resource
        ) when { ["EUR", "USD"].contains(context.currency) };
    "#;
    let gate = PolicyGate::from_cedar_source(policy_src).expect("parse ok");
    let req = GateRequest::new("FxTool", serde_json::json!({"currency": "EUR"}));
    let decision = gate.evaluate(req).await;
    assert!(
        matches!(decision, klieo_ops::gates::GateDecision::Allow),
        "string membership in a Cedar set literal must work; got {decision:?}"
    );
}
