//! High-level events emitted by the Signal Fish client.
//!
//! [`SignalFishEvent`] provides a 1:1 mapping from every [`ServerMessage`] variant
//! plus two synthetic events (`Connected` and `Disconnected`) that originate from
//! the transport layer rather than the server.
//!
//! Boxed payload types ([`RoomJoinedPayload`], [`ReconnectedPayload`],
//! [`SpectatorJoinedPayload`]) are flattened into inline fields so callers can
//! pattern-match directly without an extra dereference.
//!
//! [`ServerMessage`]: crate::protocol::ServerMessage
//! [`RoomJoinedPayload`]: crate::protocol::RoomJoinedPayload
//! [`ReconnectedPayload`]: crate::protocol::ReconnectedPayload
//! [`SpectatorJoinedPayload`]: crate::protocol::SpectatorJoinedPayload

use crate::error_codes::ErrorCode;
use crate::protocol::{
    GameDataEncoding, LobbyState, PeerConnectionInfo, PlayerId, PlayerInfo, ProtocolInfoPayload,
    RateLimitInfo, RoomId, ServerMessage, SpectatorInfo, SpectatorStateChangeReason,
};

/// Events emitted by the Signal Fish client.
///
/// Each variant corresponds to either a [`ServerMessage`] received from the
/// signaling server or a synthetic event generated by the transport layer.
///
/// # Synthetic events
///
/// | Variant | Origin |
/// |---|---|
/// | [`Connected`](Self::Connected) | Transport layer opened successfully |
/// | [`Disconnected`](Self::Disconnected) | Transport layer closed or errored |
///
/// # Example
///
/// ```text
/// // Assuming `events` is an async receiver of SignalFishEvent:
/// match event {
///     SignalFishEvent::RoomJoined { room_code, current_players, .. } => { /* … */ }
///     SignalFishEvent::PlayerJoined { player } => { /* … */ }
///     SignalFishEvent::Disconnected { reason } => { /* … */ }
///     _ => {}
/// }
/// ```
#[derive(Debug, Clone)]
pub enum SignalFishEvent {
    // ── Synthetic events ────────────────────────────────────────────
    /// The client has started and will begin communicating with the server.
    ///
    /// This is a **synthetic event** — it is not triggered by a server message.
    ///
    /// - **`SignalFishClient`** (async): emitted at the start of the transport
    ///   loop, after the transport has already been connected via
    ///   `.connect().await`.
    /// - **`SignalFishPollingClient`**: emitted once
    ///   [`Transport::is_ready()`](crate::Transport::is_ready) returns `true`
    ///   during a [`poll()`](crate::SignalFishPollingClient::poll) cycle. For
    ///   transports that are already connected at construction time, this is
    ///   the first `poll()` call. For transports with asynchronous handshakes
    ///   (e.g., `EmscriptenWebSocketTransport`), `Connected` is deferred
    ///   until the handshake completes.
    Connected,

    /// The transport connection was closed.
    Disconnected {
        /// Human-readable reason for the disconnection, if available.
        reason: Option<String>,
    },

    // ── Authentication ──────────────────────────────────────────────
    /// Authentication succeeded.
    Authenticated {
        /// Application name confirmed by the server.
        app_name: String,
        /// Organization the app belongs to, if any.
        organization: Option<String>,
        /// Rate limits enforced for this application.
        rate_limits: RateLimitInfo,
    },

    /// SDK/protocol compatibility details advertised after authentication.
    ///
    /// Wrapped as a single payload rather than flattened because most fields
    /// are optional configuration details that callers typically access as a group.
    ProtocolInfo(ProtocolInfoPayload),

    /// Authentication failed.
    AuthenticationError {
        /// Human-readable error description.
        error: String,
        /// Structured error code for programmatic handling.
        error_code: ErrorCode,
    },

    // ── Room lifecycle ──────────────────────────────────────────────
    /// Successfully joined a room. Fields are flattened from [`RoomJoinedPayload`].
    ///
    /// [`RoomJoinedPayload`]: crate::protocol::RoomJoinedPayload
    RoomJoined {
        /// Unique room identifier.
        room_id: RoomId,
        /// Human-readable room code.
        room_code: String,
        /// The local player's identifier.
        player_id: PlayerId,
        /// Name of the game this room is for.
        game_name: String,
        /// Maximum number of players allowed.
        max_players: u8,
        /// Whether the room supports authority delegation.
        supports_authority: bool,
        /// Players already present in the room.
        current_players: Vec<PlayerInfo>,
        /// Whether the local player is the authority.
        is_authority: bool,
        /// Current lobby readiness state.
        lobby_state: LobbyState,
        /// Players that have signaled readiness.
        ready_players: Vec<PlayerId>,
        /// Relay transport type label (e.g. `"auto"`, `"tcp"`).
        relay_type: String,
        /// Spectators currently watching.
        current_spectators: Vec<SpectatorInfo>,
    },

