use actix_web::http::StatusCode;
use actix_web::{HttpRequest, HttpResponse};
use chrono::Utc;
use sha256::digest;
use sqlx::postgres::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::Semaphore;
use tracing::{error, warn};
use uuid::Uuid;

use crate::AppState;
use crate::api::auth::extract_gateway_api_key;
use crate::api::response::{forbidden, service_unavailable, unauthorized};
use crate::data::api_keys::ApiKeySecretRecord;
use crate::data::api_keys::{
    AuthAttemptLogEntry, get_api_key_by_public_id, get_effective_api_key_enforcement,
    insert_auth_attempt_log, touch_api_key_last_used,
};
use crate::parser::query_builder::sanitize_identifier;
use crate::utils::logging_task_limiter::spawn_best_effort_logging_task;
use crate::utils::request_logging::RequestAuthLogContext;

static ENFORCEMENT_CACHE: OnceLock<Mutex<HashMap<String, (bool, Instant)>>> = OnceLock::new();
const ENFORCEMENT_CACHE_TTL: Duration = Duration::from_secs(2);

#[derive(Debug)]
pub struct GatewayAuthOutcome {
    pub request_id: String,
    pub log_context: RequestAuthLogContext,
    pub bound_client_name: Option<String>,
    pub response: Option<HttpResponse>,
    pub force_deferred_queue: bool,
    pub force_deferred_reason: Option<String>,
}

#[derive(Debug, Clone)]
struct PresentedApiKey {
    public_id: Option<String>,
    secret: Option<String>,
    fingerprint_hash: String,
    fingerprint_salt: String,
}

pub fn read_right_for_resource(resource: Option<&str>) -> String {
    resource_right(resource, "read", "gateway.read")
}

pub fn write_right_for_resource(resource: Option<&str>) -> String {
    resource_right(resource, "write", "gateway.write")
}

pub fn delete_right_for_resource(resource: Option<&str>) -> String {
    resource_right(resource, "delete", "gateway.delete")
}

pub fn query_right() -> String {
    "gateway.query".to_string()
}

pub fn rpc_right() -> String {
    "gateway.rpc.execute".to_string()
}

fn resource_right(resource: Option<&str>, action: &str, fallback: &str) -> String {
    resource
        .and_then(|value| {
            if value.contains('.') {
                return None;
            }
            sanitize_identifier(value).map(|sanitized| sanitized.trim_matches('"').to_string())
        })
        .map(|value| format!("{}.{}", value, action))
        .unwrap_or_else(|| fallback.to_string())
}

fn split_right(right: &str) -> Option<(&str, &str)> {
    right.split_once('.')
}

fn right_matches(granted: &str, required: &str) -> bool {
    if granted == "*" || granted == required {
        return true;
    }

    let Some((granted_resource, granted_action)) = split_right(granted) else {
        return false;
    };
    let Some((required_resource, required_action)) = split_right(required) else {
        return false;
    };

    if granted_resource == "*" && granted_action == required_action {
        return true;
    }
    if granted_resource == required_resource && granted_action == "*" {
        return true;
    }
    if granted_resource == "gateway" && granted_action == required_action {
        return true;
    }
    if granted_resource == "gateway" && granted_action == "*" {
        return true;
    }

    false
}

fn missing_required_rights(granted_rights: &[String], required_rights: &[String]) -> Vec<String> {
    required_rights
        .iter()
        .filter(|required| {
            !granted_rights
                .iter()
                .any(|granted| right_matches(granted, required))
        })
        .cloned()
        .collect()
}

fn parse_presented_api_key(raw_value: &str) -> PresentedApiKey {
    let fingerprint_salt: String =
        format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
    let fingerprint_hash: String = digest(format!("{}:{}", fingerprint_salt, raw_value));

    let (public_id, secret) = raw_value
        .strip_prefix("ath_")
        .and_then(|value| value.split_once('.'))
        .map(|(public_id, secret)| (Some(public_id.to_string()), Some(secret.to_string())))
        .unwrap_or((None, None));

    PresentedApiKey {
        public_id,
        secret,
        fingerprint_hash,
        fingerprint_salt,
    }
}

fn current_epoch_seconds() -> i64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_else(|_| Duration::from_secs(0))
        .as_secs() as i64
}

fn remote_addr(req: &HttpRequest) -> Option<String> {
    req.headers()
        .get("X-Real-IP")
        .and_then(|value| value.to_str().ok())
        .map(str::to_string)
        .or_else(|| req.peer_addr().map(|addr| addr.ip().to_string()))
}

