//! Topic-level authorization policies and rules for the realtime pub/sub hub.
//!
//! Defines [`Policy`] variants (public, authenticated, role-based, user-specific)
//! and [`AuthRule`]s that map topic patterns to subscribe/publish policies.

use serde::{Deserialize, Serialize};

use crate::topic::TopicMatcher;

/// Authorization policy for a topic action (subscribe or publish).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum Policy {
    /// Anyone can perform the action.
    Public,
    /// Only authenticated users.
    Authenticated,
    /// Only users with a specific role.
    Role(String),
    /// Only a specific user.
    UserId(String),
}

/// A rule mapping a topic pattern to subscribe/publish policies.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthRule {
    /// Topic pattern (supports wildcards `*` and `#`).
    pub pattern: String,
    /// Policy for subscribing to this topic.
    pub subscribe_policy: Policy,
    /// Policy for publishing to this topic.
    pub publish_policy: Policy,
}

/// Caller-provided identity context for authorization checks.
#[derive(Debug, Clone, Default)]
pub struct AuthContext {
    /// The user's unique ID, if authenticated.
    pub user_id: Option<String>,
    /// Roles the user holds.
    pub roles: Vec<String>,
    /// Whether the user is authenticated at all.
    pub is_authenticated: bool,
}

impl AuthContext {
    /// Create an unauthenticated context.
    pub fn anonymous() -> Self {
        Self::default()
    }

    /// Create an authenticated context with the given user ID and roles.
    pub fn authenticated(user_id: impl Into<String>, roles: Vec<String>) -> Self {
        Self {
            user_id: Some(user_id.into()),
            roles,
            is_authenticated: true,
        }
    }
}

/// Topic authorization engine.
///
/// Holds a set of rules that map topic patterns to access policies.
/// When checking access, the *first matching rule* wins. If no rule matches,
/// a configurable default policy applies (defaults to `Public` for subscribe,
/// `Authenticated` for publish).
#[derive(Debug, Clone)]
pub struct TopicAuth {
    rules: Vec<AuthRule>,
    default_subscribe: Policy,
    default_publish: Policy,
}

impl TopicAuth {
    /// Create with default rules for the standard topic namespaces.
    ///
    /// Default rules:
    /// - `system/#` — subscribe: Authenticated, publish: never (internal only)
    /// - `plugin/#` — subscribe: Public, publish: Authenticated
    /// - `custom/#` — subscribe: Public, publish: Authenticated
    pub fn with_defaults() -> Self {
        Self {
            rules: vec![
                AuthRule {
                    pattern: "system/#".to_string(),
                    subscribe_policy: Policy::Authenticated,
                    // We encode "internal only" by requiring a special role
                    // that no external user should have.
                    publish_policy: Policy::Role("__system_internal__".to_string()),
                },
                AuthRule {
                    pattern: "plugin/#".to_string(),
                    subscribe_policy: Policy::Public,
                    publish_policy: Policy::Authenticated,
                },
                AuthRule {
                    pattern: "custom/#".to_string(),
                    subscribe_policy: Policy::Public,
                    publish_policy: Policy::Authenticated,
                },
            ],
            default_subscribe: Policy::Public,
            default_publish: Policy::Authenticated,
        }
    }

    /// Create with no rules (everything falls through to defaults).
    pub fn new() -> Self {
        Self {
            rules: Vec::new(),
            default_subscribe: Policy::Public,
            default_publish: Policy::Authenticated,
        }
    }

    /// Create with custom rules.
    pub fn with_rules(rules: Vec<AuthRule>) -> Self {
        Self {
            rules,
            default_subscribe: Policy::Public,
            default_publish: Policy::Authenticated,
        }
    }

    /// Override the default subscribe policy.
    pub fn set_default_subscribe(&mut self, policy: Policy) {
        self.default_subscribe = policy;
    }

    /// Override the default publish policy.
    pub fn set_default_publish(&mut self, policy: Policy) {
        self.default_publish = policy;
    }

    /// Add a rule.
    pub fn add_rule(&mut self, rule: AuthRule) {
        self.rules.push(rule);
    }

    /// Check if the given context is allowed to subscribe to `topic`.
    pub fn check_subscribe(&self, topic: &str, ctx: &AuthContext) -> bool {
        let policy = self.find_subscribe_policy(topic);
        Self::evaluate(policy, ctx)
    }