    /// Failed to join a room.
    RoomJoinFailed {
        /// Human-readable failure reason.
        reason: String,
        /// Structured error code, if provided.
        error_code: Option<ErrorCode>,
    },

    /// Successfully left the current room.
    RoomLeft,

    // ── Player presence ─────────────────────────────────────────────
    /// Another player joined the room.
    PlayerJoined {
        /// Information about the new player.
        player: PlayerInfo,
    },

    /// Another player left the room.
    PlayerLeft {
        /// Identifier of the player who left.
        player_id: PlayerId,
    },

    // ── Game data ───────────────────────────────────────────────────
    /// JSON game data received from another player.
    GameData {
        /// Identifier of the sending player.
        from_player: PlayerId,
        /// Arbitrary JSON payload.
        data: serde_json::Value,
    },

    /// Binary game data received from another player.
    GameDataBinary {
        /// Identifier of the sending player.
        from_player: PlayerId,
        /// Encoding format of the binary payload.
        encoding: GameDataEncoding,
        /// Raw binary payload.
        payload: Vec<u8>,
    },

    // ── Authority ───────────────────────────────────────────────────
    /// The room's authority assignment changed.
    AuthorityChanged {
        /// The player who now holds authority, if any.
        authority_player: Option<PlayerId>,
        /// Whether the local player is now the authority.
        you_are_authority: bool,
    },

    /// Response to an authority request.
    AuthorityResponse {
        /// Whether the request was granted.
        granted: bool,
        /// Human-readable reason if the request was denied.
        reason: Option<String>,
        /// Structured error code, if provided.
        error_code: Option<ErrorCode>,
    },

    // ── Lobby ───────────────────────────────────────────────────────
    /// The lobby readiness state changed.
    LobbyStateChanged {
        /// New lobby state.
        lobby_state: LobbyState,
        /// Players that have signaled readiness.
        ready_players: Vec<PlayerId>,
        /// Whether all players are ready.
        all_ready: bool,
    },

    /// The game is starting with peer connection information.
    GameStarting {
        /// Connection details for every peer.
        peer_connections: Vec<PeerConnectionInfo>,
    },

    // ── Heartbeat ───────────────────────────────────────────────────
    /// Pong response to a ping.
    Pong,

    // ── Reconnection ────────────────────────────────────────────────
    /// Reconnection succeeded. Fields are flattened from [`ReconnectedPayload`].
    ///
    /// [`ReconnectedPayload`]: crate::protocol::ReconnectedPayload
    Reconnected {
        /// Unique room identifier.
        room_id: RoomId,
        /// Human-readable room code.
        room_code: String,
        /// The local player's identifier.
        player_id: PlayerId,
        /// Name of the game this room is for.
        game_name: String,
        /// Maximum number of players allowed.
        max_players: u8,
        /// Whether the room supports authority delegation.
        supports_authority: bool,
        /// Players currently in the room.
        current_players: Vec<PlayerInfo>,
        /// Whether the local player is the authority.
        is_authority: bool,
        /// Current lobby readiness state.
        lobby_state: LobbyState,
        /// Players that have signaled readiness.
        ready_players: Vec<PlayerId>,
        /// Relay transport type label.
        relay_type: String,
        /// Spectators currently watching.
        current_spectators: Vec<SpectatorInfo>,
        /// Events that occurred while the client was disconnected.
        missed_events: Vec<SignalFishEvent>,
    },

    /// Reconnection failed.
    ReconnectionFailed {
        /// Human-readable failure reason.
        reason: String,
        /// Structured error code.
        error_code: ErrorCode,
    },

    /// Another player reconnected to the room.
    PlayerReconnected {
        /// Identifier of the player who reconnected.
        player_id: PlayerId,
    },