fn user_agent(req: &HttpRequest) -> Option<String> {
    req.headers()
        .get("User-Agent")
        .and_then(|value| value.to_str().ok())
        .map(str::to_string)
}

fn auth_store_pool(state: &AppState) -> Option<PgPool> {
    state
        .gateway_auth_client_name
        .as_ref()
        .and_then(|name| state.pg_registry.get_pool(name))
}

async fn get_effective_api_key_enforcement_cached(
    pool: &PgPool,
    client_name: Option<&str>,
) -> Result<bool, sqlx::Error> {
    let key: String = format!("{}", client_name.unwrap_or_default());
    let cache = ENFORCEMENT_CACHE.get_or_init(|| Mutex::new(HashMap::new()));

    if let Ok(guard) = cache.lock()
        && let Some((value, inserted_at)) = guard.get(&key)
        && inserted_at.elapsed() < ENFORCEMENT_CACHE_TTL
    {
        return Ok(*value);
    }

    match get_effective_api_key_enforcement(pool, client_name).await {
        Ok(value) => {
            if let Ok(mut guard) = cache.lock() {
                guard.insert(key, (value, Instant::now()));
            }
            Ok(value)
        }
        Err(err) => {
            // Fail-soft: if we have any cached value (even stale), use it to avoid
            // turning transient pool pressure into request-path auth failures.
            if let Ok(guard) = cache.lock()
                && let Some((stale_value, _)) = guard.get(&key)
            {
                warn!(
                    error = %err,
                    client = client_name.unwrap_or_default(),
                    "using stale effective api key enforcement policy due to lookup failure"
                );
                return Ok(*stale_value);
            }
            Err(err)
        }
    }
}

fn is_gateway_deferred_route(req: &HttpRequest) -> bool {
    matches!(
        req.path(),
        "/gateway/query"
            | "/gateway/data"
            | "/gateway/fetch"
            | "/gateway/update"
            | "/gateway/insert"
            | "/gateway/delete"
    )
}

#[allow(clippy::too_many_arguments)]
fn build_log_context(
    request_id: String,
    presented: Option<&PresentedApiKey>,
    authenticated: bool,
    authorized: bool,
    enforced: bool,
    api_key_id: Option<String>,
    internal_api_key_id: Option<i64>,
    api_key_public_id: Option<String>,
    reason: impl Into<String>,
) -> GatewayAuthOutcome {
    GatewayAuthOutcome {
        request_id,
        log_context: RequestAuthLogContext {
            api_key_id,
            internal_api_key_id,
            presented_api_key_public_id: api_key_public_id
                .or_else(|| presented.and_then(|value| value.public_id.clone())),
            presented_api_key_hash: presented.map(|value| value.fingerprint_hash.clone()),
            presented_api_key_salt: presented.map(|value| value.fingerprint_salt.clone()),
            api_key_authenticated: authenticated,
            api_key_authorized: authorized,
            api_key_enforced: enforced,
            api_key_auth_reason: Some(reason.into()),
        },
        bound_client_name: None,
        response: None,
        force_deferred_queue: false,
        force_deferred_reason: None,
    }
}

fn rejection_response(
    status: StatusCode,
    message: impl Into<String>,
    details: impl Into<String>,
) -> HttpResponse {
    match status {
        StatusCode::FORBIDDEN => forbidden(message, details),
        StatusCode::SERVICE_UNAVAILABLE => service_unavailable(message, details),
        _ => unauthorized(message, details),
    }
}

fn spawn_auth_attempt_log(
    logging_task_limiter: Option<Arc<Semaphore>>,
    pool: Option<PgPool>,
    req: &HttpRequest,
    request_id: &str,
    client_name: Option<&str>,
    required_rights: &[String],
    granted_rights: &[String],
    log_context: &RequestAuthLogContext,
) {
    let Some(pool) = pool else {
        return;
    };

    let entry: AuthAttemptLogEntry = AuthAttemptLogEntry {
        request_id: request_id.to_string(),
        api_key_id: log_context.api_key_id.clone(),
        internal_api_key_id: log_context.internal_api_key_id,
        api_key_public_id: log_context.presented_api_key_public_id.clone(),
        client_name: client_name.map(str::to_string),
        method: req.method().as_str().to_string(),
        path: req.path().to_string(),
        presented_key_hash: log_context.presented_api_key_hash.clone(),
        presented_key_salt: log_context.presented_api_key_salt.clone(),
        required_rights: required_rights.to_vec(),
        granted_rights: granted_rights.to_vec(),
        authenticated: log_context.api_key_authenticated,
        authorized: log_context.api_key_authorized,
        enforced: log_context.api_key_enforced,
        failure_reason: log_context.api_key_auth_reason.clone(),
        remote_addr: remote_addr(req),
        user_agent: user_agent(req),
        time: current_epoch_seconds(),
    };

    spawn_best_effort_logging_task(logging_task_limiter, "api_key_auth_log", async move {
        if let Err(err) = insert_auth_attempt_log(&pool, entry).await {
            error!(error = %err, "failed to write api key auth log");
        }
    });
}

