//! # `chanora_state`
//!
//! Authoritative client-side mirror of server state: server identity,
//! channel tree, client list, our own connection state. Owns the
//! reducers that fold protocol events into a snapshot, and the
//! deltas that the bridge publishes to Flutter view-models
//! (per SAD 7.2 and SDD 5).
//!
//! ## Reducer contract
//!
//! Reducers mutate the caller-owned state passed by `&mut` and return a
//! `Reduction` containing only the emitted `Delta` values. The caller
//! (typically `chanora_core`) owns state storage, publishes deltas to the
//! bridge, and executes any side effects.
//!
//! This design satisfies SRS-056 (deterministic deltas), SRS-057
//! (per-connection ordering), and SRS-058 (reducer functions).

#![forbid(unsafe_code)]
#![warn(missing_docs)]

pub mod channel_join;

use std::collections::HashMap;

use chanora_protocol::{ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ServerSnapshot};
use thiserror::Error;

/// Errors raised while reducing protocol events into state or
/// producing deltas for the bridge.
#[derive(Debug, Error)]
pub enum StateError {
    /// Reducer received an event referencing an unknown entity.
    #[error("unknown entity: {0}")]
    Unknown(&'static str),
    /// A reducer invariant was violated (e.g. two clients claiming the same id).
    #[error("state invariant: {0}")]
    Invariant(&'static str),
}

/// Coarse connection lifecycle states surfaced to the bridge.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
    /// No connection attempt yet, or fully disconnected.
    Idle,
    /// Handshake in progress.
    Connecting,
    /// Initial state snapshot has been received; ready for use.
    Ready,
    /// Connection lost; reconnecting.
    Reconnecting,
    /// Connection lost; will not auto-reconnect at this stage.
    Lost,
}

/// Authoritative mirror of a connected server's published state.
///
/// Constructed from a [`ServerSnapshot`] and updated through delta
/// events. Owns the channel tree, client list, and server metadata.
/// Satisfies SRS-054.
#[derive(Debug, Clone)]
pub struct ServerState {
    /// Retained connection lifecycle state.
    pub connection_state: ConnectionState,
    /// Server name from the most recent snapshot.
    pub server_name: String,
    /// Welcome message / banner (server-provided markup).
    pub welcome_message: String,
    /// Server platform string.
    pub platform: String,
    /// Server version string.
    pub version: String,
    channels: HashMap<u64, ChannelInfo>,
    clients: HashMap<u64, ClientInfo>,
    channel_order: Vec<u64>,
    client_order: Vec<u64>,
    /// Our own client id, as the server reported it.
    pub own_client_id: u64,
}

impl ServerState {
    /// Build state from the initial full snapshot. Satisfies SRS-055.
    pub fn from_snapshot(snapshot: ServerSnapshot) -> Self {
        let snapshot = normalize_snapshot(snapshot);
        let channels = snapshot
            .channels
            .iter()
            .map(|c| (c.id.0, c.clone()))
            .collect();
        let clients = snapshot
            .clients
            .iter()
            .map(|c| (c.id.0, c.clone()))
            .collect();
        let channel_order: Vec<u64> = snapshot.channels.iter().map(|c| c.id.0).collect();
        let mut client_order: Vec<u64> = snapshot.clients.iter().map(|c| c.id.0).collect();
        client_order.sort_unstable();
        client_order.dedup();
        Self {
            connection_state: ConnectionState::Ready,
            server_name: snapshot.server_name,
            welcome_message: snapshot.welcome_message,
            platform: snapshot.platform,
            version: snapshot.version,
            channels,
            clients,
            channel_order,
            client_order,
            own_client_id: snapshot.own_client_id,
        }
    }

    /// Replace state with a fresh snapshot (post-reconnect). Satisfies SRS-059.
    pub fn replace_from_snapshot(&mut self, snapshot: ServerSnapshot) {
        *self = Self::from_snapshot(snapshot);
    }

    /// Look up a channel by id.
    pub fn channel(&self, id: ChannelId) -> Option<&ChannelInfo> {
        self.channels.get(&id.0)
    }

    /// Look up a client by id.
    pub fn client(&self, id: ClientId) -> Option<&ClientInfo> {
        self.clients.get(&id.0)
    }

    /// All channels in protocol snapshot order. Live inserts are appended
    /// until the next full snapshot re-normalizes order.
    pub fn channels(&self) -> impl Iterator<Item = &ChannelInfo> {
        self.channel_order
            .iter()
            .filter_map(|id| self.channels.get(id))
    }