    // ── Spectator ───────────────────────────────────────────────────
    /// Successfully joined a room as a spectator.
    /// Fields are flattened from [`SpectatorJoinedPayload`].
    ///
    /// [`SpectatorJoinedPayload`]: crate::protocol::SpectatorJoinedPayload
    SpectatorJoined {
        /// Unique room identifier.
        room_id: RoomId,
        /// Human-readable room code.
        room_code: String,
        /// The local spectator's identifier.
        spectator_id: PlayerId,
        /// Name of the game this room is for.
        game_name: String,
        /// Players currently in the room.
        current_players: Vec<PlayerInfo>,
        /// Spectators currently watching.
        current_spectators: Vec<SpectatorInfo>,
        /// Current lobby readiness state.
        lobby_state: LobbyState,
        /// Reason the spectator state changed, if applicable.
        reason: Option<SpectatorStateChangeReason>,
    },

    /// Failed to join as a spectator.
    SpectatorJoinFailed {
        /// Human-readable failure reason.
        reason: String,
        /// Structured error code, if provided.
        error_code: Option<ErrorCode>,
    },

    /// Successfully left spectator mode.
    SpectatorLeft {
        /// Room identifier, if available.
        room_id: Option<RoomId>,
        /// Room code, if available.
        room_code: Option<String>,
        /// Reason for leaving, if available.
        reason: Option<SpectatorStateChangeReason>,
        /// Remaining spectators in the room.
        current_spectators: Vec<SpectatorInfo>,
    },

    /// Another spectator joined the room.
    NewSpectatorJoined {
        /// Information about the new spectator.
        spectator: SpectatorInfo,
        /// All spectators currently watching.
        current_spectators: Vec<SpectatorInfo>,
        /// Reason for the state change, if available.
        reason: Option<SpectatorStateChangeReason>,
    },

    /// Another spectator disconnected from the room.
    SpectatorDisconnected {
        /// Identifier of the spectator who disconnected.
        spectator_id: PlayerId,
        /// Reason for disconnection, if available.
        reason: Option<SpectatorStateChangeReason>,
        /// Remaining spectators in the room.
        current_spectators: Vec<SpectatorInfo>,
    },

    // ── Errors ──────────────────────────────────────────────────────
    /// A generic server error.
    Error {
        /// Human-readable error message.
        message: String,
        /// Structured error code, if provided.
        error_code: Option<ErrorCode>,
    },
}

// ── Conversion ──────────────────────────────────────────────────────

