use super::*;
use crate::plugins::api_key_shared::build_stored_api_key_metadata;
use crate::plugins::test_helpers;
use async_trait::async_trait;
use athena_auth_core::adapters::{
    AccountOps, ApiKeyOps, MemoryDatabaseAdapter, SessionOps, UserOps, VerificationOps,
};
use athena_auth_core::entity::{AuthAccount, AuthApiKey, AuthSession};
use athena_auth_core::{AuthPlugin, AuthResult, HttpMethod};
use athena_auth_core::{
    CreateAdminAuditLog, CreateApiKey, CreateSentEmail, CreateSession, CreateUser, EmailProvider,
    GrantAssignment, GrantDescriptor, Session, User,
};
use base64::Engine;
use chrono::{Duration, Utc};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

type SentEmailRecord = (String, String, String, String, Vec<String>);
type SentEmailSink = Arc<Mutex<Vec<SentEmailRecord>>>;

struct RecordingEmailProvider {
    sent: SentEmailSink,
}

#[async_trait]
impl EmailProvider for RecordingEmailProvider {
    async fn send(&self, to: &str, subject: &str, html: &str, text: &str) -> AuthResult<()> {
        self.sent.lock().unwrap().push((
            to.to_string(),
            subject.to_string(),
            html.to_string(),
            text.to_string(),
            Vec::new(),
        ));
        Ok(())
    }

    async fn send_message(&self, message: &athena_auth_core::EmailMessage) -> AuthResult<()> {
        self.sent.lock().unwrap().push((
            message.to.clone(),
            message.subject.clone(),
            message.html.clone(),
            message.text.clone(),
            message
                .attachments
                .iter()
                .map(|attachment| attachment.filename.clone())
                .collect(),
        ));
        Ok(())
    }
}

async fn create_admin_context() -> (
    AuthContext<MemoryDatabaseAdapter>,
    User,
    Session,
    User,
    Session,
) {
    let ctx = test_helpers::create_test_context();

    // Create admin user
    let admin = test_helpers::create_user(
        &ctx,
        CreateUser::new()
            .with_email("admin@example.com")
            .with_name("Admin")
            .with_role("admin"),
    )
    .await;
    let admin_session =
        test_helpers::create_session(&ctx, admin.id.clone(), Duration::hours(24)).await;

    // Create regular user
    let user = test_helpers::create_user(
        &ctx,
        CreateUser::new()
            .with_email("user@example.com")
            .with_name("Regular User")
            .with_role("user"),
    )
    .await;
    let user_session =
        test_helpers::create_session(&ctx, user.id.clone(), Duration::hours(24)).await;

    (ctx, admin, admin_session, user, user_session)
}

async fn create_admin_context_with_config(
    config: athena_auth_core::AuthConfig,
) -> (
    AuthContext<MemoryDatabaseAdapter>,
    User,
    Session,
    User,
    Session,
) {
    let ctx = test_helpers::create_test_context_with_config(config);

    let admin = test_helpers::create_user(
        &ctx,
        CreateUser::new()
            .with_email("admin@example.com")
            .with_name("Admin")
            .with_role("admin"),
    )
    .await;
    let admin_session =
        test_helpers::create_session(&ctx, admin.id.clone(), Duration::hours(24)).await;

    let user = test_helpers::create_user(
        &ctx,
        CreateUser::new()
            .with_email("user@example.com")
            .with_name("Regular User")
            .with_role("user"),
    )
    .await;
    let user_session =
        test_helpers::create_session(&ctx, user.id.clone(), Duration::hours(24)).await;

    (ctx, admin, admin_session, user, user_session)
}

fn make_request(
    method: HttpMethod,
    path: &str,
    token: &str,
    body: Option<serde_json::Value>,
) -> AuthRequest {
    test_helpers::create_auth_json_request_no_query(method, path, Some(token), body)
}

fn make_request_with_query(
    method: HttpMethod,
    path: &str,
    token: &str,
    body: Option<serde_json::Value>,
    query: HashMap<String, String>,
) -> AuthRequest {
    test_helpers::create_auth_json_request(method, path, Some(token), body, query)
}

fn json_body(resp: &AuthResponse) -> serde_json::Value {
    serde_json::from_slice(&resp.body).unwrap()
}

fn hash_api_key_for_test(key: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(key.as_bytes());
    let digest = hasher.finalize();
    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest)
}

async fn latest_audit_log(
    ctx: &AuthContext<MemoryDatabaseAdapter>,
) -> athena_auth_core::AdminAuditLog {
    ctx.database
        .list_admin_audit_logs(1, 0)
        .await
        .unwrap()
        .into_iter()
        .next()
        .expect("expected an admin audit log")
}

async fn create_admin_api_key_with_grants(
    ctx: &AuthContext<MemoryDatabaseAdapter>,
    admin: &User,
    raw_key: &str,
    grant_keys: Vec<String>,
) {
    let metadata = build_stored_api_key_metadata(None, None, Some(&grant_keys), None, None);
    ctx.database
        .create_api_key(CreateApiKey {
            user_id: admin.id.clone(),
            name: Some("admin audit test key".to_string()),
            prefix: None,
            key_hash: hash_api_key_for_test(raw_key),
            start: Some(raw_key.chars().take(8).collect()),
            expires_at: None,
            remaining: None,
            rate_limit_enabled: false,
            rate_limit_time_window: None,
            rate_limit_max: None,
            refill_interval: None,
            refill_amount: None,
            permissions: None,
            metadata,
            enabled: true,
        })
        .await
        .unwrap();
}

// -----------------------------------------------------------------------
// Basic auth / access control
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_set_role() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-role",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "role": "moderator"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["role"], "moderator");
}

#[tokio::test]
async fn test_non_admin_rejected() {
    let (ctx, _admin, _admin_session, _user, user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-role",
        &user_session.token,
        Some(serde_json::json!({
            "userId": "someone",
            "role": "admin"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());

    let logs = ctx.database.list_admin_audit_logs(10, 0).await.unwrap();
    let last = logs.first().expect("expected an admin audit log");
    assert_eq!(last.action, "admin.set_role");
    assert!(!last.success);
    assert_eq!(last.actor_user_id.as_deref(), Some(user_session.user_id()));
    assert!(last.error_message.is_some());
}

#[tokio::test]
async fn test_admin_audit_logs_capture_trace_and_api_key_rights() {
    let (ctx, admin, _admin_session, _user, _user_session) = create_admin_context().await;
    let plugin =
        AdminPlugin::new().with_native_right_catalog(athena_rights::native_right_catalog());
    let raw_key = "admin-audit-test-key";
    create_admin_api_key_with_grants(&ctx, &admin, raw_key, vec!["payment-link.paid".to_string()])
        .await;

    let req = AuthRequest::from_parts(
        HttpMethod::Get,
        "/admin/audit-log/list".to_string(),
        HashMap::from([
            ("x-api-key".to_string(), raw_key.to_string()),
            ("x-request-id".to_string(), "trace-admin-audit".to_string()),
        ]),
        None,
        HashMap::new(),
    );

    plugin
        .write_audit(
            &req,
            &ctx,
            Some(admin.id.as_str()),
            "admin.audit_log.list",
            Some("audit_log"),
            Some("latest"),
            true,
            None,
            Some(serde_json::json!({ "test": true })),
        )
        .await;

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.trace_id.as_deref(), Some("trace-admin-audit"));
    assert_eq!(log.api_key.as_deref(), Some(raw_key));
    assert_eq!(
        log.requested_rights,
        vec!["admin.audit_log.list".to_string()]
    );
    assert!(
        log.granted_rights
            .contains(&"payment-link.paid".to_string()),
        "expected projected API-key rights to include payment-link.paid, got {:?}",
        log.granted_rights
    );
}

#[tokio::test]
async fn test_unauthenticated_rejected() {
    let (ctx, _admin, _admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-role",
        "invalid-token",
        Some(serde_json::json!({
            "userId": user.id,
            "role": "admin"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());

    let logs = ctx.database.list_admin_audit_logs(10, 0).await.unwrap();
    let last = logs.first().expect("expected an admin audit log");
    assert_eq!(last.action, "admin.set_role");
    assert!(!last.success);
    assert_eq!(last.actor_user_id, None);
    assert!(last.error_message.is_some());
}

#[tokio::test]
async fn test_core_failure_is_audited_by_middleware() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-role",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": "nonexistent-user-id",
            "role": "admin"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());

    let logs = ctx.database.list_admin_audit_logs(10, 0).await.unwrap();
    let last = logs.first().expect("expected an admin audit log");
    assert_eq!(last.action, "admin.set_role");
    assert!(!last.success);
    assert_eq!(last.actor_user_id.as_deref(), Some(admin.id.as_str()));
    assert!(last.error_message.is_some());
    assert_eq!(last.metadata["source"], "admin.on_request");
}

#[tokio::test]
async fn test_http_error_response_is_audited_by_middleware() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "broken@example.com"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert!(resp.status >= 400);

    let logs = ctx.database.list_admin_audit_logs(10, 0).await.unwrap();
    let last = logs.first().expect("expected an admin audit log");
    assert_eq!(last.action, "admin.create_user");
    assert!(!last.success);
    assert_eq!(last.actor_user_id.as_deref(), Some(admin.id.as_str()));
    assert_eq!(last.metadata["source"], "admin.on_request");
    assert_eq!(last.metadata["status"], serde_json::json!(resp.status));
}

#[tokio::test]
async fn test_admin_audit_metadata_includes_tenant_header_when_present() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let mut req = make_request(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session.token,
        None,
    );
    req.headers
        .insert("X-Athena-Tenant".to_string(), "tenant-alpha".to_string());

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.list");
    assert!(log.success);
    assert_eq!(log.metadata["source"], "admin.on_request");
    assert_eq!(log.metadata["path"], "/admin/email/list");
    assert_eq!(log.metadata["tenant"], "tenant-alpha");
}

#[tokio::test]
async fn test_admin_audit_metadata_source_path_method_cannot_be_overridden() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "customer@example.com",
            "subject": "Welcome",
            "fromAddress": "noreply@example.com",
            "provider": "smtp-primary",
            "metadata": {
                "source": "spoofed",
                "path": "/evil",
                "method": "Delete",
                "traceId": "trace-123"
            }
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.create");
    assert!(log.success);
    assert_eq!(log.metadata["source"], "admin.on_request");
    assert_eq!(log.metadata["path"], "/admin/email/create");
    assert_eq!(log.metadata["method"], "Post");
    assert_eq!(log.metadata["traceId"], "trace-123");
}

#[tokio::test]
async fn test_custom_admin_role() {
    let config = Arc::new(athena_auth_core::AuthConfig::new(
        "test-secret-key-at-least-32-chars-long",
    ));
    let database = Arc::new(MemoryDatabaseAdapter::new());
    let ctx = AuthContext::new(config, database.clone());

    // Create superadmin user with custom role
    let admin = database
        .create_user(
            CreateUser::new()
                .with_email("superadmin@example.com")
                .with_name("Super Admin")
                .with_role("superadmin"),
        )
        .await
        .unwrap();

    let admin_session = database
        .create_session(CreateSession {
            user_id: admin.id.clone(),
            expires_at: Utc::now() + Duration::hours(24),
            ip_address: None,
            user_agent: None,
            impersonated_by: None,
            active_organization_id: None,
        })
        .await
        .unwrap();

    let user = database
        .create_user(
            CreateUser::new()
                .with_email("user@example.com")
                .with_name("User")
                .with_role("user"),
        )
        .await
        .unwrap();

    let plugin = AdminPlugin::new().admin_role("superadmin");

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-role",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "role": "moderator"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["role"], "moderator");
}

