//! Axis processing for CMDR Joystick
//!
//! Responsibilities
//! - Apply gimbal mode compensation (M10/M7) to raw ADC readings
//! - Feed smoothed samples into per‑axis filters
//! - Calibrate + apply deadzones + optional expo via lookup table
//! - Implement throttle‑hold remapping on the throttle axis
//! - Maintain virtual axes updated by button presses
//! - Detect activity to throttle USB traffic
//!
//! Data flow
//! raw ADC → gimbal compensation → smoothing → `GimbalAxis::process_value`
//! → throttle hold → activity detection → USB report conversion

use crate::buttons::{Button, TOTAL_BUTTONS};
use crate::expo::{constrain, ExpoLUT};
use crate::hardware::{ADC_MAX, ADC_MIN, AXIS_CENTER, NBR_OF_GIMBAL_AXIS};
use crate::mapping::{
    BUTTON_FRONT_LEFT_EXTRA, BUTTON_FRONT_LEFT_LOWER, BUTTON_FRONT_LEFT_UPPER,
    BUTTON_FRONT_RIGHT_EXTRA,
};
use dyn_smooth::{DynamicSmootherEcoI32, I32_FRAC_BITS};

// ==================== AXIS CONSTANTS ====================

pub const GIMBAL_AXIS_LEFT_X: usize = 0;
pub const GIMBAL_AXIS_LEFT_Y: usize = 1;
pub const GIMBAL_AXIS_RIGHT_X: usize = 2;
pub const GIMBAL_AXIS_RIGHT_Y: usize = 3;
pub const GIMBAL_MODE_M10: u8 = 0;
pub const GIMBAL_MODE_M7: u8 = 1;

/// Digital smoothing parameters for `DynamicSmootherEcoI32` filters.
/// Reduce ADC noise and jitter for stable axis values.
pub const BASE_FREQ: i32 = 2 << I32_FRAC_BITS;
pub const SAMPLE_FREQ: i32 = 1000 << I32_FRAC_BITS;
pub const SENSITIVITY: i32 = (0.01 * ((1 << I32_FRAC_BITS) as f32)) as i32;

// ==================== AXIS STRUCTS ====================

#[derive(Copy, Clone)]
pub struct GimbalAxis {
    pub value: u16,
    pub value_before_hold: u16,
    pub previous_value: u16,
    pub max: u16,
    pub min: u16,
    pub center: u16,
    pub deadzone: (u16, u16, u16),
    pub expo: bool,
    pub hold: u16,
    pub hold_pending: bool,
}

impl Default for GimbalAxis {
    fn default() -> Self {
        // Apply new calibration limits supplied by the calibration manager.
        GimbalAxis {
            value: AXIS_CENTER,
            value_before_hold: AXIS_CENTER,
            previous_value: AXIS_CENTER,
            max: ADC_MAX,
            min: ADC_MIN,
            center: AXIS_CENTER,
            deadzone: (100, 50, 100),
            expo: true,
            hold: AXIS_CENTER,
            hold_pending: false,
        }
    }
}

impl GimbalAxis {
    /// Create a new GimbalAxis with default settings.
    pub fn new() -> Self {
        Self::default()
    }

    /// Create a new GimbalAxis with calibration data.
    pub fn with_calibration(min: u16, max: u16, center: u16) -> Self {
        let mut axis = Self::new();
        axis.set_calibration(min, max, center);
        axis
    }

    /// Apply calibration data to the axis.
    pub fn set_calibration(&mut self, min: u16, max: u16, center: u16) {
        self.min = min;
        self.max = max;
        self.center = center;
    }

    /// Process filtered ADC value through calibration, deadzone and optional expo.
    pub fn process_value(&mut self, filtered_value: i32, expo_lut: &ExpoLUT) {
        // Convert filtered value to u16 range
        let raw_value = constrain(filtered_value, ADC_MIN as i32, ADC_MAX as i32) as u16;

        // Apply calibration and expo processing
        self.value = calculate_axis_value(
            raw_value,
            self.min,
            self.max,
            self.center,
            self.deadzone,
            self.expo,
            expo_lut,
        );
        self.value_before_hold = self.value;
    }

    /// Set/arm a hold value for the axis (used by throttle hold).
    pub fn set_hold(&mut self, value: u16) {
        self.hold = value;
        self.hold_pending = true;
    }

