//! Penetration tests for the multi-tenant system.
//!
//! Two sections:
//!  - **Black box**: tests written from an external attacker perspective — no
//!    knowledge of internals, just trying to break things via HTTP.
//!  - **Gray box**: tests informed by code review — targeting specific
//!    weaknesses, edge cases, and missing checks discovered during audit.

use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;

use axum::{
    extract::{ConnectInfo, Request},
    middleware::{self, Next},
    response::Response,
    routing::{delete, get, patch, post},
    Router,
};
use axum_test::TestServer;
use http::Method;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use tempfile::{tempdir, TempDir};

use sirr_server::{
    auth::{require_auth, require_master_key},
    handlers::*,
    license::LicenseStatus,
    org_handlers::*,
    store::{
        crypto,
        org::{OrgRecord, PrincipalKeyRecord, PrincipalRecord},
        Store,
    },
    AppState,
};

// ── Test infrastructure ─────────────────────────────────────────────────────

const MASTER_KEY: &str = "test-master-key-12345";

async fn inject_connect_info(mut req: Request, next: Next) -> Response {
    req.extensions_mut()
        .insert(ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 12345))));
    next.run(req).await
}

fn build_test_app() -> (TestServer, Store, TempDir) {
    let dir = tempdir().unwrap();
    let key = crypto::generate_key();
    let store = Store::open(&dir.path().join("test.db"), key).unwrap();

    let state = AppState {
        store: store.clone(),
        api_key: Some(MASTER_KEY.to_string()),
        license: LicenseStatus::Free,
        validator: None,
        webhook_sender: None,
        trusted_proxies: Arc::new(vec![]),
        redact_audit_keys: false,
        webhook_allowed_origins: Arc::new(vec![]),
        enable_public_bucket: true,
    };

    let secret_read = Router::new().route("/secrets/{key}", get(get_secret).head(head_secret));

    let protected_public = Router::new()
        .route("/secrets", get(list_secrets).post(create_secret))
        .route("/secrets/{key}", patch(patch_secret).delete(delete_secret))
        .route("/prune", post(prune_secrets))
        .route("/audit", get(audit_events))
        .route("/webhooks", post(create_webhook).get(list_webhooks))
        .route("/webhooks/{id}", delete(delete_webhook))
        .layer(middleware::from_fn_with_state(
            state.clone(),
            require_master_key,
        ));

    let org_protected = Router::new()
        .route("/orgs", post(create_org).get(list_orgs))
        .route("/orgs/{org_id}", delete(delete_org))
        .route(
            "/orgs/{org_id}/principals",
            post(create_principal).get(list_principals),
        )
        .route("/orgs/{org_id}/principals/{id}", delete(delete_principal))
        .route("/orgs/{org_id}/roles", post(create_role).get(list_roles))
        .route("/orgs/{org_id}/roles/{name}", delete(delete_role))
        .route("/me", get(get_me).patch(patch_me))
        .route("/me/keys", post(create_key))
        .route("/me/keys/{key_id}", delete(delete_key))
        .route(
            "/orgs/{org_id}/secrets",
            post(create_org_secret).get(list_org_secrets),
        )
        .route(
            "/orgs/{org_id}/secrets/{key}",
            get(get_org_secret)
                .head(head_org_secret)
                .patch(patch_org_secret)
                .delete(delete_org_secret),
        )
        .route("/orgs/{org_id}/prune", post(prune_org_secrets))
        .route("/orgs/{org_id}/audit", get(org_audit_events))
        .route(
            "/orgs/{org_id}/webhooks",
            post(create_org_webhook).get(list_org_webhooks),
        )
        .route("/orgs/{org_id}/webhooks/{id}", delete(delete_org_webhook))
        .layer(middleware::from_fn_with_state(state.clone(), require_auth));

    let app: Router<()> = Router::new()
        .merge(secret_read)
        .merge(protected_public)
        .merge(org_protected)
        .with_state(state)
        .layer(middleware::from_fn(inject_connect_info));

    let server = TestServer::new(app);
    (server, store, dir)
}

fn now_secs() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64
}

