//! Format detection and audio analysis utilities.
//!
//! This module provides functions for detecting and analyzing properties
//! of audio signals, such as sample rate, fundamental frequency, and
//! silence regions.

use crate::{
    AudioSample, AudioSampleError, AudioSampleResult, AudioSamples, AudioTypeConversion, CastFrom,
    CastInto, ConvertTo, I24, LayoutError, ParameterError, RealFloat, to_precision,
};

#[cfg(feature = "statistics")]
use crate::operations::traits::AudioVoiceActivityDetection;

#[cfg(feature = "statistics")]
use crate::operations::types::VadConfig;

#[cfg(feature = "fft")]
use num_complex::Complex;
#[cfg(feature = "fft")]
use rustfft::FftPlanner;

/// Attempts to detect the sample rate of an audio signal based on its content.
///
/// This function analyzes the frequency content of the signal to estimate
/// the original sample rate. It's useful for validating sample rate metadata
/// or detecting resampled content.
///
/// # Arguments
/// * `audio` - The audio signal to analyze
///
/// # Returns
/// * `Some(sample_rate)` - The detected sample rate in Hz
/// * `None` - If sample rate cannot be reliably detected
#[cfg(feature = "fft")]
pub fn detect_sample_rate<T: AudioSample>(audio: &AudioSamples<T>) -> AudioSampleResult<Option<u32>>
where
    i16: ConvertTo<T>,
    I24: ConvertTo<T>,
    i32: ConvertTo<T>,
    f32: ConvertTo<T>,
    f64: ConvertTo<T>,
    for<'b> AudioSamples<'b, T>: AudioTypeConversion<'b, T>,
{
    let audio_f64 = audio.as_f64();

    // Use the first channel for analysis
    let data = match audio_f64.as_mono() {
        Some(mono) => mono
            .as_slice()
            .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                operation: "audio processing".to_string(),
                layout_type: "non-contiguous mono array".to_string(),
            }))?
            .to_vec(),
        None => {
            // Use the first channel of multi-channel audio
            let multi = audio_f64
                .as_multi_channel()
                .ok_or(AudioSampleError::Parameter(ParameterError::invalid_value(
                    "audio_data",
                    "Audio must be multi-channel",
                )))?;
            multi
                .row(0)
                .as_slice()
                .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                    operation: "audio processing".to_string(),
                    layout_type: "non-contiguous multi-channel array".to_string(),
                }))?
                .to_vec()
        }
    };

    // Look for high-frequency cutoff patterns that might indicate resampling
    let spectrum = compute_spectrum(&data)?;
    let nyquist_freq = audio.sample_rate().get() as f64 / 2.0;

    // Analyze the spectrum for sharp cutoffs that might indicate resampling
    let detected_rate = analyze_spectrum_for_cutoff(&spectrum, nyquist_freq);

    Ok(detected_rate)
}

/// Detects the fundamental frequency of an audio signal.
///
/// This function uses spectral analysis to find the fundamental frequency
/// of the input signal. It's useful for pitch detection and harmonic analysis.
///
/// # Arguments
/// * `audio` - The audio signal to analyze
///
/// # Returns
/// * `Some(frequency)` - The detected fundamental frequency in Hz
/// * `None` - If no clear fundamental frequency is detected
pub fn detect_fundamental_frequency<T: AudioSample>(
    audio: &AudioSamples<T>,
) -> AudioSampleResult<Option<f64>>
where
    i16: ConvertTo<T>,
    I24: ConvertTo<T>,
    i32: ConvertTo<T>,
    f32: ConvertTo<T>,
    f64: ConvertTo<T>,
    for<'b> AudioSamples<'b, T>: AudioTypeConversion<'b, T>,
{
    let audio_f64 = audio.as_f64();

    // Use the first channel for analysis
    let data = match audio_f64.as_mono() {
        Some(mono) => mono
            .as_slice()
            .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                operation: "audio processing".to_string(),
                layout_type: "non-contiguous mono array".to_string(),
            }))?
            .to_vec(),
        None => {
            // Use the first channel of multi-channel audio
            let multi = audio_f64
                .as_multi_channel()
                .ok_or(AudioSampleError::Parameter(ParameterError::invalid_value(
                    "audio_data",
                    "Audio must be multi-channel",
                )))?;
            multi
                .row(0)
                .as_slice()
                .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                    operation: "audio processing".to_string(),
                    layout_type: "non-contiguous multi-channel array".to_string(),
                }))?
                .to_vec()
        }
    };

    if data.is_empty() {
        return Ok(None);
    }

    // Use autocorrelation method for fundamental frequency detection
    let fundamental = detect_fundamental_autocorrelation(&data, audio.sample_rate().get() as f64)?;

    Ok(fundamental)
}

