//! Runtime audio hydration, bus controls, listener resolution, and diagnostics.

use std::collections::{HashMap, HashSet};

use bevy::audio::{
    AudioPlayer, AudioSink, AudioSinkPlayback, AudioSource, PlaybackMode, PlaybackSettings,
    SpatialAudioSink, SpatialListener, SpatialScale, Volume,
};
use bevy::prelude::*;
use cpal::traits::{DeviceTrait, HostTrait};
use settings::{
    AudioSettings, ProjectSettings, AUDIO_BUS_MASTER_ID, AUDIO_GAIN_DB_MAX, AUDIO_GAIN_DB_MIN,
};
use shared::{
    asset_server_path, authoring_component_active, ActorId, AudioAttenuationDesc,
    AudioListenerDesc, AudioRolloff, AudioSourceDesc, AuthoringComponentStates, InspectorOrder,
    LevelObject, COMPONENT_AUDIO_LISTENER_DESC, COMPONENT_AUDIO_SOURCE_DESC,
};
use sim::{PlayerCamera, SimEnabled};

/// Runtime override for an authored project bus. `None` preserves the project value.
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct AudioBusOverride {
    pub gain_db: Option<f32>,
    pub muted: Option<bool>,
}

/// Mutable runtime mixer state used by gameplay and options menus.
#[derive(Resource, Debug, Clone, Default)]
pub struct AudioBusRuntime {
    overrides: HashMap<String, AudioBusOverride>,
}

impl AudioBusRuntime {
    pub fn set_gain_db(&mut self, bus_id: impl Into<String>, gain_db: f32) -> Result<(), String> {
        if !gain_db.is_finite() {
            return Err("audio bus gain must be finite".into());
        }
        self.overrides.entry(bus_id.into()).or_default().gain_db =
            Some(gain_db.clamp(AUDIO_GAIN_DB_MIN, AUDIO_GAIN_DB_MAX));
        Ok(())
    }

    pub fn set_muted(&mut self, bus_id: impl Into<String>, muted: bool) {
        self.overrides.entry(bus_id.into()).or_default().muted = Some(muted);
    }

    pub fn clear_bus(&mut self, bus_id: &str) {
        self.overrides.remove(bus_id);
    }

    pub fn clear(&mut self) {
        self.overrides.clear();
    }

    pub fn bus_override(&self, bus_id: &str) -> Option<AudioBusOverride> {
        self.overrides.get(bus_id).copied()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioDeviceStatus {
    Unknown,
    Available,
    Unavailable,
}

/// User-facing audio backend and codec capability snapshot.
#[derive(Resource, Debug, Clone)]
pub struct AudioRuntimeDiagnostics {
    pub status: AudioDeviceStatus,
    pub backend: String,
    pub output_device: Option<String>,
    pub message: String,
    pub codecs: Vec<String>,
}

impl Default for AudioRuntimeDiagnostics {
    fn default() -> Self {
        Self {
            status: AudioDeviceStatus::Unknown,
            backend: String::new(),
            output_device: None,
            message: "Audio output has not been probed yet.".into(),
            codecs: vec![
                "Ogg Vorbis".into(),
                "WAV".into(),
                "MP3".into(),
                "FLAC".into(),
            ],
        }
    }
}

#[derive(Resource, Debug, Clone, Copy, Default)]
pub struct ActiveAudioListener(pub Option<Entity>);

#[derive(Component, Debug, Clone, PartialEq)]
struct RuntimeAudioSourceState {
    descriptor: AudioSourceDesc,
    desired_playing: bool,
    paused_by_sim: bool,
}

#[derive(Component, Debug, Clone, Copy, PartialEq)]
struct ManagedAudioListener {
    ear_gap: f32,
}

#[derive(Component, Debug, Clone)]
pub struct AuthoredAudioVoice {
    pub source: Entity,
    pub bus: String,
    pub source_gain_db: f32,
    pub mix_weight: f32,
    pub spatial: bool,
    pub attenuation: AudioAttenuationDesc,
    pub looping: bool,
}

/// Runtime adapter for scene-authored audio descriptors.
pub struct GameAudioPlugin;

impl Plugin for GameAudioPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<AudioBusRuntime>()
            .init_resource::<AudioRuntimeDiagnostics>()
            .init_resource::<ActiveAudioListener>()
            .add_systems(PostStartup, probe_audio_output)
            .add_systems(
                Update,
                (
                    reconcile_audio_listener,
                    reconcile_audio_sources,
                    update_voice_mix,
                    cleanup_finished_voices,
                )
                    .chain(),
            );
    }
}

