//! The identity LSX reports, ported from `fut_account.py`'s tier-1/tier-2 fields.
//!
//! SOURCED FROM `fut_account.ACCOUNT`, shared with blaze_responder_v3b.py,
//! fut_store.py, fut_seed.py and utas_server.py.
//!
//! THE CONSTRAINT IS CROSS-LAYER CONSISTENCY, NOT ANY PARTICULAR VALUE: what LSX
//! reports here must equal what Blaze returns in LoginResponse.SESS.PDTL and what
//! UTAS serves as userInfo.personaId. (An older comment blamed a mismatch for
//! AUTH_ERR_INVALID_PERSONA / AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA /
//! AUTH_ERR_PERSONA_NOT_FOUND -- those are Blaze *server* error codes and we are
//! the server. Neither "CAGE" nor "33068179" appears in FIFA17.exe, CardsDLL or
//! dbdata.dll; 33068179 lives only in stp-origin_emu.dll's own ini default. They
//! stay the defaults because they are what the working stack asserts.)
//!
//! PRECEDENCE: env var > built-in default.
//!
//! `fut_account.py` sits one tier deeper -- env > `fut_account.json` > default --
//! but the JSON is deliberately NOT read here: its persisted values for the three
//! fields LSX uses (`persona_id`, `persona_name`; `locale` is not even stored) are
//! identical to the built-in defaults, and the launcher always passes
//! `FUT_PERSONA_ID`/`FUT_PERSONA_NAME` explicitly when it spawns us
//! (openfut-launcher/src/local_services.rs), so env decides in every real run.
//! Parsing a JSON file to reach the same answer would only add a failure mode --
//! and the launcher, not a file next to the Python tools, is the identity owner
//! for this binary.

use crate::{ConfigError, Env};

// ------------------------------------------------------------------ tier 1
// LOCKED WIRE CONSTANTS. No env override on purpose: these are not preferences.

/// FIFA 17 EA offer id (retail).
pub const CONTENT_ID: &str = "1027460";

/// `TRIAL_ONLINE_ACCESS` for FIFA17_Trial.exe; retail uses this.
pub const ENTITLEMENT_TAG: &str = "ONLINE_ACCESS";

// ------------------------------------------------------------------ tier 2
pub const DEFAULT_PERSONA_ID: i64 = 33068179;
pub const DEFAULT_PERSONA_NAME: &str = "CAGE";
pub const DEFAULT_LOCALE: &str = "en_US";

/// The identity fields LSX puts on the wire.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identity {
    /// Blaze SESS.BUID / SESS.UID / PDTL.PID, LSX PersonaId/UserId, UTAS
    /// userInfo.personaId and squad.personaId.
    pub persona_id: i64,
    /// Blaze PDTL.DSNM / LSX GetProfileResponse Persona / UTAS sellerName.
    pub persona_name: String,
    /// LSX `GetSetting LANGUAGE`.
    pub locale: String,
}

impl Default for Identity {
    fn default() -> Self {
        Self {
            persona_id: DEFAULT_PERSONA_ID,
            persona_name: DEFAULT_PERSONA_NAME.to_string(),
            locale: DEFAULT_LOCALE.to_string(),
        }
    }
}

impl Identity {
    /// Read the process environment.
    pub fn from_env() -> Result<Self, ConfigError> {
        Self::from_vars(&crate::os_env)
    }

    /// A `FUT_PERSONA_ID` that `int()` would reject kills `fut_account.py` at
    /// import; we refuse to start for the same reason, rather than serve one
    /// layer of the stack a silently different persona.
    pub fn from_vars(env: Env<'_>) -> Result<Self, ConfigError> {
        let persona_id = match env("FUT_PERSONA_ID") {
            // `int()` tolerates surrounding whitespace and a sign.
            Some(v) => v.trim().parse::<i64>().map_err(|_| ConfigError {
                var: "FUT_PERSONA_ID",
                value: v.clone(),
                expected: "an integer",
            })?,
            None => DEFAULT_PERSONA_ID,
        };
        Ok(Self {
            persona_id,
            persona_name: env("FUT_PERSONA_NAME").unwrap_or_else(|| DEFAULT_PERSONA_NAME.into()),
            locale: env("FUT_LOCALE").unwrap_or_else(|| DEFAULT_LOCALE.into()),
        })
    }

    /// Blaze blazeId / userId (SESS.BUID, SESS.UID, AccountInfo.UID).
    ///
    /// DERIVED, read-only, and deliberately NOT an independent knob: the client
    /// sends both `nuc` and `nucleusPersonaId` and both came out equal, so the
    /// getter->field mapping is undetermined. Do not split them until a live test
    /// proves Blaze USER_ID may legitimately differ from PERSONA_ID.
    pub fn user_id(&self) -> i64 {
        self.persona_id
    }
}

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

    fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
        let map: HashMap<String, String> = pairs
            .iter()
            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
            .collect();
        move |k: &str| map.get(k).cloned()
    }

    #[test]
    fn defaults_are_the_working_stacks_values() {
        let id = Identity::from_vars(&env_of(&[])).unwrap();
        assert_eq!(id.persona_id, 33068179);
        assert_eq!(id.persona_name, "CAGE");
        assert_eq!(id.locale, "en_US");
        assert_eq!(id.user_id(), id.persona_id);
        assert_eq!(id, Identity::default());
    }

    #[test]
    fn env_wins_over_defaults() {
        let id = Identity::from_vars(&env_of(&[
            ("FUT_PERSONA_ID", "1234567"),
            ("FUT_PERSONA_NAME", "OTHER"),
            ("FUT_LOCALE", "de_DE"),
        ]))
        .unwrap();
        assert_eq!(id.persona_id, 1234567);
        assert_eq!(id.persona_name, "OTHER");
        assert_eq!(id.locale, "de_DE");
        assert_eq!(id.user_id(), 1234567);
    }

    #[test]
    fn empty_env_value_still_wins() {
        // `os.environ.get` returns "" for `FUT_PERSONA_NAME=`, and "" is not None.
        let id = Identity::from_vars(&env_of(&[("FUT_PERSONA_NAME", "")])).unwrap();
        assert_eq!(id.persona_name, "");
    }

    #[test]
    fn whitespace_around_the_persona_id_is_tolerated_like_int() {
        let id = Identity::from_vars(&env_of(&[("FUT_PERSONA_ID", " 42 ")])).unwrap();
        assert_eq!(id.persona_id, 42);
    }

    #[test]
    fn a_non_numeric_persona_id_is_refused() {
        let err = Identity::from_vars(&env_of(&[("FUT_PERSONA_ID", "CAGE")])).unwrap_err();
        assert_eq!(err.var, "FUT_PERSONA_ID");
        assert_eq!(err.to_string(), r#"FUT_PERSONA_ID must be an integer (got "CAGE")"#);
    }
}