/// Detects silence regions in an audio signal.
///
/// This function identifies time intervals where the audio signal
/// falls below a specified threshold, indicating silence or very quiet regions.
///
/// # Arguments
/// * `audio` - The audio signal to analyze
/// * `threshold` - The amplitude threshold below which samples are considered silence
///
/// # Returns
/// A vector of (start_time, end_time) tuples representing silence regions in seconds
pub fn detect_silence_regions<T, F>(
    audio: &AudioSamples<'_, T>,
    threshold: T,
) -> AudioSampleResult<Vec<(F, F)>>
where
    T: AudioSample + CastInto<F> + ConvertTo<F>,
    F: RealFloat + ConvertTo<T>,
{
    let mut silence_regions = Vec::new();
    let mut in_silence = false;
    let mut silence_start = 0;

    let sample_rate = to_precision::<F, _>(audio.sample_rate().get());
    let samples_per_second = sample_rate;
    let threshold: F = to_precision::<F, _>(threshold);

    // Helper function to check if a sample is below threshold (absolute value)
    let is_below_threshold = |sample: T| -> bool {
        let abs_val: F = sample.cast_into();
        let abs_val = abs_val.abs();
        abs_val < threshold
    };

    match audio.as_mono() {
        Some(mono) => {
            for (i, &sample) in mono.iter().enumerate() {
                if is_below_threshold(sample) {
                    if !in_silence {
                        silence_start = i;
                        in_silence = true;
                    }
                } else if in_silence {
                    let start_time = to_precision::<F, _>(silence_start) / samples_per_second;
                    let end_time = to_precision::<F, _>(i) / samples_per_second;
                    silence_regions.push((start_time, end_time));
                    in_silence = false;
                }
            }

            // Handle case where silence extends to the end
            if in_silence {
                let start_time = to_precision::<F, _>(silence_start) / samples_per_second;
                let end_time = to_precision::<F, _>(mono.len()) / samples_per_second;
                silence_regions.push((start_time, end_time));
            }
        }
        None => {
            // Multi-channel analysis - consider it silence only if ALL channels are below threshold
            let multi = audio.as_multi_channel().ok_or(AudioSampleError::Parameter(
                ParameterError::invalid_value("audio_data", "Audio must be multi-channel"),
            ))?;
            for i in 0..multi.ncols() {
                let all_below_threshold =
                    (0..multi.nrows()).all(|ch| is_below_threshold(multi[(ch, i)]));

                if all_below_threshold {
                    if !in_silence {
                        silence_start = i;
                        in_silence = true;
                    }
                } else if in_silence {
                    let start_time = to_precision::<F, _>(silence_start) / samples_per_second;
                    let end_time = to_precision::<F, _>(i) / samples_per_second;
                    silence_regions.push((start_time, end_time));
                    in_silence = false;
                }
            }

            // Handle case where silence extends to the end
            if in_silence {
                let start_time = to_precision::<F, _>(silence_start) / samples_per_second;
                let end_time = to_precision::<F, _>(multi.ncols()) / samples_per_second;
                silence_regions.push((start_time, end_time));
            }
        }
    }

    Ok(silence_regions)
}

/// Compute a per-frame voice/speech activity mask using the configured VAD.
///
/// This is a convenience wrapper around [`AudioVoiceActivityDetection::voice_activity_mask`].
///
/// Returns one boolean decision per analysis frame.
#[cfg(feature = "statistics")]
pub fn detect_voice_activity_mask<T: AudioSample, F: RealFloat>(
    audio: &AudioSamples<'_, T>,
    config: &VadConfig<F>,
) -> AudioSampleResult<Vec<bool>> {
    audio.voice_activity_mask(config)
}

/// Compute contiguous speech regions as `(start_sample, end_sample)` pairs.
///
/// This is a convenience wrapper around [`AudioVoiceActivityDetection::speech_regions`].
/// `end_sample` is exclusive.
#[cfg(feature = "statistics")]
pub fn detect_speech_regions<T: AudioSample, F: RealFloat>(
    audio: &AudioSamples<'_, T>,
    config: &VadConfig<F>,
) -> AudioSampleResult<Vec<(usize, usize)>> {
    audio.speech_regions(config)
}