fn probe_audio_output(mut diagnostics: ResMut<AudioRuntimeDiagnostics>) {
    let host = cpal::default_host();
    diagnostics.backend = host.id().name().to_string();
    match host.default_output_device() {
        Some(device) => {
            let name = device
                .description()
                .map(|description| description.name().to_string())
                .unwrap_or_else(|error| format!("Unknown device ({error})"));
            diagnostics.status = AudioDeviceStatus::Available;
            diagnostics.output_device = Some(name.clone());
            diagnostics.message = format!("Audio output ready on {name}.");
        }
        None => {
            diagnostics.status = AudioDeviceStatus::Unavailable;
            diagnostics.output_device = None;
            diagnostics.message = "No default audio output device is available.".into();
            warn!("{}", diagnostics.message);
        }
    }
}

/// Returns the effective linear gain for a bus including parent buses and runtime overrides.
pub fn effective_bus_gain_linear(
    settings: &AudioSettings,
    runtime: &AudioBusRuntime,
    requested_bus: &str,
) -> f32 {
    let mut current = if settings.bus(requested_bus).is_some() {
        requested_bus
    } else {
        AUDIO_BUS_MASTER_ID
    };
    let mut visited = HashSet::new();
    let mut gain_db = 0.0;

    while visited.insert(current.to_string()) {
        let Some(bus) = settings.bus(current) else {
            break;
        };
        let runtime_override = runtime.bus_override(current).unwrap_or_default();
        if runtime_override.muted.unwrap_or(bus.muted) {
            return 0.0;
        }
        gain_db += runtime_override.gain_db.unwrap_or(bus.gain_db);
        let Some(parent) = bus.parent.as_deref() else {
            break;
        };
        current = parent;
    }

    db_to_linear(gain_db)
}

pub fn db_to_linear(db: f32) -> f32 {
    if db <= AUDIO_GAIN_DB_MIN {
        0.0
    } else {
        10.0_f32.powf(db / 20.0)
    }
}

/// Equal-power 2D/spatial voice weights for a continuous authored blend.
pub fn audio_voice_mix(spatial_blend: f32) -> Vec<(bool, f32)> {
    let blend = spatial_blend.clamp(0.0, 1.0);
    if blend <= f32::EPSILON {
        vec![(false, 1.0)]
    } else if blend >= 1.0 - f32::EPSILON {
        vec![(true, 1.0)]
    } else {
        vec![(false, (1.0 - blend).sqrt()), (true, blend.sqrt())]
    }
}

/// Authored attenuation curve, normalized to full gain at `min_distance` and silence at max.
pub fn attenuation_gain(desc: &AudioAttenuationDesc, distance: f32) -> f32 {
    if !distance.is_finite() {
        return 0.0;
    }
    let min = desc.min_distance.max(0.001);
    let max = desc.max_distance.max(min + 0.001);
    if distance <= min {
        return 1.0;
    }
    if distance >= max {
        return 0.0;
    }
    let t = ((distance - min) / (max - min)).clamp(0.0, 1.0);
    let factor = desc.rolloff_factor.max(0.001);
    match desc.rolloff {
        AudioRolloff::Linear => (1.0 - t).powf(factor),
        AudioRolloff::Inverse => ((1.0 - t) / (1.0 + factor * 4.0 * t)).clamp(0.0, 1.0),
        AudioRolloff::Exponential => ((distance / min).powf(-factor) * (1.0 - t)).clamp(0.0, 1.0),
    }
}

