//! Crate-level error type.

use thiserror::Error;

/// Errors raised by `klieo-spec` traits and the loop runner.
///
/// Marked `#[non_exhaustive]` so additional variants can be introduced
/// without a major-version bump on impl crates that match on the enum.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SpecError {
    /// A downstream `Decomposer`/`Critic`/`Refiner` impl failed.
    ///
    /// Carries a context message plus the impl's original error as
    /// `#[source]` so `e.source()` traversal preserves the cause chain.
    /// Covers any downstream trait-impl failure that does not fit the
    /// typed variants.
    #[error("downstream error: {message}")]
    Downstream {
        /// Human-readable context describing the wrap site.
        message: String,
        /// Underlying error preserved for `std::error::Error::source()`.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
    },
    /// A trait implementation failed.
    #[deprecated(
        since = "0.7.0",
        note = "renamed to SpecError::Downstream — Impl will be removed in 0.8"
    )]
    #[error("trait error: {0}")]
    Impl(String),
    /// The loop ran the maximum allowed iterations without passing.
    #[error("max iterations reached ({iterations})")]
    MaxIterations {
        /// Iterations consumed before giving up.
        iterations: u8,
    },
    /// A misconfiguration in the loop (e.g. zero max iterations).
    #[error("config error: {0}")]
    Config(String),
}

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

    use std::error::Error as StdError;

    #[test]
    fn downstream_message_surfaces_through_display() {
        let e = SpecError::Downstream {
            message: "refiner: empty result".into(),
            source: None,
        };
        assert_eq!(e.to_string(), "downstream error: refiner: empty result");
    }

    #[test]
    fn downstream_preserves_source_chain() {
        let inner = std::io::Error::other("network down");
        let e = SpecError::Downstream {
            message: "critic call failed".into(),
            source: Some(Box::new(inner)),
        };
        let src = e.source().expect("source must be preserved");
        assert_eq!(src.to_string(), "network down");
    }

    #[test]
    fn downstream_without_source_returns_none() {
        let e = SpecError::Downstream {
            message: "no inner".into(),
            source: None,
        };
        assert!(e.source().is_none());
    }

    #[test]
    #[allow(deprecated)]
    fn impl_variant_still_compiles_as_deprecated_alias() {
        let e = SpecError::Impl("legacy caller".to_string());
        assert!(e.to_string().contains("trait error"));
    }

    #[test]
    fn max_iterations_display() {
        let e = SpecError::MaxIterations { iterations: 3 };
        assert_eq!(e.to_string(), "max iterations reached (3)");
    }
}