/// Detects the dynamic range of an audio signal.
///
/// This function analyzes the amplitude distribution of the signal
/// to determine its dynamic range characteristics.
///
/// # Arguments
/// * `audio` - The audio signal to analyze
///
/// # Returns
/// A tuple of (peak_amplitude, rms_amplitude, dynamic_range_db)
pub fn detect_dynamic_range<T, F>(audio: &AudioSamples<T>) -> AudioSampleResult<(F, F, F)>
where
    T: AudioSample + ConvertTo<F>,
    F: RealFloat + ConvertTo<T>,
    i16: ConvertTo<T>,
    I24: ConvertTo<T>,
    i32: ConvertTo<T>,
    f32: ConvertTo<T>,
    f64: ConvertTo<T>,
    for<'b> AudioSamples<'b, T>: AudioTypeConversion<'b, T>,
{
    let audio_f = audio.as_float::<F>();

    let data: Vec<F> = match audio_f.as_mono() {
        Some(mono) => mono
            .as_slice()
            .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                operation: "audio processing".to_string(),
                layout_type: "non-contiguous mono array".to_string(),
            }))?
            .to_vec(),
        None => {
            // Flatten multi-channel audio
            let multi = audio_f
                .as_multi_channel()
                .ok_or(AudioSampleError::Parameter(ParameterError::invalid_value(
                    "audio_data",
                    "Audio must be multi-channel",
                )))?;
            multi
                .as_slice()
                .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                    operation: "audio processing".to_string(),
                    layout_type: "non-contiguous multi-channel array".to_string(),
                }))?
                .to_vec()
        }
    };

    if data.is_empty() {
        return Ok((F::zero(), F::zero(), F::zero()));
    }

    // Calculate peak amplitude
    let peak_amplitude = data.iter().map(|&x| x.abs()).fold(F::zero(), F::max);

    // Calculate RMS amplitude
    let rms_amplitude =
        (data.iter().fold(F::zero(), |acc, x| acc + *x) / to_precision::<F, _>(data.len())).sqrt();

    // Calculate dynamic range in dB
    let dynamic_range_db = if rms_amplitude > F::zero() {
        to_precision::<F, _>(20.0) * (peak_amplitude / rms_amplitude).log10()
    } else {
        F::zero()
    };

    Ok((peak_amplitude, rms_amplitude, dynamic_range_db))
}

/// Detects clipping in an audio signal.
///
/// This function identifies regions where the audio signal is clipped
/// (reaches the maximum or minimum values and stays there).
///
/// # Arguments
/// * `audio` - The audio signal to analyze
/// * `threshold_ratio` - The ratio of max value to consider as clipping (default: 0.99)
///
/// # Returns
/// A vector of (start_time, end_time) tuples representing clipped regions in seconds
pub fn detect_clipping<'a, T, F>(
    audio: &'a AudioSamples<T>,
    threshold_ratio: F,
) -> AudioSampleResult<Vec<(F, F)>>
where
    T: AudioSample + ConvertTo<F> + CastFrom<F>,
    F: RealFloat + ConvertTo<T>,
    i16: ConvertTo<T>,
    I24: ConvertTo<T>,
    i32: ConvertTo<T>,
    f32: ConvertTo<T>,
    f64: ConvertTo<T>,
    for<'b> AudioSamples<'b, T>: AudioTypeConversion<'b, T>,
{
    let mut clipped_regions = Vec::new();
    let mut in_clipped = false;
    let mut clipped_start = 0;

    let sample_rate = to_precision::<F, _>(audio.sample_rate().get());

    // Determine clipping thresholds
    let max_val: F = to_precision::<F, _>(T::MAX);
    let min_val: F = to_precision::<F, _>(T::MIN);

    let upper_threshold: T = T::cast_from(max_val * threshold_ratio);
    let lower_threshold: T = T::cast_from(min_val * threshold_ratio);

    fn is_clipped<T: AudioSample>(sample: T, upper_threshold: T, lower_threshold: T) -> bool {
        sample >= upper_threshold || sample <= lower_threshold
    }

    match audio.as_mono() {
        Some(mono) => {
            for (i, &sample) in mono.iter().enumerate() {
                if is_clipped(sample, upper_threshold, lower_threshold) {
                    if !in_clipped {
                        clipped_start = i;
                        in_clipped = true;
                    }
                } else if in_clipped {
                    let start_time = to_precision::<F, _>(clipped_start) / sample_rate;
                    let end_time = to_precision::<F, _>(i) / sample_rate;
                    clipped_regions.push((start_time, end_time));
                    in_clipped = false;
                }
            }

            // Handle case where clipping extends to the end
            if in_clipped {
                let start_time = to_precision::<F, _>(clipped_start) / sample_rate;
                let end_time = to_precision::<F, _>(mono.len()) / sample_rate;
                clipped_regions.push((start_time, end_time));
            }
        }
        None => {
            // Multi-channel analysis - consider it clipped if ANY channel is clipped
            let multi = audio.as_multi_channel().ok_or(AudioSampleError::Parameter(
                ParameterError::invalid_value("audio_data", "Audio must be mono or multi-channel"),
            ))?;
            for i in 0..multi.ncols() {
                let mut any_clipped = false;
                for ch in 0..multi.nrows() {
                    if is_clipped(multi[(ch, i)], upper_threshold, lower_threshold) {
                        any_clipped = true;
                        break;
                    }
                }

                if any_clipped {
                    if !in_clipped {
                        clipped_start = i;
                        in_clipped = true;
                    }
                } else if in_clipped {
                    let start_time = to_precision::<F, _>(clipped_start) / sample_rate;
                    let end_time = to_precision::<F, _>(i) / sample_rate;
                    clipped_regions.push((start_time, end_time));
                    in_clipped = false;
                }
            }

            // Handle case where clipping extends to the end
            if in_clipped {
                let start_time = to_precision::<F, _>(clipped_start) / sample_rate;
                let end_time = to_precision::<F, _>(multi.ncols()) / sample_rate;
                clipped_regions.push((start_time, end_time));
            }
        }
    }

    Ok(clipped_regions)
}