#[tokio::test]
async fn test_list_grant_catalog() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new()
        .with_grant_catalog(vec![
            GrantDescriptor {
                key: "invoice.paid".to_string(),
                resource: "invoice".to_string(),
                action: "paid".to_string(),
                description: "Grant access when an invoice has been paid.".to_string(),
            },
            GrantDescriptor {
                key: "custom.access".to_string(),
                resource: "custom".to_string(),
                action: "access".to_string(),
                description: "Custom access outside the shared native-right catalog.".to_string(),
            },
        ])
        .with_native_right_catalog(vec![athena_rights::AthenaRightDescriptor {
            key: "gateway.query".to_string(),
            resource: "gateway".to_string(),
            action: "query".to_string(),
            description: "Execute the structured Athena gateway query surface.".to_string(),
            source: "gateway".to_string(),
            kind: "capability".to_string(),
            is_pattern: false,
        }])
        .with_native_right_domain_contracts(std::collections::BTreeMap::from([(
            "billing".to_string(),
            serde_json::to_value(athena_billing::contracts::billing_athena_auth_contract())
                .expect("billing contract should serialize"),
        )]));

    for path in ["/admin/grant/list", "/admin/rights/catalog"] {
        let req = make_request(HttpMethod::Get, path, &admin_session.token, None);
        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

        assert_eq!(resp.status, 200, "expected 200 for {path}");
        let body = json_body(&resp);
        assert!(
            body["grantCatalog"]
                .as_array()
                .unwrap()
                .iter()
                .any(|entry| {
                    entry["key"] == "invoice.paid"
                        && entry["resource"] == "invoice"
                        && entry["action"] == "paid"
                        && entry["nativeRight"]["source"] == "billing"
                })
        );
        assert!(
            body["rightsCatalog"]
                .as_array()
                .unwrap()
                .iter()
                .any(|entry| entry["key"] == "invoice.paid")
        );
        assert!(
            body["nativeRightCatalog"]
                .as_array()
                .unwrap()
                .iter()
                .any(|entry| {
                    entry["nativeRight"]["key"] == "invoice.paid"
                        && entry["nativeRight"]["source"] == "billing"
                        && entry["grant"]["key"] == "invoice.paid"
                })
        );
        assert!(
            body["catalogModules"]
                .as_array()
                .unwrap()
                .iter()
                .any(|module| {
                    module["key"] == "billing"
                        && module["grantCatalog"]
                            .as_array()
                            .unwrap()
                            .iter()
                            .any(|entry| entry["key"] == "invoice.paid")
                        && module["integrationContract"]["preferred_sync_billing_endpoint"]
                            == "/admin/rights/sync-billing"
                })
        );
        assert!(
            body["catalogModules"]
                .as_array()
                .unwrap()
                .iter()
                .any(|module| {
                    module["key"] == "gateway"
                        && module["nativeRightCatalog"]
                            .as_array()
                            .unwrap()
                            .iter()
                            .any(|entry| entry["nativeRight"]["key"] == "gateway.query")
                        && module["nativeRights"]
                            .as_array()
                            .unwrap()
                            .iter()
                            .any(|entry| entry["key"] == "gateway.query")
                })
        );
        assert!(
            body["catalogModules"]
                .as_array()
                .unwrap()
                .iter()
                .any(|module| {
                    module["key"] == "custom"
                        && module["grantCatalog"]
                            .as_array()
                            .unwrap()
                            .iter()
                            .any(|entry| entry["key"] == "custom.access")
                })
        );
        assert!(
            body["nativeRights"]
                .as_array()
                .unwrap()
                .iter()
                .any(|entry| entry["key"] == "gateway.query" && entry["source"] == "gateway")
        );
        assert_eq!(
            body["integrationContracts"]["billing"]["rights_catalog_endpoint"],
            "/admin/rights/catalog"
        );
        assert_eq!(
            body["integrationContracts"]["billing"]["request_shape"]["rightModules"][0]["key"],
            "billing"
        );
    }
}

#[tokio::test]
async fn test_list_grant_assignments_resolves_subject_permissions() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    ctx.database.seed_grant_assignments(
        "user",
        user.id.clone(),
        vec![
            GrantAssignment {
                subject_kind: "user".to_string(),
                subject_id: user.id.clone(),
                grant_key: "invoice.paid".to_string(),
                source_kind: "invoice".to_string(),
                source_ref: "inv_123".to_string(),
                metadata: serde_json::json!({ "provider": "mollie" }),
            },
            GrantAssignment {
                subject_kind: "user".to_string(),
                subject_id: user.id.clone(),
                grant_key: "unknown.grant".to_string(),
                source_kind: "payment".to_string(),
                source_ref: "pay_123".to_string(),
                metadata: serde_json::json!({}),
            },
        ],
    );
    let plugin = AdminPlugin::new().with_grant_catalog(vec![GrantDescriptor {
        key: "invoice.paid".to_string(),
        resource: "invoice".to_string(),
        action: "paid".to_string(),
        description: "Grant access when an invoice has been paid.".to_string(),
    }]);

    let mut query = HashMap::new();
    query.insert("subjectKind".to_string(), "user".to_string());
    query.insert("subjectId".to_string(), user.id.clone());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/grant-assignment/list",
        &admin_session.token,
        None,
        query,
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["subjectKind"], "user");
    assert_eq!(body["subjectId"], user.id);
    assert_eq!(
        body["grantKeys"],
        serde_json::json!(["invoice.paid", "unknown.grant"])
    );
    assert_eq!(body["rightKeys"], serde_json::json!(["invoice.paid"]));
    assert_eq!(body["nativeRightModules"][0]["key"], "billing");
    assert_eq!(body["grantAssignments"].as_array().unwrap().len(), 2);
    assert_eq!(body["rightAssignments"].as_array().unwrap().len(), 2);
    assert_eq!(body["resolvedGrants"].as_array().unwrap().len(), 1);
    assert_eq!(body["resolvedRights"].as_array().unwrap().len(), 1);
    assert_eq!(body["resolvedNativeRights"].as_array().unwrap().len(), 1);
    assert_eq!(body["nativeRights"][0]["key"], "invoice.paid");
    assert_eq!(body["nativeRightAssignments"].as_array().unwrap().len(), 1);
    assert_eq!(body["resolvedGrants"][0]["grant"]["resource"], "invoice");
    assert_eq!(body["resolvedGrants"][0]["rightKey"], "invoice.paid");
    assert_eq!(
        body["resolvedGrants"][0]["nativeRight"]["source"],
        "billing"
    );
    assert_eq!(body["resolvedNativeRights"][0]["rightKey"], "invoice.paid");
    assert_eq!(
        body["nativeRightAssignments"][0]["rightKey"],
        "invoice.paid"
    );
    assert_eq!(body["nativeRightAssignments"][0]["sourceRef"], "inv_123");
    assert_eq!(body["permissions"]["invoice"][0], "paid");
    assert_eq!(body["unknownGrantKeys"][0], "unknown.grant");
    assert_eq!(body["unknownRightKeys"][0], "unknown.grant");
}

#[tokio::test]
async fn test_rights_assignment_list_alias_resolves_subject_permissions() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    ctx.database.seed_grant_assignments(
        "user",
        user.id.clone(),
        vec![GrantAssignment {
            subject_kind: "user".to_string(),
            subject_id: user.id.clone(),
            grant_key: "invoice.paid".to_string(),
            source_kind: "invoice".to_string(),
            source_ref: "inv_456".to_string(),
            metadata: serde_json::json!({ "provider": "mollie" }),
        }],
    );
    let plugin = AdminPlugin::new().with_grant_catalog(vec![GrantDescriptor {
        key: "invoice.paid".to_string(),
        resource: "invoice".to_string(),
        action: "paid".to_string(),
        description: "Grant access when an invoice has been paid.".to_string(),
    }]);

    let mut query = HashMap::new();
    query.insert("subjectKind".to_string(), "user".to_string());
    query.insert("subjectId".to_string(), user.id.clone());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/rights/assignment/list",
        &admin_session.token,
        None,
        query,
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["rightKeys"], serde_json::json!(["invoice.paid"]));
    assert_eq!(body["nativeRightModules"][0]["key"], "billing");
    assert_eq!(body["nativeRights"][0]["key"], "invoice.paid");
    assert_eq!(body["resolvedNativeRights"][0]["rightKey"], "invoice.paid");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.rights.assignment.list");
    assert!(log.success);
}