    /// All clients in stable reducer order.
    ///
    /// Full snapshots normalize clients by id to avoid inheriting upstream
    /// HashMap iteration order. Live inserts are appended in arrival order
    /// until the next full snapshot re-normalizes the list.
    pub fn clients(&self) -> impl Iterator<Item = &ClientInfo> {
        self.client_order
            .iter()
            .filter_map(|id| self.clients.get(id))
    }

    /// Number of channels.
    pub fn channel_count(&self) -> usize {
        self.channels.len()
    }

    /// Number of clients.
    pub fn client_count(&self) -> usize {
        self.clients.len()
    }

    /// The channel our own client is currently in.
    pub fn own_channel(&self) -> Option<&ChannelInfo> {
        self.client(ClientId(self.own_client_id))
            .and_then(|c| self.channel(c.channel))
    }

    /// Clients in a given channel.
    pub fn clients_in_channel(&self, channel_id: ChannelId) -> impl Iterator<Item = &ClientInfo> {
        self.client_order
            .iter()
            .filter_map(|id| self.clients.get(id))
            .filter(move |c| c.channel == channel_id)
    }
}

fn normalize_snapshot(snapshot: ServerSnapshot) -> ServerSnapshot {
    let mut channels = HashMap::new();
    let mut channel_order = Vec::new();
    for channel in snapshot.channels {
        if !channels.contains_key(&channel.id.0) {
            channel_order.push(channel.id.0);
        }
        channels.insert(channel.id.0, channel);
    }
    let mut clients = HashMap::new();
    for client in snapshot.clients {
        clients.insert(client.id.0, client);
    }
    let mut client_order: Vec<u64> = clients.keys().copied().collect();
    client_order.sort_unstable();

    ServerSnapshot {
        server_name: snapshot.server_name,
        welcome_message: snapshot.welcome_message,
        platform: snapshot.platform,
        version: snapshot.version,
        channels: channel_order
            .into_iter()
            .filter_map(|id| channels.remove(&id))
            .collect(),
        clients: client_order
            .into_iter()
            .filter_map(|id| clients.remove(&id))
            .collect(),
        own_client_id: snapshot.own_client_id,
    }
}

/// A change to the server state that the bridge should publish to
/// Flutter. Deltas are cheap to construct and carry only the
/// information that changed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Delta {
    /// A full snapshot was applied; the entire UI should refresh.
    SnapshotApplied(ServerSnapshot),
    /// A channel was added or updated.
    ChannelUpserted(ChannelInfo),
    /// A channel was removed.
    ChannelRemoved(ChannelId),
    /// A client joined or was updated (includes moves).
    ClientUpserted(ClientInfo),
    /// A client left.
    ClientRemoved(ClientId),
    /// A chat message arrived.
    ChatMessage(ChatMessage),
    /// A client's speaking state changed.
    VoiceActivityChanged {
        /// Client whose voice activity changed.
        client_id: ClientId,
        /// Whether voice activity is currently observed.
        is_speaking: bool,
    },
    /// The connection state changed.
    ConnectionStateChanged(ConnectionState),
}

/// Events that flow from the protocol layer into the state reducer.
/// Each variant carries all data the reducer needs; no external
/// lookups are required.
#[derive(Debug, Clone)]
pub enum StateEvent {
    /// Initial or post-reconnect snapshot arrived.
    Snapshot(ServerSnapshot),
    /// A new channel appeared or an existing channel was updated.
    ChannelChanged(ChannelInfo),
    /// A channel was deleted.
    ChannelDeleted(ChannelId),
    /// A client joined or was updated (including channel moves and
    /// mute/speaking state changes).
    ClientChanged(ClientInfo),
    /// A client left the server.
    ClientLeft(ClientId),
    /// A text message was received.
    ChatReceived(ChatMessage),
    /// A client's speaking state changed.
    VoiceActivityChanged {
        /// Client whose voice activity changed.
        client_id: ClientId,
        /// Whether voice activity is currently observed.
        is_speaking: bool,
    },
    /// Connection lifecycle transition.
    ConnectionChanged(ConnectionState),
    /// Reconnect started; stale state must be discarded.
    ReconnectStarted,
}

/// Result of applying a single event through the reducer.
#[derive(Debug, Clone)]
pub struct Reduction {
    /// Deltas describing what changed, in application order.
    pub deltas: Vec<Delta>,
}