/// Estimate noise floor of audio signal.
pub fn estimate_noise_floor<T, F>(audio: &AudioSamples<T>) -> AudioSampleResult<Option<F>>
where
    T: AudioSample + ConvertTo<F>,
    F: RealFloat + ConvertTo<T>,
    i16: ConvertTo<T>,
    I24: ConvertTo<T>,
    i32: ConvertTo<T>,
    f32: ConvertTo<T>,
    f64: ConvertTo<T>,
    for<'b> AudioSamples<'b, T>: AudioTypeConversion<'b, T>,
{
    let audio = audio.as_float();

    let data: Vec<F> = match audio.as_mono() {
        Some(mono) => mono
            .as_slice()
            .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                operation: "audio processing".to_string(),
                layout_type: "non-contiguous mono array".to_string(),
            }))?
            .to_vec(),
        None => {
            // Use first channel
            let multi = audio.as_multi_channel().ok_or(AudioSampleError::Parameter(
                ParameterError::invalid_value("audio_data", "Audio must be multi-channel"),
            ))?;
            multi
                .row(0)
                .as_slice()
                .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                    operation: "audio processing".to_string(),
                    layout_type: "non-contiguous multi-channel array".to_string(),
                }))?
                .to_vec()
        }
    };

    if data.is_empty() {
        return Ok(None);
    }

    // Find quietest regions (bottom 10th percentile)
    let mut sorted_abs: Vec<F> = data.iter().map(|&x| x.abs()).collect();
    sorted_abs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));

    let percentile_10 = sorted_abs.len() / 10;
    if percentile_10 > 0 {
        let noise_level = sorted_abs[percentile_10];
        if noise_level > F::zero() {
            let noise_floor_db = to_precision::<F, _>(20.0) * noise_level.log10();
            return Ok(Some(noise_floor_db));
        }
    }

    Ok(None)
}