#[tokio::test]
async fn test_replace_grant_assignments_for_source_updates_subject_state() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    ctx.database.seed_grant_assignments(
        "user",
        user.id.clone(),
        vec![GrantAssignment {
            subject_kind: "user".to_string(),
            subject_id: user.id.clone(),
            grant_key: "payment.failed".to_string(),
            source_kind: "payment".to_string(),
            source_ref: "pay_123".to_string(),
            metadata: serde_json::json!({ "provider": "mollie", "attempt": 1 }),
        }],
    );
    let plugin = AdminPlugin::new().with_grant_catalog(vec![
        GrantDescriptor {
            key: "payment.failed".to_string(),
            resource: "payment".to_string(),
            action: "failed".to_string(),
            description: "Grant access when a payment has failed.".to_string(),
        },
        GrantDescriptor {
            key: "payment-link.paid".to_string(),
            resource: "payment-link".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment link has been paid.".to_string(),
        },
    ]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/grant-assignment/replace-source",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "payment",
            "sourceRef": "pay_123",
            "grantKeys": ["payment-link.paid", "payment-link.paid"],
            "metadata": {
                "provider": "mollie",
                "campaign": "spring"
            }
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["sourceKind"], "payment");
    assert_eq!(body["sourceRef"], "pay_123");
    assert_eq!(
        body["appliedGrantKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(
        body["appliedRightKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(body["appliedRightModules"][0]["key"], "billing");
    assert_eq!(
        body["appliedRightModules"][0]["rightKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(body["replacedAssignments"].as_array().unwrap().len(), 1);
    assert_eq!(
        body["replacedAssignments"][0]["grantKey"],
        "payment-link.paid"
    );
    assert_eq!(
        body["replacedNativeRightAssignments"][0]["rightKey"],
        "payment-link.paid"
    );
    assert_eq!(
        body["subjectState"]["permissions"]["payment-link"],
        serde_json::json!(["paid"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightAssignments"][0]["nativeRight"]["key"],
        "payment-link.paid"
    );
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(body["subjectState"]["rightModules"][0]["key"], "billing");
    assert_eq!(
        body["subjectState"]["rightModules"][0]["rightKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightModules"][0]["key"],
        "billing"
    );
    assert_eq!(
        body["subjectState"]["grantAssignments"][0]["metadata"]["campaign"],
        "spring"
    );
    assert_eq!(
        body["subjectState"]["rightAssignments"][0]["metadata"]["campaign"],
        "spring"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.grant_assignment.replace_source");
    assert!(log.success);
}

#[tokio::test]
async fn test_rights_assignment_replace_source_alias_updates_subject_state() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    ctx.database.seed_grant_assignments(
        "user",
        user.id.clone(),
        vec![GrantAssignment {
            subject_kind: "user".to_string(),
            subject_id: user.id.clone(),
            grant_key: "payment.failed".to_string(),
            source_kind: "payment".to_string(),
            source_ref: "pay_456".to_string(),
            metadata: serde_json::json!({ "provider": "mollie", "attempt": 1 }),
        }],
    );
    let plugin = AdminPlugin::new().with_grant_catalog(vec![
        GrantDescriptor {
            key: "payment.failed".to_string(),
            resource: "payment".to_string(),
            action: "failed".to_string(),
            description: "Grant access when a payment has failed.".to_string(),
        },
        GrantDescriptor {
            key: "payment-link.paid".to_string(),
            resource: "payment-link".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment link has been paid.".to_string(),
        },
    ]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/rights/assignment/replace-source",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "payment",
            "sourceRef": "pay_456",
            "rightKeys": ["payment-link.paid"],
            "metadata": {
                "provider": "mollie",
                "campaign": "summer"
            }
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["appliedRightKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(
        body["subjectState"]["rightAssignments"][0]["metadata"]["campaign"],
        "summer"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.rights.assignment.replace_source");
    assert!(log.success);
}

#[tokio::test]
async fn test_sync_billing_grant_assignments_uses_native_route_contract() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![
        GrantDescriptor {
            key: "payment.paid".to_string(),
            resource: "payment".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment has been paid.".to_string(),
        },
        GrantDescriptor {
            key: "payment-link.paid".to_string(),
            resource: "payment-link".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment link has been paid.".to_string(),
        },
    ]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/grant-assignment/sync-billing",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "payment",
            "sourceRef": "pay_777",
            "grantKeys": ["payment.paid", "payment-link.paid"],
            "metadata": {
                "provider": "mollie",
                "origin": "billing.webhook"
            }
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["sourceKind"], "payment");
    assert_eq!(body["sourceRef"], "pay_777");
    assert_eq!(
        body["subjectState"]["permissions"]["payment"],
        serde_json::json!(["paid"])
    );
    assert_eq!(
        body["subjectState"]["permissions"]["payment-link"],
        serde_json::json!(["paid"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightAssignments"]
            .as_array()
            .unwrap()
            .len(),
        2
    );
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["payment-link.paid", "payment.paid"])
    );
    assert_eq!(body["appliedRightModules"][0]["key"], "billing");
    assert_eq!(
        body["subjectState"]["rightModules"][0]["rightKeys"],
        serde_json::json!(["payment-link.paid", "payment.paid"])
    );
    assert_eq!(
        body["subjectState"]["grantAssignments"][0]["metadata"]["origin"],
        "billing.webhook"
    );
    assert_eq!(
        body["subjectState"]["rightAssignments"][0]["metadata"]["origin"],
        "billing.webhook"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.grant_assignment.sync_billing");
    assert!(log.success);
}

#[tokio::test]
async fn test_rights_assignment_sync_billing_alias_uses_native_route_contract() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![
        GrantDescriptor {
            key: "payment.paid".to_string(),
            resource: "payment".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment has been paid.".to_string(),
        },
        GrantDescriptor {
            key: "payment-link.paid".to_string(),
            resource: "payment-link".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment link has been paid.".to_string(),
        },
    ]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/rights/assignment/sync-billing",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "payment",
            "sourceRef": "pay_900",
            "rightKeys": ["payment.paid", "payment-link.paid"],
            "metadata": {
                "provider": "mollie",
                "origin": "billing.webhook"
            }
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["payment-link.paid", "payment.paid"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightAssignments"]
            .as_array()
            .unwrap()
            .len(),
        2
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.rights.assignment.sync_billing");
    assert!(log.success);
}

#[tokio::test]
async fn test_rights_sync_billing_alias_uses_native_route_contract() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![
        GrantDescriptor {
            key: "payment.paid".to_string(),
            resource: "payment".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment has been paid.".to_string(),
        },
        GrantDescriptor {
            key: "payment-link.paid".to_string(),
            resource: "payment-link".to_string(),
            action: "paid".to_string(),
            description: "Grant access when a payment link has been paid.".to_string(),
        },
    ]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/rights/sync-billing",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "payment",
            "sourceRef": "pay_901",
            "rightKeys": ["payment.paid", "payment-link.paid"],
            "metadata": {
                "provider": "mollie",
                "origin": "billing.webhook"
            }
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["payment-link.paid", "payment.paid"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightAssignments"]
            .as_array()
            .unwrap()
            .len(),
        2
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.rights.sync_billing");
    assert!(log.success);
}

#[tokio::test]
async fn test_sync_grant_assignments_uses_generic_native_route_contract() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![
        GrantDescriptor {
            key: "gateway.query".to_string(),
            resource: "gateway".to_string(),
            action: "query".to_string(),
            description: "Execute gateway queries.".to_string(),
        },
        GrantDescriptor {
            key: "gateway.read".to_string(),
            resource: "gateway".to_string(),
            action: "read".to_string(),
            description: "Read from the gateway.".to_string(),
        },
    ]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/grant-assignment/sync-source",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "manual",
            "sourceRef": "support-case-1",
            "grantKeys": ["gateway.query", "gateway.read"],
            "metadata": {
                "origin": "support",
                "reason": "manual override"
            }
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["sourceKind"], "manual");
    assert_eq!(body["sourceRef"], "support-case-1");
    assert_eq!(
        body["subjectState"]["permissions"]["gateway"],
        serde_json::json!(["query", "read"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightAssignments"][0]["nativeRight"]["source"],
        "gateway"
    );
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["gateway.query", "gateway.read"])
    );
    assert_eq!(
        body["subjectState"]["grantAssignments"][0]["metadata"]["reason"],
        "manual override"
    );
    assert_eq!(
        body["subjectState"]["rightAssignments"][0]["metadata"]["reason"],
        "manual override"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.grant_assignment.sync_source");
    assert!(log.success);
}

#[tokio::test]
async fn test_rights_assignment_sync_source_alias_uses_generic_native_route_contract() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![
        GrantDescriptor {
            key: "gateway.query".to_string(),
            resource: "gateway".to_string(),
            action: "query".to_string(),
            description: "Execute gateway queries.".to_string(),
        },
        GrantDescriptor {
            key: "gateway.read".to_string(),
            resource: "gateway".to_string(),
            action: "read".to_string(),
            description: "Read from the gateway.".to_string(),
        },
    ]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/rights/assignment/sync-source",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "manual",
            "sourceRef": "support-case-rights-assignment",
            "rightKeys": ["gateway.query", "gateway.read"],
            "metadata": {
                "origin": "support"
            }
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["gateway.query", "gateway.read"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightAssignments"][0]["nativeRight"]["source"],
        "gateway"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.rights.assignment.sync_source");
    assert!(log.success);
}

#[tokio::test]
async fn test_non_admin_path_returns_none() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/api/not-admin",
        &admin_session.token,
        None,
    );

    // Routes not matching /admin/* should return None (not handled by plugin)
    let result = plugin.on_request(&req, &ctx).await.unwrap();
    assert!(result.is_none());
}

// -----------------------------------------------------------------------
// Create user
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_create_user() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "new@example.com",
            "password": "securepassword123",
            "name": "New User"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["email"], "new@example.com");
    assert_eq!(body["user"]["name"], "New User");
    assert_eq!(body["user"]["role"], "user");
}

#[tokio::test]
async fn test_create_user_defaults_name_to_email_local_part() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "derived-name@example.com",
            "password": "securepassword123"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["name"], "derived-name");
}

#[tokio::test]
async fn test_create_user_with_custom_role() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "mod@example.com",
            "password": "securepassword123",
            "name": "Moderator",
            "role": "moderator"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["role"], "moderator");
}

#[tokio::test]
async fn test_create_user_creates_credential_account() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "new@example.com",
            "password": "securepassword123",
            "name": "New User"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    let user_id = body["user"]["id"].as_str().unwrap();

    // Verify a credential account was created
    let accounts = ctx.database.get_user_accounts(user_id).await.unwrap();
    assert_eq!(accounts.len(), 1);
    assert_eq!(accounts[0].provider_id(), "credential");
    assert!(accounts[0].password().is_some());
}

#[tokio::test]
async fn test_create_user_duplicate_email_rejected() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // user@example.com already exists in the context
    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "user@example.com",
            "password": "securepassword123",
            "name": "Duplicate"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_create_user_default_role_config() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().default_user_role("member");

    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "newmember@example.com",
            "password": "securepassword123",
            "name": "New Member"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["role"], "member");
}

// -----------------------------------------------------------------------
// List users
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_list_users() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Get,
        "/admin/list-users",
        &admin_session.token,
        None,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["total"], 2); // admin + regular user
    assert_eq!(body["users"].as_array().unwrap().len(), 2);
}

#[tokio::test]
async fn test_list_users_pagination() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let mut query = HashMap::new();
    query.insert("limit".to_string(), "1".to_string());
    query.insert("offset".to_string(), "0".to_string());

    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/list-users",
        &admin_session.token,
        None,
        query,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["total"], 2); // total is still 2
    assert_eq!(body["users"].as_array().unwrap().len(), 1); // but only 1 returned
    assert_eq!(body["limit"], 1);
    assert_eq!(body["offset"], 0);
}

#[tokio::test]
async fn test_list_users_respects_max_page_limit() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().max_page_limit(1);

    // Request limit=100 but max is 1
    let mut query = HashMap::new();
    query.insert("limit".to_string(), "100".to_string());

    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/list-users",
        &admin_session.token,
        None,
        query,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    // Should be clamped to max_page_limit=1
    assert_eq!(body["users"].as_array().unwrap().len(), 1);
    assert_eq!(body["limit"], 1);
}

// -----------------------------------------------------------------------
// List user sessions
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_list_user_sessions() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/list-user-sessions",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    let sessions = body["sessions"].as_array().unwrap();
    assert_eq!(sessions.len(), 1);
}

#[tokio::test]
async fn test_list_user_sessions_nonexistent_user() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/list-user-sessions",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": "nonexistent-id",
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

// -----------------------------------------------------------------------
// Ban / Unban
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_ban_unban_user() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // Ban user
    let req = make_request(
        HttpMethod::Post,
        "/admin/ban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "banReason": "spam"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["banned"], true);

    // Unban user
    let req = make_request(
        HttpMethod::Post,
        "/admin/unban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["banned"], false);
}

#[tokio::test]
async fn test_cannot_ban_self() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/ban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": admin.id,
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

/// Verifies the bug fix: unbanning clears ban_reason and ban_expires in the adapter.
#[tokio::test]
async fn test_unban_clears_ban_reason_and_expires() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // Ban with reason and expiry
    let req = make_request(
        HttpMethod::Post,
        "/admin/ban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "banReason": "spam",
            "banExpiresIn": 3600
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["banned"], true);
    assert_eq!(body["user"]["banReason"], "spam");
    assert!(!body["user"]["banExpires"].is_null());

    // Unban
    let req = make_request(
        HttpMethod::Post,
        "/admin/unban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["banned"], false);

    // Verify ban_reason and ban_expires are cleared by checking the DB directly
    let updated_user = ctx
        .database
        .get_user_by_id(&user.id)
        .await
        .unwrap()
        .unwrap();
    assert!(!updated_user.banned);
    assert!(updated_user.ban_reason.is_none());
    assert!(updated_user.ban_expires.is_none());
}

#[tokio::test]
async fn test_ban_with_expiry() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/ban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "banExpiresIn": 7200 // 2 hours
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["banned"], true);
    assert!(!body["user"]["banExpires"].is_null());
}

