use serde::{Deserialize, Serialize};

pub mod preferences;
pub mod timeline;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
    pub id: String,
    pub username: String,
    pub perfs: Option<Performances>,
    /// Only appears if the user is a titled player or a bot user
    pub title: Option<Title>,
    /// See available flair list and images: https://github.com/lichess-org/lila/tree/master/public/flair
    pub flair: Option<String>,
    pub created_at: Option<i64>,
    pub seen_at: Option<i64>,
    /// Only appears if a user's account is closed
    pub disabled: Option<bool>,
    /// Only appears if a user's account is marked for the violation of Lichess TOS
    pub tos_violation: Option<bool>,
    pub profile: Option<Profile>,
    pub play_time: Option<PlayTime>,
    #[deprecated(note = "Use patron_color value instead to determine if player is a patron.")]
    pub patron: Option<bool>,
    /// Players can choose a color for their Patron wings. The presence of this field indicates the player is an active Patron.
    pub patron_color: Option<u8>,
    pub verified: Option<bool>,
    pub url: String,
    pub playing: Option<String>,
    pub count: Option<Count>,
    pub streaming: Option<bool>,
    pub streamer: Option<Streamer>,
    /// Only appears if the request is authenticated with OAuth2
    pub followable: Option<bool>,
    /// Only appears if the request is authenticated with OAuth2
    pub following: Option<bool>,
    /// Only appears if the request is authenticated with OAuth2
    pub blocking: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Performances {
    pub chess960: Option<Performance>,
    pub atomic: Option<Performance>,
    pub racing_kings: Option<Performance>,
    pub ultra_bullet: Option<Performance>,
    pub blitz: Option<Performance>,
    pub king_of_the_hill: Option<Performance>,
    pub three_check: Option<Performance>,
    pub antichess: Option<Performance>,
    pub crazyhouse: Option<Performance>,
    pub bullet: Option<Performance>,
    pub correspondence: Option<Performance>,
    pub horde: Option<Performance>,
    pub puzzle: Option<Performance>,
    pub classical: Option<Performance>,
    pub rapid: Option<Performance>,
    pub storm: Option<StormPerf>,
    pub racer: Option<StormPerf>,
    pub streak: Option<StormPerf>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Performance {
    pub games: i32,
    pub rating: i32,
    /// Rating deviation
    pub rd: i32,
    pub prog: i32,
    /// Only appears if a user's perf rating is provisional
    pub prov: Option<bool>,
    /// Global lichess ranking, only appears for recently active players
    pub rank: Option<i32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StormPerf {
    pub runs: i32,
    pub score: i32,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Title {
    GM,
    WGM,
    IM,
    WIM,
    FM,
    WFM,
    NM,
    CM,
    WCM,
    WNM,
    LM,
    BOT,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Profile {
    pub flag: Option<String>,
    pub location: Option<String>,
    pub bio: Option<String>,
    pub real_name: Option<String>,
    /// Only appears if a user has set them
    pub fide_rating: Option<i32>,
    /// Only appears if a user has set them
    pub uscf_rating: Option<i32>,
    /// Only appears if a user has set them
    pub ecf_rating: Option<i32>,
    /// Only appears if a user has set them
    pub cfc_rating: Option<i32>,
    /// Only appears if a user has set them
    pub rcf_rating: Option<i32>,
    /// Only appears if a user has set them
    pub dsb_rating: Option<i32>,
    pub links: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayTime {
    pub total: i64,
    pub tv: i64,
    pub human: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Count {
    pub all: i32,
    pub rated: i32,
    pub ai: Option<i32>,
    pub draw: i32,
    pub draw_h: Option<i32>,
    pub loss: i32,
    pub loss_h: Option<i32>,
    pub win: i32,
    pub win_h: Option<i32>,
    pub bookmark: i32,
    pub playing: i32,
    pub import: i32,
    pub me: i32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Streamer {
    pub twitch: Option<Channel>,
    pub youtube: Option<Channel>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel {
    pub channel: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Email {
    pub email: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KidMode {
    pub kid: bool,
}

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

    #[test]
    fn test_user_deserialization() {
        let json = include_str!("../../test_data/account/user.json");

        let user: User = serde_json::from_str(json).expect("Failed to deserialize");

        assert_eq!(user.id, "bobby");
        assert_eq!(user.username, "Bobby");

        assert!(user.perfs.is_some());
        let perfs = user.perfs.as_ref().unwrap();

        assert!(perfs.bullet.is_some());
        let bullet = perfs.bullet.as_ref().unwrap();
        assert_eq!(bullet.games, 39);
        assert_eq!(bullet.rating, 1664);
        assert_eq!(bullet.rd, 51);
        assert_eq!(bullet.prog, 39);
        assert!(bullet.prov.is_none());

        // Check blitz with prov
        assert!(perfs.blitz.is_some());
        let blitz = perfs.blitz.as_ref().unwrap();
        assert_eq!(blitz.games, 68);
        assert_eq!(blitz.rating, 1719);
        assert_eq!(blitz.rd, 122);
        assert_eq!(blitz.prog, -21);
        assert_eq!(blitz.prov, Some(true));

        // Check kingOfTheHill
        assert!(perfs.king_of_the_hill.is_some());
        let koth = perfs.king_of_the_hill.as_ref().unwrap();
        assert_eq!(koth.games, 986);
        assert_eq!(koth.rating, 1686);
        assert_eq!(koth.rd, 99);
        assert_eq!(koth.prog, 28);
        assert!(koth.prov.is_none());

        // Check profile
        assert!(user.profile.is_some());
        let profile = user.profile.as_ref().unwrap();
        assert_eq!(profile.flag, Some("FR".to_string()));
        assert_eq!(profile.location, Some("Bobby City".to_string()));
        assert_eq!(profile.bio, Some("That the method employed in \"Das Kapital\" has been little understood, is shown by the various conceptions, contradictory one to another, that have been formed of it.".to_string()));
        assert_eq!(profile.fide_rating, Some(1725));
        assert_eq!(profile.uscf_rating, Some(1550));
        assert_eq!(profile.ecf_rating, Some(1765));
        assert_eq!(profile.rcf_rating, Some(1605));
        assert_eq!(profile.cfc_rating, Some(1812));
        assert_eq!(profile.dsb_rating, Some(1573));
        assert_eq!(profile.links, Some("".to_string()));

        // Check playTime
        assert!(user.play_time.is_some());
        let play_time = user.play_time.as_ref().unwrap();
        assert_eq!(play_time.total, 18907);
        assert_eq!(play_time.tv, 0);
        assert!(play_time.human.is_none()); // Not in JSON

        // Check count
        assert!(user.count.is_some());
        let count = user.count.as_ref().unwrap();
        assert_eq!(count.all, 2933);
        assert_eq!(count.rated, 2346);
        assert_eq!(count.draw, 116);
        assert_eq!(count.loss, 1443);
        assert_eq!(count.win, 1374);
        assert_eq!(count.bookmark, 0);
        assert_eq!(count.playing, 0);
        assert_eq!(count.import, 0);
        assert_eq!(count.me, 0);

        // Other top-level fields
        assert_eq!(user.flair, Some("symbols.white-flag".to_string()));
        #[allow(deprecated)]
        {
            assert_eq!(user.patron, Some(true));
        }
        assert_eq!(user.patron_color, Some(1));
        assert_eq!(user.created_at, Some(1756404871965));
        assert_eq!(user.seen_at, Some(1763791583475));
        assert_eq!(user.url, "https://lichess.org/@/Bobby");
        assert_eq!(user.followable, Some(true));
        assert_eq!(user.following, Some(false));
        assert_eq!(user.blocking, Some(false));
    }

    #[test]
    fn test_kid_mode_deserialization() {
        let json = r#"{
  "kid": false
}"#;

        let kid_mode: KidMode = serde_json::from_str(json).expect("Failed to deserialize");

        assert_eq!(kid_mode.kid, false);
    }

    #[test]
    fn test_email_deserialization() {
        let json = r#"{
  "email": "bobby@localhost"
}"#;

        let email: Email = serde_json::from_str(json).expect("Failed to deserialize");

        assert_eq!(email.email, "bobby@localhost");
    }
}