/// Estimate frequency response range.
#[cfg(feature = "fft")]
pub fn estimate_frequency_range<T, F>(audio: &AudioSamples<T>) -> AudioSampleResult<Option<(F, F)>>
where
    T: AudioSample + ConvertTo<F>,
    F: RealFloat + ConvertTo<T>,
    i16: ConvertTo<T>,
    I24: ConvertTo<T>,
    i32: ConvertTo<T>,
    f32: ConvertTo<T>,
    f64: ConvertTo<T>,
    for<'b> AudioSamples<'b, T>: AudioTypeConversion<'b, T>,
{
    let audio = audio.as_float();

    let data: Vec<F> = match audio.as_mono() {
        Some(mono) => mono
            .as_slice()
            .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                operation: "audio processing".to_string(),
                layout_type: "non-contiguous mono array".to_string(),
            }))?
            .to_vec(),
        None => {
            // Use first channel
            let multi = audio.as_multi_channel().ok_or(AudioSampleError::Parameter(
                ParameterError::invalid_value("audio_data", "Audio must be multi-channel"),
            ))?;
            multi
                .row(0)
                .as_slice()
                .ok_or(AudioSampleError::Layout(LayoutError::NonContiguous {
                    operation: "audio processing".to_string(),
                    layout_type: "non-contiguous multi-channel array".to_string(),
                }))?
                .to_vec()
        }
    };

    if data.len() < 1024 {
        return Ok(None); // Too short for meaningful analysis
    }

    // Compute spectrum
    let spectrum = compute_spectrum(&data)?;
    let sample_rate = to_precision::<F, _>(audio.sample_rate().get());
    let nyquist = sample_rate / to_precision::<F, _>(2.0);

    // Find frequency range with significant energy
    let threshold =
        spectrum.iter().fold(F::zero(), |acc, &x| acc.max(x)) * to_precision::<F, _>(0.01); // 1% of peak

    let mut low_freq = None;
    let mut high_freq = None;

    for (i, &energy) in spectrum.iter().enumerate() {
        if energy > threshold {
            let freq = (to_precision::<F, _>(i) / to_precision::<F, _>(spectrum.len())) * nyquist;
            if low_freq.is_none() {
                low_freq = Some(freq);
            }
            high_freq = Some(freq);
        }
    }

    if let (Some(low), Some(high)) = (low_freq, high_freq) {
        Ok(Some((low, high)))
    } else {
        Ok(None)
    }
}

#[cfg(feature = "fft")]
/// Computes the frequency spectrum of the given audio data using FFT.
pub fn compute_spectrum<F: RealFloat>(data: &[F]) -> AudioSampleResult<Vec<F>> {
    let mut planner = FftPlanner::new();
    let fft = planner.plan_fft_forward(data.len());

    let mut buffer: Vec<Complex<F>> = data.iter().map(|&x| Complex::new(x, F::zero())).collect();
    fft.process(&mut buffer);

    let spectrum: Vec<F> = buffer.iter().map(|c| c.norm()).collect();
    Ok(spectrum)
}

#[cfg(feature = "fft")]
/// Analyzes a frequency spectrum for potential cutoff frequencies that might indicate resampling.
///
/// # Panics
/// Panics if internal float-to-`usize` conversion fails (e.g. if `nyquist_freq` is non-finite).
pub fn analyze_spectrum_for_cutoff<F: RealFloat>(spectrum: &[F], nyquist_freq: F) -> Option<u32> {
    // Look for sharp cutoffs that might indicate resampling
    let len = spectrum.len();
    let half_len = len / 2;

    // Check for energy drops that might indicate filtering
    let mut candidates = Vec::new();

    // Common resampling target frequencies
    let common_rates = [8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000];

    for &rate in &common_rates {
        let target_nyquist = to_precision::<F, _>(rate) / to_precision::<F, _>(2.0);
        if target_nyquist < nyquist_freq {
            // Check if there's a significant drop in energy around this frequency
            let bin_index = (target_nyquist / nyquist_freq * to_precision::<F, _>(half_len))
                .to_usize()
                .expect("Should not fail");
            if bin_index < spectrum.len() - 10 {
                let energy_before: F = spectrum[bin_index.saturating_sub(5)..bin_index]
                    .iter()
                    .fold(F::zero(), |acc, &x| acc + x);
                let energy_after = spectrum[bin_index..bin_index + 10]
                    .iter()
                    .fold(F::zero(), |acc, &x| acc + x);

                if energy_before > energy_after * to_precision::<F, _>(2.0) {
                    candidates.push(rate);
                }
            }
        }
    }

    // Return the most likely candidate
    candidates.first().copied()
}