fn spawn_last_used_touch(
    logging_task_limiter: Option<Arc<Semaphore>>,
    pool: Option<PgPool>,
    api_key_id: Option<String>,
) {
    let (Some(pool), Some(api_key_id)) = (pool, api_key_id) else {
        return;
    };

    let Ok(api_key_id) = Uuid::parse_str(&api_key_id) else {
        return;
    };

    spawn_best_effort_logging_task(
        logging_task_limiter,
        "api_key_last_used_touch",
        async move {
            if let Err(err) = touch_api_key_last_used(&pool, api_key_id).await {
                error!(error = %err, "failed to update api key last_used_at");
            }
        },
    );
}

pub async fn authorize_gateway_request(
    req: &HttpRequest,
    app_state: &AppState,
    client_name: Option<&str>,
    required_rights: Vec<String>,
) -> GatewayAuthOutcome {
    let request_id: String = Uuid::new_v4().to_string();
    let presented: Option<PresentedApiKey> =
        extract_gateway_api_key(req).map(|value| parse_presented_api_key(&value));
    let auth_pool: Option<sqlx::Pool<sqlx::Postgres>> = auth_store_pool(app_state);
    let logging_task_limiter = app_state.logging_task_limiter.clone();
    let auth_store_configured: bool = app_state.gateway_auth_client_name.is_some();
    let fail_open: bool = app_state
        .gateway_api_key_fail_mode
        .eq_ignore_ascii_case("fail_open");

    let Some(pool) = auth_pool.clone() else {
        let reason: &str = if auth_store_configured {
            "api_key_store_unavailable"
        } else {
            "api_key_store_not_configured"
        };
        if auth_store_configured && fail_open {
            warn!(
                "gateway auth store is configured but unavailable; allowing request without API key enforcement"
            );
        }

        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            presented.as_ref(),
            false,
            fail_open,
            !fail_open,
            None,
            None,
            None,
            if auth_store_configured && fail_open {
                "api_key_store_unavailable_fail_open"
            } else {
                reason
            },
        );
        if auth_store_configured && fail_open && is_gateway_deferred_route(req) {
            outcome.force_deferred_queue = true;
            outcome.force_deferred_reason = Some("api_key_store_unavailable".to_string());
        }
        if auth_store_configured && !fail_open {
            outcome.response = Some(HttpResponse::ServiceUnavailable().json(serde_json::json!({
                "status": "error",
                "code": "auth_store_unavailable",
                "message": "API key validation unavailable",
                "error": "Gateway auth store is configured but currently unreachable."
            })));
        }
        return outcome;
    };

    let enforced: bool = match get_effective_api_key_enforcement_cached(&pool, client_name).await {
        Ok(value) => value,
        Err(err) => {
            error!(error = %err, "failed to load effective api key enforcement");
            if fail_open {
                warn!(
                    error = %err,
                    "api key policy lookup failed; allowing request without API key enforcement (fail_open)"
                );
                let mut outcome: GatewayAuthOutcome = build_log_context(
                    request_id,
                    presented.as_ref(),
                    false,
                    true,
                    false,
                    None,
                    None,
                    None,
                    "api_key_policy_lookup_failed_fail_open",
                );
                if is_gateway_deferred_route(req) {
                    outcome.force_deferred_queue = true;
                    outcome.force_deferred_reason =
                        Some("api_key_policy_lookup_failed".to_string());
                }
                return outcome;
            }
            let mut outcome: GatewayAuthOutcome = build_log_context(
                request_id,
                presented.as_ref(),
                false,
                false,
                false,
                None,
                None,
                None,
                "api_key_policy_lookup_failed",
            );
            outcome.response = Some(rejection_response(
                StatusCode::SERVICE_UNAVAILABLE,
                "API key policy unavailable",
                "Gateway API key enforcement could not be evaluated.",
            ));
            return outcome;
        }
    };

    let required_rights: Vec<String> = if required_rights.is_empty() {
        vec!["gateway.access".to_string()]
    } else {
        required_rights
    };

    let Some(presented) = presented else {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            None,
            false,
            !enforced,
            enforced,
            None,
            None,
            None,
            if enforced {
                "missing_api_key"
            } else {
                "api_key_not_enforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::UNAUTHORIZED,
                "API key required",
                "Missing API key. Use X-Athena-Key.",
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &[],
            &outcome.log_context,
        );
        return outcome;
    };

    let Some(public_id) = presented.public_id.as_deref() else {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            false,
            !enforced,
            enforced,
            None,
            None,
            None,
            if enforced {
                "invalid_api_key_format"
            } else {
                "invalid_api_key_format_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::UNAUTHORIZED,
                "Invalid API key",
                "The API key format is invalid.",
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &[],
            &outcome.log_context,
        );
        return outcome;
    };

    let Some(secret) = presented.secret.as_deref() else {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            false,
            !enforced,
            enforced,
            None,
            None,
            Some(public_id.to_string()),
            if enforced {
                "invalid_api_key_format"
            } else {
                "invalid_api_key_format_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::UNAUTHORIZED,
                "Invalid API key",
                "The API key format is invalid.",
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &[],
            &outcome.log_context,
        );
        return outcome;
    };

    let record: Option<ApiKeySecretRecord> = match get_api_key_by_public_id(&pool, public_id).await
    {
        Ok(value) => value,
        Err(err) => {
            error!(error = %err, "failed to look up api key");
            if fail_open {
                warn!(
                    error = %err,
                    "api key lookup failed; allowing request without API key enforcement (fail_open)"
                );
                let mut outcome: GatewayAuthOutcome = build_log_context(
                    request_id,
                    Some(&presented),
                    false,
                    true,
                    false,
                    None,
                    None,
                    Some(public_id.to_string()),
                    "api_key_lookup_failed_fail_open",
                );
                if is_gateway_deferred_route(req) {
                    outcome.force_deferred_queue = true;
                    outcome.force_deferred_reason = Some("api_key_lookup_failed".to_string());
                }
                spawn_auth_attempt_log(
                    logging_task_limiter.clone(),
                    Some(pool),
                    req,
                    &outcome.request_id,
                    client_name,
                    &required_rights,
                    &[],
                    &outcome.log_context,
                );
                return outcome;
            }
            let mut outcome: GatewayAuthOutcome = build_log_context(
                request_id,
                Some(&presented),
                false,
                false,
                enforced,
                None,
                None,
                Some(public_id.to_string()),
                "api_key_lookup_failed",
            );
            outcome.response = Some(rejection_response(
                StatusCode::SERVICE_UNAVAILABLE,
                "API key validation unavailable",
                "The API key store could not validate the presented key.",
            ));
            return outcome;
        }
    };

    let Some(record) = record else {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            false,
            !enforced,
            enforced,
            None,
            None,
            Some(public_id.to_string()),
            if enforced {
                "unknown_api_key"
            } else {
                "unknown_api_key_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::UNAUTHORIZED,
                "Invalid API key",
                "No API key matched the provided identifier.",
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &[],
            &outcome.log_context,
        );
        return outcome;
    };

    let expected_hash: String = digest(format!("{}:{}", record.key_salt, secret));
    if expected_hash != record.key_hash {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            false,
            !enforced,
            enforced,
            Some(record.record.id.clone()),
            Some(record.internal_id),
            Some(record.record.public_id.clone()),
            if enforced {
                "invalid_api_key_secret"
            } else {
                "invalid_api_key_secret_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::UNAUTHORIZED,
                "Invalid API key",
                "The provided API key secret does not match.",
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &record.record.rights,
            &outcome.log_context,
        );
        return outcome;
    }

    if !record.record.is_active {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            false,
            !enforced,
            enforced,
            Some(record.record.id.clone()),
            Some(record.internal_id),
            Some(record.record.public_id.clone()),
            if enforced {
                "inactive_api_key"
            } else {
                "inactive_api_key_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::UNAUTHORIZED,
                "Inactive API key",
                "The API key has been disabled.",
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &record.record.rights,
            &outcome.log_context,
        );
        return outcome;
    }

    if let Some(expires_at) = record.record.expires_at
        && expires_at < Utc::now()
    {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            false,
            !enforced,
            enforced,
            Some(record.record.id.clone()),
            Some(record.internal_id),
            Some(record.record.public_id.clone()),
            if enforced {
                "expired_api_key"
            } else {
                "expired_api_key_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::UNAUTHORIZED,
                "Expired API key",
                "The API key has expired.",
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &record.record.rights,
            &outcome.log_context,
        );
        return outcome;
    }

    if let Some(bound_client_name) = record.record.client_name.as_deref()
        && client_name.filter(|value| !value.is_empty()) != Some(bound_client_name)
    {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            true,
            !enforced,
            enforced,
            Some(record.record.id.clone()),
            Some(record.internal_id),
            Some(record.record.public_id.clone()),
            if enforced {
                "api_key_client_mismatch"
            } else {
                "api_key_client_mismatch_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::FORBIDDEN,
                "API key client mismatch",
                format!(
                    "The API key is scoped to client '{}', not '{}'.",
                    bound_client_name,
                    client_name.unwrap_or_default()
                ),
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &record.record.rights,
            &outcome.log_context,
        );
        return outcome;
    }

    let missing_rights: Vec<String> =
        missing_required_rights(&record.record.rights, &required_rights);
    if !missing_rights.is_empty() {
        let mut outcome: GatewayAuthOutcome = build_log_context(
            request_id,
            Some(&presented),
            true,
            !enforced,
            enforced,
            Some(record.record.id.clone()),
            Some(record.internal_id),
            Some(record.record.public_id.clone()),
            if enforced {
                "missing_api_key_rights"
            } else {
                "missing_api_key_rights_unenforced"
            },
        );
        if enforced {
            outcome.response = Some(rejection_response(
                StatusCode::FORBIDDEN,
                "Insufficient API key rights",
                format!("Missing required rights: {}", missing_rights.join(", ")),
            ));
        }
        spawn_auth_attempt_log(
            logging_task_limiter.clone(),
            Some(pool.clone()),
            req,
            &outcome.request_id,
            client_name,
            &required_rights,
            &record.record.rights,
            &outcome.log_context,
        );
        spawn_last_used_touch(
            logging_task_limiter.clone(),
            Some(pool),
            Some(record.record.id.clone()),
        );
        return outcome;
    }

    let mut outcome: GatewayAuthOutcome = build_log_context(
        request_id,
        Some(&presented),
        true,
        true,
        enforced,
        Some(record.record.id.clone()),
        Some(record.internal_id),
        Some(record.record.public_id.clone()),
        if enforced {
            "api_key_authenticated"
        } else {
            "api_key_authenticated_unenforced"
        },
    );
    outcome.bound_client_name = record.record.client_name.clone();
    spawn_auth_attempt_log(
        logging_task_limiter.clone(),
        Some(pool.clone()),
        req,
        &outcome.request_id,
        client_name,
        &required_rights,
        &record.record.rights,
        &outcome.log_context,
    );
    spawn_last_used_touch(logging_task_limiter, Some(pool), Some(record.record.id));
    outcome
}