/// Coordinate scale used only for rodio's left/right spatial panning.
///
/// Rodio also applies an inverse-square distance gain. Keeping every audible point inside its
/// unity-distance zone preserves the interaural panning ratio while leaving the authored
/// attenuation curve as the sole distance-gain authority.
pub fn spatial_panning_scale(desc: &AudioAttenuationDesc, ear_gap: f32) -> f32 {
    const RODIO_UNITY_ZONE_FRACTION: f32 = 0.25;

    let audible_radius = if desc.max_distance.is_finite() {
        desc.max_distance.max(0.001)
    } else {
        0.001
    };
    let ear_gap = if ear_gap.is_finite() {
        ear_gap.max(0.001)
    } else {
        0.001
    };
    (RODIO_UNITY_ZONE_FRACTION / (audible_radius + ear_gap * 0.5)).max(f32::MIN_POSITIVE)
}

fn reconcile_audio_listener(world: &mut World) {
    let enabled = world.resource::<SimEnabled>().0;
    let mut candidates = Vec::new();
    {
        let mut query = world.query_filtered::<(
            Entity,
            &AudioListenerDesc,
            Option<&ActorId>,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>();
        for (entity, desc, actor_id, states, legacy_order) in query.iter(world) {
            if desc.enabled
                && authoring_component_active(states, legacy_order, COMPONENT_AUDIO_LISTENER_DESC)
            {
                candidates.push((
                    entity,
                    *desc,
                    actor_id.map(|id| id.0.clone()).unwrap_or_default(),
                ));
            }
        }
    }
    candidates.sort_by(|a, b| {
        b.1.priority
            .cmp(&a.1.priority)
            .then_with(|| a.2.cmp(&b.2))
            .then_with(|| a.0.to_bits().cmp(&b.0.to_bits()))
    });

    let selected = if enabled {
        candidates
            .first()
            .map(|(entity, desc, _)| (*entity, desc.ear_gap))
            .or_else(|| {
                let mut query = world.query_filtered::<Entity, With<PlayerCamera>>();
                query
                    .iter(world)
                    .min_by_key(|entity| entity.to_bits())
                    .map(|entity| (entity, 0.2))
            })
    } else {
        None
    };

    let mut managed_query = world.query::<(Entity, &ManagedAudioListener)>();
    let managed: Vec<(Entity, ManagedAudioListener)> = managed_query
        .iter(world)
        .map(|(entity, listener)| (entity, *listener))
        .collect();
    for (entity, _) in managed {
        if selected.is_none_or(|(selected, _)| selected != entity) {
            if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
                entity_mut
                    .remove::<SpatialListener>()
                    .remove::<ManagedAudioListener>();
            }
        }
    }

    if let Some((entity, ear_gap)) = selected {
        let ear_gap = ear_gap.max(0.001);
        let is_current = world
            .get::<ManagedAudioListener>(entity)
            .is_some_and(|listener| listener.ear_gap == ear_gap)
            && world.get::<SpatialListener>(entity).is_some();
        if !is_current {
            if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
                entity_mut.insert((
                    SpatialListener::new(ear_gap),
                    ManagedAudioListener { ear_gap },
                ));
            }
        }
    }
    let selected_entity = selected.map(|(entity, _)| entity);
    let mut active = world.resource_mut::<ActiveAudioListener>();
    if active.0 != selected_entity {
        active.0 = selected_entity;
    }
}

