//! Tests for el-aop.

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Arc;

    use crate::{
        aspects::*,
        chain::AspectChain,
        registry::AspectRegistry,
        AopError, Aspect, InvocationContext, InvocationResult,
    };

    fn succeed_proceed(value: impl Into<String> + Clone) -> crate::ProceedFn {
        let v = value.into();
        Box::new(move |_ctx| Ok(InvocationResult::new(v.clone())))
    }

    fn fail_proceed(msg: impl Into<String> + Clone) -> crate::ProceedFn {
        let m = msg.into();
        Box::new(move |_ctx| Err(AopError::Aspect(m.clone())))
    }

    fn ctx(target: &str, method: &str) -> InvocationContext {
        InvocationContext::new(target, method)
    }

    fn authed_ctx(target: &str, method: &str) -> InvocationContext {
        ctx(target, method).with_meta("user_id", "user-123")
    }

    fn admin_ctx(target: &str, method: &str) -> InvocationContext {
        ctx(target, method)
            .with_meta("user_id", "admin-1")
            .with_meta("roles", "admin,user")
    }

    // ── Test 1: AuthenticateAspect rejects unauthenticated calls ─────────────
    #[test]
    fn test_authenticate_rejects_unauthenticated() {
        let aspect = AuthenticateAspect;
        let mut ctx = ctx("AdminDashboard", "load");
        let result = aspect.before(&mut ctx);
        assert!(result.is_err());
        assert!(matches!(result, Err(AopError::Unauthenticated)));
    }

    // ── Test 2: AuthenticateAspect allows authenticated calls ─────────────────
    #[test]
    fn test_authenticate_allows_authenticated() {
        let aspect = AuthenticateAspect;
        let mut ctx = authed_ctx("AdminDashboard", "load");
        let result = aspect.before(&mut ctx);
        assert!(result.is_ok());
    }

    // ── Test 3: AuthorizeAspect rejects wrong role ────────────────────────────
    #[test]
    fn test_authorize_rejects_wrong_role() {
        let aspect = AuthorizeAspect::new("admin");
        let mut ctx = authed_ctx("Dashboard", "delete").with_meta("roles", "user");
        let result = aspect.before(&mut ctx);
        assert!(matches!(result, Err(AopError::Forbidden { .. })));
    }

    // ── Test 4: AuthorizeAspect allows correct role ───────────────────────────
    #[test]
    fn test_authorize_allows_correct_role() {
        let aspect = AuthorizeAspect::new("admin");
        let mut ctx = admin_ctx("Dashboard", "delete");
        let result = aspect.before(&mut ctx);
        assert!(result.is_ok());
    }

    // ── Test 5: CacheAspect returns cached result on second call ──────────────
    #[test]
    fn test_cache_returns_cached_result() {
        let aspect = CacheAspect::new(300);
        let ctx = authed_ctx("OrderService", "get_orders");
        let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
        let cc = call_count.clone();
        let proceed: crate::ProceedFn = Box::new(move |_ctx| {
            let n = cc.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
            Ok(InvocationResult::new(format!("result-{}", n)))
        });
        // First call — executes proceed
        let r1 = aspect.around(ctx.clone(), &proceed).unwrap();
        // Second call — should return cached (proceed not called again)
        let proceed2: crate::ProceedFn = Box::new(|_ctx| {
            panic!("proceed should not be called on cache hit");
        });
        let r2 = aspect.around(ctx.clone(), &proceed2).unwrap();
        assert_eq!(r1.value, r2.value, "cached value should be returned");
    }

    // ── Test 6: RateLimitAspect blocks after limit exceeded ───────────────────
    #[test]
    fn test_rate_limit_blocks_after_limit() {
        let aspect = RateLimitAspect::new(2, 60);
        let mut ctx = authed_ctx("OrderService", "create_order");

        // First two calls succeed
        assert!(aspect.before(&mut ctx).is_ok());
        assert!(aspect.before(&mut ctx).is_ok());
        // Third call should be blocked
        let result = aspect.before(&mut ctx);
        assert!(matches!(result, Err(AopError::RateLimited { .. })));
    }

    // ── Test 7: LogAspect passes through to proceed ───────────────────────────
    #[test]
    fn test_log_aspect_passthrough() {
        let aspect = LogAspect::new("info");
        let ctx = authed_ctx("UserService", "get_user");
        let result = aspect.around(ctx, &succeed_proceed("user-data")).unwrap();
        assert_eq!(result.value, "user-data");
    }

    // ── Test 8: ValidateAspect rejects required field missing ────────────────
    #[test]
    fn test_validate_required_field() {
        let aspect = ValidateAspect::new();
        aspect.add_rule("UserService", "create_user", "name", "required");
        let mut ctx = authed_ctx("UserService", "create_user");
        // No "name" arg
        let result = aspect.before(&mut ctx);
        assert!(matches!(result, Err(AopError::ValidationFailed(_))));
    }

    // ── Test 9: ValidateAspect passes when field is present ──────────────────
    #[test]
    fn test_validate_required_field_present() {
        let aspect = ValidateAspect::new();
        aspect.add_rule("UserService", "create_user", "email", "email");
        let mut ctx = authed_ctx("UserService", "create_user")
            .with_arg("email", "alice@example.com");
        assert!(aspect.before(&mut ctx).is_ok());
    }

    // ── Test 10: ValidateAspect rejects invalid email ─────────────────────────
    #[test]
    fn test_validate_email_rule() {
        let aspect = ValidateAspect::new();
        aspect.add_rule("UserService", "create_user", "email", "email");
        let mut ctx = authed_ctx("UserService", "create_user")
            .with_arg("email", "not-an-email");
        let result = aspect.before(&mut ctx);
        assert!(matches!(result, Err(AopError::ValidationFailed(_))));
    }

    // ── Test 11: RetryAspect retries on failure ───────────────────────────────
    #[test]
    fn test_retry_succeeds_on_third_attempt() {
        let aspect = RetryAspect::new(3);
        let ctx = authed_ctx("OrderService", "place_order");
        let attempt = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
        let attempt_clone = attempt.clone();
        let proceed: crate::ProceedFn = Box::new(move |_ctx| {
            let n = attempt_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            if n < 2 {
                Err(AopError::Aspect("transient error".into()))
            } else {
                Ok(InvocationResult::new("success"))
            }
        });
        let result = aspect.around(ctx, &proceed).unwrap();
        assert_eq!(result.value, "success");
    }

    // ── Test 12: RetryAspect exhausts all attempts ───────────────────────────
    #[test]
    fn test_retry_exhausted() {
        let aspect = RetryAspect::new(3);
        let ctx = authed_ctx("OrderService", "place_order");
        let result = aspect.around(ctx, &fail_proceed("always fails"));
        assert!(matches!(result, Err(AopError::RetriesExhausted { attempts: 3, .. })));
    }

    // ── Test 13: TraceAspect injects trace/span IDs ───────────────────────────
    #[test]
    fn test_trace_aspect_injects_ids() {
        let aspect = TraceAspect::new("el-ui");
        let ctx = authed_ctx("UserService", "get_user");
        let result = aspect.around(ctx, &succeed_proceed("data")).unwrap();
        assert!(
            result.metadata.contains_key("trace_id"),
            "should inject trace_id"
        );
        assert!(
            result.metadata.contains_key("span_id"),
            "should inject span_id"
        );
    }

    // ── Test 14: AspectChain executes aspects in order ────────────────────────
    #[test]
    fn test_aspect_chain_ordering() {
        let order = Arc::new(std::sync::Mutex::new(Vec::new()));

        struct OrderTracker {
            name: &'static str,
            order: Arc<std::sync::Mutex<Vec<&'static str>>>,
        }
        impl Aspect for OrderTracker {
            fn name(&self) -> &'static str { self.name }
            fn around(&self, ctx: InvocationContext, proceed: &crate::ProceedFn) -> crate::AopResult<InvocationResult> {
                self.order.lock().unwrap().push(self.name);
                proceed(ctx)
            }
        }

        let chain = AspectChain::new()
            .add(Arc::new(OrderTracker { name: "first", order: order.clone() }))
            .add(Arc::new(OrderTracker { name: "second", order: order.clone() }))
            .add(Arc::new(OrderTracker { name: "third", order: order.clone() }));

        let ctx = authed_ctx("MyService", "my_method");
        chain.execute(ctx, succeed_proceed("ok")).unwrap();

        let recorded = order.lock().unwrap();
        assert_eq!(*recorded, vec!["first", "second", "third"]);
    }

    // ── Test 15: AspectChain with auth + authorize rejects unauthenticated ────
    #[test]
    fn test_aspect_chain_auth_flow() {
        let chain = AspectChain::new()
            .add(Arc::new(AuthenticateAspect))
            .add(Arc::new(AuthorizeAspect::new("admin")));

        // Unauthenticated — should fail at authenticate
        let ctx = ctx("AdminDashboard", "load");
        let result = chain.execute(ctx, succeed_proceed("ok"));
        assert!(matches!(result, Err(AopError::Unauthenticated)));

        // Authenticated but wrong role — should fail at authorize
        let ctx = authed_ctx("AdminDashboard", "load").with_meta("roles", "user");
        let result = chain.execute(ctx, succeed_proceed("ok"));
        assert!(matches!(result, Err(AopError::Forbidden { .. })));

        // Admin — should succeed
        let ctx = admin_ctx("AdminDashboard", "load");
        let result = chain.execute(ctx, succeed_proceed("ok"));
        assert!(result.is_ok());
    }

    // ── Test 16: AspectRegistry registers all builtins ────────────────────────
    #[test]
    fn test_registry_has_builtins() {
        let registry = AspectRegistry::with_builtins();
        for name in ["authenticate", "authorize", "cache", "rate_limit", "log", "validate", "retry", "trace"] {
            assert!(registry.contains(name), "should have built-in: {}", name);
        }
    }

    // ── Test 17: AspectRegistry creates aspects from params ───────────────────
    #[test]
    fn test_registry_creates_aspect() {
        let registry = AspectRegistry::with_builtins();
        let mut params = HashMap::new();
        params.insert("role".into(), "admin".into());
        let aspect = registry.create("authorize", &params);
        assert!(aspect.is_some(), "should create authorize aspect");
        assert_eq!(aspect.unwrap().name(), "authorize");
    }

    // ── Test 18: AspectRegistry::create returns None for unknown aspect ───────
    #[test]
    fn test_registry_unknown_aspect() {
        let registry = AspectRegistry::with_builtins();
        let result = registry.create("unknown_aspect", &HashMap::new());
        assert!(result.is_none());
    }

    // ── Test 19: CacheAspect with zero TTL doesn't serve stale data ──────────
    #[test]
    fn test_cache_zero_ttl() {
        let aspect = CacheAspect::new(0); // Immediate expiry
        let ctx = authed_ctx("Service", "method");
        let n = Arc::new(std::sync::atomic::AtomicU32::new(0));
        let nc = n.clone();
        // Both calls should hit proceed since ttl=0 means instant expiry
        let p1: crate::ProceedFn = Box::new(move |_ctx| {
            let v = nc.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            Ok(InvocationResult::new(v.to_string()))
        });
        // Zero TTL will expire immediately; we just verify it doesn't panic
        let _ = aspect.around(ctx.clone(), &p1);
        // Second call should also execute proceed
        let p2: crate::ProceedFn = Box::new(|_ctx| Ok(InvocationResult::new("fresh")));
        let r = aspect.around(ctx, &p2).unwrap();
        assert_eq!(r.value, "fresh");
    }

    // ── Test 20: Empty AspectChain calls proceed directly ─────────────────────
    #[test]
    fn test_empty_chain_calls_proceed() {
        let chain = AspectChain::new();
        assert!(chain.is_empty());
        let ctx = authed_ctx("Service", "method");
        let result = chain.execute(ctx, succeed_proceed("direct")).unwrap();
        assert_eq!(result.value, "direct");
    }
}
