//! Deployment posture profiles that bundle several fail-open guards into a
//! single fail-closed opt-in. See ADR-022 (tenant binding).

/// Bundles several individually-opt-in tenant-isolation guards into one
/// fail-closed posture. The default ([`Unprofiled`](Self::Unprofiled))
/// leaves each guard's fail-open default in place (ADR-022 partial-deployment
/// posture); [`RegulatedMultiTenant`](Self::RegulatedMultiTenant) flips them
/// closed and refuses to build if a prerequisite is missing.
///
/// Asserts exactly `{strict tenant binding, named principal}`. It does NOT
/// govern bind address or leader-election fail-open, and does NOT verify that
/// peer replicas run the same profile (a lenient peer reintroduces CWE-639 —
/// deferred Part 2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DeploymentProfile {
    /// Default. Each guard is opted into individually; fail-open defaults stand.
    #[default]
    Unprofiled,
    /// Strict tenant binding + a named (non-anonymous) principal required;
    /// fail-closed at build if either is missing.
    RegulatedMultiTenant,
}

/// A way a builder's configuration fails to satisfy its [`DeploymentProfile`].
/// Transports wrap this in their own build-error type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ProfileViolation {
    /// Profile requires tenant binding but no tenant KV store was configured.
    #[error(
        "regulated profile requires tenant binding: \
         call with_tenant_binding[_strict](kv) before build"
    )]
    MissingTenantKv,
    /// Profile requires a named principal but the authenticator is absent or
    /// permits anonymous callers.
    #[error(
        "regulated profile requires a named (non-anonymous) principal: \
         wire a non-anonymous Authenticator (not AllowAnonymous)"
    )]
    AnonymousAuth,
}

impl DeploymentProfile {
    /// True when a build must upgrade its `OwnershipRegistry` to strict
    /// (fail-closed) even if only a lenient binding was wired.
    pub fn requires_strict_binding(self) -> bool {
        matches!(self, Self::RegulatedMultiTenant)
    }

    /// True when a build must reject an absent or anonymous authenticator.
    pub fn requires_named_principal(self) -> bool {
        matches!(self, Self::RegulatedMultiTenant)
    }

    /// Check a builder's configuration against this profile.
    ///
    /// A named principal is satisfied only by `Some(false)`; `None`
    /// (no authenticator wired) counts as anonymous. Correctness depends on the
    /// authenticator reporting `allows_anonymous()` honestly — the same contract
    /// the non-loopback-bind gate relies on.
    pub fn validate(
        self,
        has_tenant_kv: bool,
        authenticator_allows_anonymous: Option<bool>,
    ) -> Result<(), ProfileViolation> {
        if self.requires_strict_binding() && !has_tenant_kv {
            return Err(ProfileViolation::MissingTenantKv);
        }
        if self.requires_named_principal() && authenticator_allows_anonymous != Some(false) {
            return Err(ProfileViolation::AnonymousAuth);
        }
        Ok(())
    }
}

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

    #[test]
    fn unprofiled_requires_no_guards() {
        let p = DeploymentProfile::Unprofiled;
        assert!(!p.requires_strict_binding());
        assert!(!p.requires_named_principal());
    }

    #[test]
    fn regulated_requires_strict_binding_and_named_principal() {
        let p = DeploymentProfile::RegulatedMultiTenant;
        assert!(p.requires_strict_binding());
        assert!(p.requires_named_principal());
    }

    #[test]
    fn unprofiled_validates_any_config() {
        assert_eq!(DeploymentProfile::Unprofiled.validate(false, None), Ok(()));
    }

    #[test]
    fn regulated_without_tenant_kv_is_missing_kv() {
        let err = DeploymentProfile::RegulatedMultiTenant
            .validate(false, Some(false))
            .unwrap_err();
        assert_eq!(err, ProfileViolation::MissingTenantKv);
    }

    #[test]
    fn regulated_with_anonymous_authenticator_is_anonymous_auth() {
        let err = DeploymentProfile::RegulatedMultiTenant
            .validate(true, Some(true))
            .unwrap_err();
        assert_eq!(err, ProfileViolation::AnonymousAuth);
    }

    #[test]
    fn regulated_without_any_authenticator_is_anonymous_auth() {
        let err = DeploymentProfile::RegulatedMultiTenant
            .validate(true, None)
            .unwrap_err();
        assert_eq!(err, ProfileViolation::AnonymousAuth);
    }

    #[test]
    fn regulated_with_named_principal_and_kv_is_ok() {
        assert_eq!(
            DeploymentProfile::RegulatedMultiTenant.validate(true, Some(false)),
            Ok(())
        );
    }
}
