//! Error types for diagram operations.

use std::fmt::{self, Display};

/// Errors that can occur when building or working with diagrams.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum DiagramError {
    /// A combination references a set that was never defined.
    UndefinedSet(String),

    /// A value provided is negative or invalid.
    InvalidValue { combination: String, value: f64 },

    /// No sets were defined.
    EmptySets,

    /// Duplicate combination definition.
    DuplicateCombination(String),

    /// Invalid combination format.
    InvalidCombination(String),

    /// Number of sets is not supported for the requested operation.
    ///
    /// For example, canonical Venn arrangements with ellipses only exist for
    /// `n ∈ 1..=5`; higher `n` requires non-ellipse curves (Edwards's
    /// cogwheels) which eunoia does not support.
    UnsupportedSetCount(usize),

    /// Diagram specification exceeds the supported number of sets. The
    /// `max` field reports the limit that was in effect when the spec was
    /// built — by default [`crate::constants::MAX_SETS`], or whatever
    /// override was set via [`crate::DiagramSpecBuilder::max_sets`] (capped
    /// at [`crate::constants::MAX_SETS_HARD_CAP`]).
    TooManySets { requested: usize, max: usize },

    /// A solver pool configured on the [`crate::Fitter`] was empty. The
    /// `which` field names the offending builder method
    /// (`"optimizer_pool"` or `"initial_solver_pool"`). Restart `i` indexes
    /// the pool as `pool[i % pool.len()]`, so an empty pool has no solver to
    /// run; the error surfaces from [`crate::Fitter::fit`] /
    /// [`crate::Fitter::fit_initial_only`] rather than panicking at the
    /// setter.
    EmptySolverPool {
        /// The builder method whose pool was empty.
        which: &'static str,
    },

    /// A geometric shape was constructed with an out-of-range parameter
    /// (e.g. a non-positive radius). Returned by the `try_new` constructors
    /// on shape types so callers can recover from invalid user input
    /// without catching a panic.
    InvalidShapeParameter {
        /// Shape type name (e.g. `"Circle"`, `"Ellipse"`, `"Square"`).
        shape: &'static str,
        /// Parameter name that failed validation (e.g. `"radius"`,
        /// `"semi_major"`, `"side"`).
        param: &'static str,
        /// The offending value.
        value: f64,
    },
}

impl Display for DiagramError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DiagramError::UndefinedSet(set) => {
                write!(f, "Set '{set}' is referenced but never defined")
            }
            DiagramError::InvalidValue { combination, value } => {
                write!(f, "Invalid value {value} for combination '{combination}'")
            }
            DiagramError::EmptySets => {
                write!(f, "No sets defined in diagram")
            }
            DiagramError::DuplicateCombination(combo) => {
                write!(f, "Combination '{combo}' defined multiple times")
            }
            DiagramError::InvalidCombination(combo) => {
                write!(f, "Invalid combination format: '{combo}'")
            }
            DiagramError::UnsupportedSetCount(n) => {
                write!(f, "Unsupported set count: {n}")
            }
            DiagramError::TooManySets { requested, max } => {
                write!(
                    f,
                    "Too many sets: {requested} requested, but maximum supported is {max}"
                )
            }
            DiagramError::EmptySolverPool { which } => {
                write!(f, "Solver pool '{which}' must not be empty")
            }
            DiagramError::InvalidShapeParameter {
                shape,
                param,
                value,
            } => {
                write!(f, "Invalid {param} for {shape}: {value} must be > 0")
            }
        }
    }
}

impl std::error::Error for DiagramError {}

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

    #[test]
    fn test_undefined_set_error() {
        let error = DiagramError::UndefinedSet("X".to_string());
        assert_eq!(error, DiagramError::UndefinedSet("X".to_string()));
        assert_eq!(
            format!("{error}"),
            "Set 'X' is referenced but never defined"
        );
    }

    #[test]
    fn test_invalid_value_error() {
        let error = DiagramError::InvalidValue {
            combination: "A&B".to_string(),
            value: -5.0,
        };
        assert_eq!(
            error,
            DiagramError::InvalidValue {
                combination: "A&B".to_string(),
                value: -5.0,
            }
        );
        assert_eq!(format!("{error}"), "Invalid value -5 for combination 'A&B'");
    }

    #[test]
    fn test_empty_sets_error() {
        let error = DiagramError::EmptySets;
        assert_eq!(error, DiagramError::EmptySets);
        assert_eq!(format!("{error}"), "No sets defined in diagram");
    }

    #[test]
    fn test_duplicate_combination_error() {
        let error = DiagramError::DuplicateCombination("A&B".to_string());
        assert_eq!(error, DiagramError::DuplicateCombination("A&B".to_string()));
        assert_eq!(
            format!("{error}"),
            "Combination 'A&B' defined multiple times"
        );
    }

    #[test]
    fn test_invalid_combination_error() {
        let error = DiagramError::InvalidCombination("A&".to_string());
        assert_eq!(error, DiagramError::InvalidCombination("A&".to_string()));
        assert_eq!(format!("{error}"), "Invalid combination format: 'A&'");
    }

    #[test]
    fn test_unsupported_set_count_error() {
        let error = DiagramError::UnsupportedSetCount(7);
        assert_eq!(error, DiagramError::UnsupportedSetCount(7));
        assert_eq!(format!("{error}"), "Unsupported set count: 7");
    }

    #[test]
    fn test_error_trait_implementation() {
        let error: Box<dyn std::error::Error> = Box::new(DiagramError::EmptySets);
        assert_eq!(format!("{error}"), "No sets defined in diagram");
    }

    #[test]
    fn test_debug_implementation() {
        let error = DiagramError::UndefinedSet("A".to_string());
        let debug_str = format!("{error:?}");
        assert!(debug_str.contains("UndefinedSet"));
        assert!(debug_str.contains("A"));
    }

    #[test]
    fn test_empty_solver_pool_error() {
        let error = DiagramError::EmptySolverPool {
            which: "optimizer_pool",
        };
        assert_eq!(
            error,
            DiagramError::EmptySolverPool {
                which: "optimizer_pool"
            }
        );
        assert_eq!(
            format!("{error}"),
            "Solver pool 'optimizer_pool' must not be empty"
        );
    }

    #[test]
    fn test_invalid_shape_parameter_error() {
        let error = DiagramError::InvalidShapeParameter {
            shape: "Circle",
            param: "radius",
            value: -1.5,
        };
        assert_eq!(
            format!("{error}"),
            "Invalid radius for Circle: -1.5 must be > 0"
        );
    }

    #[test]
    fn test_clone_implementation() {
        let error = DiagramError::InvalidValue {
            combination: "test".to_string(),
            value: 1.5,
        };
        let cloned = error.clone();
        assert_eq!(error, cloned);
    }
}
