//! BENV-VAL — Input-validation contract pin for `EnvSecretBroker`.
//!
//! Scope: lock the broker's behavior on edge-case keys so future refactors
//! cannot silently regress the supervisor's exposure to bad operator input.
//!
//! Each test pins one axis. Where the broker deliberately delegates a check to
//! `std::env::var`, the test pins the *resulting* observable behavior (typed
//! error, not a panic) so we catch any change in delegation.
//!
//! Hard contract guarantees pinned here:
//!
//! * **Empty key** is rejected with `CellosError::SecretBroker` (no panic, no
//!   accidental `CELLOS_SECRET_` lookup).
//! * **NUL byte in key** is rejected — `std::env::var` would otherwise panic.
//! * **`=` in key** is rejected — `std::env::var` would otherwise panic.
//! * **Very long key** (≫ typical max env var name) yields a typed
//!   `SecretBroker` error (NotPresent surfaces as a typed error, not a
//!   panic and not a crash on overlong allocation).
//! * **Unset var** yields a typed `CellosError::SecretBroker`, never a panic.
//! * **Set var** resolves to the exact value (with [`zeroize::Zeroizing`]
//!   wrapping preserved).
//! * **Error path does NOT leak the value** — even if a partially-set value
//!   is involved, the error message references only the env var **name**.
//! * **Trait object Send + Sync** — broker is usable behind `Arc<dyn …>`
//!   across `await` points.
//!
//! Allowlist note: the env broker has no allowlist by design. Keys are
//! mapped 1:1 to `CELLOS_SECRET_<UPPER>` and the host env is the source of
//! truth. The "no allowlist" axis is therefore pinned implicitly: any
//! operator-supplied key that survives [`EnvSecretBroker::validate_key`]
//! reaches `std::env::var`. If a future change introduces an allowlist,
//! tests in this file MUST be updated to reflect the new contract.

use std::sync::Arc;

use cellos_broker_env::EnvSecretBroker;
use cellos_core::ports::SecretBroker;
use cellos_core::CellosError;

/// Per-test env var names (test binary shares one process; do not collide).
const VAL_PRESENT_KEY: &str = "VAL_PRESENT_TOKEN";
const VAL_PRESENT_ENV: &str = "CELLOS_SECRET_VAL_PRESENT_TOKEN";
const VAL_PRESENT_VALUE: &str = "value-do-not-leak";

const VAL_MISSING_KEY: &str = "VAL_DEFINITELY_NOT_SET_FOR_BENV_VAL";
const VAL_MISSING_ENV: &str = "CELLOS_SECRET_VAL_DEFINITELY_NOT_SET_FOR_BENV_VAL";

#[tokio::test]
async fn empty_key_is_rejected_with_typed_error() {
    let broker = EnvSecretBroker::new();
    let err = broker
        .resolve("", "cell-val-empty", 60)
        .await
        .expect_err("empty key must yield a typed error, not panic and not Ok");
    match err {
        CellosError::SecretBroker(msg) => {
            assert!(
                msg.to_lowercase().contains("empty"),
                "expected message to mention empty, got: {msg}"
            );
        }
        other => panic!("expected CellosError::SecretBroker, got {other:?}"),
    }
}

#[tokio::test]
async fn key_with_nul_byte_is_rejected_not_panicked() {
    // Without validation, `std::env::var` would panic with
    // "key contains NUL character". The broker MUST convert that to a typed
    // SecretBroker error so the supervisor's panic path is not weaponizable
    // by malformed operator input.
    let bad_key = "BAD\0KEY";
    let broker = EnvSecretBroker::new();
    let err = broker
        .resolve(bad_key, "cell-val-nul", 60)
        .await
        .expect_err("NUL-byte key must yield a typed error, not panic");
    match err {
        CellosError::SecretBroker(msg) => {
            assert!(
                msg.to_uppercase().contains("NUL"),
                "expected message to mention NUL, got: {msg}"
            );
            // Defense-in-depth: ensure the (possibly-set) value is never echoed.
            assert!(
                !msg.contains(VAL_PRESENT_VALUE),
                "error message must not leak any secret value, got: {msg}"
            );
        }
        other => panic!("expected CellosError::SecretBroker, got {other:?}"),
    }
}