fn bootstrap_org_with_key(
    store: &Store,
    org_name: &str,
    principal_name: &str,
    role: &str,
) -> (String, String, String) {
    let now = now_secs();
    let org_id = format!("org_{org_name}");
    let principal_id = format!("p_{principal_name}");

    let raw_key = format!("sirr_key_{}", hex::encode(rand::random::<[u8; 16]>()));
    let key_hash = Sha256::digest(raw_key.as_bytes()).to_vec();

    store
        .put_org(&OrgRecord {
            id: org_id.clone(),
            name: org_name.into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();

    store
        .put_principal(&PrincipalRecord {
            id: principal_id.clone(),
            org_id: org_id.clone(),
            name: principal_name.into(),
            role: role.into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();

    store
        .put_principal_key(&PrincipalKeyRecord {
            id: format!("pk_{principal_name}"),
            principal_id: principal_id.clone(),
            org_id: org_id.clone(),
            name: "default_key".into(),
            key_hash,
            valid_after: now - 60,
            valid_before: now + 3600,
            created_at: now,
        })
        .unwrap();

    (org_id, principal_id, raw_key)
}

fn add_named_key(
    store: &Store,
    org_id: &str,
    principal_id: &str,
    key_id: &str,
    key_name: &str,
) -> String {
    let now = now_secs();
    let raw_key = format!("sirr_key_{}", hex::encode(rand::random::<[u8; 16]>()));
    let key_hash = Sha256::digest(raw_key.as_bytes()).to_vec();

    store
        .put_principal_key(&PrincipalKeyRecord {
            id: key_id.into(),
            principal_id: principal_id.into(),
            org_id: org_id.into(),
            name: key_name.into(),
            key_hash,
            valid_after: now - 60,
            valid_before: now + 3600,
            created_at: now,
        })
        .unwrap();

    raw_key
}

fn add_expired_key(
    store: &Store,
    org_id: &str,
    principal_id: &str,
    key_id: &str,
    key_name: &str,
) -> String {
    let now = now_secs();
    let raw_key = format!("sirr_key_{}", hex::encode(rand::random::<[u8; 16]>()));
    let key_hash = Sha256::digest(raw_key.as_bytes()).to_vec();

    store
        .put_principal_key(&PrincipalKeyRecord {
            id: key_id.into(),
            principal_id: principal_id.into(),
            org_id: org_id.into(),
            name: key_name.into(),
            key_hash,
            valid_after: now - 7200,
            valid_before: now - 3600, // expired 1 hour ago
            created_at: now - 7200,
        })
        .unwrap();

    raw_key
}

fn add_not_yet_valid_key(
    store: &Store,
    org_id: &str,
    principal_id: &str,
    key_id: &str,
    key_name: &str,
) -> String {
    let now = now_secs();
    let raw_key = format!("sirr_key_{}", hex::encode(rand::random::<[u8; 16]>()));
    let key_hash = Sha256::digest(raw_key.as_bytes()).to_vec();

    store
        .put_principal_key(&PrincipalKeyRecord {
            id: key_id.into(),
            principal_id: principal_id.into(),
            org_id: org_id.into(),
            name: key_name.into(),
            key_hash,
            valid_after: now + 3600, // valid 1 hour from now
            valid_before: now + 7200,
            created_at: now,
        })
        .unwrap();

    raw_key
}

// ═══════════════════════════════════════════════════════════════════════════════
//  BLACK BOX TESTS — external attacker perspective, no code knowledge
// ═══════════════════════════════════════════════════════════════════════════════

// ── BB-01: Auth bypass — garbage/malformed tokens ───────────────────────────

#[tokio::test]
async fn bb01_garbage_tokens_rejected() {
    let (server, store, _dir) = build_test_app();
    let (org_id, _, _) = bootstrap_org_with_key(&store, "bb01", "user1", "writer");

    // Note: axum-test rejects tokens with non-ASCII/control chars as invalid
    // HeaderValue. We test only tokens that are valid HTTP header values.
    let garbage_tokens = [
        "not-a-real-key",            // random string
        "sirr_key_0000000000000000", // looks like a key but isn't
        "test-master-key-1234",      // master key off by one char
        "test-master-key-123456",    // master key with extra char
        "TEST-MASTER-KEY-12345",     // master key wrong case
        "null",                      // null string
        "../../../etc/passwd",       // path traversal in token
        "' OR 1=1 --",               // SQL injection attempt
    ];
    let long_token = "A".repeat(10_000);

    for token in garbage_tokens
        .iter()
        .chain(std::iter::once(&long_token.as_str()))
    {
        let resp = server
            .get(&format!("/orgs/{org_id}/secrets"))
            .authorization_bearer(token)
            .await;
        assert!(
            resp.status_code() == axum::http::StatusCode::UNAUTHORIZED
                || resp.status_code() == axum::http::StatusCode::FORBIDDEN,
            "token {:?} should be rejected, got {}",
            &token[..token.len().min(40)],
            resp.status_code()
        );
    }
}

// ── BB-02: Cross-org secret access (IDOR) ───────────────────────────────────

#[tokio::test]
async fn bb02_cross_org_idor_all_methods() {
    let (server, store, _dir) = build_test_app();

    // Org A has a secret.
    let (org_a, _, key_a) = bootstrap_org_with_key(&store, "bb02a", "alice", "admin");
    // Org B attacker.
    let (_org_b, _, key_b) = bootstrap_org_with_key(&store, "bb02b", "eve", "admin");

    // Alice creates a secret.
    let resp = server
        .post(&format!("/orgs/{org_a}/secrets"))
        .authorization_bearer(&key_a)
        .json(&json!({"key": "CROWN_JEWEL", "value": "top-secret-data", "delete": false}))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // Eve tries every HTTP method to access Alice's secret.

    // GET
    let resp = server
        .get(&format!("/orgs/{org_a}/secrets/CROWN_JEWEL"))
        .authorization_bearer(&key_b)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // HEAD
    let resp = server
        .method(Method::HEAD, &format!("/orgs/{org_a}/secrets/CROWN_JEWEL"))
        .authorization_bearer(&key_b)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // PATCH
    let resp = server
        .patch(&format!("/orgs/{org_a}/secrets/CROWN_JEWEL"))
        .authorization_bearer(&key_b)
        .json(&json!({"value": "pwned"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // DELETE
    let resp = server
        .delete(&format!("/orgs/{org_a}/secrets/CROWN_JEWEL"))
        .authorization_bearer(&key_b)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // LIST
    let resp = server
        .get(&format!("/orgs/{org_a}/secrets"))
        .authorization_bearer(&key_b)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Verify Alice's secret is still intact.
    let resp = server
        .get(&format!("/orgs/{org_a}/secrets/CROWN_JEWEL"))
        .authorization_bearer(&key_a)
        .await;
    resp.assert_status(axum::http::StatusCode::OK);
    assert_eq!(resp.json::<Value>()["value"], "top-secret-data");
}

// ── BB-03: Cross-org management (IDOR on principals/roles) ──────────────────

#[tokio::test]
async fn bb03_cross_org_management_idor() {
    let (server, store, _dir) = build_test_app();

    let (org_a, _, _key_a) = bootstrap_org_with_key(&store, "bb03a", "alice", "admin");
    let (_org_b, _, key_b) = bootstrap_org_with_key(&store, "bb03b", "eve", "admin");

    // Eve tries to create a principal in Alice's org.
    let resp = server
        .post(&format!("/orgs/{org_a}/principals"))
        .authorization_bearer(&key_b)
        .json(&json!({"name": "backdoor", "role": "admin"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Eve tries to list Alice's principals.
    let resp = server
        .get(&format!("/orgs/{org_a}/principals"))
        .authorization_bearer(&key_b)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Eve tries to create a role in Alice's org.
    let resp = server
        .post(&format!("/orgs/{org_a}/roles"))
        .authorization_bearer(&key_b)
        .json(&json!({"name": "god-mode", "permissions": "rRlLcCpPaAmMdD"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Eve tries to list Alice's roles.
    let resp = server
        .get(&format!("/orgs/{org_a}/roles"))
        .authorization_bearer(&key_b)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);
}

// ── BB-04: Privilege escalation — reader tries write operations ─────────────

#[tokio::test]
async fn bb04_privilege_escalation_reader() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "bb04", "writer-user", "writer");

    // Create a reader.
    let now = now_secs();
    store
        .put_principal(&PrincipalRecord {
            id: "p_reader".into(),
            org_id: org_id.clone(),
            name: "reader-user".into(),
            role: "reader".into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();
    let reader_key = add_named_key(&store, &org_id, "p_reader", "pk_reader", "reader_key");

    // Writer creates a secret.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&writer_key)
        .json(&json!({"key": "DATA", "value": "secret-value", "delete": false}))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // Reader tries to CREATE a secret.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&reader_key)
        .json(&json!({"key": "EVIL", "value": "injected"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Reader tries to PATCH a secret.
    let resp = server
        .patch(&format!("/orgs/{org_id}/secrets/DATA"))
        .authorization_bearer(&reader_key)
        .json(&json!({"value": "tampered"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Reader tries to DELETE a secret.
    let resp = server
        .delete(&format!("/orgs/{org_id}/secrets/DATA"))
        .authorization_bearer(&reader_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Reader tries to manage principals.
    let resp = server
        .post(&format!("/orgs/{org_id}/principals"))
        .authorization_bearer(&reader_key)
        .json(&json!({"name": "evil-principal", "role": "admin"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Reader tries to create a role.
    let resp = server
        .post(&format!("/orgs/{org_id}/roles"))
        .authorization_bearer(&reader_key)
        .json(&json!({"name": "superuser", "permissions": "rRlLcCpPaAmMdD"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);
}

// ── BB-05: Principal key tries to use master-only routes ────────────────────

#[tokio::test]
async fn bb05_principal_key_on_master_routes() {
    let (server, store, _dir) = build_test_app();

    let (_, _, principal_key) = bootstrap_org_with_key(&store, "bb05", "agent", "admin");

    // Principal key tries to use public-bucket write routes (master-only).
    let resp = server
        .post("/secrets")
        .authorization_bearer(&principal_key)
        .json(&json!({"key": "STOLEN", "value": "data"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::UNAUTHORIZED);

    // List public secrets.
    let resp = server
        .get("/secrets")
        .authorization_bearer(&principal_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::UNAUTHORIZED);

    // Audit log.
    let resp = server
        .get("/audit")
        .authorization_bearer(&principal_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::UNAUTHORIZED);

    // Prune public bucket.
    let resp = server
        .post("/prune")
        .authorization_bearer(&principal_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::UNAUTHORIZED);
}

// ── BB-06: Path traversal in key names ──────────────────────────────────────

#[tokio::test]
async fn bb06_path_traversal_in_key_names() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, key) = bootstrap_org_with_key(&store, "bb06", "user", "writer");

    let evil_keys = [
        "../../../etc/passwd",
        "..%2F..%2F..%2Fetc%2Fpasswd",
        "key/../../secret",
        "key\x00evil",
        "%00",
        "key/../admin",
        "org_bb06:HIDDEN", // try compound key injection
    ];

    for evil_key in &evil_keys {
        let resp = server
            .post(&format!("/orgs/{org_id}/secrets"))
            .authorization_bearer(&key)
            .json(&json!({"key": evil_key, "value": "x"}))
            .await;
        assert!(
            resp.status_code() == axum::http::StatusCode::BAD_REQUEST
                || resp.status_code() == axum::http::StatusCode::UNPROCESSABLE_ENTITY,
            "key {evil_key:?} should be rejected, got {}",
            resp.status_code()
        );
    }
}

// ── BB-07: Org ID injection/traversal ───────────────────────────────────────

#[tokio::test]
async fn bb07_org_id_traversal() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, key) = bootstrap_org_with_key(&store, "bb07", "user", "writer");

    // Push a real secret.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&key)
        .json(&json!({"key": "SAFE", "value": "safe-value"}))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // Try to access via crafted org_id that might match compound key pattern.
    // Internally compound key is "{org_id}:{secret_key}" — try to bypass.
    let resp = server
        .get(&format!("/orgs/{org_id}:SAFE/secrets/ANYTHING"))
        .authorization_bearer(&key)
        .await;
    // Should fail: org_id from key won't match the crafted path.
    assert_ne!(resp.status_code(), axum::http::StatusCode::OK);
}

// ── BB-08: Expired key should not authenticate ──────────────────────────────

#[tokio::test]
async fn bb08_expired_key_rejected() {
    let (server, store, _dir) = build_test_app();

    let (org_id, principal_id, _valid_key) =
        bootstrap_org_with_key(&store, "bb08", "user", "writer");

    let expired_key = add_expired_key(&store, &org_id, &principal_id, "pk_exp", "expired_key");

    // Try to use expired key.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&expired_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::UNAUTHORIZED);
}

// ── BB-09: Not-yet-valid key should not authenticate ────────────────────────

#[tokio::test]
async fn bb09_not_yet_valid_key_rejected() {
    let (server, store, _dir) = build_test_app();

    let (org_id, principal_id, _valid_key) =
        bootstrap_org_with_key(&store, "bb09", "user", "writer");

    let future_key =
        add_not_yet_valid_key(&store, &org_id, &principal_id, "pk_future", "future_key");

    let resp = server
        .get(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&future_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::UNAUTHORIZED);
}

// ── BB-10: Mass enumeration attempt ─────────────────────────────────────────

#[tokio::test]
async fn bb10_secret_enumeration_blocked() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "bb10", "writer", "writer");

    // Create reader in same org.
    let now = now_secs();
    store
        .put_principal(&PrincipalRecord {
            id: "p_reader_enum".into(),
            org_id: org_id.clone(),
            name: "reader-enum".into(),
            role: "reader".into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();
    let reader_key = add_named_key(&store, &org_id, "p_reader_enum", "pk_renum", "reader_key");

    // Writer creates several secrets.
    for i in 0..5 {
        server
            .post(&format!("/orgs/{org_id}/secrets"))
            .authorization_bearer(&writer_key)
            .json(&json!({"key": format!("SECRET_{i}"), "value": format!("val_{i}")}))
            .await;
    }

    // Reader lists — should only see own (none, because reader created none).
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&reader_key)
        .await;
    resp.assert_status(axum::http::StatusCode::OK);
    let body: Value = resp.json();
    let secrets = body["secrets"].as_array().unwrap();
    assert_eq!(
        secrets.len(),
        0,
        "reader with ListMy should not see writer's secrets in list"
    );
}

// ── BB-11: No auth header at all on every protected route ───────────────────

#[tokio::test]
async fn bb11_no_auth_header_everywhere() {
    let (server, _store, _dir) = build_test_app();

    let routes_401 = [
        ("GET", "/orgs"),
        ("POST", "/orgs"),
        ("POST", "/orgs/x/principals"),
        ("GET", "/orgs/x/principals"),
        ("POST", "/orgs/x/roles"),
        ("GET", "/orgs/x/roles"),
        ("GET", "/me"),
        ("POST", "/me/keys"),
        ("GET", "/orgs/x/secrets"),
        ("POST", "/orgs/x/secrets"),
        ("GET", "/orgs/x/secrets/KEY"),
        ("DELETE", "/orgs/x/secrets/KEY"),
        ("GET", "/orgs/x/audit"),
        ("POST", "/secrets"),
        ("GET", "/secrets"),
        ("GET", "/audit"),
    ];

    for (method, path) in routes_401 {
        let resp = match method {
            "GET" => server.get(path).await,
            "POST" => server.post(path).json(&json!({})).await,
            "DELETE" => server.delete(path).await,
            _ => unreachable!(),
        };
        assert_eq!(
            resp.status_code(),
            axum::http::StatusCode::UNAUTHORIZED,
            "{method} {path} should return 401 without auth, got {}",
            resp.status_code()
        );
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
//  GRAY BOX TESTS — informed by code review, targeting specific weaknesses
// ═══════════════════════════════════════════════════════════════════════════════

// ── GB-01: HEAD bypasses key binding (FINDING: info leak) ───────────────────

#[tokio::test]
async fn gb01_head_bypasses_key_binding() {
    let (server, store, _dir) = build_test_app();

    let (org_id, principal_id, _default_key) =
        bootstrap_org_with_key(&store, "gb01", "user", "writer");

    let deploy_key = add_named_key(&store, &org_id, &principal_id, "pk_deploy", "deploy_key");
    let ci_key = add_named_key(&store, &org_id, &principal_id, "pk_ci", "ci_key");

    // Create secret bound to deploy_key only.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&deploy_key)
        .json(&json!({
            "key": "BOUND_SECRET",
            "value": "secret-value",
            "allowed_keys": ["deploy_key"]
        }))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // ci_key should NOT be able to GET the secret.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/BOUND_SECRET"))
        .authorization_bearer(&ci_key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::FORBIDDEN,
        "GET with wrong key should be forbidden"
    );

    // FIXED: HEAD now checks key binding — ci_key should be forbidden.
    let resp = server
        .method(
            Method::HEAD,
            &format!("/orgs/{org_id}/secrets/BOUND_SECRET"),
        )
        .authorization_bearer(&ci_key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::FORBIDDEN,
        "HEAD should enforce key binding (GB-01 fix)"
    );
}

// ── GB-02: ReadMy without ownership check on GET (FINDING: BROKEN ACCESS) ───

#[tokio::test]
async fn gb02_readmy_reads_others_secrets() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "gb02", "writer", "writer");

    // Create a second writer in same org.
    let now = now_secs();
    store
        .put_principal(&PrincipalRecord {
            id: "p_other".into(),
            org_id: org_id.clone(),
            name: "other-writer".into(),
            role: "writer".into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();
    let other_key = add_named_key(&store, &org_id, "p_other", "pk_other", "other_key");

    // Writer #1 creates a secret.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&writer_key)
        .json(&json!({"key": "PRIVATE_SECRET", "value": "writer1-owns-this"}))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // FIXED: Writer #2 (has ReadMy but NOT ReadOrg) cannot read writer #1's secret.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/PRIVATE_SECRET"))
        .authorization_bearer(&other_key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::FORBIDDEN,
        "ReadMy should not grant access to other principals' secrets (GB-02 fix)"
    );
}

// ── GB-03: DeleteMy without ownership check (FINDING: BROKEN ACCESS) ────────

#[tokio::test]
async fn gb03_deletemy_deletes_others_secrets() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "gb03", "writer", "writer");

    // Second writer in same org.
    let now = now_secs();
    store
        .put_principal(&PrincipalRecord {
            id: "p_attacker".into(),
            org_id: org_id.clone(),
            name: "attacker".into(),
            role: "writer".into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();
    let attacker_key = add_named_key(&store, &org_id, "p_attacker", "pk_attacker", "atk_key");

    // Writer creates a secret.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&writer_key)
        .json(&json!({"key": "VICTIM_SECRET", "value": "do-not-delete"}))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // FIXED: Attacker (has DeleteMy but NOT DeleteOrg) cannot delete victim's secret.
    let resp = server
        .delete(&format!("/orgs/{org_id}/secrets/VICTIM_SECRET"))
        .authorization_bearer(&attacker_key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::FORBIDDEN,
        "DeleteMy should not grant access to other principals' secrets (GB-03 fix)"
    );

    // Verify secret still exists.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/VICTIM_SECRET"))
        .authorization_bearer(&writer_key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::OK,
        "victim's secret should still exist after blocked delete"
    );
}

// ── GB-04: PatchMy without ownership check (FINDING: BROKEN ACCESS) ─────────

#[tokio::test]
async fn gb04_patchmy_patches_others_secrets() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "gb04", "writer", "writer");

    // Second writer.
    let now = now_secs();
    store
        .put_principal(&PrincipalRecord {
            id: "p_tamper".into(),
            org_id: org_id.clone(),
            name: "tamper".into(),
            role: "writer".into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();
    let tamper_key = add_named_key(&store, &org_id, "p_tamper", "pk_tamper", "tamper_key");

    // Writer creates a sealable secret (delete=false allows PATCH).
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&writer_key)
        .json(&json!({"key": "PATCHABLE", "value": "original", "delete": false}))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // FIXED: Tamper (has PatchMy but NOT PatchOrg) cannot patch victim's secret.
    let resp = server
        .patch(&format!("/orgs/{org_id}/secrets/PATCHABLE"))
        .authorization_bearer(&tamper_key)
        .json(&json!({"value": "TAMPERED-VALUE"}))
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::FORBIDDEN,
        "PatchMy should not grant access to other principals' secrets (GB-04 fix)"
    );

    // Verify original value unchanged.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/PATCHABLE"))
        .authorization_bearer(&writer_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::OK);
    assert_eq!(
        resp.json::<Value>()["value"],
        "original",
        "secret value should be unchanged after blocked patch"
    );
}

// ── GB-05: SirrAdmin bit injection via custom role ──────────────────────────

#[tokio::test]
async fn gb05_sirr_admin_bit_injection() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, admin_key) = bootstrap_org_with_key(&store, "gb05", "admin", "admin");

    // Admin tries to create a role with S (SirrAdmin) bit — should be rejected.
    let resp = server
        .post(&format!("/orgs/{org_id}/roles"))
        .authorization_bearer(&admin_key)
        .json(&json!({"name": "godmode", "permissions": "rRlLcCpPaAmMSdD"}))
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::BAD_REQUEST,
        "SirrAdmin bit in org-scoped role should be rejected"
    );

    // Try sneaky: just S alone.
    let resp = server
        .post(&format!("/orgs/{org_id}/roles"))
        .authorization_bearer(&admin_key)
        .json(&json!({"name": "sneaky", "permissions": "S"}))
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::BAD_REQUEST,
        "S-only role should be rejected"
    );

    // Try with master key — even master should not create org-scoped role with S.
    let resp = server
        .post(&format!("/orgs/{org_id}/roles"))
        .authorization_bearer(MASTER_KEY)
        .json(&json!({"name": "master-god", "permissions": "rRlLcCpPaAmMSdD"}))
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::BAD_REQUEST,
        "master should not be able to create org role with S bit"
    );
}

// ── GB-06: Writer tries to escalate to admin via org management ─────────────

#[tokio::test]
async fn gb06_writer_cannot_manage_org() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "gb06", "writer", "writer");

    // Writer should not be able to create principals.
    let resp = server
        .post(&format!("/orgs/{org_id}/principals"))
        .authorization_bearer(&writer_key)
        .json(&json!({"name": "evil", "role": "admin"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Writer should not be able to create roles.
    let resp = server
        .post(&format!("/orgs/{org_id}/roles"))
        .authorization_bearer(&writer_key)
        .json(&json!({"name": "superuser", "permissions": "rRlLcCpPaAmMdD"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Writer should not be able to list orgs (master-only).
    let resp = server.get("/orgs").authorization_bearer(&writer_key).await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Writer should not be able to create orgs.
    let resp = server
        .post("/orgs")
        .authorization_bearer(&writer_key)
        .json(&json!({"name": "evil-org"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Writer should not be able to delete their org.
    let resp = server
        .delete(&format!("/orgs/{org_id}"))
        .authorization_bearer(&writer_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);
}

// ── GB-07: Deleted principal's key should not authenticate ──────────────────

#[tokio::test]
async fn gb07_deleted_principal_key_rejected() {
    let (server, store, _dir) = build_test_app();

    let (org_id, principal_id, key) = bootstrap_org_with_key(&store, "gb07", "temp-user", "writer");

    // Push a secret first.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&key)
        .json(&json!({"key": "TEMP", "value": "temp-data"}))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // Delete the principal's key, then the principal.
    store
        .delete_principal_key(&principal_id, "pk_temp-user")
        .unwrap();
    store.delete_principal(&org_id, &principal_id).unwrap();

    // Try to use the deleted principal's key.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/TEMP"))
        .authorization_bearer(&key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::UNAUTHORIZED,
        "deleted principal's key should not authenticate"
    );
}

// ── GB-08: Compound key injection in org_id ─────────────────────────────────

#[tokio::test]
async fn gb08_compound_key_injection() {
    let (server, store, _dir) = build_test_app();

    // Create org "target" with a secret.
    let (target_org, _, target_key) = bootstrap_org_with_key(&store, "target", "user", "writer");
    server
        .post(&format!("/orgs/{target_org}/secrets"))
        .authorization_bearer(&target_key)
        .json(&json!({"key": "TROPHY", "value": "you-win"}))
        .await;

    // Attacker org: try to create org_id that, when combined with key as
    // "{org_id}:{key}", would collide with target's compound key.
    // Target compound key = "org_target:TROPHY"
    // Attacker tries org_id = "org_target" and key = "TROPHY" but from wrong org.
    let (_atk_org, _, atk_key) = bootstrap_org_with_key(&store, "evil", "attacker", "writer");

    // Direct attempt: use target's org_id in path.
    let resp = server
        .get(&format!("/orgs/{target_org}/secrets/TROPHY"))
        .authorization_bearer(&atk_key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::FORBIDDEN,
        "attacker's key should not access target's org via path"
    );

    // Sneaky: try org_id containing a colon to mess with compound key format.
    // e.g., org_id = "org_target:TROPHY" + key = "anything"
    // This would make compound key = "org_target:TROPHY:anything" which won't match.
    // But test that the auth check catches it first.
    let resp = server
        .get("/orgs/org_target:TROPHY/secrets/anything")
        .authorization_bearer(&atk_key)
        .await;
    assert_ne!(
        resp.status_code(),
        axum::http::StatusCode::OK,
        "colon-in-org-id should not bypass isolation"
    );
}

// ── GB-09: Master key cannot access org secrets directly ────────────────────

#[tokio::test]
async fn gb09_master_key_no_org_secret_access() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "gb09", "writer", "writer");

    // Writer creates an org secret.
    server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&writer_key)
        .json(&json!({"key": "ORG_SECRET", "value": "org-value"}))
        .await;

    // Master key tries to read org secret.
    // Master has no org_id → auth.org_id() == None != Some("org_gb09") → 403.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/ORG_SECRET"))
        .authorization_bearer(MASTER_KEY)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::FORBIDDEN,
        "master key should not be able to read org secrets (no org context)"
    );

    // Master key tries to list org secrets.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(MASTER_KEY)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Master key tries to create org secret.
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(MASTER_KEY)
        .json(&json!({"key": "INJECTED", "value": "evil"}))
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);

    // Master key tries to delete org secret.
    let resp = server
        .delete(&format!("/orgs/{org_id}/secrets/ORG_SECRET"))
        .authorization_bearer(MASTER_KEY)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);
}

// ── GB-10: Timing oracle on master key comparison ───────────────────────────

#[tokio::test]
async fn gb10_master_key_uses_constant_time_eq() {
    // This is a code-level assertion, not a timing attack.
    // The auth.rs code uses `constant_time_eq` for master key comparison.
    // We verify by checking that near-miss keys still get 401 (not 403 or timing diff).
    let (server, _store, _dir) = build_test_app();

    // Near-miss keys should all get identical 401 responses.
    // Note: cannot test null-byte appended key via axum-test (invalid HeaderValue).
    let near_miss_keys = [
        "test-master-key-12344",  // last digit off
        "test-master-key-12346",  // last digit off
        "test-master-key-1234",   // one short
        "Aest-master-key-12345",  // first char off
        "test-master-key-12345x", // extra char appended
    ];

    for key in &near_miss_keys {
        let resp = server.get("/orgs").authorization_bearer(key).await;
        assert_eq!(
            resp.status_code(),
            axum::http::StatusCode::UNAUTHORIZED,
            "near-miss key {key:?} should get 401"
        );
    }
}

// ── GB-11: Metadata validation — oversized and over-count ───────────────────

#[tokio::test]
async fn gb11_metadata_validation() {
    let (server, _store, _dir) = build_test_app();

    // Try to create org with too many metadata keys (>10).
    let mut big_meta = HashMap::new();
    for i in 0..15 {
        big_meta.insert(format!("key_{i}"), format!("val_{i}"));
    }

    let resp = server
        .post("/orgs")
        .authorization_bearer(MASTER_KEY)
        .json(&json!({"name": "bad-org", "metadata": big_meta}))
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::BAD_REQUEST,
        "metadata with >10 keys should be rejected"
    );

    // Try with oversized value (>100 bytes).
    let mut big_val_meta = HashMap::new();
    big_val_meta.insert("key", "A".repeat(200));

    let resp = server
        .post("/orgs")
        .authorization_bearer(MASTER_KEY)
        .json(&json!({"name": "bad-org2", "metadata": big_val_meta}))
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::BAD_REQUEST,
        "metadata with >100-byte value should be rejected"
    );
}

// ── GB-12: Org deletion blocked if principals exist ─────────────────────────

#[tokio::test]
async fn gb12_org_deletion_requires_cleanup() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, _key) = bootstrap_org_with_key(&store, "gb12", "user", "writer");

    // Try to delete org with active principal.
    let resp = server
        .delete(&format!("/orgs/{org_id}"))
        .authorization_bearer(MASTER_KEY)
        .await;

    // Should fail: principal still exists.
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::CONFLICT,
        "org with active principals should not be deletable"
    );
}

// ── GB-13: Public bucket read is truly unauthenticated ──────────────────────

#[tokio::test]
async fn gb13_public_bucket_no_auth_reads() {
    let (server, _store, _dir) = build_test_app();

    // Push to public bucket.
    server
        .post("/secrets")
        .authorization_bearer(MASTER_KEY)
        .json(&json!({"key": "PUBLIC_TOKEN", "value": "share-me"}))
        .await;

    // Read without any auth header.
    let resp = server.get("/secrets/PUBLIC_TOKEN").await;
    resp.assert_status(axum::http::StatusCode::OK);
    assert_eq!(resp.json::<Value>()["value"], "share-me");

    // HEAD without auth.
    let resp = server.method(Method::HEAD, "/secrets/PUBLIC_TOKEN").await;
    resp.assert_status(axum::http::StatusCode::OK);

    // But LIST requires master key.
    let resp = server.get("/secrets").await;
    resp.assert_status(axum::http::StatusCode::UNAUTHORIZED);
}

// ── GB-14: Public and org namespaces truly isolated ─────────────────────────

#[tokio::test]
async fn gb14_namespace_isolation() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, key) = bootstrap_org_with_key(&store, "gb14", "user", "writer");

    // Push same key "SHARED" to both namespaces.
    server
        .post("/secrets")
        .authorization_bearer(MASTER_KEY)
        .json(&json!({"key": "SHARED", "value": "public-v"}))
        .await;

    server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&key)
        .json(&json!({"key": "SHARED", "value": "org-v"}))
        .await;

    // Public read.
    let resp = server.get("/secrets/SHARED").await;
    assert_eq!(resp.json::<Value>()["value"], "public-v");

    // Org read.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/SHARED"))
        .authorization_bearer(&key)
        .await;
    assert_eq!(resp.json::<Value>()["value"], "org-v");

    // Delete public — org should survive.
    server
        .delete("/secrets/SHARED")
        .authorization_bearer(MASTER_KEY)
        .await;

    let resp = server.get("/secrets/SHARED").await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::NOT_FOUND);

    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/SHARED"))
        .authorization_bearer(&key)
        .await;
    assert_eq!(resp.json::<Value>()["value"], "org-v");
}

// ── GB-15: Multiple key bindings — all or nothing ───────────────────────────

#[tokio::test]
async fn gb15_key_binding_all_or_nothing() {
    let (server, store, _dir) = build_test_app();

    let (org_id, principal_id, _default) = bootstrap_org_with_key(&store, "gb15", "user", "writer");

    let key_a = add_named_key(&store, &org_id, &principal_id, "pk_a", "key_a");
    let key_b = add_named_key(&store, &org_id, &principal_id, "pk_b", "key_b");
    let key_c = add_named_key(&store, &org_id, &principal_id, "pk_c", "key_c");

    // Secret allows key_a and key_b but NOT key_c.
    server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&key_a)
        .json(&json!({
            "key": "MULTI_BOUND",
            "value": "allowed-data",
            "allowed_keys": ["key_a", "key_b"]
        }))
        .await;

    // key_a → OK
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/MULTI_BOUND"))
        .authorization_bearer(&key_a)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::OK);

    // key_b → OK
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/MULTI_BOUND"))
        .authorization_bearer(&key_b)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::OK);

    // key_c → Forbidden
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/MULTI_BOUND"))
        .authorization_bearer(&key_c)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::FORBIDDEN);
}

// ── GB-16: Secret with no allowed_keys is open to all org keys ──────────────

#[tokio::test]
async fn gb16_no_binding_means_open() {
    let (server, store, _dir) = build_test_app();

    let (org_id, principal_id, default_key) =
        bootstrap_org_with_key(&store, "gb16", "user", "writer");

    let extra_key = add_named_key(&store, &org_id, &principal_id, "pk_extra", "extra_key");

    // Secret with no allowed_keys.
    server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&default_key)
        .json(&json!({"key": "OPEN_SECRET", "value": "anyone-in-org"}))
        .await;

    // Both keys should be able to read.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/OPEN_SECRET"))
        .authorization_bearer(&default_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::OK);

    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/OPEN_SECRET"))
        .authorization_bearer(&extra_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::OK);
}

// ── GB-17: Admin can read other principals' secrets ─────────────────────────

#[tokio::test]
async fn gb17_admin_reads_all_org_secrets() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, writer_key) = bootstrap_org_with_key(&store, "gb17w", "writer", "writer");

    // Fix: both principals in same org.
    let now = now_secs();
    store
        .put_principal(&PrincipalRecord {
            id: "p_admin_gb17".into(),
            org_id: org_id.clone(),
            name: "admin-user".into(),
            role: "admin".into(),
            metadata: HashMap::new(),
            created_at: now,
        })
        .unwrap();
    let admin_key = add_named_key(&store, &org_id, "p_admin_gb17", "pk_adm17", "admin_key");

    // Writer creates a secret.
    server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&writer_key)
        .json(&json!({"key": "WRITER_SECRET", "value": "hidden-from-peers"}))
        .await;

    // Admin (has ReadOrg) should see it.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/WRITER_SECRET"))
        .authorization_bearer(&admin_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::OK);
    assert_eq!(resp.json::<Value>()["value"], "hidden-from-peers");

    // Admin lists all secrets.
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&admin_key)
        .await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::OK);
    let body: Value = resp.json();
    let secrets = body["secrets"].as_array().unwrap();
    assert!(
        secrets.iter().any(|s| s["key"] == "WRITER_SECRET"),
        "admin should see writer's secret in list"
    );
}

