use serde::{Deserialize, Serialize};

use super::media::MediaItem;

/// Tracks why playback ended to determine autoplay behavior
///
/// TRACES: UR-005 | DR-001
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EndReason {
    /// Track played to completion (natural end) - trigger autoplay
    Finished,
    /// User pressed next/previous - already handled, don't autoplay
    UserSkip,
    /// User stopped playback - don't autoplay
    UserStop,
    /// Playback error - don't autoplay
    Error,
    /// User selected a different track - don't autoplay
    NewTrackLoaded,
}

/// Player state machine (6 states: Idle, Loading, Playing, Paused, Seeking, Error)
///
/// TRACES: UR-005 | DR-001
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum PlayerState {
    #[default]
    /// No media loaded
    Idle,
    /// Media is being loaded/buffered
    Loading { media: MediaItem },
    /// Media is playing
    Playing {
        media: MediaItem,
        /// Current position in seconds
        position: f64,
        /// Total duration in seconds
        duration: f64,
    },
    /// Media is paused
    Paused {
        media: MediaItem,
        /// Current position in seconds
        position: f64,
        /// Total duration in seconds
        duration: f64,
    },
    /// Seeking to a new position
    Seeking {
        media: MediaItem,
        /// Target position in seconds
        target: f64,
    },
    /// An error occurred
    Error {
        media: Option<MediaItem>,
        error: String,
    },
}

impl PlayerState {
    /// Get the current playback position if available.
    ///
    /// Note: this is the position snapshot embedded in the state at the last
    /// state transition, not the live backend position. For an up-to-date
    /// value use `PlayerController::position()`.
    #[allow(dead_code)]
    pub fn position(&self) -> Option<f64> {
        match self {
            PlayerState::Playing { position, .. } => Some(*position),
            PlayerState::Paused { position, .. } => Some(*position),
            _ => None,
        }
    }

    /// Check if the player is currently playing
    pub fn is_playing(&self) -> bool {
        matches!(self, PlayerState::Playing { .. })
    }