#[tokio::test]
async fn test_ban_revokes_user_sessions() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // Confirm user has sessions
    let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
    assert!(!sessions.is_empty());

    // Ban user
    let req = make_request(
        HttpMethod::Post,
        "/admin/ban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "banReason": "bad behavior"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    // After ban, sessions should be revoked
    let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
    assert!(sessions.is_empty());
}

#[tokio::test]
async fn test_cannot_ban_admin_by_default() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // Create second admin
    let admin2 = ctx
        .database
        .create_user(
            CreateUser::new()
                .with_email("admin2@example.com")
                .with_name("Admin 2")
                .with_role("admin"),
        )
        .await
        .unwrap();

    let req = make_request(
        HttpMethod::Post,
        "/admin/ban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": admin2.id,
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_allow_ban_admin_config() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().allow_ban_admin(true);

    // Create second admin
    let admin2 = ctx
        .database
        .create_user(
            CreateUser::new()
                .with_email("admin2@example.com")
                .with_name("Admin 2")
                .with_role("admin"),
        )
        .await
        .unwrap();

    let req = make_request(
        HttpMethod::Post,
        "/admin/ban-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": admin2.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["banned"], true);
}

// -----------------------------------------------------------------------
// Impersonation
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_impersonate_and_stop() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // Impersonate
    let req = make_request(
        HttpMethod::Post,
        "/admin/impersonate-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["user"]["email"], "user@example.com");
    assert!(body["session"]["token"].is_string());
}

/// Verifies the impersonation session has the impersonated_by field set.
#[tokio::test]
async fn test_impersonate_session_has_impersonated_by() {
    let (ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/impersonate-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    let imp_token = body["session"]["token"].as_str().unwrap();

    // Look up the impersonation session and check impersonated_by
    let imp_session = ctx.database.get_session(imp_token).await.unwrap().unwrap();
    assert_eq!(
        imp_session.impersonated_by().unwrap(),
        admin.id,
        "impersonated_by should be the admin's user id"
    );
}

/// Verifies the bug fix: stop-impersonating creates a new admin session.
#[tokio::test]
async fn test_stop_impersonating_creates_admin_session() {
    let (ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // Impersonate
    let req = make_request(
        HttpMethod::Post,
        "/admin/impersonate-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    let body = json_body(&resp);
    let imp_token = body["session"]["token"].as_str().unwrap().to_string();

    // Stop impersonating using the impersonation session token
    let req = make_request(
        HttpMethod::Post,
        "/admin/stop-impersonating",
        &imp_token,
        None,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);

    // Should return admin user and a new session
    assert_eq!(body["user"]["email"], "admin@example.com");
    assert!(body["session"]["token"].is_string());

    // The new session token should be for the admin
    let new_token = body["session"]["token"].as_str().unwrap();
    let new_session = ctx.database.get_session(new_token).await.unwrap().unwrap();
    assert_eq!(new_session.user_id, admin.id);
    assert!(
        new_session.impersonated_by.is_none(),
        "new admin session should not be an impersonation session"
    );

    // The response should include a Set-Cookie header
    assert!(resp.headers.contains_key("Set-Cookie"));

    // The old impersonation session should be deleted
    let old_session = ctx.database.get_session(&imp_token).await.unwrap();
    assert!(old_session.is_none());
}

#[tokio::test]
async fn test_stop_impersonating_non_impersonation_session_rejected() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // Try to stop impersonating with a normal admin session (not impersonation)
    let req = make_request(
        HttpMethod::Post,
        "/admin/stop-impersonating",
        &admin_session.token,
        None,
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_cannot_impersonate_self() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/impersonate-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": admin.id,
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_impersonate_nonexistent_user_rejected() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/impersonate-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": "nonexistent-user-id",
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_impersonate_response_has_set_cookie() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/impersonate-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    assert!(
        resp.headers.contains_key("Set-Cookie"),
        "impersonate response should set a session cookie"
    );
}

// -----------------------------------------------------------------------
// Session management
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_revoke_user_sessions() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/revoke-user-sessions",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    assert!(
        !resp.headers.contains_key("Set-Cookie"),
        "revoking another user's sessions must not clear admin cookie"
    );
    let body = json_body(&resp);
    assert_eq!(body["success"], true);

    // Verify sessions are actually deleted
    let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
    assert!(sessions.is_empty());
}

#[tokio::test]
async fn test_revoke_specific_session() {
    let (ctx, _admin, admin_session, _user, user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/revoke-user-session",
        &admin_session.token,
        Some(serde_json::json!({
            "sessionToken": user_session.token,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    assert!(
        !resp.headers.contains_key("Set-Cookie"),
        "revoking another user's session must not clear admin cookie"
    );
    let body = json_body(&resp);
    assert_eq!(body["success"], true);

    // Verify specific session is deleted
    let session = ctx.database.get_session(&user_session.token).await.unwrap();
    assert!(session.is_none());
}

#[tokio::test]
async fn test_revoke_specific_session_self_clears_cookie() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/revoke-user-session",
        &admin_session.token,
        Some(serde_json::json!({
            "sessionToken": admin_session.token,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    assert!(
        resp.headers.contains_key("Set-Cookie"),
        "admin self-revoke must clear the caller cookie"
    );

    let session = ctx
        .database
        .get_session(&admin_session.token)
        .await
        .unwrap();
    assert!(session.is_none());
}

#[tokio::test]
async fn test_revoke_user_sessions_self_clears_cookie() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/revoke-user-sessions",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": admin.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    assert!(
        resp.headers.contains_key("Set-Cookie"),
        "admin self-revoke-all must clear the caller cookie"
    );

    let sessions = ctx.database.get_user_sessions(&admin.id).await.unwrap();
    assert!(sessions.is_empty());
}

// -----------------------------------------------------------------------
// Remove user
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_remove_user() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/remove-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["success"], true);

    // Verify user is deleted
    let deleted = ctx.database.get_user_by_id(&user.id).await.unwrap();
    assert!(deleted.is_none());
}

#[tokio::test]
async fn test_cannot_remove_self() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/remove-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": admin.id,
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_remove_user_cleans_up_sessions_and_accounts() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // First create a user with an account
    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "tobedeleted@example.com",
            "password": "securepassword123",
            "name": "To Be Deleted"
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    let body = json_body(&resp);
    let user_id = body["user"]["id"].as_str().unwrap().to_string();

    // Verify user has an account
    let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
    assert_eq!(accounts.len(), 1);

    // Remove the user
    let req = make_request(
        HttpMethod::Post,
        "/admin/remove-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user_id,
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    // Verify user is deleted
    let deleted = ctx.database.get_user_by_id(&user_id).await.unwrap();
    assert!(deleted.is_none());

    // Verify accounts are cleaned up
    let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
    assert!(accounts.is_empty());
}

#[tokio::test]
async fn test_remove_nonexistent_user() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/remove-user",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": "nonexistent-user-id",
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

// -----------------------------------------------------------------------
// Set user password
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_set_user_password() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // First create a user with a credential account
    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "pwuser@example.com",
            "password": "oldpassword123",
            "name": "PW User"
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    let body = json_body(&resp);
    let user_id = body["user"]["id"].as_str().unwrap().to_string();

    // Set new password
    let req = make_request(
        HttpMethod::Post,
        "/admin/set-user-password",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user_id,
            "newPassword": "newpassword456"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["status"], true);
}

/// Verifies the bug fix: set-user-password also updates the credential account password.
#[tokio::test]
async fn test_set_user_password_updates_account() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    // First create a user with a credential account
    let req = make_request(
        HttpMethod::Post,
        "/admin/create-user",
        &admin_session.token,
        Some(serde_json::json!({
            "email": "pwuser@example.com",
            "password": "oldpassword123",
            "name": "PW User"
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    let body = json_body(&resp);
    let user_id = body["user"]["id"].as_str().unwrap().to_string();

    // Get the old password hash from the credential account
    let accounts_before = ctx.database.get_user_accounts(&user_id).await.unwrap();
    let old_password = accounts_before[0].password().unwrap().to_string();

    // Set new password
    let req = make_request(
        HttpMethod::Post,
        "/admin/set-user-password",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user_id,
            "newPassword": "newpassword456"
        })),
    );
    plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    // Verify the credential account password was updated
    let accounts_after = ctx.database.get_user_accounts(&user_id).await.unwrap();
    let new_password = accounts_after[0].password().unwrap().to_string();
    assert_ne!(
        old_password, new_password,
        "credential account password should be updated"
    );
}

#[tokio::test]
async fn test_set_user_password_too_short() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-user-password",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "newPassword": "ab" // too short
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_set_password_nonexistent_user() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-user-password",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": "nonexistent-user-id",
            "newPassword": "newpassword456"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

// -----------------------------------------------------------------------
// Permissions
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_has_permission_admin() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();
    let permissions = serde_json::json!({
        "users": ["read", "write"]
    });

    let req = make_request(
        HttpMethod::Post,
        "/admin/has-permission",
        &admin_session.token,
        Some(serde_json::json!({
            "permissions": permissions.clone()
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["success"], true);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.has_permission");
    assert_eq!(log.actor_user_id.as_deref(), Some(admin.id.as_str()));
    assert_eq!(log.target_type.as_deref(), Some("permissions"));
    let logged_target: serde_json::Value =
        serde_json::from_str(log.target_id.as_deref().expect("expected target_id")).unwrap();
    assert_eq!(logged_target, permissions);
    assert!(log.success);
    assert!(log.error_message.is_none());
}

#[tokio::test]
async fn test_has_permission_non_admin() {
    let (ctx, _admin, _admin_session, user, user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();
    let permissions = serde_json::json!({
        "users": ["read", "write"]
    });

    let req = make_request(
        HttpMethod::Post,
        "/admin/has-permission",
        &user_session.token,
        Some(serde_json::json!({
            "permissions": permissions.clone()
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["success"], false);
    assert!(body["error"].is_string());

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.has_permission");
    assert_eq!(log.actor_user_id.as_deref(), Some(user.id.as_str()));
    assert_eq!(log.target_type.as_deref(), Some("permissions"));
    let logged_target: serde_json::Value =
        serde_json::from_str(log.target_id.as_deref().expect("expected target_id")).unwrap();
    assert_eq!(logged_target, permissions);
    assert!(!log.success);
    assert_eq!(
        log.error_message.as_deref(),
        Some("User does not have the required permissions")
    );
}

#[tokio::test]
async fn test_has_permission_admin_with_grant_keys_records_audit_target() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();
    let grant_keys = serde_json::json!(["payment-link.paid"]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/has-permission",
        &admin_session.token,
        Some(serde_json::json!({
            "grantKeys": grant_keys.clone()
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["success"], true);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.has_permission");
    assert_eq!(log.actor_user_id.as_deref(), Some(admin.id.as_str()));
    assert_eq!(log.target_type.as_deref(), Some("grantKeys"));
    let logged_target: serde_json::Value =
        serde_json::from_str(log.target_id.as_deref().expect("expected target_id")).unwrap();
    assert_eq!(logged_target, grant_keys);
    assert!(log.success);
    assert!(log.error_message.is_none());
}

#[tokio::test]
async fn test_has_permission_non_admin_with_grant_keys_records_audit_target() {
    let (ctx, _admin, _admin_session, user, user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();
    let grant_keys = serde_json::json!(["payment-link.paid"]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/has-permission",
        &user_session.token,
        Some(serde_json::json!({
            "grantKeys": grant_keys.clone()
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["success"], false);
    assert!(body["error"].is_string());

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.has_permission");
    assert_eq!(log.actor_user_id.as_deref(), Some(user.id.as_str()));
    assert_eq!(log.target_type.as_deref(), Some("grantKeys"));
    let logged_target: serde_json::Value =
        serde_json::from_str(log.target_id.as_deref().expect("expected target_id")).unwrap();
    assert_eq!(logged_target, grant_keys);
    assert!(!log.success);
    assert_eq!(
        log.error_message.as_deref(),
        Some("User does not have the required permissions")
    );
}

#[tokio::test]
async fn test_has_permission_admin_with_right_modules_records_audit_target() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();
    let right_modules = serde_json::json!([{
        "key": "billing",
        "rightKeys": ["payment-link.paid"]
    }]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/has-permission",
        &admin_session.token,
        Some(serde_json::json!({
            "rightModules": right_modules.clone()
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["success"], true);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.has_permission");
    assert_eq!(log.actor_user_id.as_deref(), Some(admin.id.as_str()));
    assert_eq!(log.target_type.as_deref(), Some("rightModules"));
    let logged_target: serde_json::Value =
        serde_json::from_str(log.target_id.as_deref().expect("expected target_id")).unwrap();
    assert_eq!(logged_target, right_modules);
    assert!(log.success);
    assert!(log.error_message.is_none());
}

// -----------------------------------------------------------------------
// Set role edge cases
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_set_role_nonexistent_user() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-role",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": "nonexistent-user-id",
            "role": "admin"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_set_role_persists_in_database() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/set-role",
        &admin_session.token,
        Some(serde_json::json!({
            "userId": user.id,
            "role": "editor"
        })),
    );

    plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    // Verify role is persisted in the database
    let updated = ctx
        .database
        .get_user_by_id(&user.id)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(updated.role.as_deref(), Some("editor"));
}

// -----------------------------------------------------------------------
// Plugin name / routes
// -----------------------------------------------------------------------

#[tokio::test]
async fn test_plugin_name() {
    let plugin = AdminPlugin::new();
    assert_eq!(
        <AdminPlugin as AuthPlugin<MemoryDatabaseAdapter>>::name(&plugin),
        "admin"
    );
}

#[tokio::test]
async fn test_plugin_routes_count() {
    let plugin = AdminPlugin::new();
    let routes = <AdminPlugin as AuthPlugin<MemoryDatabaseAdapter>>::routes(&plugin);
    assert_eq!(
        routes.len(),
        ADMIN_ROUTES.len(),
        "admin plugin routes should stay in sync with the canonical route registry",
    );
}

#[tokio::test]
async fn test_list_sent_emails_route() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session.token,
        None,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert!(body["emails"].is_array());
    assert_eq!(body["total"], 0);
}

#[tokio::test]
async fn test_sent_email_management_routes() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    ctx.database
        .create_sent_email(CreateSentEmail {
            recipient_email: "customer@example.com".to_string(),
            subject: "Welcome".to_string(),
            from_address: "noreply@example.com".to_string(),
            from_name: Some("Athena Auth".to_string()),
            text_body: Some("Welcome".to_string()),
            html_body: Some("<p>Welcome</p>".to_string()),
            provider: "unit-provider".to_string(),
            flow: Some("welcome".to_string()),
            metadata: Some(serde_json::json!({"seed": true})),
        })
        .await
        .unwrap();

    let mut list_query = HashMap::new();
    list_query.insert(
        "recipientEmail".to_string(),
        "customer@example.com".to_string(),
    );
    list_query.insert("provider".to_string(), "unit-provider".to_string());
    list_query.insert("flow".to_string(), "welcome".to_string());
    list_query.insert("subjectContains".to_string(), "Wel".to_string());
    let list_req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session.token,
        None,
        list_query,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let list_body = json_body(&list_resp);
    assert_eq!(list_body["total"], 1);
    let email_id = list_body["emails"][0]["id"].as_str().unwrap().to_string();

    let mut get_query = HashMap::new();
    get_query.insert("id".to_string(), email_id.clone());
    let get_req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email/get",
        &admin_session.token,
        None,
        get_query,
    );
    let get_resp = plugin.on_request(&get_req, &ctx).await.unwrap().unwrap();
    assert_eq!(get_resp.status, 200);
    let get_body = json_body(&get_resp);
    assert_eq!(get_body["email"]["subject"], "Welcome");

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": email_id,
            "subject": "Welcome (updated)",
            "flow": null
        })),
    );
    let update_resp = plugin.on_request(&update_req, &ctx).await.unwrap().unwrap();
    assert_eq!(update_resp.status, 200);
    let update_body = json_body(&update_resp);
    assert_eq!(update_body["email"]["subject"], "Welcome (updated)");
    assert!(update_body["email"]["flow"].is_null());

    let delete_req = make_request(
        HttpMethod::Post,
        "/admin/email/delete",
        &admin_session.token,
        Some(serde_json::json!({
            "id": email_id
        })),
    );
    let delete_resp = plugin.on_request(&delete_req, &ctx).await.unwrap().unwrap();
    assert_eq!(delete_resp.status, 200);
    assert_eq!(json_body(&delete_resp)["success"], true);
}

#[tokio::test]
async fn test_create_sent_email_rejects_blank_subject_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "customer@example.com",
            "subject": "   ",
            "fromAddress": "noreply@example.com",
            "provider": "smtp-primary"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "blank subject should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.create");
    assert!(!log.success);
}

#[tokio::test]
async fn test_create_sent_email_rejects_non_object_metadata_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "customer@example.com",
            "subject": "Welcome",
            "fromAddress": "noreply@example.com",
            "provider": "smtp-primary",
            "metadata": ["invalid"]
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "non-object metadata should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.create");
    assert!(!log.success);
}

#[tokio::test]
async fn test_update_sent_email_rejects_blank_provider_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": "some-id",
            "provider": "   "
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "blank provider should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.update");
    assert!(!log.success);
}

#[tokio::test]
async fn test_update_sent_email_rejects_non_object_metadata_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "customer@example.com",
            "subject": "Welcome",
            "fromAddress": "noreply@example.com",
            "provider": "smtp-primary"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);

    let mut query = HashMap::new();
    query.insert(
        "recipientEmail".to_string(),
        "customer@example.com".to_string(),
    );
    let list_req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session.token,
        None,
        query,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let email_id = json_body(&list_resp)["emails"][0]["id"]
        .as_str()
        .unwrap()
        .to_string();

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": email_id,
            "metadata": ["invalid"]
        })),
    );

    let result = plugin.on_request(&update_req, &ctx).await;
    assert!(result.is_err(), "non-object metadata should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.update");
    assert!(!log.success);
}