    /// Check if the axis value changed since the last probe (activity flag).
    pub fn check_activity(&mut self) -> bool {
        let activity = self.value != self.previous_value;
        self.previous_value = self.value;
        activity
    }

    /// Throttle‑hold remapping for throttle axis (center → hold; below/above remapped).
    pub fn process_throttle_hold(&mut self, throttle_hold_enable: bool) {
        if throttle_hold_enable && self.value < AXIS_CENTER && !self.hold_pending {
            self.value = remap(self.value, ADC_MIN, AXIS_CENTER, ADC_MIN, self.hold);
        } else if throttle_hold_enable && self.value > AXIS_CENTER && !self.hold_pending {
            self.value = remap(self.value, AXIS_CENTER, ADC_MAX, self.hold, ADC_MAX);
        } else if throttle_hold_enable && self.value == AXIS_CENTER {
            self.value = self.hold;
            self.hold_pending = false;
        } else if throttle_hold_enable {
            self.value = self.hold;
        }
    }
}

#[derive(Copy, Clone)]
pub struct VirtualAxis {
    pub value: u16,
    step: u16,
}

impl Default for VirtualAxis {
    fn default() -> Self {
        // Create a virtual axis starting at center with the supplied step size.
        VirtualAxis {
            value: AXIS_CENTER,
            step: 5,
        }
    }
}

impl VirtualAxis {
    pub fn new(step: u16) -> Self {
        VirtualAxis {
            value: AXIS_CENTER,
            step,
        }
    }

    /// Update virtual axis based on button inputs.
    /// Returns true if USB activity should be signaled.
    pub fn update(&mut self, up_pressed: bool, down_pressed: bool, _vt_enable: bool) -> bool {
        let mut activity = false;

        // Compensate value when changing direction
        if up_pressed && !down_pressed && self.value < AXIS_CENTER {
            self.value = AXIS_CENTER + (AXIS_CENTER - self.value) / 2;
        } else if down_pressed && !up_pressed && self.value > AXIS_CENTER {
            self.value = AXIS_CENTER - (self.value - AXIS_CENTER) / 2;
        }

        // Move virtual axis
        if up_pressed && !down_pressed && self.value < ADC_MAX - self.step {
            self.value += self.step;
            activity = true;
        } else if down_pressed && !up_pressed && self.value > ADC_MIN + self.step {
            self.value -= self.step;
            activity = true;
        } else if (self.value != AXIS_CENTER && !up_pressed && !down_pressed)
            || (up_pressed && down_pressed)
        {
            // Return to center when no buttons are pressed or both are pressed
            let before = self.value;
            if self.value > AXIS_CENTER {
                self.value = self.value.saturating_sub(self.step);
            } else if self.value < AXIS_CENTER {
                self.value = self.value.saturating_add(self.step);
            }
            activity |= self.value != before;
        }

        activity
    }
}

// ==================== AXIS MANAGER ====================

pub struct AxisManager {
    pub axes: [GimbalAxis; NBR_OF_GIMBAL_AXIS],
    pub virtual_ry: VirtualAxis,
    pub virtual_rz: VirtualAxis,
    pub gimbal_mode: u8,
    pub throttle_hold_enable: bool,
}

impl Default for AxisManager {
    fn default() -> Self {
        // Delegate to `new` so the default stays aligned with explicit constructor logic.
        Self::new()
    }
}

impl AxisManager {
    pub fn new() -> Self {
        Self {
            axes: [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS],
            virtual_ry: VirtualAxis::new(5),
            virtual_rz: VirtualAxis::new(5),
            gimbal_mode: GIMBAL_MODE_M10,
            throttle_hold_enable: false,
        }
    }

    pub fn set_gimbal_mode(&mut self, mode: u8) {
        self.gimbal_mode = mode;
    }