impl From<ServerMessage> for SignalFishEvent {
    fn from(msg: ServerMessage) -> Self {
        match msg {
            ServerMessage::Authenticated {
                app_name,
                organization,
                rate_limits,
            } => Self::Authenticated {
                app_name,
                organization,
                rate_limits,
            },
            ServerMessage::ProtocolInfo(payload) => Self::ProtocolInfo(payload),
            ServerMessage::AuthenticationError { error, error_code } => {
                Self::AuthenticationError { error, error_code }
            }
            ServerMessage::RoomJoined(payload) => {
                let p = *payload;
                Self::RoomJoined {
                    room_id: p.room_id,
                    room_code: p.room_code,
                    player_id: p.player_id,
                    game_name: p.game_name,
                    max_players: p.max_players,
                    supports_authority: p.supports_authority,
                    current_players: p.current_players,
                    is_authority: p.is_authority,
                    lobby_state: p.lobby_state,
                    ready_players: p.ready_players,
                    relay_type: p.relay_type,
                    current_spectators: p.current_spectators,
                }
            }
            ServerMessage::RoomJoinFailed { reason, error_code } => {
                Self::RoomJoinFailed { reason, error_code }
            }
            ServerMessage::RoomLeft => Self::RoomLeft,
            ServerMessage::PlayerJoined { player } => Self::PlayerJoined { player },
            ServerMessage::PlayerLeft { player_id } => Self::PlayerLeft { player_id },
            ServerMessage::GameData { from_player, data } => Self::GameData { from_player, data },
            ServerMessage::GameDataBinary {
                from_player,
                encoding,
                payload,
            } => Self::GameDataBinary {
                from_player,
                encoding,
                payload,
            },
            ServerMessage::AuthorityChanged {
                authority_player,
                you_are_authority,
            } => Self::AuthorityChanged {
                authority_player,
                you_are_authority,
            },
            ServerMessage::AuthorityResponse {
                granted,
                reason,
                error_code,
            } => Self::AuthorityResponse {
                granted,
                reason,
                error_code,
            },
            ServerMessage::LobbyStateChanged {
                lobby_state,
                ready_players,
                all_ready,
            } => Self::LobbyStateChanged {
                lobby_state,
                ready_players,
                all_ready,
            },
            ServerMessage::GameStarting { peer_connections } => {
                Self::GameStarting { peer_connections }
            }
            ServerMessage::Pong => Self::Pong,
            ServerMessage::Reconnected(payload) => {
                let p = *payload;
                Self::Reconnected {
                    room_id: p.room_id,
                    room_code: p.room_code,
                    player_id: p.player_id,
                    game_name: p.game_name,
                    max_players: p.max_players,
                    supports_authority: p.supports_authority,
                    current_players: p.current_players,
                    is_authority: p.is_authority,
                    lobby_state: p.lobby_state,
                    ready_players: p.ready_players,
                    relay_type: p.relay_type,
                    current_spectators: p.current_spectators,
                    missed_events: p
                        .missed_events
                        .into_iter()
                        .map(SignalFishEvent::from)
                        .collect(),
                }
            }
            ServerMessage::ReconnectionFailed { reason, error_code } => {
                Self::ReconnectionFailed { reason, error_code }
            }
            ServerMessage::PlayerReconnected { player_id } => Self::PlayerReconnected { player_id },
            ServerMessage::SpectatorJoined(payload) => {
                let p = *payload;
                Self::SpectatorJoined {
                    room_id: p.room_id,
                    room_code: p.room_code,
                    spectator_id: p.spectator_id,
                    game_name: p.game_name,
                    current_players: p.current_players,
                    current_spectators: p.current_spectators,
                    lobby_state: p.lobby_state,
                    reason: p.reason,
                }
            }
            ServerMessage::SpectatorJoinFailed { reason, error_code } => {
                Self::SpectatorJoinFailed { reason, error_code }
            }
            ServerMessage::SpectatorLeft {
                room_id,
                room_code,
                reason,
                current_spectators,
            } => Self::SpectatorLeft {
                room_id,
                room_code,
                reason,
                current_spectators,
            },
            ServerMessage::NewSpectatorJoined {
                spectator,
                current_spectators,
                reason,
            } => Self::NewSpectatorJoined {
                spectator,
                current_spectators,
                reason,
            },
            ServerMessage::SpectatorDisconnected {
                spectator_id,
                reason,
                current_spectators,
            } => Self::SpectatorDisconnected {
                spectator_id,
                reason,
                current_spectators,
            },
            ServerMessage::Error {
                message,
                error_code,
            } => Self::Error {
                message,
                error_code,
            },
        }
    }
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::todo,
    clippy::unimplemented,
    clippy::indexing_slicing
)]
mod tests {
    use super::*;
    use crate::protocol::{
        LobbyState, ReconnectedPayload, RoomJoinedPayload, SpectatorJoinedPayload,
    };

    #[test]
    fn connected_event_is_constructible() {
        let event = SignalFishEvent::Connected;
        let debug = format!("{event:?}");
        assert!(debug.contains("Connected"));
    }

    #[test]
    fn disconnected_event_contains_reason() {
        let event = SignalFishEvent::Disconnected {
            reason: Some("server shutdown".into()),
        };
        if let SignalFishEvent::Disconnected { reason } = event {
            assert_eq!(reason.as_deref(), Some("server shutdown"));
        } else {
            panic!("expected Disconnected variant");
        }
    }

    #[test]
    fn from_server_message_pong() {
        let event = SignalFishEvent::from(ServerMessage::Pong);
        assert!(matches!(event, SignalFishEvent::Pong));
    }

    #[test]
    fn from_server_message_room_left() {
        let event = SignalFishEvent::from(ServerMessage::RoomLeft);
        assert!(matches!(event, SignalFishEvent::RoomLeft));
    }

    #[test]
    fn from_server_message_room_joined_flattens_payload() {
        let payload = RoomJoinedPayload {
            room_id: uuid::Uuid::nil(),
            room_code: "ABC123".into(),
            player_id: uuid::Uuid::nil(),
            game_name: "test-game".into(),
            max_players: 4,
            supports_authority: true,
            current_players: vec![],
            is_authority: false,
            lobby_state: LobbyState::Waiting,
            ready_players: vec![],
            relay_type: "auto".into(),
            current_spectators: vec![],
        };
        let msg = ServerMessage::RoomJoined(Box::new(payload));
        let event = SignalFishEvent::from(msg);
        if let SignalFishEvent::RoomJoined {
            room_code,
            max_players,
            game_name,
            ..
        } = event
        {
            assert_eq!(room_code, "ABC123");
            assert_eq!(max_players, 4);
            assert_eq!(game_name, "test-game");
        } else {
            panic!("expected RoomJoined variant");
        }
    }