fn reconcile_audio_sources(world: &mut World) {
    let sim_enabled = world.resource::<SimEnabled>().0;
    let mut sources = Vec::new();
    {
        let mut query = world.query_filtered::<(
            Entity,
            &AudioSourceDesc,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>();
        for (entity, desc, states, legacy_order) in query.iter(world) {
            if authoring_component_active(states, legacy_order, COMPONENT_AUDIO_SOURCE_DESC) {
                sources.push((entity, desc.clone()));
            }
        }
    }
    let source_ids: HashSet<Entity> = sources.iter().map(|(entity, _)| *entity).collect();
    let mut voice_query = world.query::<&AuthoredAudioVoice>();
    let mut voiced_sources: HashSet<Entity> =
        voice_query.iter(world).map(|voice| voice.source).collect();
    let mut state_query = world.query_filtered::<Entity, With<RuntimeAudioSourceState>>();
    let stale: Vec<Entity> = state_query
        .iter(world)
        .filter(|entity| !source_ids.contains(entity))
        .collect();
    for entity in stale {
        stop_voice_entities(world, entity);
        if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
            entity_mut.remove::<RuntimeAudioSourceState>();
        }
    }

    for (entity, descriptor) in sources {
        let prior = world.get::<RuntimeAudioSourceState>(entity).cloned();
        let mut state = prior.clone().unwrap_or_else(|| RuntimeAudioSourceState {
            descriptor: descriptor.clone(),
            desired_playing: descriptor.autoplay,
            paused_by_sim: false,
        });
        if state.descriptor != descriptor {
            let autoplay_changed = state.descriptor.autoplay != descriptor.autoplay;
            stop_voice_entities(world, entity);
            voiced_sources.remove(&entity);
            state.descriptor = descriptor.clone();
            if autoplay_changed {
                state.desired_playing = descriptor.autoplay;
            }
        }

        if !sim_enabled {
            if !state.paused_by_sim {
                set_source_paused(world, entity, true);
                state.paused_by_sim = true;
            }
        } else {
            if state.paused_by_sim {
                set_source_paused(world, entity, false);
                state.paused_by_sim = false;
            }
            if state.desired_playing && !voiced_sources.contains(&entity) {
                if let Err(error) = spawn_source_voices(world, entity, &descriptor) {
                    warn!("Could not start authored audio source {entity:?}: {error}");
                    state.desired_playing = false;
                } else {
                    voiced_sources.insert(entity);
                }
            }
        }
        if prior.as_ref() != Some(&state) {
            if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
                entity_mut.insert(state);
            }
        }
    }
}

fn spawn_source_voices(
    world: &mut World,
    source: Entity,
    descriptor: &AudioSourceDesc,
) -> Result<(), String> {
    let clip = descriptor
        .clip
        .as_ref()
        .ok_or_else(|| "no audio clip is assigned".to_string())?;
    let source_path = clip
        .source_path
        .as_deref()
        .filter(|path| !path.trim().is_empty())
        .ok_or_else(|| "audio clip has no runtime source path".to_string())?;
    let asset_server = world.resource::<AssetServer>().clone();
    let handle: Handle<AudioSource> = asset_server.load(asset_server_path(source_path));
    let mode = if descriptor.looping {
        PlaybackMode::Loop
    } else {
        PlaybackMode::Once
    };
    let ear_gap = active_listener_ear_gap(world);
    let panning_scale = spatial_panning_scale(&descriptor.attenuation, ear_gap);

    for (spatial, mix_weight) in audio_voice_mix(descriptor.spatial_blend) {
        let child = world
            .spawn((
                Name::new(if spatial {
                    "Authored Spatial Audio Voice"
                } else {
                    "Authored 2D Audio Voice"
                }),
                Transform::IDENTITY,
                AudioPlayer::new(handle.clone()),
                PlaybackSettings {
                    mode,
                    volume: Volume::Linear(0.0),
                    speed: descriptor.pitch,
                    paused: false,
                    muted: false,
                    spatial,
                    spatial_scale: spatial.then(|| SpatialScale::new(panning_scale)),
                    start_position: None,
                    duration: None,
                },
                AuthoredAudioVoice {
                    source,
                    bus: descriptor.bus.clone(),
                    source_gain_db: descriptor.gain_db,
                    mix_weight,
                    spatial,
                    attenuation: descriptor.attenuation,
                    looping: descriptor.looping,
                },
                ChildOf(source),
            ))
            .id();
        trace!("Spawned authored audio voice {child:?} for {source:?}");
    }
    Ok(())
}