    /// Apply gimbal mode compensation to raw ADC values.
    pub fn apply_gimbal_compensation(&self, raw_values: &mut [u16; 4]) {
        if self.gimbal_mode == GIMBAL_MODE_M10 {
            // Invert X1 and Y2 axis (M10 gimbals)
            raw_values[GIMBAL_AXIS_LEFT_X] = ADC_MAX - raw_values[GIMBAL_AXIS_LEFT_X];
            raw_values[GIMBAL_AXIS_RIGHT_Y] = ADC_MAX - raw_values[GIMBAL_AXIS_RIGHT_Y];
        } else if self.gimbal_mode == GIMBAL_MODE_M7 {
            // Invert Y1 and X2 axis (M7 gimbals)
            raw_values[GIMBAL_AXIS_LEFT_Y] = ADC_MAX - raw_values[GIMBAL_AXIS_LEFT_Y];
            raw_values[GIMBAL_AXIS_RIGHT_X] = ADC_MAX - raw_values[GIMBAL_AXIS_RIGHT_X];
        }
    }

    /// Tick smoothing filters with latest raw samples.
    pub fn update_smoothers(
        &self,
        smoother: &mut [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
        raw_values: &[u16; 4],
    ) {
        smoother[GIMBAL_AXIS_LEFT_X].tick(raw_values[GIMBAL_AXIS_LEFT_X] as i32);
        smoother[GIMBAL_AXIS_LEFT_Y].tick(raw_values[GIMBAL_AXIS_LEFT_Y] as i32);
        smoother[GIMBAL_AXIS_RIGHT_X].tick(raw_values[GIMBAL_AXIS_RIGHT_X] as i32);
        smoother[GIMBAL_AXIS_RIGHT_Y].tick(raw_values[GIMBAL_AXIS_RIGHT_Y] as i32);
    }

    /// Update all axes from smoothers (calibration, deadzone, expo) and apply holds.
    pub fn process_axis_values(
        &mut self,
        smoother: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
        expo_lut: &ExpoLUT,
    ) -> bool {
        for (index, axis) in self.axes.iter_mut().enumerate() {
            axis.process_value(smoother[index].value(), expo_lut);
        }
        self.process_throttle_hold();
        self.check_activity()
    }

    /// Apply throttle‑hold to the left‑Y axis and derive enable flag.
    pub fn process_throttle_hold(&mut self) {
        self.throttle_hold_enable = self.axes[GIMBAL_AXIS_LEFT_Y].hold != AXIS_CENTER;
        self.axes[GIMBAL_AXIS_LEFT_Y].process_throttle_hold(self.throttle_hold_enable);
    }

    /// Clear throttle‑hold state for left‑Y axis (reset to initial state).
    pub fn clear_throttle_hold(&mut self) {
        self.axes[GIMBAL_AXIS_LEFT_Y].hold = AXIS_CENTER;
        self.axes[GIMBAL_AXIS_LEFT_Y].hold_pending = false;
    }

    /// Update virtual axes based on button inputs.
    /// Returns true if USB activity should be signaled.
    pub fn update_virtual_axes(
        &mut self,
        buttons: &[Button; TOTAL_BUTTONS],
        vt_enable: bool,
    ) -> bool {
        let mut activity = false;

        // Update Virtual RY
        let ry_activity = self.virtual_ry.update(
            buttons[BUTTON_FRONT_LEFT_UPPER].pressed,
            buttons[BUTTON_FRONT_LEFT_LOWER].pressed,
            vt_enable,
        );
        if ry_activity {
            activity = true;
        }

        // Update Virtual RZ
        let rz_activity = self.virtual_rz.update(
            buttons[BUTTON_FRONT_RIGHT_EXTRA].pressed,
            buttons[BUTTON_FRONT_LEFT_EXTRA].pressed,
            vt_enable,
        );
        if rz_activity {
            activity = true;
        }

        activity
    }

    /// Check for axis activity (movement from previous value).
    pub fn check_activity(&mut self) -> bool {
        let mut activity = false;

        for axis in self.axes.iter_mut() {
            if axis.check_activity() {
                activity = true;
            }
        }

        activity
    }

    /// Set throttle‑hold value for left‑Y axis (original behavior).
    pub fn set_throttle_hold(&mut self, hold_value: u16) {
        self.axes[GIMBAL_AXIS_LEFT_Y].set_hold(hold_value);
    }

    /// Get pre‑hold value for special button combinations (original logic).
    pub fn get_value_before_hold(&self) -> u16 {
        // Original code captured axis.value BEFORE throttle hold was applied
        self.axes[GIMBAL_AXIS_LEFT_Y].value_before_hold
    }

    /// Get virtual RY axis value for joystick report.
    pub fn get_virtual_ry_value(&self, expo_lut: &ExpoLUT) -> u16 {
        calculate_axis_value(
            self.virtual_ry.value,
            ADC_MIN,
            ADC_MAX,
            AXIS_CENTER,
            (0, 0, 0),
            true,
            expo_lut,
        )
    }

    /// Get virtual RZ axis value for joystick report.
    pub fn get_virtual_rz_value(&self, expo_lut: &ExpoLUT) -> u16 {
        calculate_axis_value(
            self.virtual_rz.value,
            ADC_MIN,
            ADC_MAX,
            AXIS_CENTER,
            (0, 0, 0),
            true,
            expo_lut,
        )
    }

    /// Initialize digital smoothing filters for each gimbal axis.
    /// Creates an array of `DynamicSmootherEcoI32` filters configured with shared DSP parameters.
    pub fn create_smoothers() -> [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS] {
        [
            DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
            DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
            DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
            DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
        ]
    }
}

// ==================== AXIS PROCESSING FUNCTIONS ====================

/// Remap a value from one range to another using integer math and clamping.
///
/// # Arguments
/// * `value` - Value to remap
/// * `in_min` - Lower bound of the value's current range
/// * `in_max` - Upper bound of the value's current range
/// * `out_min` - Lower bound of the value's target range
/// * `out_max` - Upper bound of the value's target range
pub fn remap(value: u16, in_min: u16, in_max: u16, out_min: u16, out_max: u16) -> u16 {
    constrain(
        (value as i64 - in_min as i64) * (out_max as i64 - out_min as i64)
            / (in_max as i64 - in_min as i64)
            + out_min as i64,
        out_min as i64,
        out_max as i64,
    ) as u16
}

/// Calculate calibrated axis value with deadzone and optional expo.
///
/// Process raw input through [min, center, max] calibration, apply deadzones,
/// then apply expo via LUT if enabled for smooth response.
///
/// # Arguments
/// * `value` - Raw axis value to process
/// * `min` - Lower bound of the axis calibrated range
/// * `max` - Upper bound of the axis calibrated range
/// * `center` - Center position of the axis calibrated range
/// * `deadzone` - Deadzone settings (min, center, max) for axis
/// * `expo` - Whether exponential curve is enabled for this axis
/// * `expo_lut` - Exponential curve lookup table for smooth response
///
/// # Returns
/// Processed axis value after calibration, deadzone, and optional expo
pub fn calculate_axis_value(
    value: u16,
    min: u16,
    max: u16,
    center: u16,
    deadzone: (u16, u16, u16),
    expo: bool,
    expo_lut: &ExpoLUT,
) -> u16 {
    use crate::hardware::{ADC_MAX, ADC_MIN, AXIS_CENTER};

    if min >= max || center <= min || center >= max {
        return center;
    }

    if value <= min {
        return ADC_MIN;
    }
    if value >= max {
        return ADC_MAX;
    }

    let mut calibrated_value = AXIS_CENTER;

    if value > (center + deadzone.1) {
        calibrated_value = remap(
            value,
            center + deadzone.1,
            max - deadzone.2,
            AXIS_CENTER,
            ADC_MAX,
        );
    } else if value < (center - deadzone.1) {
        calibrated_value = remap(
            value,
            min + deadzone.0,
            center - deadzone.1,
            ADC_MIN,
            AXIS_CENTER,
        );
    }

    if expo && calibrated_value != AXIS_CENTER {
        calibrated_value = expo_lut.apply(calibrated_value);
    }

    calibrated_value
}

// ==================== TESTS ====================

#[cfg(all(test, feature = "std"))]
mod tests {
    use super::*;

