// SPDX-License-Identifier: GPL-3.0-or-later
//! Proportional sync math — pure functions, 100% test coverage target.
//!
//! BDD Specs (P5):
//! Given display A at 50%, display B at 30%, proportional sync on
//! When display A shifts from 50% to 70% (delta +20%)
//! Then display B shifts from 30% to 50%
//!
//! Given display A at 90%, display B at 70%, proportional sync on
//! When display A shifts from 90% to 100% (delta +10%)
//! Then display B shifts from 70% to 80%
//!
//! Given display A at 90%, display B at 95%, proportional sync on
//! When display A shifts from 90% to 100% (delta +10%)
//! Then display B shifts from 95% to 100% (clamped from 105%)

/// Compute the sync delta between old and new brightness.
pub fn compute_sync_delta(old_pct: f64, new_pct: f64) -> f64 {
    new_pct - old_pct
}

/// Apply a sync delta to a display's brightness, clamping to [0, 100].
pub fn apply_sync_delta(current_pct: f64, delta: f64) -> f64 {
    (current_pct + delta).clamp(0.0, 100.0)
}

/// Map a source display's percentage proportionally to a target display's range.
///
/// Both source and target have a [min_pct, max_pct] range. The source's current
/// value is expressed as a fraction of its range, then mapped to the same fraction
/// of the target's range.
///
/// Example: source range [0, 100] at 50% → target range [20, 40] → 30%
pub fn proportional_map(
    source_val: f64,
    source_min: f64,
    source_max: f64,
    target_min: f64,
    target_max: f64,
) -> f64 {
    let source_span = source_max - source_min;
    if source_span.abs() < f64::EPSILON {
        return target_min;
    }
    let fraction = (source_val - source_min) / source_span;
    let target_span = target_max - target_min;
    target_min + fraction * target_span
}

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

    #[test]
    fn delta_positive() {
        assert!((compute_sync_delta(50.0, 70.0) - 20.0).abs() < f64::EPSILON);
    }

    #[test]
    fn delta_negative() {
        assert!((compute_sync_delta(70.0, 50.0) - (-20.0)).abs() < f64::EPSILON);
    }

    #[test]
    fn delta_zero() {
        assert!((compute_sync_delta(50.0, 50.0)).abs() < f64::EPSILON);
    }

    #[test]
    fn apply_positive_delta() {
        assert!((apply_sync_delta(30.0, 20.0) - 50.0).abs() < f64::EPSILON);
    }

    #[test]
    fn apply_negative_delta() {
        assert!((apply_sync_delta(50.0, -20.0) - 30.0).abs() < f64::EPSILON);
    }

    #[test]
    fn apply_delta_clamps_high() {
        // 95 + 10 = 105, clamped to 100
        assert!((apply_sync_delta(95.0, 10.0) - 100.0).abs() < f64::EPSILON);
    }

    #[test]
    fn apply_delta_clamps_low() {
        // 5 - 10 = -5, clamped to 0
        assert!((apply_sync_delta(5.0, -10.0)).abs() < f64::EPSILON);
    }

    // BDD scenario tests
    #[test]
    fn scenario_a_50_to_70_b_30_to_50() {
        let delta = compute_sync_delta(50.0, 70.0);
        let new_b = apply_sync_delta(30.0, delta);
        assert!((new_b - 50.0).abs() < f64::EPSILON);
    }

    #[test]
    fn scenario_a_90_to_100_b_70_to_80() {
        let delta = compute_sync_delta(90.0, 100.0);
        let new_b = apply_sync_delta(70.0, delta);
        assert!((new_b - 80.0).abs() < f64::EPSILON);
    }

    #[test]
    fn scenario_a_90_to_100_b_95_clamps_to_100() {
        let delta = compute_sync_delta(90.0, 100.0);
        let new_b = apply_sync_delta(95.0, delta);
        assert!((new_b - 100.0).abs() < f64::EPSILON);
    }

    // Proportional map tests
    #[test]
    fn prop_map_basic() {
        // source: 0-100 at 50% → target: 20-40 → 30%
        let result = proportional_map(50.0, 0.0, 100.0, 20.0, 40.0);
        assert!((result - 30.0).abs() < 0.01);
    }

    #[test]
    fn prop_map_min() {
        // source at min → target at min
        let result = proportional_map(0.0, 0.0, 100.0, 20.0, 40.0);
        assert!((result - 20.0).abs() < 0.01);
    }

    #[test]
    fn prop_map_max() {
        // source at max → target at max
        let result = proportional_map(100.0, 0.0, 100.0, 20.0, 40.0);
        assert!((result - 40.0).abs() < 0.01);
    }

    #[test]
    fn prop_map_same_range() {
        // Same range → same value
        let result = proportional_map(75.0, 0.0, 100.0, 0.0, 100.0);
        assert!((result - 75.0).abs() < 0.01);
    }

    #[test]
    fn prop_map_zero_source_span() {
        // Degenerate: source span is 0 → return target min
        let result = proportional_map(50.0, 50.0, 50.0, 20.0, 40.0);
        assert!((result - 20.0).abs() < f64::EPSILON);
    }

    #[test]
    fn prop_map_quarter() {
        // source: 0-100 at 25% → target: 0-100 → 25%
        let result = proportional_map(25.0, 0.0, 100.0, 0.0, 100.0);
        assert!((result - 25.0).abs() < 0.01);
    }

    #[test]
    fn prop_map_narrow_target() {
        // source: 0-100 at 50% → target: 40-60 → 50
        let result = proportional_map(50.0, 0.0, 100.0, 40.0, 60.0);
        assert!((result - 50.0).abs() < 0.01);
    }
}