    /// Check if the player is currently paused
    #[allow(dead_code)]
    pub fn is_paused(&self) -> bool {
        matches!(self, PlayerState::Paused { .. })
    }
}

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

    #[test]
    fn test_end_reason_finished() {
        let reason = EndReason::Finished;
        let json = serde_json::to_string(&reason);
        assert!(json.is_ok());
        let serialized = json.unwrap();
        assert!(serialized.contains("finished"));
    }

    #[test]
    fn test_end_reason_user_skip() {
        let reason = EndReason::UserSkip;
        let json = serde_json::to_string(&reason);
        assert!(json.is_ok());
        let serialized = json.unwrap();
        assert!(serialized.contains("userskip"));
    }

    #[test]
    fn test_end_reason_user_stop() {
        let reason = EndReason::UserStop;
        let json = serde_json::to_string(&reason).unwrap();
        assert!(json.contains("userstop"));
    }

    #[test]
    fn test_end_reason_error() {
        let reason = EndReason::Error;
        let json = serde_json::to_string(&reason).unwrap();
        assert!(json.contains("error"));
    }

    #[test]
    fn test_end_reason_new_track_loaded() {
        let reason = EndReason::NewTrackLoaded;
        let json = serde_json::to_string(&reason).unwrap();
        assert!(json.contains("newtrackloa"));
    }

    #[test]
    fn test_end_reason_all_variants() {
        let reasons = vec![
            EndReason::Finished,
            EndReason::UserSkip,
            EndReason::UserStop,
            EndReason::Error,
            EndReason::NewTrackLoaded,
        ];

        for reason in reasons {
            let json = serde_json::to_string(&reason);
            assert!(json.is_ok());
        }
    }

    #[test]
    fn test_end_reason_equality() {
        let reason1 = EndReason::Finished;
        let reason2 = EndReason::Finished;
        assert_eq!(reason1, reason2);

        let reason3 = EndReason::UserSkip;
        assert_ne!(reason1, reason3);
    }

    #[test]
    fn test_end_reason_clone() {
        let reason = EndReason::Finished;
        // Deliberately exercising the derived `Clone` impl, not a plain copy:
        // `EndReason` is also `Copy`, so clippy flags the call as redundant.
        #[allow(clippy::clone_on_copy)]
        let cloned = reason.clone();
        assert_eq!(reason, cloned);
    }

    #[test]
    fn test_player_state_idle_default() {
        let state = PlayerState::default();
        assert!(matches!(state, PlayerState::Idle));
    }

    #[test]
    fn test_player_state_idle_serialization() {
        let state = PlayerState::Idle;
        let json = serde_json::to_string(&state);
        assert!(json.is_ok());
        let serialized = json.unwrap();
        assert!(serialized.contains("idle"));
    }

    #[test]
    fn test_player_state_position_when_idle() {
        let state = PlayerState::Idle;
        assert_eq!(state.position(), None);
    }

    #[test]
    fn test_player_state_is_playing_when_idle() {
        let state = PlayerState::Idle;
        assert!(!state.is_playing());
    }

    #[test]
    fn test_player_state_is_paused_when_idle() {
        let state = PlayerState::Idle;
        assert!(!state.is_paused());
    }

    #[test]
    fn test_player_state_loading() {
        let media = create_test_media_item("item-1", "Test Item");
        let state = PlayerState::Loading {
            media: media.clone(),
        };
        assert!(matches!(state, PlayerState::Loading { .. }));
        assert_eq!(state.position(), None);
        assert!(!state.is_playing());
    }

    #[test]
    fn test_player_state_playing_position() {
        let media = create_test_media_item("item-2", "Playing Item");
        let state = PlayerState::Playing {
            media,
            position: 45.5,
            duration: 180.0,
        };

        assert!(state.is_playing());
        assert!(!state.is_paused());
        assert_eq!(state.position(), Some(45.5));
    }

    #[test]
    fn test_player_state_paused_position() {
        let media = create_test_media_item("item-3", "Paused Item");
        let state = PlayerState::Paused {
            media,
            position: 123.75,
            duration: 300.0,
        };

        assert!(state.is_paused());
        assert!(!state.is_playing());
        assert_eq!(state.position(), Some(123.75));
    }

    #[test]
    fn test_player_state_seeking() {
        let media = create_test_media_item("item-4", "Seeking Item");
        let state = PlayerState::Seeking {
            media,
            target: 60.0,
        };

        assert!(!state.is_playing());
        assert!(!state.is_paused());
        assert_eq!(state.position(), None);
    }

    #[test]
    fn test_player_state_error_with_media() {
        let media = create_test_media_item("item-5", "Error Item");
        let state = PlayerState::Error {
            media: Some(media),
            error: "Playback failed".to_string(),
        };

        assert!(!state.is_playing());
        assert_eq!(state.position(), None);
    }

    #[test]
    fn test_player_state_error_without_media() {
        let state = PlayerState::Error {
            media: None,
            error: "Connection lost".to_string(),
        };

        assert!(!state.is_playing());
        assert_eq!(state.position(), None);
    }

    #[test]
    fn test_player_state_clone() {
        let state = PlayerState::Idle;
        let cloned = state.clone();
        assert!(matches!(cloned, PlayerState::Idle));
    }

    #[test]
    fn test_player_state_playing_serialization() {
        let media = create_test_media_item("item-6", "Serial Item");
        let state = PlayerState::Playing {
            media,
            position: 30.0,
            duration: 120.0,
        };

        let json = serde_json::to_string(&state);
        assert!(json.is_ok());
        let serialized = json.unwrap();
        assert!(serialized.contains("playing"));
        assert!(serialized.contains("30"));
    }

    #[test]
    fn test_player_state_multiple_positions() {
        let positions = vec![0.0, 45.5, 100.0, 999.99];

        for pos in positions {
            let media = create_test_media_item("item-test", "Test");
            let state = PlayerState::Playing {
                media,
                position: pos,
                duration: 1000.0,
            };

            assert_eq!(state.position(), Some(pos));
        }
    }

    // Helper function to create test MediaItem instances
    fn create_test_media_item(id: &str, title: &str) -> MediaItem {
        MediaItem {
            id: id.to_string(),
            title: title.to_string(),
            name: None,
            artist: None,
            album: None,
            album_name: None,
            album_id: None,
            artist_items: None,
            artists: None,
            primary_image_tag: None,
            image_id: None,
            item_type: Some("Video".to_string()),
            playlist_id: None,
            duration: Some(100.0),
            artwork_url: None,
            media_type: super::super::media::MediaType::Video,
            source: super::super::media::MediaSource::DirectUrl {
                url: "http://example.com/media".to_string(),
            },
            video_codec: None,
            needs_transcoding: false,
            video_width: None,
            video_height: None,
            subtitles: vec![],
            series_id: None,
            server_id: None,
        }
    }
}