    #[test]
    fn test_gimbal_axis_default() {
        // Factory defaults should leave every axis field pointing at the center span.
        let axis = GimbalAxis::default();
        assert_eq!(axis.value, AXIS_CENTER);
        assert_eq!(axis.min, ADC_MIN);
        assert_eq!(axis.max, ADC_MAX);
        assert_eq!(axis.center, AXIS_CENTER);
        assert_eq!(axis.hold, AXIS_CENTER);
        assert!(!axis.hold_pending);
    }

    #[test]
    fn test_gimbal_axis_new() {
        // `new` should be a thin wrapper over `Default` without changing members.
        let axis = GimbalAxis::new();
        assert_eq!(axis.value, AXIS_CENTER);
        assert_eq!(axis.min, ADC_MIN);
        assert_eq!(axis.max, ADC_MAX);
        assert_eq!(axis.center, AXIS_CENTER);
        assert!(!axis.hold_pending);
    }

    #[test]
    fn test_gimbal_axis_with_calibration() {
        // Axis constructed with explicit calibration should adopt all provided bounds.
        let axis = GimbalAxis::with_calibration(100, 3900, 2000);
        assert_eq!(axis.min, 100);
        assert_eq!(axis.max, 3900);
        assert_eq!(axis.center, 2000);
    }