fn active_listener_ear_gap(world: &World) -> f32 {
    world
        .resource::<ActiveAudioListener>()
        .0
        .and_then(|entity| world.get::<ManagedAudioListener>(entity))
        .map(|listener| listener.ear_gap)
        .unwrap_or(0.2)
}

fn voice_entities(world: &mut World, source: Entity) -> Vec<Entity> {
    let mut query = world.query::<(Entity, &AuthoredAudioVoice)>();
    query
        .iter(world)
        .filter_map(|(entity, voice)| (voice.source == source).then_some(entity))
        .collect()
}

fn stop_voice_entities(world: &mut World, source: Entity) {
    for entity in voice_entities(world, source) {
        if let Some(sink) = world.get::<AudioSink>(entity) {
            sink.stop();
        }
        if let Some(sink) = world.get::<SpatialAudioSink>(entity) {
            sink.stop();
        }
        if let Ok(entity_mut) = world.get_entity_mut(entity) {
            entity_mut.despawn();
        }
    }
}

fn set_source_paused(world: &mut World, source: Entity, paused: bool) {
    for entity in voice_entities(world, source) {
        if let Some(mut settings) = world.get_mut::<PlaybackSettings>(entity) {
            settings.paused = paused;
        }
        if let Some(sink) = world.get::<AudioSink>(entity) {
            if paused {
                sink.pause();
            } else {
                sink.play();
            }
        }
        if let Some(sink) = world.get::<SpatialAudioSink>(entity) {
            if paused {
                sink.pause();
            } else {
                sink.play();
            }
        }
    }
}

/// Starts an authored source through the same runtime adapter used by autoplay.
pub fn play_authored_audio_source(world: &mut World, source: Entity) -> Result<(), String> {
    let descriptor = world
        .get::<AudioSourceDesc>(source)
        .cloned()
        .ok_or_else(|| "entity has no AudioSourceDesc".to_string())?;
    stop_voice_entities(world, source);
    let sim_enabled = world.resource::<SimEnabled>().0;
    let state = RuntimeAudioSourceState {
        descriptor: descriptor.clone(),
        desired_playing: true,
        paused_by_sim: !sim_enabled,
    };
    world.entity_mut(source).insert(state);
    if sim_enabled {
        spawn_source_voices(world, source, &descriptor)?;
    }
    Ok(())
}

/// Stops an authored source and disables replay until another explicit play or autoplay change.
pub fn stop_authored_audio_source(world: &mut World, source: Entity) {
    stop_voice_entities(world, source);
    if let Some(mut state) = world.get_mut::<RuntimeAudioSourceState>(source) {
        state.desired_playing = false;
        state.paused_by_sim = false;
    }
}

/// Clears PIE-owned playback/listeners so the next session starts from authored state.
pub fn reset_authored_audio_runtime(world: &mut World) {
    let mut query = world.query_filtered::<Entity, With<RuntimeAudioSourceState>>();
    let sources: Vec<Entity> = query.iter(world).collect();
    for source in sources {
        stop_voice_entities(world, source);
        if let Ok(mut entity_mut) = world.get_entity_mut(source) {
            entity_mut.remove::<RuntimeAudioSourceState>();
        }
    }
    let mut listener_query = world.query_filtered::<Entity, With<ManagedAudioListener>>();
    let listeners: Vec<Entity> = listener_query.iter(world).collect();
    for listener in listeners {
        if let Ok(mut entity_mut) = world.get_entity_mut(listener) {
            entity_mut
                .remove::<SpatialListener>()
                .remove::<ManagedAudioListener>();
        }
    }
    world.resource_mut::<ActiveAudioListener>().0 = None;
}

