//! Rounding functions: round, floor, ceil, round_to.
//!
//! - `round(x)`         — round to the nearest integer (half rounds away from 0)
//! - `round(x, n)`      — round to n decimal places
//! - `floor(x)`         — round toward negative infinity
//! - `ceil(x)`          — round toward positive infinity
//! - `round_to(x, step)` — round x to the nearest multiple of step

use super::{FunctionError, FunctionRegistry};

fn floor_fn(args: &[f64]) -> Result<f64, FunctionError> {
    Ok(args[0].floor())
}

fn ceil_fn(args: &[f64]) -> Result<f64, FunctionError> {
    Ok(args[0].ceil())
}

fn round_fn(args: &[f64]) -> Result<f64, FunctionError> {
    let value = args[0];
    if args.len() == 1 {
        return Ok(value.round());
    }
    let decimals = args[1];
    if decimals.fract() != 0.0 || decimals < 0.0 {
        return Err(FunctionError::new(
            "round decimal places must be a non-negative integer",
        ));
    }
    let factor = 10f64.powi(decimals as i32);
    Ok((value * factor).round() / factor)
}

/// Round x to the nearest multiple of step.
/// `round_to(17, 5)` => 15, `round_to(18, 5)` => 20.
fn round_to_fn(args: &[f64]) -> Result<f64, FunctionError> {
    let x = args[0];
    let step = args[1];
    if step == 0.0 {
        return Err(FunctionError::new(
            "round_to step must be non-zero",
        ));
    }
    Ok((x / step).round() * step)
}

/// Register rounding functions.
pub fn register(reg: &mut FunctionRegistry) {
    reg.register_fixed("floor", 1, floor_fn);
    reg.register_fixed("ceil", 1, ceil_fn);
    reg.register_range_arity("round", 1, 2, round_fn);
    reg.register_fixed("round_to", 2, round_to_fn);
}

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

    fn reg() -> FunctionRegistry {
        FunctionRegistry::new()
    }

    // --- floor ---

    #[test]
    fn floor_positive_fraction() {
        let v = reg().call("floor", &[3.7]).unwrap();
        assert!((v - 3.0).abs() < 1e-10);
    }

    #[test]
    fn floor_negative_fraction() {
        let v = reg().call("floor", &[-3.2]).unwrap();
        assert!((v - (-4.0)).abs() < 1e-10);
    }

    #[test]
    fn floor_integer_unchanged() {
        let v = reg().call("floor", &[5.0]).unwrap();
        assert!((v - 5.0).abs() < 1e-10);
    }

    #[test]
    fn floor_zero() {
        let v = reg().call("floor", &[0.0]).unwrap();
        assert!(v.abs() < 1e-10);
    }

    // --- ceil ---

    #[test]
    fn ceil_positive_fraction() {
        let v = reg().call("ceil", &[3.2]).unwrap();
        assert!((v - 4.0).abs() < 1e-10);
    }

    #[test]
    fn ceil_negative_fraction() {
        let v = reg().call("ceil", &[-3.7]).unwrap();
        assert!((v - (-3.0)).abs() < 1e-10);
    }

    #[test]
    fn ceil_integer_unchanged() {
        let v = reg().call("ceil", &[5.0]).unwrap();
        assert!((v - 5.0).abs() < 1e-10);
    }

    #[test]
    fn ceil_zero() {
        let v = reg().call("ceil", &[0.0]).unwrap();
        assert!(v.abs() < 1e-10);
    }

    // --- round ---

    #[test]
    fn round_half_up() {
        let v = reg().call("round", &[2.5]).unwrap();
        assert!((v - 3.0).abs() < 1e-10);
    }

    #[test]
    fn round_1_5() {
        let v = reg().call("round", &[1.5]).unwrap();
        assert!((v - 2.0).abs() < 1e-10);
    }

    #[test]
    fn round_negative() {
        let v = reg().call("round", &[-1.5]).unwrap();
        // Rust's f64::round rounds half away from zero, so -1.5 => -2.0
        assert!((v - (-2.0)).abs() < 1e-10);
    }

    #[test]
    fn round_with_decimal_places() {
        let v = reg().call("round", &[3.456, 2.0]).unwrap();
        assert!((v - 3.46).abs() < 1e-10);
    }

    #[test]
    fn round_with_zero_places() {
        let v = reg().call("round", &[3.456, 0.0]).unwrap();
        assert!((v - 3.0).abs() < 1e-10);
    }

    #[test]
    fn round_with_one_place() {
        let v = reg().call("round", &[1.234, 1.0]).unwrap();
        assert!((v - 1.2).abs() < 1e-10);
    }

    #[test]
    fn round_negative_decimal_places_error() {
        let err = reg().call("round", &[3.456, -1.0]).unwrap_err();
        assert!(err.message.contains("non-negative"));
    }

    // --- round_to (nearest N) ---

    #[test]
    fn round_to_nearest_5() {
        let v = reg().call("round_to", &[17.0, 5.0]).unwrap();
        assert!((v - 15.0).abs() < 1e-10);
    }

    #[test]
    fn round_to_nearest_5_up() {
        let v = reg().call("round_to", &[18.0, 5.0]).unwrap();
        assert!((v - 20.0).abs() < 1e-10);
    }

    #[test]
    fn round_to_nearest_10() {
        let v = reg().call("round_to", &[84.0, 10.0]).unwrap();
        assert!((v - 80.0).abs() < 1e-10);
    }

    #[test]
    fn round_to_nearest_0_25() {
        let v = reg().call("round_to", &[3.3, 0.25]).unwrap();
        assert!((v - 3.25).abs() < 1e-10);
    }

    #[test]
    fn round_to_zero_step_error() {
        let err = reg().call("round_to", &[10.0, 0.0]).unwrap_err();
        assert!(err.message.contains("non-zero"));
    }
}