    #[test]
    fn test_gimbal_axis_activity_detection() {
        // Activity flag should only flip when the axis value actually changes.
        let mut axis = GimbalAxis::new();

        // Initially no activity (same as previous)
        assert!(!axis.check_activity());

        // Change value
        axis.value = 3000;
        assert!(axis.check_activity());

        // No change from previous check
        assert!(!axis.check_activity());
    }

    #[test]
    fn test_gimbal_axis_throttle_hold_processing() {
        // Throttle hold remapping should cover remap, center latch, and pending hold cases.
        let mut axis = GimbalAxis::new();
        axis.set_hold(1500); // Set hold value below center

        // Test when not held, no processing occurs
        axis.hold = AXIS_CENTER;
        axis.hold_pending = false;
        axis.value = 1000;
        axis.process_throttle_hold(true);
        assert_eq!(axis.value, 1000); // Should remain unchanged

        // Test when held but hold_pending = false, remapping occurs
        axis.set_hold(1500);
        axis.value = 1000;
        axis.hold_pending = false; // This allows remapping
        axis.process_throttle_hold(true);
        let expected = remap(1000, ADC_MIN, AXIS_CENTER, ADC_MIN, 1500);
        assert_eq!(axis.value, expected);

        // Test center value gets hold value and clears pending flag
        axis.set_hold(1500);
        axis.value = AXIS_CENTER;
        axis.process_throttle_hold(true);
        assert_eq!(axis.value, 1500);
        assert!(!axis.hold_pending); // Should clear pending flag

        // Test when hold_pending = true, just uses hold value
        axis.set_hold(1500);
        axis.value = 2000;
        axis.hold_pending = true;
        axis.process_throttle_hold(true);
        assert_eq!(axis.value, 1500);
    }

    #[test]
    fn test_virtual_axis_default() {
        // Virtual axis should boot at center with the configured step size.
        let virtual_axis = VirtualAxis::default();
        assert_eq!(virtual_axis.value, AXIS_CENTER);
        assert_eq!(virtual_axis.step, 5);
    }

    #[test]
    fn test_virtual_axis_movement_up() {
        // An upward press should increment the virtual axis and flag activity.
        let mut virtual_axis = VirtualAxis::new(10);

        // Test upward movement
        let activity = virtual_axis.update(true, false, false);
        assert!(activity);
        assert_eq!(virtual_axis.value, AXIS_CENTER + 10);
    }

    #[test]
    fn test_virtual_axis_movement_down() {
        // A downward press should decrement the virtual axis and flag activity.
        let mut virtual_axis = VirtualAxis::new(10);

        // Test downward movement
        let activity = virtual_axis.update(false, true, false);
        assert!(activity);
        assert_eq!(virtual_axis.value, AXIS_CENTER - 10);
    }

    #[test]
    fn test_virtual_axis_return_to_center() {
        // Without inputs the virtual axis should ease back toward the center value.
        let mut virtual_axis = VirtualAxis::new(10);
        virtual_axis.value = AXIS_CENTER + 20;

        // Test return to center when no buttons pressed
        let activity = virtual_axis.update(false, false, false);
        assert!(activity);
        assert_eq!(virtual_axis.value, AXIS_CENTER + 10);
    }

    #[test]
    fn test_virtual_axis_direction_compensation() {
        // Reversing direction should recenter before stepping to avoid large jumps.
        let mut virtual_axis = VirtualAxis::new(10);
        virtual_axis.value = AXIS_CENTER - 100;

        // Test direction change compensation (includes compensation + movement)
        virtual_axis.update(true, false, false);
        let compensated = AXIS_CENTER + (AXIS_CENTER - (AXIS_CENTER - 100)) / 2;
        let expected = compensated + 10; // Plus step movement
        assert_eq!(virtual_axis.value, expected);
    }