    /// Check if the given context is allowed to publish to `topic`.
    pub fn check_publish(&self, topic: &str, ctx: &AuthContext) -> bool {
        let policy = self.find_publish_policy(topic);
        Self::evaluate(policy, ctx)
    }

    /// Return the list of rules.
    pub fn rules(&self) -> &[AuthRule] {
        &self.rules
    }

    // ── Private ─────────────────────────────────────────────────────

    fn find_subscribe_policy(&self, topic: &str) -> &Policy {
        for rule in &self.rules {
            if TopicMatcher::matches(&rule.pattern, topic) {
                return &rule.subscribe_policy;
            }
        }
        &self.default_subscribe
    }

    fn find_publish_policy(&self, topic: &str) -> &Policy {
        for rule in &self.rules {
            if TopicMatcher::matches(&rule.pattern, topic) {
                return &rule.publish_policy;
            }
        }
        &self.default_publish
    }

    fn evaluate(policy: &Policy, ctx: &AuthContext) -> bool {
        match policy {
            Policy::Public => true,
            Policy::Authenticated => ctx.is_authenticated,
            Policy::Role(required_role) => ctx.roles.contains(required_role),
            Policy::UserId(required_id) => ctx.user_id.as_deref() == Some(required_id.as_str()),
        }
    }
}

