//! Shared error enums for the ops layer.

use thiserror::Error;

/// Build-time configuration error. Raised only by `OpsRuntime::build`.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BuildError {
    /// A registered primitive needs another primitive that wasn't provided.
    #[error("primitive `{needed_by}` requires `{missing}` to be configured")]
    MissingDependency {
        /// The primitive that has the unmet dependency.
        needed_by: String,
        /// The dependency that was not supplied.
        missing: String,
    },
}

/// Runtime error surfaced by ops primitives + the SupervisedAgent wrapper.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum OpsError {
    /// A gate refused; the operation is denied.
    #[error("denied: {code}")]
    Denied {
        /// Stable policy code.
        code: String,
        /// Optional human-readable reason.
        reason: Option<String>,
    },
    /// A gate suspended; downstream must wait for resolution (Phase B).
    #[error("require approval")]
    RequireApproval,
    /// Operation timed out at the named phase.
    #[error("timed out at phase `{phase}`")]
    TimedOut {
        /// Phase name.
        phase: String,
    },
    /// Governor refused — budget exhausted.
    #[error("saturated: {scope}")]
    Saturated {
        /// Scope display.
        scope: String,
    },
    /// Kill-switch tripped; runtime is halted.
    #[error("halted")]
    Halted,
    /// Downstream storage / channel unreachable.
    #[error("unavailable: {component}")]
    Unavailable {
        /// Affected component name.
        component: String,
    },
    /// Integrity / signature / canonicalisation failure.
    #[error("integrity failed: {reasons}")]
    IntegrityFailed {
        /// Concatenated failure reasons.
        reasons: String,
    },
    /// Build-time configuration error promoted to runtime path.
    #[error(transparent)]
    Build(#[from] BuildError),
    /// Any other internal error.
    #[error("internal: {source}")]
    Internal {
        /// Wrapped source.
        source: Box<dyn std::error::Error + Send + Sync>,
    },
}

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

    #[test]
    fn build_error_displays_missing_dependency() {
        let e = BuildError::MissingDependency {
            needed_by: "gates".into(),
            missing: "escalation".into(),
        };
        let s = format!("{e}");
        assert!(s.contains("gates"));
        assert!(s.contains("escalation"));
    }

    #[test]
    fn ops_error_halted_is_distinct_from_unavailable() {
        let a = OpsError::Halted;
        let b = OpsError::Unavailable {
            component: "kv".into(),
        };
        assert_ne!(format!("{a}"), format!("{b}"));
    }
}