    #[test]
    fn test_axis_manager_creation() {
        // Manager construction should initialize all axes and state to defaults.
        let manager = AxisManager::new();
        assert_eq!(manager.axes.len(), NBR_OF_GIMBAL_AXIS);
        assert_eq!(manager.gimbal_mode, GIMBAL_MODE_M10);
        assert_eq!(manager.virtual_ry.value, AXIS_CENTER);
        assert_eq!(manager.virtual_rz.value, AXIS_CENTER);
    }

    #[test]
    fn test_gimbal_compensation_m10() {
        // M10 gimbal compensation must invert the correct pair of axes.
        let manager = AxisManager::new(); // Default is M10
        let mut raw_values = [1000, 1500, 2000, 2500];

        manager.apply_gimbal_compensation(&mut raw_values);

        // M10 mode inverts X1 and Y2
        assert_eq!(raw_values[GIMBAL_AXIS_LEFT_X], ADC_MAX - 1000);
        assert_eq!(raw_values[GIMBAL_AXIS_LEFT_Y], 1500); // Not inverted
        assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_X], 2000); // Not inverted
        assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_Y], ADC_MAX - 2500);
    }

    #[test]
    fn test_gimbal_compensation_m7() {
        // M7 gimbal compensation must invert the complementary axes.
        let mut manager = AxisManager::new();
        manager.set_gimbal_mode(GIMBAL_MODE_M7);
        let mut raw_values = [1000, 1500, 2000, 2500];

        manager.apply_gimbal_compensation(&mut raw_values);

        // M7 mode inverts Y1 and X2
        assert_eq!(raw_values[GIMBAL_AXIS_LEFT_X], 1000); // Not inverted
        assert_eq!(raw_values[GIMBAL_AXIS_LEFT_Y], ADC_MAX - 1500);
        assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_X], ADC_MAX - 2000);
        assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_Y], 2500); // Not inverted
    }

    #[test]
    fn test_axis_activity_detection() {
        // Manager activity flag should reflect when any axis value changes.
        let mut manager = AxisManager::new();

        // No activity initially
        assert!(!manager.check_activity());

        // Change value and check activity
        manager.axes[0].value = 1000;
        assert!(manager.check_activity());

        // No activity after previous_value is updated
        assert!(!manager.check_activity());
    }

    #[test]
    fn test_calculate_axis_value_boundaries() {
        // Axis conversion should clamp inputs below min or above max calibration values.
        let expo_lut = ExpoLUT::new(0.0); // No expo for testing

        // Test min boundary
        let result = calculate_axis_value(0, 100, 3000, 2000, (50, 50, 50), false, &expo_lut);
        assert_eq!(result, ADC_MIN);

        // Test max boundary
        let result = calculate_axis_value(4000, 100, 3000, 2000, (50, 50, 50), false, &expo_lut);
        assert_eq!(result, ADC_MAX);
    }

    #[test]
    fn test_calculate_axis_value_deadzone() {
        // Inputs inside the center deadzone should resolve to the exact center value.
        let expo_lut = ExpoLUT::new(0.0); // No expo for testing

        // Test center deadzone
        let result = calculate_axis_value(2000, 100, 3000, 2000, (50, 50, 50), false, &expo_lut);
        assert_eq!(result, AXIS_CENTER);
    }

    #[test]
    fn test_calculate_axis_value_degenerate_calibration() {
        // Degenerate calibration inputs should return the provided center without panicking.
        let expo_lut = ExpoLUT::new(0.0);

        // When calibration collapses to a single point (min=max=center),
        // the output should remain at that center rather than clamping low/high.
        let result = calculate_axis_value(2100, 2100, 2100, 2100, (0, 0, 0), false, &expo_lut);
        assert_eq!(result, 2100);

        // Also handle centers outside the valid window by returning center directly.
        let result = calculate_axis_value(1500, 1400, 2000, 1400, (0, 0, 0), false, &expo_lut);
        assert_eq!(result, 1400);
    }

    #[test]
    fn test_remap_function() {
        // Test basic remapping
        let result = remap(50, 0, 100, 0, 200);
        assert_eq!(result, 100);

        // Test reverse remapping (constrain limits to out_min when out_min > out_max)
        let result = remap(25, 0, 100, 100, 0);
        assert_eq!(result, 100);
    }
}