#[tokio::test]
async fn test_sent_email_list_total_counts_all_matches_with_pagination() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    for subject in ["Welcome A", "Welcome B"] {
        ctx.database
            .create_sent_email(CreateSentEmail {
                recipient_email: "customer@example.com".to_string(),
                subject: subject.to_string(),
                from_address: "noreply@example.com".to_string(),
                from_name: Some("Athena Auth".to_string()),
                text_body: Some("Welcome".to_string()),
                html_body: Some("<p>Welcome</p>".to_string()),
                provider: "unit-provider".to_string(),
                flow: Some("welcome".to_string()),
                metadata: Some(serde_json::json!({"seed": true})),
            })
            .await
            .unwrap();
    }

    let mut query = HashMap::new();
    query.insert(
        "recipientEmail".to_string(),
        "customer@example.com".to_string(),
    );
    query.insert("flow".to_string(), "welcome".to_string());
    query.insert("limit".to_string(), "1".to_string());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session.token,
        None,
        query,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["total"], 2);
    assert_eq!(body["emails"].as_array().unwrap().len(), 1);
}

#[tokio::test]
async fn test_create_sent_email_returns_record_and_audits_target_id() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "audit-target@example.com",
            "subject": "Audit Target",
            "fromAddress": "noreply@example.com",
            "provider": "unit-provider"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let create_body = json_body(&create_resp);
    let created_id = create_body["email"]["id"].as_str().unwrap().to_string();
    assert_eq!(
        create_body["email"]["recipientEmail"],
        "audit-target@example.com"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.create");
    assert!(log.success);
    assert_eq!(log.target_type.as_deref(), Some("email"));
    assert_eq!(log.target_id.as_deref(), Some(created_id.as_str()));
}

#[tokio::test]
async fn test_email_send_failure_management_routes() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "ops@example.com",
            "flow": "password_reset",
            "provider": "unit-provider",
            "errorMessage": "temporary network issue",
            "metadata": { "attempt": 1 }
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let created_failure = json_body(&create_resp)["emailSendFailure"].clone();
    assert!(created_failure["id"].is_string());
    assert_eq!(created_failure["recipientEmail"], "ops@example.com");

    let mut list_query = HashMap::new();
    list_query.insert("recipientEmail".to_string(), "ops@example.com".to_string());
    list_query.insert("flow".to_string(), "password_reset".to_string());
    list_query.insert("resolved".to_string(), "false".to_string());
    let list_req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session.token,
        None,
        list_query,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let list_body = json_body(&list_resp);
    assert_eq!(list_body["total"], 1);
    let failure_id = list_body["emailSendFailures"][0]["id"]
        .as_str()
        .unwrap()
        .to_string();

    let mut get_query = HashMap::new();
    get_query.insert("id".to_string(), failure_id.clone());
    let get_req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-failure/get",
        &admin_session.token,
        None,
        get_query,
    );
    let get_resp = plugin.on_request(&get_req, &ctx).await.unwrap().unwrap();
    assert_eq!(get_resp.status, 200);
    let get_body = json_body(&get_resp);
    assert_eq!(get_body["emailSendFailure"]["resolved"], false);

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": failure_id,
            "resolved": true,
            "resolutionNote": "retried successfully"
        })),
    );
    let update_resp = plugin.on_request(&update_req, &ctx).await.unwrap().unwrap();
    assert_eq!(update_resp.status, 200);
    let update_body = json_body(&update_resp);
    assert_eq!(update_body["emailSendFailure"]["resolved"], true);
    assert_eq!(
        update_body["emailSendFailure"]["resolutionNote"],
        "retried successfully"
    );

    let delete_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/delete",
        &admin_session.token,
        Some(serde_json::json!({
            "id": failure_id
        })),
    );
    let delete_resp = plugin.on_request(&delete_req, &ctx).await.unwrap().unwrap();
    assert_eq!(delete_resp.status, 200);
    assert_eq!(json_body(&delete_resp)["success"], true);
}

#[tokio::test]
async fn test_create_email_failure_returns_record_and_audits_target_id() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "audit-failure-target@example.com",
            "flow": "password_reset",
            "provider": "unit-provider",
            "errorMessage": "transient SMTP failure"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    let created_id = body["emailSendFailure"]["id"].as_str().unwrap().to_string();
    assert_eq!(
        body["emailSendFailure"]["recipientEmail"],
        "audit-failure-target@example.com"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_failure.create");
    assert!(log.success);
    assert_eq!(log.target_type.as_deref(), Some("email_send_failure"));
    assert_eq!(log.target_id.as_deref(), Some(created_id.as_str()));
}

#[tokio::test]
async fn test_create_email_failure_rejects_non_object_metadata_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "ops@example.com",
            "flow": "password_reset",
            "provider": "unit-provider",
            "errorMessage": "temporary network issue",
            "metadata": ["invalid"]
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "non-object metadata should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_failure.create");
    assert!(!log.success);
}