#[cfg(test)]
mod tests {
    use super::{
        delete_right_for_resource, parse_presented_api_key, query_right, read_right_for_resource,
        right_matches, rpc_right, write_right_for_resource,
    };
    use crate::api::gateway::auth::PresentedApiKey;

    #[test]
    fn api_key_parser_extracts_identifier_and_secret() {
        let parsed: PresentedApiKey = parse_presented_api_key("ath_public123.secret456");
        assert_eq!(parsed.public_id.as_deref(), Some("public123"));
        assert_eq!(parsed.secret.as_deref(), Some("secret456"));
        assert!(!parsed.fingerprint_hash.is_empty());
        assert!(!parsed.fingerprint_salt.is_empty());
    }

    #[test]
    fn wildcard_rights_match_expected_shapes() {
        assert!(right_matches("users.read", "users.read"));
        assert!(right_matches("users.*", "users.read"));
        assert!(right_matches("*.read", "users.read"));
        assert!(right_matches("gateway.read", "users.read"));
        assert!(right_matches("gateway.*", "users.delete"));
        assert!(right_matches("*", "users.read"));
        assert!(!right_matches("users.write", "users.read"));
        assert!(!right_matches("tickets.read", "users.read"));
    }

    #[test]
    fn resource_right_helpers_fall_back_to_gateway_scopes() {
        assert_eq!(read_right_for_resource(Some("users")), "users.read");
        assert_eq!(write_right_for_resource(Some("users")), "users.write");
        assert_eq!(delete_right_for_resource(Some("users")), "users.delete");
        assert_eq!(
            read_right_for_resource(Some("public.users")),
            "gateway.read"
        );
        assert_eq!(query_right(), "gateway.query");
        assert_eq!(rpc_right(), "gateway.rpc.execute");
    }
}