#[tokio::test]
async fn key_with_equals_is_rejected_not_panicked() {
    // `std::env::var` panics on '=' in the key. The broker must reject it.
    let bad_key = "BAD=KEY";
    let broker = EnvSecretBroker::new();
    let err = broker
        .resolve(bad_key, "cell-val-eq", 60)
        .await
        .expect_err("key with '=' must yield a typed error, not panic");
    match err {
        CellosError::SecretBroker(msg) => {
            assert!(
                msg.contains('='),
                "expected message to mention '=', got: {msg}"
            );
        }
        other => panic!("expected CellosError::SecretBroker, got {other:?}"),
    }
}

#[tokio::test]
async fn very_long_key_is_handled_without_panic() {
    // 4 KiB key — well past any sensible env var name length.
    // Either the broker rejects, or std::env::var returns NotPresent.
    // Either way: typed error, no panic, no allocation crash.
    let long_key: String = "A".repeat(4096);
    let broker = EnvSecretBroker::new();
    let err = broker
        .resolve(&long_key, "cell-val-long", 60)
        .await
        .expect_err("overlong key must yield a typed error");
    match err {
        CellosError::SecretBroker(_) => {}
        other => panic!("expected CellosError::SecretBroker, got {other:?}"),
    }
}

#[tokio::test]
async fn unset_var_returns_typed_error_not_panic() {
    std::env::remove_var(VAL_MISSING_ENV);
    let broker = EnvSecretBroker::new();
    let err = broker
        .resolve(VAL_MISSING_KEY, "cell-val-unset", 60)
        .await
        .expect_err("unset var must surface a typed error");
    match err {
        CellosError::SecretBroker(msg) => {
            assert!(
                msg.contains(VAL_MISSING_ENV),
                "expected error to name the env var, got: {msg}"
            );
        }
        other => panic!("expected CellosError::SecretBroker, got {other:?}"),
    }
}

#[tokio::test]
async fn set_var_resolves_to_exact_value() {
    std::env::set_var(VAL_PRESENT_ENV, VAL_PRESENT_VALUE);
    let broker = EnvSecretBroker::new();
    let view = broker
        .resolve(VAL_PRESENT_KEY, "cell-val-present", 60)
        .await
        .expect("set var must resolve");
    assert_eq!(view.key, VAL_PRESENT_KEY);
    assert_eq!(view.value.as_str(), VAL_PRESENT_VALUE);
    std::env::remove_var(VAL_PRESENT_ENV);
}

#[tokio::test]
async fn error_message_does_not_leak_value() {
    // Set a sibling secret that happens to contain a sentinel string; resolve
    // a *different* (unset) key and assert the sibling's value is not echoed.
    let sentinel = "SENTINEL-VALUE-MUST-NOT-APPEAR-IN-ERR";
    std::env::set_var("CELLOS_SECRET_VAL_LEAK_SIBLING", sentinel);
    std::env::remove_var("CELLOS_SECRET_VAL_LEAK_TARGET");
    let broker = EnvSecretBroker::new();
    let err = broker
        .resolve("VAL_LEAK_TARGET", "cell-val-leak", 60)
        .await
        .expect_err("unset target must error");
    let msg = err.to_string();
    assert!(
        !msg.contains(sentinel),
        "error message must not leak any other secret's value, got: {msg}"
    );
    std::env::remove_var("CELLOS_SECRET_VAL_LEAK_SIBLING");
}

#[tokio::test]
async fn broker_is_send_sync_behind_trait_object() {
    // Compile-time + runtime pin: trait object usable across awaits.
    let broker: Arc<dyn SecretBroker> = Arc::new(EnvSecretBroker::new());
    let handle = tokio::spawn(async move {
        // Resolve something we know is unset; we only care that the handle
        // is Send and the trait object resolves end-to-end.
        let _ = broker
            .resolve("VAL_SEND_SYNC_PROBE_UNSET", "cell-val-ss", 60)
            .await;
    });
    handle
        .await
        .expect("trait object must be Send across spawn");
}

#[tokio::test]
async fn shell_special_chars_in_key_are_safe() {
    // Shell-special characters do not get interpreted — they simply produce a
    // non-matching env var name and yield a typed NotPresent error. Pin this
    // so a future change cannot accidentally interpolate or shell-execute.
    for bad in [
        "WITH$DOLLAR",
        "WITH;SEMI",
        "WITH|PIPE",
        "WITH`TICK",
        "WITH SPACE",
    ] {
        let broker = EnvSecretBroker::new();
        let err = broker
            .resolve(bad, "cell-val-shell", 60)
            .await
            .expect_err("unset shell-charish key must error, not interpret");
        match err {
            CellosError::SecretBroker(_) => {}
            other => panic!("expected CellosError::SecretBroker for {bad:?}, got {other:?}"),
        }
    }
}