    #[test]
    fn from_server_message_error() {
        let msg = ServerMessage::Error {
            message: "oops".into(),
            error_code: Some(ErrorCode::InternalError),
        };
        let event = SignalFishEvent::from(msg);
        if let SignalFishEvent::Error {
            message,
            error_code,
        } = event
        {
            assert_eq!(message, "oops");
            assert_eq!(error_code, Some(ErrorCode::InternalError));
        } else {
            panic!("expected Error variant");
        }
    }

    #[test]
    fn event_is_clone() {
        let event = SignalFishEvent::Pong;
        let cloned = event.clone();
        assert!(matches!(cloned, SignalFishEvent::Pong));
    }

    #[test]
    fn from_server_message_reconnected_flattens_payload() {
        let payload = ReconnectedPayload {
            room_id: uuid::Uuid::nil(),
            room_code: "RECON1".into(),
            player_id: uuid::Uuid::nil(),
            game_name: "recon-game".into(),
            max_players: 6,
            supports_authority: false,
            current_players: vec![],
            is_authority: true,
            lobby_state: LobbyState::Waiting,
            ready_players: vec![],
            relay_type: "tcp".into(),
            current_spectators: vec![],
            missed_events: vec![ServerMessage::Pong],
        };
        let msg = ServerMessage::Reconnected(Box::new(payload));
        let event = SignalFishEvent::from(msg);
        if let SignalFishEvent::Reconnected {
            room_code,
            max_players,
            is_authority,
            missed_events,
            ..
        } = event
        {
            assert_eq!(room_code, "RECON1");
            assert_eq!(max_players, 6);
            assert!(is_authority);
            assert_eq!(missed_events.len(), 1);
            assert!(matches!(missed_events[0], SignalFishEvent::Pong));
        } else {
            panic!("expected Reconnected variant");
        }
    }

    #[test]
    fn from_server_message_spectator_joined_flattens_payload() {
        let payload = SpectatorJoinedPayload {
            room_id: uuid::Uuid::nil(),
            room_code: "SPEC1".into(),
            spectator_id: uuid::Uuid::nil(),
            game_name: "spec-game".into(),
            current_players: vec![],
            current_spectators: vec![],
            lobby_state: LobbyState::Waiting,
            reason: None,
        };
        let msg = ServerMessage::SpectatorJoined(Box::new(payload));
        let event = SignalFishEvent::from(msg);
        if let SignalFishEvent::SpectatorJoined {
            room_code,
            game_name,
            reason,
            ..
        } = event
        {
            assert_eq!(room_code, "SPEC1");
            assert_eq!(game_name, "spec-game");
            assert!(reason.is_none());
        } else {
            panic!("expected SpectatorJoined variant");
        }
    }

    #[test]
    fn from_server_message_game_data_binary() {
        let msg = ServerMessage::GameDataBinary {
            from_player: uuid::Uuid::nil(),
            encoding: GameDataEncoding::MessagePack,
            payload: vec![0xDE, 0xAD],
        };
        let event = SignalFishEvent::from(msg);
        if let SignalFishEvent::GameDataBinary {
            from_player,
            encoding,
            payload,
        } = event
        {
            assert_eq!(from_player, uuid::Uuid::nil());
            assert!(matches!(encoding, GameDataEncoding::MessagePack));
            assert_eq!(payload, vec![0xDE, 0xAD]);
        } else {
            panic!("expected GameDataBinary variant");
        }
    }

    #[test]
    fn from_server_message_authentication_error() {
        let msg = ServerMessage::AuthenticationError {
            error: "bad token".into(),
            error_code: ErrorCode::InvalidAppId,
        };
        let event = SignalFishEvent::from(msg);
        if let SignalFishEvent::AuthenticationError { error, error_code } = event {
            assert_eq!(error, "bad token");
            assert_eq!(error_code, ErrorCode::InvalidAppId);
        } else {
            panic!("expected AuthenticationError variant");
        }
    }
}