#[tokio::test]
async fn test_update_email_failure_rejects_non_object_metadata_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "ops@example.com",
            "flow": "password_reset",
            "provider": "unit-provider",
            "errorMessage": "temporary network issue"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);

    let list_req = make_request(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session.token,
        None,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let failure_id = json_body(&list_resp)["emailSendFailures"][0]["id"]
        .as_str()
        .unwrap()
        .to_string();

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": failure_id,
            "metadata": ["invalid"]
        })),
    );

    let result = plugin.on_request(&update_req, &ctx).await;
    assert!(result.is_err(), "non-object metadata should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_failure.update");
    assert!(!log.success);
}

#[tokio::test]
async fn test_update_email_failure_whitespace_resolution_note_clears_value() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "ops@example.com",
            "flow": "password_reset",
            "provider": "unit-provider",
            "errorMessage": "temporary network issue"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);

    let list_req = make_request(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session.token,
        None,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let failure_id = json_body(&list_resp)["emailSendFailures"][0]["id"]
        .as_str()
        .unwrap()
        .to_string();

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": failure_id,
            "resolved": true,
            "resolutionNote": "   "
        })),
    );
    let update_resp = plugin.on_request(&update_req, &ctx).await.unwrap().unwrap();
    assert_eq!(update_resp.status, 200);
    let update_body = json_body(&update_resp);
    assert!(update_body["emailSendFailure"]["resolutionNote"].is_null());
}

#[tokio::test]
async fn test_create_email_failure_rejects_invalid_recipient_email_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "not-an-email",
            "flow": "password_reset",
            "errorMessage": "smtp timeout"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    match result {
        Ok(Some(resp)) => assert!(resp.status >= 400),
        Ok(None) => panic!("route unexpectedly returned None"),
        Err(_) => {}
    }

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_failure.create");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_failure_list_total_counts_all_matches_with_pagination() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    for error_message in ["temporary issue A", "temporary issue B"] {
        let create_req = make_request(
            HttpMethod::Post,
            "/admin/email-failure/create",
            &admin_session.token,
            Some(serde_json::json!({
                "recipientEmail": "ops@example.com",
                "flow": "password_reset",
                "provider": "unit-provider",
                "errorMessage": error_message
            })),
        );
        let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
        assert_eq!(create_resp.status, 200);
    }

    let mut query = HashMap::new();
    query.insert("recipientEmail".to_string(), "ops@example.com".to_string());
    query.insert("flow".to_string(), "password_reset".to_string());
    query.insert("resolved".to_string(), "false".to_string());
    query.insert("limit".to_string(), "1".to_string());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session.token,
        None,
        query,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["total"], 2);
    assert_eq!(body["emailSendFailures"].as_array().unwrap().len(), 1);
}

#[tokio::test]
async fn test_email_send_failure_create_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "audit-failure@example.com",
            "flow": "password_reset",
            "errorMessage": "smtp timeout"
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_failure.create");
    assert!(log.success);
}

#[tokio::test]
async fn test_sent_email_crud_success_paths_are_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "audit-sent@example.com",
            "subject": "Audit Create",
            "fromAddress": "noreply@example.com",
            "provider": "audit-provider"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let create_log = latest_audit_log(&ctx).await;
    assert_eq!(create_log.action, "admin.email.create");
    assert!(create_log.success);

    let list_req = make_request(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session.token,
        None,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let list_body = json_body(&list_resp);
    let email_id = list_body["emails"][0]["id"].as_str().unwrap().to_string();

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": email_id,
            "subject": "Audit Update"
        })),
    );
    let update_resp = plugin.on_request(&update_req, &ctx).await.unwrap().unwrap();
    assert_eq!(update_resp.status, 200);
    let update_log = latest_audit_log(&ctx).await;
    assert_eq!(update_log.action, "admin.email.update");
    assert!(update_log.success);

    let delete_req = make_request(
        HttpMethod::Post,
        "/admin/email/delete",
        &admin_session.token,
        Some(serde_json::json!({
            "id": email_id
        })),
    );
    let delete_resp = plugin.on_request(&delete_req, &ctx).await.unwrap().unwrap();
    assert_eq!(delete_resp.status, 200);
    let delete_log = latest_audit_log(&ctx).await;
    assert_eq!(delete_log.action, "admin.email.delete");
    assert!(delete_log.success);
}

#[tokio::test]
async fn test_email_failure_crud_success_paths_are_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session.token,
        Some(serde_json::json!({
            "recipientEmail": "audit-fail@example.com",
            "flow": "two_factor",
            "errorMessage": "smtp timeout"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let create_log = latest_audit_log(&ctx).await;
    assert_eq!(create_log.action, "admin.email_failure.create");
    assert!(create_log.success);

    let list_req = make_request(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session.token,
        None,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let list_body = json_body(&list_resp);
    let failure_id = list_body["emailSendFailures"][0]["id"]
        .as_str()
        .unwrap()
        .to_string();

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": failure_id,
            "resolved": true
        })),
    );
    let update_resp = plugin.on_request(&update_req, &ctx).await.unwrap().unwrap();
    assert_eq!(update_resp.status, 200);
    let update_log = latest_audit_log(&ctx).await;
    assert_eq!(update_log.action, "admin.email_failure.update");
    assert!(update_log.success);

    let delete_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/delete",
        &admin_session.token,
        Some(serde_json::json!({
            "id": failure_id
        })),
    );
    let delete_resp = plugin.on_request(&delete_req, &ctx).await.unwrap().unwrap();
    assert_eq!(delete_resp.status, 200);
    let delete_log = latest_audit_log(&ctx).await;
    assert_eq!(delete_log.action, "admin.email_failure.delete");
    assert!(delete_log.success);
}

#[tokio::test]
async fn test_email_admin_routes_are_isolated_per_runtime_context() {
    let (ctx_a, _admin_a, admin_session_a, _user_a, _user_session_a) = create_admin_context().await;
    let (ctx_b, _admin_b, admin_session_b, _user_b, _user_session_b) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_email_req = make_request(
        HttpMethod::Post,
        "/admin/email/create",
        &admin_session_a.token,
        Some(serde_json::json!({
            "recipientEmail": "tenant-a@example.com",
            "subject": "Tenant A mail",
            "fromAddress": "noreply@example.com",
            "provider": "tenant-a-provider",
            "flow": "tenant_a_flow"
        })),
    );
    let create_email_resp = plugin
        .on_request(&create_email_req, &ctx_a)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(create_email_resp.status, 200);

    let create_failure_req = make_request(
        HttpMethod::Post,
        "/admin/email-failure/create",
        &admin_session_a.token,
        Some(serde_json::json!({
            "recipientEmail": "tenant-a@example.com",
            "flow": "tenant_a_flow",
            "provider": "tenant-a-provider",
            "errorMessage": "simulated"
        })),
    );
    let create_failure_resp = plugin
        .on_request(&create_failure_req, &ctx_a)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(create_failure_resp.status, 200);

    let mut email_query_a = HashMap::new();
    email_query_a.insert("flow".to_string(), "tenant_a_flow".to_string());
    let list_email_a = make_request_with_query(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session_a.token,
        None,
        email_query_a,
    );
    let resp_a = plugin
        .on_request(&list_email_a, &ctx_a)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(resp_a.status, 200);
    assert_eq!(json_body(&resp_a)["total"], 1);

    let mut email_query_b = HashMap::new();
    email_query_b.insert("flow".to_string(), "tenant_a_flow".to_string());
    let list_email_b = make_request_with_query(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session_b.token,
        None,
        email_query_b,
    );
    let resp_b = plugin
        .on_request(&list_email_b, &ctx_b)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(resp_b.status, 200);
    assert_eq!(json_body(&resp_b)["total"], 0);

    let mut failure_query_a = HashMap::new();
    failure_query_a.insert("flow".to_string(), "tenant_a_flow".to_string());
    let list_failure_a = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session_a.token,
        None,
        failure_query_a,
    );
    let fail_resp_a = plugin
        .on_request(&list_failure_a, &ctx_a)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(fail_resp_a.status, 200);
    assert_eq!(json_body(&fail_resp_a)["total"], 1);

    let mut failure_query_b = HashMap::new();
    failure_query_b.insert("flow".to_string(), "tenant_a_flow".to_string());
    let list_failure_b = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session_b.token,
        None,
        failure_query_b,
    );
    let fail_resp_b = plugin
        .on_request(&list_failure_b, &ctx_b)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(fail_resp_b.status, 200);
    assert_eq!(json_body(&fail_resp_b)["total"], 0);
}

#[tokio::test]
async fn test_email_list_rejects_invalid_created_after() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let mut query = HashMap::new();
    query.insert("createdAfter".to_string(), "not-a-timestamp".to_string());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email/list",
        &admin_session.token,
        None,
        query,
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "invalid createdAfter should return error");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email.list");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_failure_list_rejects_invalid_created_before() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let mut query = HashMap::new();
    query.insert("createdBefore".to_string(), "bad-time".to_string());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-failure/list",
        &admin_session.token,
        None,
        query,
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "invalid createdBefore should return error");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_failure.list");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_template_crud_routes() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "template_key": "verification_email",
            "locale": "en",
            "subject_template": "Welcome to {{app_name}}",
            "text_template": "Verify at {{verification_url}}",
            "html_template": "<p>Verify <a href=\"{{verification_url}}\">here</a></p>",
            "variables": ["app_name", "verification_url"],
            "variable_bindings": [
                {
                    "name": "user_id",
                    "source": "athena.users.id",
                    "required": true
                }
            ],
            "attachments": [
                {
                    "file_url": "https://cdn.example.com/verify.pdf",
                    "filename": "verify.pdf"
                }
            ],
            "attachment_failure_mode": "skip",
            "is_active": true,
            "metadata": { "source": "admin-test" }
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let created = json_body(&create_resp);
    let template_id = created["id"].as_str().unwrap().to_string();
    assert_eq!(created["template_key"], "verification_email");
    assert_eq!(created["event_type"], "user.email.verify");
    assert_eq!(created["locale"], "en");
    assert_eq!(created["variable_bindings"][0]["name"], "user_id");
    assert_eq!(created["variable_bindings"][0]["source"], "athena.users.id");
    assert_eq!(created["variable_bindings"][0]["required"], true);
    assert_eq!(
        created["attachments"][0]["file_url"],
        "https://cdn.example.com/verify.pdf"
    );
    assert_eq!(created["attachments"][0]["filename"], "verify.pdf");
    assert_eq!(created["attachment_failure_mode"], "skip");

    let mut query = HashMap::new();
    query.insert("template_key".to_string(), "verification_email".to_string());
    let list_req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-template/list",
        &admin_session.token,
        None,
        query,
    );
    let list_resp = plugin.on_request(&list_req, &ctx).await.unwrap().unwrap();
    assert_eq!(list_resp.status, 200);
    let listed = json_body(&list_resp);
    assert!(listed["email_templates"].is_array());
    assert_eq!(listed["total"], 1);

    let mut get_query = HashMap::new();
    get_query.insert("id".to_string(), template_id.clone());
    let get_req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-template/get",
        &admin_session.token,
        None,
        get_query,
    );
    let get_resp = plugin.on_request(&get_req, &ctx).await.unwrap().unwrap();
    assert_eq!(get_resp.status, 200);
    let fetched = json_body(&get_resp);
    assert_eq!(fetched["email_template"]["id"], template_id);
    assert_eq!(
        fetched["email_template"]["template_key"],
        "verification_email"
    );

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": template_id,
            "subject_template": "Verify your account at {{app_name}}",
            "is_active": false,
            "text_template": null,
            "attachments": null,
            "attachment_failure_mode": null,
            "variable_bindings": [
                {
                    "name": "organization_id",
                    "source": "athena.organizations.id"
                }
            ]
        })),
    );
    let update_resp = plugin.on_request(&update_req, &ctx).await.unwrap().unwrap();
    assert_eq!(update_resp.status, 200);
    let updated = json_body(&update_resp);
    assert_eq!(
        updated["subject_template"],
        "Verify your account at {{app_name}}"
    );
    assert_eq!(updated["is_active"], false);
    assert!(updated["text_template"].is_null());
    assert_eq!(updated["attachments"], serde_json::json!([]));
    assert_eq!(updated["attachment_failure_mode"], "fail");
    assert_eq!(updated["variable_bindings"][0]["name"], "organization_id");
    assert_eq!(
        updated["variable_bindings"][0]["source"],
        "athena.organizations.id"
    );
    assert_eq!(updated["variable_bindings"][0]["required"], false);

    let delete_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/delete",
        &admin_session.token,
        Some(serde_json::json!({
            "id": template_id
        })),
    );
    let delete_resp = plugin.on_request(&delete_req, &ctx).await.unwrap().unwrap();
    assert_eq!(delete_resp.status, 200);
    let deleted = json_body(&delete_resp);
    assert_eq!(deleted["success"], true);
}