/// Apply a [`StateEvent`] to optional [`ServerState`], returning
/// the resulting [`Reduction`].
///
/// The state is taken by `&mut` so the caller retains ownership.
/// When `state` is `None` (no active connection), most events are
/// ignored except `ConnectionChanged` and `Snapshot` (which creates
/// the state).
///
/// Deterministic: the same `(state, event)` pair always produces the
/// same `Reduction`. Satisfies SRS-056.
pub fn reduce(state: &mut Option<ServerState>, event: StateEvent) -> Reduction {
    match event {
        StateEvent::ConnectionChanged(cs) => {
            if let Some(s) = state {
                s.connection_state = cs;
            }
            Reduction {
                deltas: vec![Delta::ConnectionStateChanged(cs)],
            }
        }

        StateEvent::ReconnectStarted => {
            *state = None;
            Reduction {
                deltas: vec![Delta::ConnectionStateChanged(ConnectionState::Reconnecting)],
            }
        }

        StateEvent::Snapshot(snap) => {
            let normalized = normalize_snapshot(snap);
            *state = Some(ServerState::from_snapshot(normalized.clone()));
            Reduction {
                deltas: vec![
                    Delta::ConnectionStateChanged(ConnectionState::Ready),
                    Delta::SnapshotApplied(normalized),
                ],
            }
        }

        StateEvent::ChannelChanged(info) => match state {
            Some(s) if s.connection_state == ConnectionState::Ready => {
                if !s.channels.contains_key(&info.id.0) {
                    s.channel_order.push(info.id.0);
                }
                s.channels.insert(info.id.0, info.clone());
                Reduction {
                    deltas: vec![Delta::ChannelUpserted(info)],
                }
            }
            _ => Reduction { deltas: vec![] },
        },

        StateEvent::ChannelDeleted(id) => match state {
            Some(s) if s.connection_state == ConnectionState::Ready => {
                let removed_clients: Vec<u64> = s
                    .client_order
                    .iter()
                    .copied()
                    .filter(|client_id| {
                        s.clients
                            .get(client_id)
                            .is_some_and(|client| client.channel == id)
                    })
                    .collect();
                for client_id in &removed_clients {
                    s.clients.remove(client_id);
                }
                s.client_order
                    .retain(|existing| !removed_clients.contains(existing));
                let removed_channel = s.channels.remove(&id.0).is_some();
                s.channel_order.retain(|existing| *existing != id.0);
                let mut deltas: Vec<Delta> = removed_clients
                    .into_iter()
                    .map(|client_id| Delta::ClientRemoved(ClientId(client_id)))
                    .collect();
                if removed_channel {
                    deltas.push(Delta::ChannelRemoved(id));
                }
                Reduction {
                    deltas,
                }
            }
            _ => Reduction { deltas: vec![] },
        },

        StateEvent::ClientChanged(info) => match state {
            Some(s) if s.connection_state == ConnectionState::Ready => {
                if !s.clients.contains_key(&info.id.0) {
                    s.client_order.push(info.id.0);
                }
                s.clients.insert(info.id.0, info.clone());
                Reduction {
                    deltas: vec![Delta::ClientUpserted(info)],
                }
            }
            _ => Reduction { deltas: vec![] },
        },

        StateEvent::ClientLeft(id) => match state {
            Some(s) if s.connection_state == ConnectionState::Ready => {
                s.clients.remove(&id.0);
                s.client_order.retain(|existing| *existing != id.0);
                Reduction {
                    deltas: vec![Delta::ClientRemoved(id)],
                }
            }
            _ => Reduction { deltas: vec![] },
        },

        StateEvent::ChatReceived(msg) => match state {
            Some(s) if s.connection_state == ConnectionState::Ready => Reduction {
                deltas: vec![Delta::ChatMessage(msg)],
            },
            _ => Reduction { deltas: vec![] },
        },

        StateEvent::VoiceActivityChanged {
            client_id,
            is_speaking,
        } => match state {
            Some(s) if s.connection_state == ConnectionState::Ready => {
                if let Some(client) = s.clients.get_mut(&client_id.0) {
                    client.is_speaking = is_speaking;
                    Reduction {
                        deltas: vec![Delta::VoiceActivityChanged {
                            client_id,
                            is_speaking,
                        }],
                    }
                } else {
                    Reduction { deltas: vec![] }
                }
            }
            _ => Reduction { deltas: vec![] },
        },
    }
}