// ── GB-18: Empty allowed_keys array behavior ────────────────────────────────

#[tokio::test]
async fn gb18_empty_allowed_keys_array() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, key) = bootstrap_org_with_key(&store, "gb18", "user", "writer");

    // Create secret with empty allowed_keys array — what happens?
    let resp = server
        .post(&format!("/orgs/{org_id}/secrets"))
        .authorization_bearer(&key)
        .json(&json!({
            "key": "EMPTY_BIND",
            "value": "edge-case",
            "allowed_keys": []
        }))
        .await;
    resp.assert_status(axum::http::StatusCode::CREATED);

    // FIXED: Empty allowed_keys=[] is treated as "no restriction" (same as None).
    let resp = server
        .get(&format!("/orgs/{org_id}/secrets/EMPTY_BIND"))
        .authorization_bearer(&key)
        .await;
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::OK,
        "empty allowed_keys=[] should be treated as no restriction (GB-18 fix)"
    );
}

// ── GB-19: Role deletion while in use ───────────────────────────────────────

#[tokio::test]
async fn gb19_role_deletion_while_in_use() {
    let (server, store, _dir) = build_test_app();

    let (org_id, _, _key) = bootstrap_org_with_key(&store, "gb19", "admin", "admin");

    // Create a custom role directly via store.
    use sirr_server::store::org::RoleRecord;
    use sirr_server::store::permissions::Permissions;
    store
        .put_role(&RoleRecord {
            name: "custom-writer".into(),
            org_id: Some(org_id.clone()),
            permissions: Permissions::parse("rlcd").unwrap(),
            built_in: false,
            created_at: now_secs(),
        })
        .unwrap();

    // Create a principal using the custom role (bypass license check via store).
    store
        .put_principal(&PrincipalRecord {
            id: "p_custom_user".into(),
            org_id: org_id.clone(),
            name: "custom-user".into(),
            role: "custom-writer".into(),
            metadata: HashMap::new(),
            created_at: now_secs(),
        })
        .unwrap();

    // Try to delete the role while it's in use.
    let resp = server
        .delete(&format!("/orgs/{org_id}/roles/custom-writer"))
        .authorization_bearer(MASTER_KEY)
        .await;

    // Should be blocked: role is in use.
    assert_eq!(
        resp.status_code(),
        axum::http::StatusCode::CONFLICT,
        "should not be able to delete role while principals use it"
    );
}