#[tokio::test]
async fn test_email_template_create_accepts_camel_case_aliases() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "templateKey": "verification_email_camel_alias",
            "eventType": "user.password.reset",
            "subjectTemplate": "Reset your password",
            "attachments": {
                "fileUrl": "https://cdn.example.com/reset.txt",
                "filename": "reset.txt"
            },
            "attachmentFailureMode": "skip"
        })),
    );

    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);

    let created = json_body(&create_resp);
    assert_eq!(created["template_key"], "verification_email_camel_alias");
    assert_eq!(created["event_type"], "user.password.reset");
    assert_eq!(created["attachments"].as_array().unwrap().len(), 1);
    assert_eq!(
        created["attachments"][0]["file_url"],
        "https://cdn.example.com/reset.txt"
    );
    assert_eq!(created["attachments"][0]["filename"], "reset.txt");
    assert_eq!(created["attachment_failure_mode"], "skip");
}

#[tokio::test]
async fn test_email_template_send_route_sends_with_snake_case_contract() {
    let sent = Arc::new(Mutex::new(Vec::new()));
    let mut config = test_helpers::create_test_config();
    config.email_provider = Some(Arc::new(RecordingEmailProvider { sent: sent.clone() }));
    let (ctx, _admin, admin_session, user, _user_session) =
        create_admin_context_with_config(config).await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "template_key": "custom_admin_notice",
            "subject_template": "Hello {{user_name}}",
            "text_template": "User {{user_id}} from {{app_name}}",
            "variable_bindings": [
                {
                    "name": "user_id",
                    "source": "athena.users.id",
                    "required": true
                }
            ]
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let template_id = json_body(&create_resp)["id"].as_str().unwrap().to_string();

    let send_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/send",
        &admin_session.token,
        Some(serde_json::json!({
            "template_id": template_id,
            "recipient_email": "customer@example.com",
            "render_variables": {
                "user_name": "Casey"
            },
            "user_id": user.id
        })),
    );
    let send_resp = plugin.on_request(&send_req, &ctx).await.unwrap().unwrap();
    assert_eq!(send_resp.status, 200);

    let body = json_body(&send_resp);
    assert_eq!(body["success"], true);
    assert_eq!(body["flow"], "admin.email_template.send");
    assert_eq!(body["recipient_email"], "customer@example.com");
    assert_eq!(body["template_key"], "custom_admin_notice");
    assert_eq!(body["subject"], "Hello Casey");
    assert_eq!(body["attachment_count"], 0);
    assert!(body.get("email_send_failure_id").is_none());

    let sent_messages = sent.lock().unwrap();
    assert_eq!(sent_messages.len(), 1);
    assert_eq!(sent_messages[0].0, "customer@example.com");
    assert_eq!(sent_messages[0].1, "Hello Casey");
    assert!(sent_messages[0].3.contains("User "));
    drop(sent_messages);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.send");
    assert!(log.success);
}

#[tokio::test]
async fn test_email_template_send_route_accepts_camel_case_aliases_and_logs_missing_provider() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "template_key": "custom_admin_notice_failure",
            "subject_template": "Hello",
            "text_template": "Body"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let template_id = json_body(&create_resp)["id"].as_str().unwrap().to_string();

    let send_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/send",
        &admin_session.token,
        Some(serde_json::json!({
            "templateId": template_id,
            "recipientEmail": "customer@example.com",
            "renderVariables": {
                "user_name": "Casey"
            }
        })),
    );
    let send_resp = plugin.on_request(&send_req, &ctx).await.unwrap().unwrap();
    assert_eq!(send_resp.status, 503);

    let body = json_body(&send_resp);
    assert_eq!(body["success"], false);
    assert_eq!(body["flow"], "admin.email_template.send");
    assert_eq!(body["error"], "No email provider configured");
    assert!(body["email_send_failure_id"].as_str().is_some());

    let failure_id = body["email_send_failure_id"].as_str().unwrap().to_string();
    let failure = ctx
        .database
        .get_email_send_failure_by_id(&failure_id)
        .await
        .unwrap()
        .expect("failure row should be stored");
    assert_eq!(failure.flow, "admin.email_template.send");
    assert_eq!(failure.recipient_email, "customer@example.com");
    assert_eq!(failure.error_message, "No email provider configured");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.send");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_template_send_route_accepts_request_attachments_and_failure_mode_aliases() {
    let sent = Arc::new(Mutex::new(Vec::new()));
    let mut config = test_helpers::create_test_config();
    config.email_provider = Some(Arc::new(RecordingEmailProvider { sent: sent.clone() }));
    let (ctx, _admin, admin_session, _user, _user_session) =
        create_admin_context_with_config(config).await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "template_key": "custom_admin_notice_send_attachment_aliases",
            "subject_template": "Hello",
            "text_template": "Body"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let template_id = json_body(&create_resp)["id"].as_str().unwrap().to_string();

    let send_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/send",
        &admin_session.token,
        Some(serde_json::json!({
            "templateId": template_id,
            "recipientEmail": "customer@example.com",
            "attachments": {
                "fileUrl": "http://127.0.0.1/private.txt",
                "filename": "private.txt"
            },
            "attachmentFailureMode": "skip"
        })),
    );
    let send_resp = plugin.on_request(&send_req, &ctx).await.unwrap().unwrap();
    assert_eq!(send_resp.status, 200);

    let body = json_body(&send_resp);
    assert_eq!(body["success"], true);
    assert_eq!(body["attachment_count"], 0);
    assert_eq!(
        body["delivery_metadata"]["attachments"]["skipped"][0]["file_url"],
        "http://127.0.0.1/private.txt"
    );
    assert_eq!(body["delivery_metadata"]["attachments"]["mode"], "skip");
    assert!(body.get("email_send_failure_id").is_none());

    let sent_messages = sent.lock().unwrap();
    assert_eq!(sent_messages.len(), 1);
    assert!(sent_messages[0].4.is_empty());
}

#[tokio::test]
async fn test_email_template_send_route_stores_attachment_resolution_failures() {
    let sent = Arc::new(Mutex::new(Vec::new()));
    let mut config = test_helpers::create_test_config();
    config.email_provider = Some(Arc::new(RecordingEmailProvider { sent: sent.clone() }));
    let (ctx, _admin, admin_session, _user, _user_session) =
        create_admin_context_with_config(config).await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "template_key": "custom_admin_notice_attachment_failure",
            "subject_template": "Hello",
            "text_template": "Body",
            "attachments": [
                {
                    "file_url": "http://127.0.0.1/private.txt",
                    "filename": "private.txt"
                }
            ]
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let template_id = json_body(&create_resp)["id"].as_str().unwrap().to_string();

    let send_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/send",
        &admin_session.token,
        Some(serde_json::json!({
            "template_id": template_id,
            "recipient_email": "customer@example.com"
        })),
    );
    let send_resp = plugin.on_request(&send_req, &ctx).await.unwrap().unwrap();
    assert_eq!(send_resp.status, 502);

    let body = json_body(&send_resp);
    assert_eq!(body["success"], false);
    assert_eq!(
        body["delivery_metadata"]["attachments"]["failures"][0]["error_code"],
        "private_address_blocked"
    );
    assert!(body["email_send_failure_id"].as_str().is_some());
    assert!(sent.lock().unwrap().is_empty());
}

#[tokio::test]
async fn test_email_template_create_rejects_duplicate_variables_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "templateKey": "verification_email_duplicates",
            "subjectTemplate": "Welcome to {{app_name}}",
            "variables": ["app_name", " app_name "]
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "duplicate variables should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.create");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_template_create_rejects_duplicate_variable_bindings_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "templateKey": "verification_email_bindings_duplicates",
            "subjectTemplate": "Welcome to {{app_name}}",
            "variableBindings": [
                {
                    "name": "user_id",
                    "source": "athena.users.id"
                },
                {
                    "name": " user_id ",
                    "source": "athena.organizations.id"
                }
            ]
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(
        result.is_err(),
        "duplicate variable bindings should be rejected"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.create");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_template_create_rejects_blank_template_key_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "templateKey": "   ",
            "subjectTemplate": "Welcome to {{app_name}}"
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "blank templateKey should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.create");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_template_update_rejects_non_object_metadata_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "templateKey": "verification_email_update_guard",
            "subjectTemplate": "Welcome to {{app_name}}",
            "variables": ["app_name"]
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let template_id = json_body(&create_resp)["id"].as_str().unwrap().to_string();

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": template_id,
            "metadata": ["invalid"]
        })),
    );

    let result = plugin.on_request(&update_req, &ctx).await;
    assert!(result.is_err(), "non-object metadata should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.update");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_template_update_rejects_blank_subject_template_and_audits_failure() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let create_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "templateKey": "verification_email_subject_guard",
            "subjectTemplate": "Welcome to {{app_name}}"
        })),
    );
    let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
    assert_eq!(create_resp.status, 200);
    let template_id = json_body(&create_resp)["id"].as_str().unwrap().to_string();

    let update_req = make_request(
        HttpMethod::Post,
        "/admin/email-template/update",
        &admin_session.token,
        Some(serde_json::json!({
            "id": template_id,
            "subjectTemplate": "   "
        })),
    );

    let result = plugin.on_request(&update_req, &ctx).await;
    assert!(result.is_err(), "blank subjectTemplate should be rejected");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.update");
    assert!(!log.success);
}

#[tokio::test]
async fn test_email_template_list_total_counts_all_matches_with_pagination() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    for template_key in ["verification_email", "password_reset_email"] {
        let create_req = make_request(
            HttpMethod::Post,
            "/admin/email-template/create",
            &admin_session.token,
            Some(serde_json::json!({
                "template_key": template_key,
                "locale": "en",
                "subject_template": format!("{template_key} subject"),
                "is_active": true
            })),
        );
        let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
        assert_eq!(create_resp.status, 200);
    }

    let mut query = HashMap::new();
    query.insert("is_active".to_string(), "true".to_string());
    query.insert("limit".to_string(), "1".to_string());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-template/list",
        &admin_session.token,
        None,
        query,
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["total"], 2);
    assert_eq!(body["email_templates"].as_array().unwrap().len(), 1);
}

