use crate::config::{AudioProfile, NetworkMode, RecordingMode, ScreenShareSettings, ShareQuality};
use crate::friends::Friend;
use crate::network::PeerState;
use crate::presence::{FriendPresence, PresenceMode};
use iroh::{EndpointAddr, EndpointId};

#[derive(Debug, Clone)]
pub enum CoreCommand {
    /// Join a room. `ticket` is "create" (or empty) to mint a fresh room, else a
    /// share ticket to join. `room_name` is the creator's chosen cosmetic label
    /// for a NEW room; it's ignored when joining (the label rides in the ticket).
    Join {
        name: String,
        ticket: String,
        room_name: String,
        input_device: Option<String>,
        output_device: Option<String>,
        echo_cancellation: bool,
        avatar: crate::avatar::Avatar,
    },
    Leave,
    /// Orderly app shutdown: finalize recordings, leave any active room, stop local
    /// audio/screen-share work, close the persistent network stack, then ack with
    /// [`UiEvent::ShutdownComplete`].
    Shutdown,
    ToggleMute,
    /// Change our avatar (W4) and re-announce it to the room over presence.
    SetAvatar(crate::avatar::Avatar),
    ToggleDeafen,
    SetPttMode(bool),
    SetPttActive(bool),
    SetPeerVolume(EndpointId, f32),
    /// Listener-side per-peer EQ. Local only; never leaves this app instance.
    SetPeerEq(EndpointId, crate::audio::eq::EqSettings),
    /// Listener-side per-peer pan. Local only; never leaves this app instance.
    SetPeerPan(EndpointId, f32),
    /// Listener-side per-peer noise gate threshold (normalized RMS, `0.0` = off).
    /// Applies the same smooth gate as the mic path to a peer's incoming audio,
    /// to suppress their background noise on our end. Local only.
    SetPeerGate(EndpointId, f32),
    /// Locally mute/unmute a peer: when muted, their audio is decoded (so levels
    /// still show) but not mixed into our output.
    SetPeerMuted(EndpointId, bool),
    SetNoiseGateThreshold(f32),
    /// App-internal capture gain (mic), applied before the gate/encode. 1.0 = unity.
    SetInputVolume(f32),
    /// App-internal playback gain on the mixed output. 1.0 = unity.
    SetOutputVolume(f32),
    /// Start/stop a standalone capture-only stream that reports the raw mic
    /// level via [`UiEvent::MicLevel`], for gate calibration outside a call.
    /// Ignored while a room session is active (the in-call meter covers that).
    SetMicMonitor {
        enabled: bool,
        input_device: Option<String>,
    },
    /// Set the relay/discovery posture. Takes effect on the next room join,
    /// since the endpoint is (re)built then.
    SetNetworkMode(NetworkMode),
    /// Set the Opus encoder / network-resilience profile (W12). Applies live to
    /// the running capture encoder, and to the next call's encoder. Sent at
    /// startup from config and whenever the user changes it.
    SetAudioProfile(AudioProfile),
    /// Start/stop recording the call to a local WAV (your mic + the incoming
    /// mix). No-op start if already recording / not in a call.
    SetRecording(bool),
    /// Set what a recording captures (mixed / per-peer stems / both). Takes
    /// effect on the next recording start. Sent at startup from config.
    SetRecordingMode(RecordingMode),
    /// Broadcast a room text-chat message. `local_id` is the app's local-only
    /// handle for this send — it never goes on the wire; the core echoes it back
    /// in [`UiEvent::ChatSendResult`] so the UI can mark the matching local echo
    /// honestly (chat-hardening Phase 5). Not being in a call is a FAILURE
    /// result, not a silent no-op.
    SendChat {
        local_id: u64,
        text: String,
    },
    /// Send a chat message carrying a file attachment. The app has already read +
    /// capped the file and built the descriptor; core makes the bytes available
    /// on the file plane and broadcasts the descriptor. `local_id` as in
    /// [`CoreCommand::SendChat`].
    SendChatFile {
        local_id: u64,
        text: String,
        attachment: crate::files::ChatAttachment,
        /// Shared, not owned: the same allocation is retained by the UI cache
        /// and handed to the serve store, so a 25 MiB attachment is held once,
        /// not copied across UI / command queue / serve store (Phase 3C).
        data: std::sync::Arc<Vec<u8>>,
    },
    /// Fetch a received attachment's bytes from its sender over the file plane
    /// (used for on-demand file/chip downloads; images are auto-fetched on
    /// receipt). Replies with `AttachmentReady`/`AttachmentFailed`.
    FetchAttachment {
        from: EndpointId,
        attachment: crate::files::ChatAttachment,
    },
    /// Register `data` as fetchable under `id` for room members (the current
    /// broadcast track). Called once per track when broadcasting.
    ServeMusicTrack {
        id: crate::files::AttachmentId,
        data: std::sync::Arc<Vec<u8>>,
    },
    /// Drop a music blob that is no longer current-or-next.
    ForgetMusicTrack(crate::files::AttachmentId),
    /// Set (or clear) our broadcast music timeline and re-announce presence.
    SetMusicPresence(Option<crate::network::MusicPresence>),
    /// Fetch a source peer's current track bytes after tuning into them.
    FetchMusic {
        from: EndpointId,
        id: crate::files::AttachmentId,
        size: u64,
    },
    /// Fetch a source peer's advertised next track bytes before it becomes current.
    PrefetchMusic {
        from: EndpointId,
        id: crate::files::AttachmentId,
        size: u64,
    },
    /// Set the pixelpass binary location (config override, empty = use `$PATH`).
    /// Sent at startup so screen-share can resolve the binary.
    SetPixelpassPath(Option<String>),
    /// Enumerate apps currently producing audio (for the screen-share audio
    /// picker, A23). Replies with [`UiEvent::AudioAppsListed`]. Cheap shell-out;
    /// safe to call each time the picker opens.
    ListAudioApps,
    /// Start sharing our screen: spawn a pixelpass host and announce its ticket
    /// on our presence so the room can watch. No-op when not in a call.
    /// `audio_app` selects which app's audio to capture: `Some(name)` captures
    /// only that app (avoiding the call-loopback echo, A23); `None` shares the
    /// whole desktop audio (the legacy behavior).
    StartScreenShare {
        audio_app: Option<String>,
        settings: ScreenShareSettings,
        quality: ShareQuality,
    },
    /// Stop sharing our screen: kill the pixelpass host and clear the presence
    /// ticket. No-op when not sharing.
    StopScreenShare,
    /// **Core-internal.** The running pixelpass host's stdout ended — the
    /// process died (or its event stream broke), so the share identified by
    /// `generation` is over: reap the child, pull the ticket off presence, and
    /// tell the user. Synthesized by the core's own notice-forwarder task; the
    /// UI never sends it. `generation` scopes the fault to one specific host
    /// spawn, so a stale fault (the user already stopped, or started a new
    /// share) is ignored rather than tearing down the wrong share.
    ScreenShareHostFault {
        generation: u64,
    },
    /// Watch a peer's screen share: spawn a pixelpass viewer for `ticket` and
    /// open it in a local player.
    ViewShare {
        ticket: String,
        settings: ScreenShareSettings,
    },
    /// Mint a fresh persistent identity (W7), discarding the old one. Takes effect
    /// on the next room join (the endpoint is rebuilt then). The core replies with
    /// an updated [`UiEvent::IdentityStatus`].
    RegenerateIdentity,
    /// Add a friend (W7). Core owns the friends store: it mutates + persists it and
    /// replies with [`UiEvent::FriendsUpdated`]. `addr` seeds `last_addr` if known
    /// (e.g. added from a room). Idempotent — re-adding an existing id is a no-op.
    AddFriend {
        id: EndpointId,
        name: String,
        addr: Option<EndpointAddr>,
    },
    /// Remove a friend by id (W7).
    RemoveFriend(EndpointId),
    /// Locally rename a friend (W7).
    RenameFriend(EndpointId, String),
    /// Run an immediate presence-refresh pass over all friends (the manual
    /// "Rescan" button). Same work the 60s scheduler does on each tick, on demand —
    /// no waiting for the next interval. A no-op while Invisible.
    RefreshFriends,
    /// Set our presence posture (W7). Gates the idle listener (answer friends-only /
    /// invisible) and the outbound ping scheduler (invisible = fully dark). Sent at
    /// startup from config and whenever the user changes it.
    SetPresenceMode(PresenceMode),
    /// Toggle broadcasting the detected game as presence (game detection). Opt-in,
    /// default OFF. Enabling immediately publishes the current game; disabling
    /// immediately publishes `game: None`. Detection for the local background runs
    /// regardless. Sent at startup from config and on user toggle.
    SetGamePresenceEnabled(bool),
    /// Set the manual game-detection override (`Auto` / `None` / a forced game).
    /// Forwarded to the detector and applied immediately (bypasses debounce).
    SetGameOverride(crate::game::ManualOverride),
    /// Replace the user process→display-name mappings used by the non-Steam
    /// detection fallback. Sent at startup from config and after Settings edits.
    SetGameProcessMap(std::collections::BTreeMap<String, String>),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeliveryClass {
    Reliable,
    BestEffort,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CoalesceKey {
    InputVolume,
    OutputVolume,
    NoiseGate,
    PeerVolume(EndpointId),
    PeerPan(EndpointId),
    PeerGate(EndpointId),
    PeerEq(EndpointId),
}

/// Route a command by how bad it is to drop it. Discrete, human-paced user
/// actions are Reliable (must land). The only high-frequency commands are the
/// continuous audio sliders, where dropping intermediate values is harmless;
/// those are BestEffort.
pub fn delivery_class(cmd: &CoreCommand) -> DeliveryClass {
    match cmd {
        CoreCommand::SetPeerVolume(_, _)
        | CoreCommand::SetPeerPan(_, _)
        | CoreCommand::SetPeerGate(_, _)
        | CoreCommand::SetPeerEq(_, _)
        | CoreCommand::SetInputVolume(_)
        | CoreCommand::SetOutputVolume(_)
        | CoreCommand::SetNoiseGateThreshold(_) => DeliveryClass::BestEffort,

        CoreCommand::Join {
            name: _,
            ticket: _,
            room_name: _,
            input_device: _,
            output_device: _,
            echo_cancellation: _,
            avatar: _,
        }
        | CoreCommand::Leave
        | CoreCommand::Shutdown
        | CoreCommand::ToggleMute
        | CoreCommand::SetAvatar(_)
        | CoreCommand::ToggleDeafen
        | CoreCommand::SetPttMode(_)
        | CoreCommand::SetPttActive(_)
        | CoreCommand::SetPeerMuted(_, _)
        | CoreCommand::SetMicMonitor {
            enabled: _,
            input_device: _,
        }
        | CoreCommand::SetNetworkMode(_)
        | CoreCommand::SetAudioProfile(_)
        | CoreCommand::SetRecording(_)
        | CoreCommand::SetRecordingMode(_)
        | CoreCommand::SendChat {
            local_id: _,
            text: _,
        }
        | CoreCommand::SendChatFile {
            local_id: _,
            text: _,
            attachment: _,
            data: _,
        }
        | CoreCommand::FetchAttachment {
            from: _,
            attachment: _,
        }
        | CoreCommand::ServeMusicTrack { id: _, data: _ }
        | CoreCommand::ForgetMusicTrack(_)
        | CoreCommand::SetMusicPresence(_)
        | CoreCommand::FetchMusic {
            from: _,
            id: _,
            size: _,
        }
        | CoreCommand::PrefetchMusic {
            from: _,
            id: _,
            size: _,
        }
        | CoreCommand::SetPixelpassPath(_)
        | CoreCommand::ListAudioApps
        | CoreCommand::StartScreenShare {
            audio_app: _,
            settings: _,
            quality: _,
        }
        | CoreCommand::StopScreenShare
        | CoreCommand::ScreenShareHostFault { generation: _ }
        | CoreCommand::ViewShare {
            ticket: _,
            settings: _,
        }
        | CoreCommand::RegenerateIdentity
        | CoreCommand::AddFriend {
            id: _,
            name: _,
            addr: _,
        }
        | CoreCommand::RemoveFriend(_)
        | CoreCommand::RenameFriend(_, _)
        | CoreCommand::RefreshFriends
        | CoreCommand::SetPresenceMode(_)
        | CoreCommand::SetGamePresenceEnabled(_)
        | CoreCommand::SetGameOverride(_)
        | CoreCommand::SetGameProcessMap(_) => DeliveryClass::Reliable,
    }
}

/// Coalescing bucket for high-frequency continuous controls. A key exists
/// exactly for [`DeliveryClass::BestEffort`] commands.
pub fn coalesce_key(cmd: &CoreCommand) -> Option<CoalesceKey> {
    match cmd {
        CoreCommand::SetPeerVolume(peer_id, _) => Some(CoalesceKey::PeerVolume(*peer_id)),
        CoreCommand::SetPeerPan(peer_id, _) => Some(CoalesceKey::PeerPan(*peer_id)),
        CoreCommand::SetPeerGate(peer_id, _) => Some(CoalesceKey::PeerGate(*peer_id)),
        CoreCommand::SetPeerEq(peer_id, _) => Some(CoalesceKey::PeerEq(*peer_id)),
        CoreCommand::SetInputVolume(_) => Some(CoalesceKey::InputVolume),
        CoreCommand::SetOutputVolume(_) => Some(CoalesceKey::OutputVolume),
        CoreCommand::SetNoiseGateThreshold(_) => Some(CoalesceKey::NoiseGate),

        CoreCommand::Join {
            name: _,
            ticket: _,
            room_name: _,
            input_device: _,
            output_device: _,
            echo_cancellation: _,
            avatar: _,
        }
        | CoreCommand::Leave
        | CoreCommand::Shutdown
        | CoreCommand::ToggleMute
        | CoreCommand::SetAvatar(_)
        | CoreCommand::ToggleDeafen
        | CoreCommand::SetPttMode(_)
        | CoreCommand::SetPttActive(_)
        | CoreCommand::SetPeerMuted(_, _)
        | CoreCommand::SetMicMonitor {
            enabled: _,
            input_device: _,
        }
        | CoreCommand::SetNetworkMode(_)
        | CoreCommand::SetAudioProfile(_)
        | CoreCommand::SetRecording(_)
        | CoreCommand::SetRecordingMode(_)
        | CoreCommand::SendChat {
            local_id: _,
            text: _,
        }
        | CoreCommand::SendChatFile {
            local_id: _,
            text: _,
            attachment: _,
            data: _,
        }
        | CoreCommand::FetchAttachment {
            from: _,
            attachment: _,
        }
        | CoreCommand::ServeMusicTrack { id: _, data: _ }
        | CoreCommand::ForgetMusicTrack(_)
        | CoreCommand::SetMusicPresence(_)
        | CoreCommand::FetchMusic {
            from: _,
            id: _,
            size: _,
        }
        | CoreCommand::PrefetchMusic {
            from: _,
            id: _,
            size: _,
        }
        | CoreCommand::SetPixelpassPath(_)
        | CoreCommand::ListAudioApps
        | CoreCommand::StartScreenShare {
            audio_app: _,
            settings: _,
            quality: _,
        }
        | CoreCommand::StopScreenShare
        | CoreCommand::ScreenShareHostFault { generation: _ }
        | CoreCommand::ViewShare {
            ticket: _,
            settings: _,
        }
        | CoreCommand::RegenerateIdentity
        | CoreCommand::AddFriend {
            id: _,
            name: _,
            addr: _,
        }
        | CoreCommand::RemoveFriend(_)
        | CoreCommand::RenameFriend(_, _)
        | CoreCommand::RefreshFriends
        | CoreCommand::SetPresenceMode(_)
        | CoreCommand::SetGamePresenceEnabled(_)
        | CoreCommand::SetGameOverride(_)
        | CoreCommand::SetGameProcessMap(_) => None,
    }
}

#[derive(Debug, Clone)]
pub enum UiEvent {
    RoomJoined {
        ticket: String,
        self_id: String,
    },
    RoomLeft,
    /// Clear room-scoped UI state after a failed in-call room switch, without a
    /// leave chime. The persistent identity remains unchanged.
    RoomReset,
    PeerJoined {
        id: EndpointId,
        state: PeerState,
    },
    PeerLeft {
        id: EndpointId,
    },
    /// The fixed reconnect grace expired and bounded background gossip recovery
    /// has started. This is non-terminal and must not play the failure chime.
    PeerRecoveryStarted {
        id: EndpointId,
    },
    PeerConnectionFailed {
        id: EndpointId,
    },
    PeerUpdated {
        id: EndpointId,
        state: PeerState,
    },
    /// Audio link to a peer is being (re)established — show a connecting state.
    PeerConnecting {
        id: EndpointId,
    },
    /// Audio link to a peer is up and carrying audio.
    PeerConnected {
        id: EndpointId,
    },
    AudioLevels(Vec<(EndpointId, f32)>),
    /// Periodic per-peer connection transparency snapshot (~1/sec): path type
    /// (direct/relay), RTT, and window loss/bitrate for every peer with a live
    /// audio link. A FULL replacement each time — a peer absent from the list
    /// has no live link right now, so its badge should disappear.
    ConnectionStats(Vec<(EndpointId, crate::core::connstats::PeerConnInfo)>),
    /// Raw (pre-gate, pre-mute) normalized RMS of the local mic, `0.0..=1.0`,
    /// for the settings level meter. Throttled to ~10/sec.
    MicLevel(f32),
    /// Call recording started; carries the absolute WAV path being written.
    RecordingStarted {
        path: String,
    },
    /// Call recording stopped; carries the finished WAV path.
    RecordingStopped {
        path: String,
    },
    /// The outcome of one locally initiated chat send (chat-hardening Phase 5).
    /// `error = None` means our signed broadcast was handed to the gossip swarm
    /// — deliberately NOT a delivery/read receipt; PeerSpeak has no peer
    /// acknowledgements. `local_id` is the app's own handle from the
    /// `SendChat`/`SendChatFile` command and never appears on the wire.
    ChatSendResult {
        local_id: u64,
        error: Option<String>,
    },
    /// A room text-chat message arrived from a peer (never our own — local
    /// messages are echoed by the UI on send). `from` is the sender's node id
    /// string, used to key their avatar (W4).
    ChatMessage {
        from: String,
        name: String,
        text: String,
        attachment: Option<crate::files::ChatAttachment>,
    },
    /// An attachment's bytes are now available (auto-fetched for images, or
    /// fetched on demand for files). Keyed by `(from, id)`: the id is
    /// attacker-chosen, so a malicious peer can reuse a victim's id — the author
    /// disambiguates whose bytes these are and stops content aliasing (Tier C
    /// F-12).
    AttachmentReady {
        from: EndpointId,
        id: crate::files::AttachmentId,
        data: std::sync::Arc<Vec<u8>>,
    },
    /// An attachment fetch task was spawned (auto or on demand). Lets the UI
    /// show a real "loading" state instead of inferring it from cache absence —
    /// absence now means NOT fetched (e.g. auto-fetch was skipped), which
    /// renders a Load button rather than an indefinite "loading…" (Phase 3B).
    AttachmentFetchStarted {
        from: EndpointId,
        id: crate::files::AttachmentId,
    },
    /// An attachment fetch failed (sender gone, too large, decode error, etc.).
    AttachmentFailed {
        from: EndpointId,
        id: crate::files::AttachmentId,
        error: String,
    },
    /// A tuned-in source's track bytes arrived; play them in the music sink.
    MusicReady {
        from: EndpointId,
        id: crate::files::AttachmentId,
        data: Vec<u8>,
    },
    /// A tuned-in source's next-track bytes arrived; cache them for a gapless swap.
    MusicPrefetched {
        from: EndpointId,
        id: crate::files::AttachmentId,
        data: Vec<u8>,
    },
    /// A music-track fetch failed (source gone, too large, etc.).
    MusicFetchFailed {
        from: EndpointId,
        id: crate::files::AttachmentId,
        error: String,
    },
    /// The apps currently producing audio, for the screen-share audio picker
    /// (A23). Sorted, deduplicated `application.name`s; empty when nothing is
    /// playing or enumeration isn't available. `app_audio_supported` reports
    /// whether the resolved pixelpass understands `--strict-audio`: when `false`
    /// (an older pixelpass) the picker must offer whole-desktop audio only, since
    /// a per-app share would pass a flag that older binary rejects (audit P2).
    AudioAppsListed {
        apps: Vec<String>,
        app_audio_supported: bool,
    },
    /// Our own screen share started; the UI flips the Share button to "Stop".
    ScreenShareStarted,
    /// Our own screen share stopped (or failed to start).
    ScreenShareStopped,
    /// Per-app screen-share audio routing state (A23). `true` = the app we chose
    /// is now reaching viewers; `false` = its audio stopped, so under our strict
    /// run viewers currently hear silence. The UI shows a transient warning while
    /// `false`. Only meaningful while sharing a specific app (not whole-desktop).
    ShareAudioActive(bool),
    /// A validly signed peer cannot be admitted because its gossip timestamp is
    /// outside the replay freshness window. `peer_ahead` describes the peer's
    /// sender-stamped timestamp relative to this machine's clock.
    ClockSkewWarning {
        skew_secs: u64,
        peer_ahead: bool,
    },
    /// Our node identity (W7): the current node id string, and whether it is
    /// PERSISTED to disk. Sent once at startup and again after a regenerate.
    /// `persisted = false` means the key file couldn't be read/written and we're
    /// running on an ephemeral fallback — a degraded state the UI must surface,
    /// since the id (and thus friend recognition) won't survive the next launch.
    /// `error` carries the reason when degraded, for the UI explainer.
    IdentityStatus {
        node_id: String,
        persisted: bool,
        error: Option<String>,
    },
    /// The friends list (W7), now owned by core. Sent at startup (after load) and
    /// after every add/remove/rename so the GUI renders from this snapshot instead
    /// of owning the store. `read_only` is true when `friends.json` failed to load
    /// (malformed) — the GUI shows a degraded warning and disables edits so we never
    /// overwrite the damaged file (backlog A16).
    FriendsUpdated {
        friends: Vec<Friend>,
        read_only: bool,
    },
    /// A friend's live presence from a successful ping reply (W7): online, or in a
    /// joinable gathering (with a one-click ticket). Emitted by the outbound ping
    /// scheduler; absence of a recent event = treat as offline.
    FriendPresence {
        id: EndpointId,
        presence: FriendPresence,
    },
    /// A manual "Rescan" pass finished (every friend has been probed and its
    /// per-friend `FriendPresence` already emitted). Lets the GUI clear the
    /// transient "Rescanning…" status. Sent only for the on-demand button, not the
    /// periodic auto-refresh, so the status bar isn't churned every interval.
    FriendsRescanned,
    /// Core corrected the committed presence posture. Usually the Discoverable
    /// time-box elapsed and the core auto-reverted to `Normal`; on discovery apply
    /// failure, this carries the previous truthful mode. The GUI must mirror +
    /// persist this so its presence picker matches the endpoint's discovery state.
    PresenceModeReverted {
        mode: PresenceMode,
    },
    /// The locally-detected running game changed (game detection). Carries the
    /// debounced `DetectedGame` (id + display name + source) or `None` when nothing
    /// is detected. The GUI uses the stable `id` to switch the per-game background
    /// (W18) and may show a local "Playing …" indicator. Emitted regardless of
    /// whether game presence is being broadcast — the broadcast is core's own job.
    GameChanged(Option<crate::game::DetectedGame>),
    /// Core finished orderly app shutdown and the GUI can exit.
    ShutdownComplete,
    Error(String),
}

#[cfg(test)]
mod tests {
    use super::{CoalesceKey, CoreCommand, DeliveryClass, coalesce_key, delivery_class};
    use crate::audio::eq::EqSettings;
    use crate::presence::PresenceMode;
    use iroh::{EndpointId, SecretKey};

    fn endpoint_id() -> EndpointId {
        SecretKey::generate().public()
    }

    #[test]
    fn continuous_audio_controls_are_best_effort() {
        let peer = endpoint_id();
        let commands = [
            (
                CoreCommand::SetPeerVolume(peer, 0.7),
                CoalesceKey::PeerVolume(peer),
            ),
            (
                CoreCommand::SetPeerPan(peer, -0.2),
                CoalesceKey::PeerPan(peer),
            ),
            (
                CoreCommand::SetPeerGate(peer, 0.1),
                CoalesceKey::PeerGate(peer),
            ),
            (
                CoreCommand::SetPeerEq(peer, EqSettings::default()),
                CoalesceKey::PeerEq(peer),
            ),
            (CoreCommand::SetInputVolume(0.8), CoalesceKey::InputVolume),
            (CoreCommand::SetOutputVolume(0.9), CoalesceKey::OutputVolume),
            (
                CoreCommand::SetNoiseGateThreshold(0.02),
                CoalesceKey::NoiseGate,
            ),
        ];

        for (cmd, key) in commands {
            assert_eq!(delivery_class(&cmd), DeliveryClass::BestEffort);
            assert_eq!(coalesce_key(&cmd), Some(key));
            assert_eq!(
                coalesce_key(&cmd).is_some(),
                delivery_class(&cmd) == DeliveryClass::BestEffort
            );
        }
    }

    #[test]
    fn discrete_user_actions_are_reliable() {
        let peer = endpoint_id();
        let commands = [
            CoreCommand::ToggleMute,
            CoreCommand::SetPttActive(false),
            CoreCommand::Leave,
            CoreCommand::RegenerateIdentity,
            CoreCommand::Join {
                name: "Peer".to_string(),
                ticket: "create".to_string(),
                room_name: "Room".to_string(),
                input_device: None,
                output_device: None,
                echo_cancellation: true,
                avatar: crate::avatar::Avatar::default(),
            },
            CoreCommand::SetPeerMuted(peer, true),
            CoreCommand::SetPresenceMode(PresenceMode::Normal),
            CoreCommand::SetAudioProfile(crate::config::AudioProfile::BadNetwork),
            CoreCommand::SendChat {
                local_id: 1,
                text: "hello".to_string(),
            },
        ];

        for cmd in commands {
            assert_eq!(delivery_class(&cmd), DeliveryClass::Reliable);
            assert_eq!(coalesce_key(&cmd), None);
            assert_eq!(
                coalesce_key(&cmd).is_some(),
                delivery_class(&cmd) == DeliveryClass::BestEffort
            );
        }
    }
}