// ── GB-20: Built-in role deletion blocked ───────────────────────────────────

#[tokio::test]
async fn gb20_builtin_role_deletion_blocked() {
    let (server, _store, _dir) = build_test_app();

    // Try to delete built-in roles via master key.
    for role in &["reader", "writer", "admin", "owner"] {
        // Built-in roles have no org_id, so try the path without org too.
        // The handler expects /orgs/{org_id}/roles/{name}, so try org_id = "global".
        let resp = server
            .delete(&format!("/orgs/any_org/roles/{role}"))
            .authorization_bearer(MASTER_KEY)
            .await;
        // Should either be forbidden (wrong org) or conflict (built-in).
        assert_ne!(
            resp.status_code(),
            axum::http::StatusCode::OK,
            "should not be able to delete built-in role {role}"
        );
    }
}

// ── GB-21: Audit log records failed read attempts ───────────────────────────

#[tokio::test]
async fn gb21_audit_records_failures() {
    let (server, _store, _dir) = build_test_app();

    // Use public bucket to test audit (global /audit endpoint has org_id: None).
    // Push a public secret then read a non-existent one.
    server
        .post("/secrets")
        .authorization_bearer(MASTER_KEY)
        .json(&json!({"key": "EXISTS", "value": "v"}))
        .await;

    // Read non-existent public secret (no auth needed for public reads).
    let resp = server.get("/secrets/NOPE").await;
    assert_eq!(resp.status_code(), axum::http::StatusCode::NOT_FOUND);

    // Check global audit log.
    let resp = server.get("/audit").authorization_bearer(MASTER_KEY).await;
    resp.assert_status(axum::http::StatusCode::OK);
    let body: Value = resp.json();
    let events = body["events"].as_array().unwrap();

    // Should have at least the failed read logged.
    let has_failed = events
        .iter()
        .any(|e| e["action"] == "secret.read" && e["success"] == false);
    assert!(
        has_failed,
        "audit log should record failed read attempts. Events: {:?}",
        events
    );
}