fn update_voice_mix(world: &mut World) {
    let project_audio = world.resource::<ProjectSettings>().audio.clone();
    let bus_runtime = world.resource::<AudioBusRuntime>().clone();
    let listener_position = world
        .resource::<ActiveAudioListener>()
        .0
        .and_then(|entity| world.get::<GlobalTransform>(entity))
        .map(GlobalTransform::translation);
    let listener_ear_gap = active_listener_ear_gap(world);
    let mut query = world.query::<(Entity, &AuthoredAudioVoice, Option<&GlobalTransform>)>();
    let voices: Vec<(Entity, AuthoredAudioVoice, Option<Vec3>)> = query
        .iter(world)
        .map(|(entity, voice, transform)| {
            (
                entity,
                voice.clone(),
                transform.map(GlobalTransform::translation),
            )
        })
        .collect();

    for (entity, voice, source_position) in voices {
        if voice.spatial {
            let scale = spatial_panning_scale(&voice.attenuation, listener_ear_gap);
            let desired = SpatialScale::new(scale);
            if let Some(mut settings) = world.get_mut::<PlaybackSettings>(entity) {
                if settings
                    .spatial_scale
                    .is_none_or(|current| current.0 != desired.0)
                {
                    settings.spatial_scale = Some(desired);
                }
            }
        }
        let bus_gain = effective_bus_gain_linear(&project_audio, &bus_runtime, &voice.bus);
        let attenuation = if voice.spatial {
            match (listener_position, source_position) {
                (Some(listener), Some(source)) => {
                    attenuation_gain(&voice.attenuation, listener.distance(source))
                }
                _ => 0.0,
            }
        } else {
            1.0
        };
        let gain = db_to_linear(voice.source_gain_db) * bus_gain * voice.mix_weight * attenuation;
        if let Some(mut sink) = world.get_mut::<AudioSink>(entity) {
            sink.set_volume(Volume::Linear(gain));
        }
        if let Some(mut sink) = world.get_mut::<SpatialAudioSink>(entity) {
            sink.set_volume(Volume::Linear(gain));
        }
    }
}