/// Detects the fundamental frequency using autocorrelation analysis.
///
/// # Panics
/// Panics if internal float-to-`usize` conversions fail (e.g. if `sample_rate` is non-finite).
pub fn detect_fundamental_autocorrelation<F: RealFloat>(
    data: &[F],
    sample_rate: F,
) -> AudioSampleResult<Option<F>> {
    if data.len() < 2 {
        return Ok(None);
    }

    let min_freq = to_precision::<F, _>(50.0); // Minimum fundamental frequency to detect
    let max_freq = to_precision::<F, _>(2000.0); // Maximum fundamental frequency to detect

    let min_period = (sample_rate / max_freq)
        .to_usize()
        .expect("Should not fail");
    let max_period = (sample_rate / min_freq)
        .to_usize()
        .expect("Should not fail");

    if max_period >= data.len() {
        return Ok(None);
    }

    let mut max_correlation = F::zero();
    let mut best_period = 0;

    for period in min_period..max_period.min(data.len() / 2) {
        let mut correlation = F::zero();
        let mut count = 0;

        for i in 0..(data.len() - period) {
            correlation += data[i] * data[i + period];
            count += 1;
        }

        if count > 0 {
            correlation /= to_precision::<F, _>(count);

            if correlation > max_correlation {
                max_correlation = correlation;
                best_period = period;
            }
        }
    }

    if best_period > 0 && max_correlation > to_precision::<F, _>(0.1) {
        let fundamental_freq = sample_rate / to_precision::<F, _>(best_period);
        Ok(Some(fundamental_freq))
    } else {
        Ok(None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sample_rate;
    use ndarray::array;
    use std::f64::consts::PI;

    #[test]
    fn test_detect_silence_regions() {
        // Create a signal with silence regions
        let data = array![0.0f32, 0.0, 0.0, 1.0, 2.0, 0.0, 0.0, 3.0, 0.0, 0.0];
        let audio: AudioSamples<'_, f32> = AudioSamples::new_mono(data, sample_rate!(10)); // 10 Hz sample rate for easy calculation

        let silence_regions: Vec<(f32, f32)> =
            detect_silence_regions(&audio, 0.5_f32).expect("Failed to detect silence");

        assert!(!silence_regions.is_empty());
        // First silence region should be at the beginning
        assert_eq!(silence_regions[0].0, 0.0);
        assert_eq!(silence_regions[0].1, 0.3);
    }

    #[test]
    fn test_detect_dynamic_range() {
        // Create a signal with known dynamic range
        let data = array![0.1f32, 0.5, 1.0, 0.2, 0.8];
        let audio = AudioSamples::new_mono(data, sample_rate!(44100));

        let (peak, rms, dynamic_range): (f32, f32, f32) = detect_dynamic_range(&audio).unwrap();

        assert_eq!(peak, 1.0);
        assert!(rms > 0.0);
        assert!(dynamic_range > 0.0);
    }

    #[test]
    fn test_detect_clipping() {
        // Create a signal with clipping
        let data = array![0.5f32, 1.0, 1.0, 1.0, 0.5, -1.0, -1.0, 0.0];
        let audio = AudioSamples::new_mono(data, sample_rate!(8)); // 8 Hz sample rate for easy calculation

        let clipped_regions = detect_clipping(&audio, 0.99).expect("Failed to detect clipping");

        assert!(!clipped_regions.is_empty());
    }

    #[test]
    fn test_detect_fundamental_frequency() {
        // Create a sine wave with known frequency
        let sample_rate = 44100.0;
        let frequency = 440.0; // A4
        let duration = 1.0; // 1 second

        let samples: Vec<f32> = (0..(sample_rate * duration) as usize)
            .map(|i| {
                let t = i as f64 / sample_rate;
                (2.0 * PI * frequency * t).sin() as f32
            })
            .collect();

        let data = ndarray::Array1::from_vec(samples);
        let audio = AudioSamples::new_mono(data, sample_rate!(44100));

        let detected_freq = detect_fundamental_frequency(&audio).unwrap();

        if let Some(freq) = detected_freq {
            // Allow broader tolerance for autocorrelation-based detection on pure tones
            // Pure sine waves can confuse autocorrelation algorithms
            let tolerance =
                if (freq - frequency / 2.0).abs() < 20.0 || (freq - frequency / 4.0).abs() < 20.0 {
                    // If it's detecting a subharmonic (half or quarter frequency), that's acceptable
                    400.0
                } else {
                    50.0 // Otherwise use more relaxed tolerance
                };

            assert!(
                (freq - frequency).abs() < tolerance,
                "Expected: {} Hz, Detected: {} Hz, Difference: {} Hz",
                frequency,
                freq,
                (freq - frequency).abs()
            );
        } else {
            panic!("No frequency detected");
        }
    }
}