impl Default for TopicAuth {
    fn default() -> Self {
        Self::with_defaults()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn anonymous() -> AuthContext {
        AuthContext::anonymous()
    }

    fn authenticated_user(user_id: &str) -> AuthContext {
        AuthContext::authenticated(user_id, vec![])
    }

    fn user_with_role(user_id: &str, role: &str) -> AuthContext {
        AuthContext::authenticated(user_id, vec![role.to_string()])
    }

    fn user_with_roles(user_id: &str, roles: Vec<&str>) -> AuthContext {
        AuthContext::authenticated(user_id, roles.into_iter().map(String::from).collect())
    }

    // ── Policy::Public ──────────────────────────────────────────────

    #[test]
    fn public_allows_anonymous() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "test/#".to_string(),
            subscribe_policy: Policy::Public,
            publish_policy: Policy::Public,
        }]);

        assert!(auth.check_subscribe("test/foo", &anonymous()));
        assert!(auth.check_publish("test/foo", &anonymous()));
    }

    #[test]
    fn public_allows_authenticated() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "test/#".to_string(),
            subscribe_policy: Policy::Public,
            publish_policy: Policy::Public,
        }]);

        let ctx = authenticated_user("alice");
        assert!(auth.check_subscribe("test/foo", &ctx));
        assert!(auth.check_publish("test/foo", &ctx));
    }

    // ── Policy::Authenticated ───────────────────────────────────────

    #[test]
    fn authenticated_denies_anonymous() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "test/#".to_string(),
            subscribe_policy: Policy::Authenticated,
            publish_policy: Policy::Authenticated,
        }]);

        assert!(!auth.check_subscribe("test/foo", &anonymous()));
        assert!(!auth.check_publish("test/foo", &anonymous()));
    }

    #[test]
    fn authenticated_allows_logged_in() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "test/#".to_string(),
            subscribe_policy: Policy::Authenticated,
            publish_policy: Policy::Authenticated,
        }]);

        let ctx = authenticated_user("bob");
        assert!(auth.check_subscribe("test/foo", &ctx));
        assert!(auth.check_publish("test/foo", &ctx));
    }

    // ── Policy::Role ────────────────────────────────────────────────

    #[test]
    fn role_denies_wrong_role() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "admin/#".to_string(),
            subscribe_policy: Policy::Role("admin".to_string()),
            publish_policy: Policy::Role("admin".to_string()),
        }]);

        let ctx = user_with_role("alice", "viewer");
        assert!(!auth.check_subscribe("admin/settings", &ctx));
        assert!(!auth.check_publish("admin/settings", &ctx));
    }

    #[test]
    fn role_allows_correct_role() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "admin/#".to_string(),
            subscribe_policy: Policy::Role("admin".to_string()),
            publish_policy: Policy::Role("admin".to_string()),
        }]);

        let ctx = user_with_role("alice", "admin");
        assert!(auth.check_subscribe("admin/settings", &ctx));
        assert!(auth.check_publish("admin/settings", &ctx));
    }

    #[test]
    fn role_denies_anonymous() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "admin/#".to_string(),
            subscribe_policy: Policy::Role("admin".to_string()),
            publish_policy: Policy::Role("admin".to_string()),
        }]);

        assert!(!auth.check_subscribe("admin/settings", &anonymous()));
    }

    #[test]
    fn role_check_with_multiple_roles() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "ops/#".to_string(),
            subscribe_policy: Policy::Role("operator".to_string()),
            publish_policy: Policy::Role("operator".to_string()),
        }]);

        let ctx = user_with_roles("bob", vec!["viewer", "operator"]);
        assert!(auth.check_subscribe("ops/deploy", &ctx));
    }

    // ── Policy::UserId ──────────────────────────────────────────────

    #[test]
    fn userid_allows_matching_user() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "user/alice/#".to_string(),
            subscribe_policy: Policy::UserId("alice".to_string()),
            publish_policy: Policy::UserId("alice".to_string()),
        }]);

        let ctx = authenticated_user("alice");
        assert!(auth.check_subscribe("user/alice/inbox", &ctx));
        assert!(auth.check_publish("user/alice/inbox", &ctx));
    }

    #[test]
    fn userid_denies_different_user() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "user/alice/#".to_string(),
            subscribe_policy: Policy::UserId("alice".to_string()),
            publish_policy: Policy::UserId("alice".to_string()),
        }]);

        let ctx = authenticated_user("bob");
        assert!(!auth.check_subscribe("user/alice/inbox", &ctx));
        assert!(!auth.check_publish("user/alice/inbox", &ctx));
    }

    #[test]
    fn userid_denies_anonymous() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "user/alice/#".to_string(),
            subscribe_policy: Policy::UserId("alice".to_string()),
            publish_policy: Policy::UserId("alice".to_string()),
        }]);

        assert!(!auth.check_subscribe("user/alice/inbox", &anonymous()));
    }

    // ── Default rules ───────────────────────────────────────────────

    #[test]
    fn default_system_subscribe_requires_auth() {
        let auth = TopicAuth::with_defaults();
        assert!(!auth.check_subscribe("system/deploy", &anonymous()));
        assert!(auth.check_subscribe("system/deploy", &authenticated_user("u")));
    }

    #[test]
    fn default_system_publish_internal_only() {
        let auth = TopicAuth::with_defaults();
        // Even authenticated users can't publish to system topics
        assert!(!auth.check_publish("system/deploy", &authenticated_user("u")));
        assert!(!auth.check_publish("system/health", &user_with_role("u", "admin")));
        // Only the special internal role can
        assert!(auth.check_publish(
            "system/deploy",
            &user_with_role("internal", "__system_internal__")
        ));
    }

    #[test]
    fn default_plugin_subscribe_public() {
        let auth = TopicAuth::with_defaults();
        assert!(auth.check_subscribe("plugin/analytics/events", &anonymous()));
    }

    #[test]
    fn default_plugin_publish_requires_auth() {
        let auth = TopicAuth::with_defaults();
        assert!(!auth.check_publish("plugin/analytics/events", &anonymous()));
        assert!(auth.check_publish("plugin/analytics/events", &authenticated_user("u")));
    }

    #[test]
    fn default_custom_subscribe_public() {
        let auth = TopicAuth::with_defaults();
        assert!(auth.check_subscribe("custom/chat", &anonymous()));
    }

    #[test]
    fn default_custom_publish_requires_auth() {
        let auth = TopicAuth::with_defaults();
        assert!(!auth.check_publish("custom/chat", &anonymous()));
        assert!(auth.check_publish("custom/chat", &authenticated_user("u")));
    }

    // ── Fallthrough to defaults ─────────────────────────────────────

    #[test]
    fn unknown_topic_uses_default_subscribe_public() {
        let auth = TopicAuth::with_defaults();
        assert!(auth.check_subscribe("unknown/topic", &anonymous()));
    }

    #[test]
    fn unknown_topic_uses_default_publish_authenticated() {
        let auth = TopicAuth::with_defaults();
        assert!(!auth.check_publish("unknown/topic", &anonymous()));
        assert!(auth.check_publish("unknown/topic", &authenticated_user("u")));
    }

    // ── First matching rule wins ────────────────────────────────────

    #[test]
    fn first_matching_rule_wins() {
        let auth = TopicAuth::with_rules(vec![
            AuthRule {
                pattern: "data/secret".to_string(),
                subscribe_policy: Policy::Role("admin".to_string()),
                publish_policy: Policy::Role("admin".to_string()),
            },
            AuthRule {
                pattern: "data/#".to_string(),
                subscribe_policy: Policy::Public,
                publish_policy: Policy::Public,
            },
        ]);

        // "data/secret" matches the first rule (admin only)
        assert!(!auth.check_subscribe("data/secret", &anonymous()));
        assert!(auth.check_subscribe("data/secret", &user_with_role("u", "admin")));

        // "data/other" matches the second rule (public)
        assert!(auth.check_subscribe("data/other", &anonymous()));
    }

    // ── Mixed subscribe/publish policies ────────────────────────────

    #[test]
    fn asymmetric_policies() {
        let auth = TopicAuth::with_rules(vec![AuthRule {
            pattern: "broadcast/#".to_string(),
            subscribe_policy: Policy::Public,
            publish_policy: Policy::Role("broadcaster".to_string()),
        }]);

        let viewer = anonymous();
        let broadcaster = user_with_role("alice", "broadcaster");

        // Anyone can subscribe
        assert!(auth.check_subscribe("broadcast/news", &viewer));
        assert!(auth.check_subscribe("broadcast/news", &broadcaster));

        // Only broadcasters can publish
        assert!(!auth.check_publish("broadcast/news", &viewer));
        assert!(auth.check_publish("broadcast/news", &broadcaster));
    }

    // ── add_rule / set_defaults ─────────────────────────────────────

    #[test]
    fn add_rule_extends_rules() {
        let mut auth = TopicAuth::new();
        auth.add_rule(AuthRule {
            pattern: "secret/#".to_string(),
            subscribe_policy: Policy::Authenticated,
            publish_policy: Policy::Authenticated,
        });

        assert!(!auth.check_subscribe("secret/data", &anonymous()));
        assert!(auth.check_subscribe("secret/data", &authenticated_user("u")));
    }

    #[test]
    fn set_default_subscribe_changes_fallthrough() {
        let mut auth = TopicAuth::new();
        auth.set_default_subscribe(Policy::Authenticated);

        assert!(!auth.check_subscribe("anything", &anonymous()));
        assert!(auth.check_subscribe("anything", &authenticated_user("u")));
    }

    #[test]
    fn set_default_publish_changes_fallthrough() {
        let mut auth = TopicAuth::new();
        auth.set_default_publish(Policy::Public);

        assert!(auth.check_publish("anything", &anonymous()));
    }

    // ── Policy serialization ────────────────────────────────────────

    #[test]
    fn policy_roundtrip() {
        let policies = vec![
            Policy::Public,
            Policy::Authenticated,
            Policy::Role("admin".to_string()),
            Policy::UserId("alice".to_string()),
        ];

        for p in policies {
            let json_str = serde_json::to_string(&p).unwrap();
            let deserialized: Policy = serde_json::from_str(&json_str).unwrap();
            assert_eq!(p, deserialized);
        }
    }

    #[test]
    fn auth_rule_roundtrip() {
        let rule = AuthRule {
            pattern: "test/#".to_string(),
            subscribe_policy: Policy::Public,
            publish_policy: Policy::Role("admin".to_string()),
        };
        let json_str = serde_json::to_string(&rule).unwrap();
        let deserialized: AuthRule = serde_json::from_str(&json_str).unwrap();
        assert_eq!(rule.pattern, deserialized.pattern);
    }

    // ── AuthContext constructors ─────────────────────────────────────

    #[test]
    fn anonymous_context() {
        let ctx = AuthContext::anonymous();
        assert!(ctx.user_id.is_none());
        assert!(ctx.roles.is_empty());
        assert!(!ctx.is_authenticated);
    }

    #[test]
    fn authenticated_context() {
        let ctx = AuthContext::authenticated("alice", vec!["admin".to_string()]);
        assert_eq!(ctx.user_id, Some("alice".to_string()));
        assert_eq!(ctx.roles, vec!["admin"]);
        assert!(ctx.is_authenticated);
    }
}