fn cleanup_finished_voices(world: &mut World) {
    let mut query = world.query::<(Entity, &AuthoredAudioVoice)>();
    let voices: Vec<(Entity, Entity, bool)> = query
        .iter(world)
        .map(|(entity, voice)| (entity, voice.source, voice.looping))
        .collect();
    let mut completed_sources = HashSet::new();
    for (entity, source, looping) in voices {
        if looping {
            continue;
        }
        let finished = world
            .get::<AudioSink>(entity)
            .is_some_and(AudioSinkPlayback::empty)
            || world
                .get::<SpatialAudioSink>(entity)
                .is_some_and(AudioSinkPlayback::empty);
        if finished {
            if let Ok(entity_mut) = world.get_entity_mut(entity) {
                entity_mut.despawn();
            }
            completed_sources.insert(source);
        }
    }
    for source in completed_sources {
        if voice_entities(world, source).is_empty() {
            if let Some(mut state) = world.get_mut::<RuntimeAudioSourceState>(source) {
                state.desired_playing = false;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bevy::asset::{AssetApp, AssetPlugin};
    use settings::{AudioBusSettings, AUDIO_BUS_MUSIC_ID, AUDIO_BUS_SFX_ID};
    use shared::{EditorAssetRef, AUDIO_CLIP_SUB_ASSET_ID};

    #[test]
    fn bus_gain_accumulates_parents_and_runtime_overrides() {
        let settings = AudioSettings::default();
        let mut runtime = AudioBusRuntime::default();
        runtime.set_gain_db(AUDIO_BUS_MASTER_ID, -6.0).unwrap();
        runtime.set_gain_db(AUDIO_BUS_MUSIC_ID, -6.0).unwrap();
        let gain = effective_bus_gain_linear(&settings, &runtime, AUDIO_BUS_MUSIC_ID);
        assert!((gain - db_to_linear(-12.0)).abs() < 0.0001);
        runtime.set_muted(AUDIO_BUS_MASTER_ID, true);
        assert_eq!(
            effective_bus_gain_linear(&settings, &runtime, AUDIO_BUS_MUSIC_ID),
            0.0
        );
    }

    #[test]
    fn missing_bus_falls_back_to_master() {
        let mut settings = AudioSettings::default();
        settings.bus_mut(AUDIO_BUS_MASTER_ID).unwrap().gain_db = -3.0;
        assert!(
            (effective_bus_gain_linear(&settings, &AudioBusRuntime::default(), "missing")
                - db_to_linear(-3.0))
            .abs()
                < 0.0001
        );
    }

    #[test]
    fn spatial_blend_is_equal_power_and_keeps_endpoint_voice_counts() {
        assert_eq!(audio_voice_mix(0.0), vec![(false, 1.0)]);
        assert_eq!(audio_voice_mix(1.0), vec![(true, 1.0)]);
        let mixed = audio_voice_mix(0.5);
        assert_eq!(mixed.len(), 2);
        let power: f32 = mixed.iter().map(|(_, weight)| weight * weight).sum();
        assert!((power - 1.0).abs() < 0.0001);
    }

    #[test]
    fn attenuation_is_full_inside_min_and_silent_at_max() {
        for rolloff in [
            AudioRolloff::Linear,
            AudioRolloff::Inverse,
            AudioRolloff::Exponential,
        ] {
            let desc = AudioAttenuationDesc {
                min_distance: 2.0,
                max_distance: 10.0,
                rolloff,
                rolloff_factor: 1.0,
            };
            assert_eq!(attenuation_gain(&desc, 1.0), 1.0);
            assert_eq!(attenuation_gain(&desc, 10.0), 0.0);
            assert!(attenuation_gain(&desc, 5.0) > 0.0);
        }
    }

    #[test]
    fn panning_scale_keeps_rodio_distance_gain_neutral_in_authored_radius() {
        let attenuation = AudioAttenuationDesc {
            min_distance: 1.0,
            max_distance: 25.0,
            rolloff: AudioRolloff::Inverse,
            rolloff_factor: 1.0,
        };
        let ear_gap = 0.2;
        let scale = spatial_panning_scale(&attenuation, ear_gap);

        // Rodio's distance gain remains 1.0 while each ear is at most one rodio unit away.
        let furthest_audible_ear = (attenuation.max_distance + ear_gap * 0.5) * scale;
        assert!(scale.is_finite() && scale > 0.0);
        assert!(furthest_audible_ear <= 0.25 + f32::EPSILON);
    }

    #[test]
    fn unchanged_reconciliation_does_not_reinsert_runtime_components() {
        let mut world = World::new();
        world.insert_resource(SimEnabled(true));
        world.insert_resource(ActiveAudioListener::default());
        let listener = world
            .spawn((
                LevelObject,
                ActorId("audio-listener".into()),
                AudioListenerDesc::default(),
            ))
            .id();
        let source = world
            .spawn((
                LevelObject,
                AudioSourceDesc {
                    autoplay: false,
                    ..Default::default()
                },
            ))
            .id();

        reconcile_audio_listener(&mut world);
        reconcile_audio_sources(&mut world);
        world.clear_trackers();
        reconcile_audio_listener(&mut world);
        reconcile_audio_sources(&mut world);

        assert!(!world
            .entity(listener)
            .get_ref::<ManagedAudioListener>()
            .unwrap()
            .is_changed());
        assert!(!world
            .entity(source)
            .get_ref::<RuntimeAudioSourceState>()
            .unwrap()
            .is_changed());
    }

    #[test]
    fn authored_audio_hydrates_and_reset_removes_all_runtime_state() {
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, AssetPlugin::default()))
            .init_asset::<AudioSource>()
            .insert_resource(SimEnabled(true))
            .init_resource::<ActiveAudioListener>();
        let listener = app
            .world_mut()
            .spawn((
                LevelObject,
                ActorId("audio-listener".into()),
                AudioListenerDesc::default(),
                Transform::IDENTITY,
            ))
            .id();
        let source = app
            .world_mut()
            .spawn((
                LevelObject,
                Transform::from_xyz(0.0, 0.0, -2.0),
                AudioSourceDesc {
                    clip: Some(
                        EditorAssetRef::new("audio-id", AUDIO_CLIP_SUB_ASSET_ID, "Tone")
                            .with_source_path("audio/tone.ogg"),
                    ),
                    spatial_blend: 0.5,
                    ..Default::default()
                },
            ))
            .id();

        reconcile_audio_listener(app.world_mut());
        reconcile_audio_sources(app.world_mut());

        assert_eq!(
            app.world().resource::<ActiveAudioListener>().0,
            Some(listener)
        );
        assert!(app.world().get::<SpatialListener>(listener).is_some());
        assert!(app.world().get::<RuntimeAudioSourceState>(source).is_some());
        let voices = voice_entities(app.world_mut(), source);
        assert_eq!(voices.len(), 2);
        assert!(voices
            .iter()
            .all(|voice| app.world().get::<AudioPlayer>(*voice).is_some()));

        reset_authored_audio_runtime(app.world_mut());

        assert_eq!(app.world().resource::<ActiveAudioListener>().0, None);
        assert!(app.world().get::<SpatialListener>(listener).is_none());
        assert!(app.world().get::<RuntimeAudioSourceState>(source).is_none());
        assert!(voice_entities(app.world_mut(), source).is_empty());
    }

    #[test]
    fn listener_selection_prefers_priority_then_stable_actor_id() {
        let mut world = World::new();
        world.insert_resource(SimEnabled(true));
        world.insert_resource(ActiveAudioListener::default());
        let low_priority = world
            .spawn((
                LevelObject,
                ActorId("listener-low".into()),
                AudioListenerDesc {
                    priority: 1,
                    ..Default::default()
                },
            ))
            .id();
        let tied_later = world
            .spawn((
                LevelObject,
                ActorId("listener-z".into()),
                AudioListenerDesc {
                    priority: 10,
                    ..Default::default()
                },
            ))
            .id();
        let tied_first = world
            .spawn((
                LevelObject,
                ActorId("listener-a".into()),
                AudioListenerDesc {
                    priority: 10,
                    ..Default::default()
                },
            ))
            .id();

        reconcile_audio_listener(&mut world);
        assert_eq!(world.resource::<ActiveAudioListener>().0, Some(tied_first));
        assert_ne!(
            world.resource::<ActiveAudioListener>().0,
            Some(low_priority)
        );

        world
            .get_mut::<AudioListenerDesc>(tied_first)
            .unwrap()
            .enabled = false;
        reconcile_audio_listener(&mut world);
        assert_eq!(world.resource::<ActiveAudioListener>().0, Some(tied_later));
    }

    #[test]
    fn bus_cycle_is_defensively_bounded_at_runtime() {
        let settings = AudioSettings {
            buses: vec![
                AudioBusSettings {
                    id: AUDIO_BUS_MASTER_ID.into(),
                    label: "Master".into(),
                    parent: Some(AUDIO_BUS_SFX_ID.into()),
                    gain_db: 0.0,
                    muted: false,
                },
                AudioBusSettings {
                    id: AUDIO_BUS_SFX_ID.into(),
                    label: "SFX".into(),
                    parent: Some(AUDIO_BUS_MASTER_ID.into()),
                    gain_db: 0.0,
                    muted: false,
                },
            ],
        };
        assert_eq!(
            effective_bus_gain_linear(&settings, &AudioBusRuntime::default(), AUDIO_BUS_SFX_ID),
            1.0
        );
    }
}
