//! PolicyGate per-tenant bundle dispatch.

use klieo_ops::gates::{Gate, GateDecision, GateRequest, PolicyGate};
use klieo_ops::tenant::scope_tenant;
use klieo_ops::types::TenantId;
use std::collections::HashMap;

const BUNDLE_AUTO: &str = r#"
    permit(
        principal,
        action == Action::"invoke",
        resource
    ) when { context.amount < 5000 };
"#;

const BUNDLE_MOTOR: &str = r#"
    permit(
        principal,
        action == Action::"invoke",
        resource
    ) when { context.amount < 1000 };
"#;

const DEFAULT_BUNDLE: &str = r#"
    permit(
        principal,
        action == Action::"invoke",
        resource
    );
"#;

fn req(amount: i64) -> GateRequest {
    GateRequest::new("payout", serde_json::json!({"amount": amount}))
}

#[tokio::test]
async fn tenant_specific_bundle_dispatches_under_scope() {
    let bundles: HashMap<TenantId, String> = HashMap::from([
        (TenantId("BL_auto".into()), BUNDLE_AUTO.into()),
        (TenantId("BL_motor".into()), BUNDLE_MOTOR.into()),
    ]);
    let gate = PolicyGate::per_tenant(bundles).expect("build");

    let decision_auto_3000 = scope_tenant(Some(TenantId("BL_auto".into())), async {
        gate.evaluate(req(3000)).await
    })
    .await;
    assert!(
        matches!(decision_auto_3000, GateDecision::Allow),
        "BL_auto permits < 5000"
    );

    let decision_motor_3000 = scope_tenant(Some(TenantId("BL_motor".into())), async {
        gate.evaluate(req(3000)).await
    })
    .await;
    assert!(
        matches!(decision_motor_3000, GateDecision::Deny { .. }),
        "BL_motor caps at 1000"
    );
}

#[tokio::test]
async fn falls_back_to_default_bundle_when_tenant_missing() {
    let per_tenant: HashMap<TenantId, String> =
        HashMap::from([(TenantId("BL_auto".into()), BUNDLE_AUTO.into())]);
    let gate = PolicyGate::with_default_and_per_tenant(DEFAULT_BUNDLE, per_tenant).expect("build");

    let decision = scope_tenant(Some(TenantId("BL_unknown".into())), async {
        gate.evaluate(req(99999)).await
    })
    .await;
    assert!(
        matches!(decision, GateDecision::Allow),
        "unknown tenant uses default (permit-all)"
    );
}

#[tokio::test]
async fn fail_closed_when_no_bundle_and_no_default() {
    let bundles: HashMap<TenantId, String> =
        HashMap::from([(TenantId("BL_auto".into()), BUNDLE_AUTO.into())]);
    let gate = PolicyGate::per_tenant(bundles).expect("build");

    let decision = scope_tenant(Some(TenantId("BL_missing".into())), async {
        gate.evaluate(req(1)).await
    })
    .await;
    assert!(
        matches!(
            decision,
            GateDecision::Deny { ref code, .. } if code == "policy.no_bundle_for_tenant"
        ),
        "no bundle + no default = fail-CLOSED with stable code"
    );
}

#[tokio::test]
async fn no_tenant_in_scope_uses_default() {
    let gate = PolicyGate::from_cedar_source(DEFAULT_BUNDLE).expect("build");
    // No scope_tenant wrapping — current_tenant() returns None, which resolves
    // to the None-keyed default bundle installed by from_cedar_source.
    let decision = gate.evaluate(req(1)).await;
    assert!(matches!(decision, GateDecision::Allow));
}