/// Replace server state with a fresh snapshot after a reconnect.
/// Discards all previous state unconditionally (SRS-059).
pub fn reduce_reconnect_snapshot(
    state: &mut Option<ServerState>,
    snap: ServerSnapshot,
) -> Reduction {
    let normalized = normalize_snapshot(snap);
    *state = Some(ServerState::from_snapshot(normalized.clone()));
    Reduction {
        deltas: vec![
            Delta::ConnectionStateChanged(ConnectionState::Ready),
            Delta::SnapshotApplied(normalized),
        ],
    }
}

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

    fn sample_channel(id: u64) -> ChannelInfo {
        ChannelInfo {
            id: ChannelId(id),
            parent: ChannelId(0),
            name: format!("channel-{id}"),
            order: 0,
            has_password: false,
            needed_talk_power: None,
        }
    }

    fn sample_client(id: u64, channel: u64) -> ClientInfo {
        ClientInfo {
            id: ClientId(id),
            channel: ChannelId(channel),
            name: format!("client-{id}"),
            input_muted: false,
            output_muted: false,
            is_speaking: false,
            is_server_query: false,
            talk_power: 0,
            talk_power_granted: false,
        }
    }

    fn sample_snapshot() -> ServerSnapshot {
        ServerSnapshot {
            server_name: "Test Server".into(),
            welcome_message: String::new(),
            platform: "Linux".into(),
            version: "3.13".into(),
            channels: vec![sample_channel(1), sample_channel(2)],
            clients: vec![sample_client(10, 1)],
            own_client_id: 10,
        }
    }

    #[test]
    fn snapshot_creates_state() {
        let mut state = None;
        let snapshot = sample_snapshot();
        let reduction = reduce(&mut state, StateEvent::Snapshot(snapshot.clone()));
        assert!(state.is_some());
        let s = state.as_ref().unwrap();
        assert_eq!(s.connection_state, ConnectionState::Ready);
        assert_eq!(s.channel_count(), 2);
        assert_eq!(s.client_count(), 1);
        assert_eq!(s.own_client_id, 10);
        assert_eq!(
            reduction.deltas,
            vec![
                Delta::ConnectionStateChanged(ConnectionState::Ready),
                Delta::SnapshotApplied(snapshot),
            ]
        );
    }

    #[test]
    fn channel_upsert_adds_and_updates() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let ch = ChannelInfo {
            id: ChannelId(3),
            parent: ChannelId(1),
            name: "new-channel".into(),
            order: 1,
            has_password: true,
            needed_talk_power: Some(50),
        };
        let reduction = reduce(&mut state, StateEvent::ChannelChanged(ch.clone()));
        let s = state.as_ref().unwrap();
        assert_eq!(s.channel_count(), 3);
        assert!(s.channel(ChannelId(3)).is_some());
        assert!(matches!(&reduction.deltas[..], [Delta::ChannelUpserted(_)]));
        let updated = ChannelInfo {
            name: "renamed".into(),
            ..ch
        };
        reduce(&mut state, StateEvent::ChannelChanged(updated));
        assert_eq!(
            state.as_ref().unwrap().channel(ChannelId(3)).unwrap().name,
            "renamed"
        );
        assert_eq!(state.as_ref().unwrap().channel_count(), 3);
    }

    #[test]
    fn channel_delete_removes() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let reduction = reduce(&mut state, StateEvent::ChannelDeleted(ChannelId(2)));
        let s = state.as_ref().unwrap();
        assert_eq!(s.channel_count(), 1);
        assert!(s.channel(ChannelId(2)).is_none());
        assert!(matches!(&reduction.deltas[..], [Delta::ChannelRemoved(_)]));
    }

    #[test]
    fn channel_delete_removes_clients_in_deleted_channel() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        reduce(&mut state, StateEvent::ClientChanged(sample_client(20, 2)));
        reduce(&mut state, StateEvent::ClientChanged(sample_client(30, 2)));

        let reduction = reduce(&mut state, StateEvent::ChannelDeleted(ChannelId(2)));
        let s = state.as_ref().unwrap();

        assert!(s.channel(ChannelId(2)).is_none());
        assert!(s.client(ClientId(20)).is_none());
        assert!(s.client(ClientId(30)).is_none());
        assert_eq!(s.client_count(), 1);
        assert_eq!(s.clients_in_channel(ChannelId(2)).count(), 0);
        assert_eq!(
            reduction.deltas,
            vec![
                Delta::ClientRemoved(ClientId(20)),
                Delta::ClientRemoved(ClientId(30)),
                Delta::ChannelRemoved(ChannelId(2)),
            ]
        );
    }

    #[test]
    fn client_upsert_adds_and_moves() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let new_client = sample_client(20, 2);
        let reduction = reduce(&mut state, StateEvent::ClientChanged(new_client));
        let s = state.as_ref().unwrap();
        assert_eq!(s.client_count(), 2);
        assert!(matches!(&reduction.deltas[..], [Delta::ClientUpserted(_)]));
        let moved = ClientInfo {
            channel: ChannelId(2),
            ..sample_client(10, 2)
        };
        reduce(&mut state, StateEvent::ClientChanged(moved));
        assert_eq!(
            state
                .as_ref()
                .unwrap()
                .client(ClientId(10))
                .unwrap()
                .channel,
            ChannelId(2)
        );
    }

    #[test]
    fn client_leave_removes() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let reduction = reduce(&mut state, StateEvent::ClientLeft(ClientId(10)));
        assert_eq!(state.as_ref().unwrap().client_count(), 0);
        assert!(matches!(&reduction.deltas[..], [Delta::ClientRemoved(_)]));
    }

    #[test]
    fn reconnect_discards_stale_state() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        assert_eq!(state.as_ref().unwrap().channel_count(), 2);
        let reduction = reduce(&mut state, StateEvent::ReconnectStarted);
        assert!(state.is_none());
        assert!(matches!(
            &reduction.deltas[..],
            [Delta::ConnectionStateChanged(ConnectionState::Reconnecting)]
        ));
        let snap2 = ServerSnapshot {
            server_name: "New Server".into(),
            channels: vec![sample_channel(100)],
            clients: vec![sample_client(200, 100)],
            ..sample_snapshot()
        };
        reduce_reconnect_snapshot(&mut state, snap2);
        let s = state.as_ref().unwrap();
        assert_eq!(s.server_name, "New Server");
        assert_eq!(s.channel_count(), 1);
        assert_eq!(s.client_count(), 1);
    }

    #[test]
    fn deltas_ignored_when_disconnected() {
        let mut state: Option<ServerState> = None;
        let r1 = reduce(&mut state, StateEvent::ChannelChanged(sample_channel(1)));
        assert!(r1.deltas.is_empty());
        let r2 = reduce(&mut state, StateEvent::ClientChanged(sample_client(1, 1)));
        assert!(r2.deltas.is_empty());
    }

    #[test]
    fn chat_message_requires_connected_state() {
        let mut state: Option<ServerState> = None;
        let msg = ChatMessage {
            sender_id: ClientId(10),
            sender_name: "Alice".into(),
            message: "hello".into(),
            target: MessageTarget::Channel,
        };
        let disconnected = reduce(&mut state, StateEvent::ChatReceived(msg.clone()));
        assert!(disconnected.deltas.is_empty());

        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        reduce(
            &mut state,
            StateEvent::ConnectionChanged(ConnectionState::Lost),
        );
        let lost = reduce(&mut state, StateEvent::ChatReceived(msg.clone()));
        assert!(lost.deltas.is_empty());

        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let reduction = reduce(&mut state, StateEvent::ChatReceived(msg));
        assert!(matches!(&reduction.deltas[..], [Delta::ChatMessage(_)]));
    }

    #[test]
    fn connection_state_change_produces_delta() {
        let mut state: Option<ServerState> = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let reduction = reduce(
            &mut state,
            StateEvent::ConnectionChanged(ConnectionState::Connecting),
        );
        assert_eq!(
            state.as_ref().unwrap().connection_state,
            ConnectionState::Connecting
        );
        assert!(matches!(
            &reduction.deltas[..],
            [Delta::ConnectionStateChanged(ConnectionState::Connecting)]
        ));
    }

    #[test]
    fn snapshot_order_is_deterministic_and_live_inserts_append() {
        let mut state = None;
        let snapshot = ServerSnapshot {
            channels: vec![sample_channel(20), sample_channel(10)],
            clients: vec![sample_client(30, 20), sample_client(10, 10)],
            ..sample_snapshot()
        };
        reduce(&mut state, StateEvent::Snapshot(snapshot));
        reduce(&mut state, StateEvent::ChannelChanged(sample_channel(15)));
        reduce(&mut state, StateEvent::ClientChanged(sample_client(20, 2)));
        let s = state.as_ref().unwrap();
        let channel_ids: Vec<u64> = s.channels().map(|c| c.id.0).collect();
        let client_ids: Vec<u64> = s.clients().map(|c| c.id.0).collect();
        assert_eq!(channel_ids, vec![20, 10, 15]);
        assert_eq!(client_ids, vec![10, 30, 20]);
    }

    #[test]
    fn malformed_snapshot_duplicate_ids_are_deduplicated_in_order_vectors() {
        let mut snapshot = sample_snapshot();
        snapshot.channels.push(ChannelInfo {
            name: "duplicate".into(),
            ..sample_channel(1)
        });
        snapshot.clients.push(ClientInfo {
            name: "duplicate".into(),
            ..sample_client(10, 2)
        });

        let mut state = None;
        let reduction = reduce(&mut state, StateEvent::Snapshot(snapshot));
        let s = state.as_ref().unwrap();

        assert_eq!(s.channel_count(), 2);
        assert_eq!(s.channels().count(), 2);
        assert_eq!(s.client_count(), 1);
        assert_eq!(s.clients().count(), 1);
        assert_eq!(s.channel(ChannelId(1)).unwrap().name, "duplicate");
        assert_eq!(s.client(ClientId(10)).unwrap().channel, ChannelId(2));
        let Delta::SnapshotApplied(emitted) = &reduction.deltas[1] else {
            panic!("expected snapshot delta");
        };
        assert_eq!(emitted.channels.len(), 2);
        assert_eq!(emitted.clients.len(), 1);
    }

    #[test]
    fn voice_activity_updates_existing_client_and_emits_delta() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let reduction = reduce(
            &mut state,
            StateEvent::VoiceActivityChanged {
                client_id: ClientId(10),
                is_speaking: true,
            },
        );
        assert!(
            state
                .as_ref()
                .unwrap()
                .client(ClientId(10))
                .unwrap()
                .is_speaking
        );
        assert_eq!(
            reduction.deltas,
            vec![Delta::VoiceActivityChanged {
                client_id: ClientId(10),
                is_speaking: true,
            }]
        );
    }

    #[test]
    fn voice_activity_for_unknown_client_is_ignored() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let reduction = reduce(
            &mut state,
            StateEvent::VoiceActivityChanged {
                client_id: ClientId(999),
                is_speaking: true,
            },
        );
        assert!(reduction.deltas.is_empty());
    }

    #[test]
    fn own_channel_returns_correct_channel() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let own = state.as_ref().unwrap().own_channel();
        assert!(own.is_some());
        assert_eq!(own.unwrap().id, ChannelId(1));
    }

    #[test]
    fn clients_in_channel_filters_correctly() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        reduce(&mut state, StateEvent::ClientChanged(sample_client(20, 1)));
        reduce(&mut state, StateEvent::ClientChanged(sample_client(30, 2)));
        let s = state.as_ref().unwrap();
        assert_eq!(s.clients_in_channel(ChannelId(1)).count(), 2);
        assert_eq!(s.clients_in_channel(ChannelId(2)).count(), 1);
    }

    #[test]
    fn malformed_delete_is_tolerated() {
        let mut state = None;
        reduce(&mut state, StateEvent::Snapshot(sample_snapshot()));
        let reduction = reduce(&mut state, StateEvent::ChannelDeleted(ChannelId(999)));
        assert!(reduction.deltas.is_empty());
        assert_eq!(state.as_ref().unwrap().channel_count(), 2);
    }

    #[test]
    fn same_event_sequence_produces_same_state() {
        let mut a: Option<ServerState> = None;
        let mut b: Option<ServerState> = None;
        let events: Vec<StateEvent> = vec![
            StateEvent::Snapshot(sample_snapshot()),
            StateEvent::ClientChanged(sample_client(20, 2)),
            StateEvent::ChannelChanged(ChannelInfo {
                id: ChannelId(3),
                parent: ChannelId(1),
                name: "x".into(),
                order: 0,
                has_password: false,
                needed_talk_power: None,
            }),
            StateEvent::ClientLeft(ClientId(20)),
        ];
        let mut deltas_a = Vec::new();
        let mut deltas_b = Vec::new();
        for e in &events {
            deltas_a.extend(reduce(&mut a, e.clone()).deltas);
        }
        for e in &events {
            deltas_b.extend(reduce(&mut b, e.clone()).deltas);
        }
        assert_eq!(
            a.as_ref().unwrap().channel_count(),
            b.as_ref().unwrap().channel_count()
        );
        assert_eq!(
            a.as_ref().unwrap().client_count(),
            b.as_ref().unwrap().client_count()
        );
        assert_eq!(
            a.as_ref().unwrap().own_client_id,
            b.as_ref().unwrap().own_client_id
        );
        assert_eq!(deltas_a, deltas_b);
    }
}