#[tokio::test]
async fn test_email_event_type_list_route_supports_filters() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let mut query = HashMap::new();
    query.insert("category".to_string(), "organization_lifecycle".to_string());
    query.insert("isActive".to_string(), "true".to_string());
    query.insert("limit".to_string(), "5".to_string());

    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-event-type/list",
        &admin_session.token,
        None,
        query,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);

    assert!(body["emailEventTypes"].is_array());
    assert!(body["total"].as_u64().unwrap() >= 1);
    assert_eq!(body["limit"], 5);
    assert_eq!(
        body["emailEventTypes"][0]["category"],
        "organization_lifecycle"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_event_type.list");
    assert!(log.success);
}

#[tokio::test]
async fn test_email_template_list_supports_event_type_filter() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    for payload in [
        serde_json::json!({
            "template_key": "verification_email_filter_target",
            "event_type": "user.email.verify",
            "subject_template": "Verify your account"
        }),
        serde_json::json!({
            "template_key": "org_invite_filter_target",
            "event_type": "organization.member.invite",
            "subject_template": "Join organization"
        }),
    ] {
        let create_req = make_request(
            HttpMethod::Post,
            "/admin/email-template/create",
            &admin_session.token,
            Some(payload),
        );
        let create_resp = plugin.on_request(&create_req, &ctx).await.unwrap().unwrap();
        assert_eq!(create_resp.status, 200);
    }

    let mut query = HashMap::new();
    query.insert("event_type".to_string(), "user.email.verify".to_string());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/email-template/list",
        &admin_session.token,
        None,
        query,
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);

    assert_eq!(body["total"], 1);
    assert_eq!(body["email_templates"].as_array().unwrap().len(), 1);
    assert_eq!(
        body["email_templates"][0]["event_type"],
        "user.email.verify"
    );
}

#[tokio::test]
async fn test_email_template_create_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/email-template/create",
        &admin_session.token,
        Some(serde_json::json!({
            "templateKey": "password_reset_email",
            "subjectTemplate": "Reset your password"
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.email_template.create");
    assert!(log.success);
}

#[tokio::test]
async fn test_create_admin_api_key_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/api-key/create",
        &admin_session.token,
        Some(serde_json::json!({
            "name": "audit-key"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.api_key.create");
    assert!(log.success);
}

#[tokio::test]
async fn test_create_admin_api_key_projects_native_grant_keys() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![GrantDescriptor {
        key: "payment-link.paid".to_string(),
        resource: "payment-link".to_string(),
        action: "paid".to_string(),
        description: "Grant access when a payment link has been paid.".to_string(),
    }]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/api-key/create",
        &admin_session.token,
        Some(serde_json::json!({
            "name": "billing-admin-key",
            "grantKeys": ["payment-link.paid", "payment-link.paid"],
            "metadata": { "env": "prod" }
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["apiKey"]["permissions"],
        serde_json::json!({
            "admin": ["*"],
            "payment-link": ["paid"]
        })
    );
    assert_eq!(
        body["apiKey"]["grantKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(
        body["apiKey"]["rightKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(
        body["apiKey"]["nativeRights"][0]["key"],
        "payment-link.paid"
    );
    assert_eq!(body["apiKey"]["nativeRightModules"][0]["key"], "billing");
    assert_eq!(
        body["apiKey"]["metadata"],
        serde_json::json!({ "env": "prod" })
    );
    assert!(body["apiKey"]["metadata"].get("_athenaAuth").is_none());

    let stored = ctx
        .database
        .list_api_keys_by_user(&admin.id)
        .await
        .unwrap()
        .into_iter()
        .find(|key| key.name() == Some("billing-admin-key"))
        .expect("expected stored admin api key");
    assert_eq!(
        stored.permissions().unwrap(),
        r#"{"admin":["*"],"payment-link":["paid"]}"#
    );
    let stored_metadata: serde_json::Value =
        serde_json::from_str(stored.metadata().unwrap()).expect("stored metadata json");
    assert_eq!(
        stored_metadata["_athenaAuth"]["grantKeys"],
        serde_json::json!(["payment-link.paid"])
    );
}

#[tokio::test]
async fn test_sync_source_accepts_right_keys_alias() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![GrantDescriptor {
        key: "gateway.query".to_string(),
        resource: "gateway".to_string(),
        action: "query".to_string(),
        description: "Execute gateway queries.".to_string(),
    }]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/grant-assignment/sync-source",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "manual",
            "sourceRef": "support-case-right-keys",
            "rightKeys": ["gateway.query"]
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["appliedGrantKeys"],
        serde_json::json!(["gateway.query"])
    );
    assert_eq!(
        body["appliedRightKeys"],
        serde_json::json!(["gateway.query"])
    );
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["gateway.query"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightModules"][0]["key"],
        "gateway"
    );
}

#[tokio::test]
async fn test_rights_sync_source_alias_accepts_right_keys() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![GrantDescriptor {
        key: "gateway.query".to_string(),
        resource: "gateway".to_string(),
        action: "query".to_string(),
        description: "Execute gateway queries.".to_string(),
    }]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/rights/sync-source",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "manual",
            "sourceRef": "support-case-rights-route",
            "rightKeys": ["gateway.query"]
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["appliedGrantKeys"],
        serde_json::json!(["gateway.query"])
    );
    assert_eq!(
        body["appliedRightKeys"],
        serde_json::json!(["gateway.query"])
    );
    assert_eq!(
        body["subjectState"]["rightKeys"],
        serde_json::json!(["gateway.query"])
    );
    assert_eq!(
        body["subjectState"]["nativeRightModules"][0]["key"],
        "gateway"
    );

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.rights.sync_source");
    assert_eq!(
        log.target_type.as_deref(),
        Some("native_right_assignment_source")
    );
    assert!(log.success);
}

#[tokio::test]
async fn test_sync_source_accepts_right_modules_alias() {
    let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new().with_grant_catalog(vec![GrantDescriptor {
        key: "payment-link.paid".to_string(),
        resource: "payment-link".to_string(),
        action: "paid".to_string(),
        description: "Grant access when a payment-link payment has succeeded.".to_string(),
    }]);

    let req = make_request(
        HttpMethod::Post,
        "/admin/rights/sync-source",
        &admin_session.token,
        Some(serde_json::json!({
            "subjectKind": "user",
            "subjectId": user.id,
            "sourceKind": "billing",
            "sourceRef": "payment-link-module-source",
            "rightModules": [{
                "key": "billing",
                "rightKeys": ["payment-link.paid"]
            }]
        })),
    );
    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();

    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(
        body["appliedGrantKeys"],
        serde_json::json!(["payment-link.paid"])
    );
    assert_eq!(body["appliedNativeRightModules"][0]["key"], "billing");
    assert_eq!(
        body["appliedNativeRightModules"][0]["rights"][0]["key"],
        "payment-link.paid"
    );
    assert_eq!(
        body["subjectState"]["nativeRightModules"][0]["key"],
        "billing"
    );
}

#[tokio::test]
async fn test_create_athena_client_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/athena-client/create",
        &admin_session.token,
        Some(serde_json::json!({
            "clientName": "audit-client"
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.athena_client.create");
    assert!(log.success);
}

#[tokio::test]
async fn test_list_athena_clients_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Get,
        "/admin/athena-client/list",
        &admin_session.token,
        None,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.athena_client.list");
    assert!(log.success);
}

#[tokio::test]
async fn test_create_oauth_client_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/oauth-client/create",
        &admin_session.token,
        Some(serde_json::json!({
            "clientName": "audit-oauth-client",
            "provider": "google",
            "redirectUris": ["https://api.xbp.app/callbacks/oauth/google"],
        })),
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.oauth_client.create");
    assert!(log.success);
}

#[tokio::test]
async fn test_list_oauth_clients_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Get,
        "/admin/oauth-client/list",
        &admin_session.token,
        None,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.oauth_client.list");
    assert!(log.success);
}

#[tokio::test]
async fn test_create_oauth_client_rejects_blank_redirect_uris() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Post,
        "/admin/oauth-client/create",
        &admin_session.token,
        Some(serde_json::json!({
            "clientName": "bad-oauth-client",
            "provider": "google",
            "redirectUris": ["   ", ""],
        })),
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "blank redirect URIs should be rejected");
}

#[tokio::test]
async fn test_list_audit_logs_route_is_audited() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let req = make_request(
        HttpMethod::Get,
        "/admin/audit-log/list",
        &admin_session.token,
        None,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.audit_log.list");
    assert!(log.success);
}

#[tokio::test]
async fn test_audit_log_list_total_counts_all_matches_with_pagination() {
    let (ctx, admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    for action in [
        "admin.email.create",
        "admin.email.create",
        "admin.email.delete",
    ] {
        ctx.database
            .create_admin_audit_log(CreateAdminAuditLog {
                actor_user_id: Some(admin.id.clone()),
                actor_session_token: Some(admin_session.token.clone()),
                trace_id: Some(format!("trace-{action}")),
                api_key: None,
                action: action.to_string(),
                target_type: Some("email".to_string()),
                target_id: None,
                success: true,
                error_message: None,
                granted_rights: Vec::new(),
                requested_rights: vec![action.to_string()],
                metadata: Some(serde_json::json!({ "seed": true })),
            })
            .await
            .unwrap();
    }

    let mut query = HashMap::new();
    query.insert("limit".to_string(), "1".to_string());
    query.insert("action".to_string(), "admin.email.create".to_string());
    query.insert("success".to_string(), "true".to_string());
    query.insert("actorUserId".to_string(), admin.id);
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/audit-log/list",
        &admin_session.token,
        None,
        query,
    );

    let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
    assert_eq!(resp.status, 200);
    let body = json_body(&resp);
    assert_eq!(body["total"], 2);
    assert_eq!(body["auditLogs"].as_array().unwrap().len(), 1);
    assert_eq!(body["auditLogs"][0]["action"], "admin.email.create");
}

#[tokio::test]
async fn test_audit_log_list_rejects_invalid_created_after() {
    let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    let mut query = HashMap::new();
    query.insert("createdAfter".to_string(), "bad-time".to_string());
    let req = make_request_with_query(
        HttpMethod::Get,
        "/admin/audit-log/list",
        &admin_session.token,
        None,
        query,
    );

    let result = plugin.on_request(&req, &ctx).await;
    assert!(result.is_err(), "invalid createdAfter should return error");

    let log = latest_audit_log(&ctx).await;
    assert_eq!(log.action, "admin.audit_log.list");
    assert!(!log.success);
}

#[tokio::test]
async fn test_every_admin_route_logs_unauthenticated_failure() {
    let (ctx, _admin, _admin_session, _user, _user_session) = create_admin_context().await;
    let plugin = AdminPlugin::new();

    for (method, path, _, action) in ADMIN_ROUTES {
        let req = make_request(method.clone(), path, "invalid-token", None);
        let result = plugin.on_request(&req, &ctx).await;

        match result {
            Ok(Some(resp)) => {
                assert!(
                    resp.status >= 400,
                    "route {} should fail for unauthenticated requests",
                    path
                );
            }
            Ok(None) => panic!("route {} unexpectedly returned None", path),
            Err(_) => {}
        }

        let log = latest_audit_log(&ctx).await;
        assert_eq!(
            log.action, *action,
            "route {} should map to {}",
            path, action
        );
        assert!(!log.success, "route {} should log a failed audit", path);
    }
}
