use std::collections::HashMap;

use serde_json::{Value, json};
use sha2::{Digest, Sha256};

use crate::api::{ApiClient, ApiErrorCode};
use crate::base64::{base64url_decode, base64url_encode};
use crate::crypto::aes::aes128_ecb_decrypt_block;
use crate::crypto::{
    decrypt_key, decrypt_private_key, decrypt_session_id, derive_key_v2, encrypt_key,
    make_password_key, make_random_key, make_username_hash, verify_tsid,
};
use crate::error::{MegaError, Result};

use super::core::Session;
use super::device_id::device_id_hash;

#[derive(Debug, Clone, Copy)]
enum UpgradeOutcome {
    NotNeeded,
    Upgraded,
    AlreadyUpgraded,
    Failed,
}

impl Session {
    /// Login with email and password.
    ///
    /// This creates a new authenticated session with MEGA.
    ///
    /// # Example
    /// ```no_run
    /// use megalib::SessionHandle;
    ///
    /// # async fn example() -> megalib::error::Result<()> {
    /// let session = SessionHandle::login("user@example.com", "password").await?;
    /// let info = session.account_info().await?;
    /// println!("Logged in as: {}", info.email);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn login(email: &str, password: &str) -> Result<Self> {
        Self::login_internal(email, password, None).await
    }

    /// Login with email, password, and HTTP proxy.
    ///
    /// # Arguments
    /// * `email` - User's email address
    /// * `password` - User's password
    /// * `proxy` - Proxy URL (e.g., "http://proxy:8080" or "socks5://proxy:1080")
    ///
    /// # Example
    /// ```no_run
    /// use megalib::SessionHandle;
    ///
    /// # async fn example() -> megalib::error::Result<()> {
    /// let session = SessionHandle::login_with_proxy(
    ///     "user@example.com",
    ///     "password",
    ///     "http://proxy.example.com:8080"
    /// ).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn login_with_proxy(email: &str, password: &str, proxy: &str) -> Result<Self> {
        Self::login_internal(email, password, Some(proxy)).await
    }

    /// Internal login implementation.
    async fn login_internal(email: &str, password: &str, proxy: Option<&str>) -> Result<Self> {
        let mut api = match proxy {
            Some(p) => ApiClient::with_proxy(p)?,
            None => ApiClient::new(),
        };
        let email_lower = email.to_lowercase();

        // Step 1: Pre-login to determine login variant
        let pre_login = api
            .request(json!({
                "a": "us0",
                "user": &email_lower
            }))
            .await?;

        let login_variant = pre_login["v"].as_i64().unwrap_or(0);

        // Step 2: Compute password key and user hash based on variant
        let (password_key, user_hash) = if login_variant == 2 {
            // V2 login: PBKDF2-SHA512
            let salt_b64 = pre_login["s"].as_str().ok_or(MegaError::InvalidResponse)?;
            let salt = base64url_decode(salt_b64)?;

            let derived = derive_key_v2(password, &salt)?;
            let password_key: [u8; 16] = derived[..16].try_into().unwrap();
            let user_hash = base64url_encode(&derived[16..32]);

            (password_key, user_hash)
        } else {
            // V1 login: Legacy password key derivation
            let password_key = make_password_key(password);
            let user_hash_bytes = make_username_hash(&email_lower, &password_key);
            let user_hash = base64url_encode(&user_hash_bytes);

            (password_key, user_hash)
        };

        let sek = make_random_key();
        let sek_b64 = base64url_encode(&sek);
        let si = device_id_hash();
        let mut login_payload = json!({
            "a": "us",
            "user": &email_lower,
            "uh": &user_hash,
            "sek": &sek_b64
        });
        if let Some(si) = si {
            login_payload["si"] = Value::String(si);
        }

        // Step 3: Login request
        let login_response = api.request(login_payload).await?;

        // Step 4: Decrypt master key
        let k_b64 = login_response["k"]
            .as_str()
            .ok_or(MegaError::InvalidResponse)?;
        let master_key = decrypt_key(k_b64, &password_key)?;

        let session_key = match login_response.get("sek").and_then(|v| v.as_str()) {
            Some(sek_b64) => {
                let decoded = base64url_decode(sek_b64)?;
                if decoded.len() != 16 {
                    return Err(MegaError::InvalidResponse);
                }
                let mut key = [0u8; 16];
                key.copy_from_slice(&decoded);
                Some(key)
            }
            None => None,
        };

        // Step 5: Session bootstrap follows SDK branching:
        // if `tsid` exists, validate challenge and use it directly;
        // otherwise require RSA `privk` + `csid`.
        let (session_id, rsa_key) =
            if let Some(tsid) = login_response.get("tsid").and_then(|v| v.as_str()) {
                if !verify_tsid(tsid, &master_key)? {
                    return Err(Self::invalid_tsid_error());
                }
                let sid = tsid.to_string();
                api.set_session_id(sid.clone());
                (sid, Self::empty_rsa_key())
            } else {
                let privk_b64 = login_response["privk"]
                    .as_str()
                    .ok_or(MegaError::InvalidResponse)?;
                let rsa_key = decrypt_private_key(privk_b64, &master_key)?;
                let csid_b64 = login_response["csid"]
                    .as_str()
                    .ok_or(MegaError::InvalidResponse)?;
                let session_id = decrypt_session_id(csid_b64, &rsa_key)?;
                api.set_session_id(session_id.clone());
                (session_id, rsa_key)
            };

        let mut upgrade_outcome = UpgradeOutcome::NotNeeded;
        if login_variant == 1 {
            upgrade_outcome = Self::attempt_account_upgrade(&mut api, password, &master_key)
                .await
                .unwrap_or(UpgradeOutcome::Failed);

            let mut batch = Vec::new();
            if matches!(upgrade_outcome, UpgradeOutcome::Upgraded) {
                batch.push(json!({
                    "a": "log",
                    "e": 99473,
                    "m": "Account successfully upgraded to v2"
                }));
            }
            batch.push(json!({"a": "uq", "pro": 1, "src": -1, "v": 2}));
            let _ = api.request_batch(batch).await;
        } else {
            api.request_batch(vec![
                json!({"a": "stp"}),
                json!({"a": "uq", "pro": 1, "src": -1, "v": 2}),
            ])
            .await?;
        }

        // Step 7: Get user info
        let user_info = api.request(json!({"a": "ug", "v": 1})).await?;

        let user_handle = user_info["u"]
            .as_str()
            .ok_or(MegaError::InvalidResponse)?
            .to_string();
        let user_email = user_info["email"]
            .as_str()
            .unwrap_or(&email_lower)
            .to_string();
        let user_name = user_info["name"].as_str().map(|s| s.to_string());
        let scsn = user_info
            .get("sn")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());
        let user_attr_cache = Self::collect_user_attrs_from_ug(&user_info);
        let user_attr_versions = Self::collect_user_attr_versions_from_ug(&user_info);

        let mut session = Session::new_internal(
            api,
            session_id,
            session_key,
            master_key,
            rsa_key,
            user_email,
            user_name,
            user_handle,
            user_attr_cache,
            user_attr_versions,
            scsn,
        );
        session.install_default_persistence_runtime()?;

        let account_is_v2 = login_variant == 2
            || matches!(
                upgrade_outcome,
                UpgradeOutcome::Upgraded | UpgradeOutcome::AlreadyUpgraded
            );

        if account_is_v2 {
            let _ = session.initialize_account_keys().await;
        }

        // On login, attempt to load ^!keys and process pending promotions.
        let _ = session.load_keys_attribute().await;
        let _ = session.promote_pending_shares().await;
        if session.clear_inuse_flags_for_missing_shares() {
            let _ = session.persist_keys_with_retry().await;
        }

        Ok(session)
    }

    fn collect_user_attrs_from_ug(user_info: &Value) -> HashMap<String, Vec<u8>> {
        let mut cache = HashMap::new();
        let Some(obj) = user_info.as_object() else {
            return cache;
        };

        let attrs = [
            "^!keys",
            "*keyring",
            "*~usk",
            "*~jscd",
            "+puCu255",
            "+puEd255",
            "+sigCu255",
            "+sigPubk",
        ];

        for attr in attrs {
            if let Some(av) = obj
                .get(attr)
                .and_then(|v| v.get("av"))
                .and_then(|v| v.as_str())
            {
                if av.is_empty() {
                    continue;
                }
                if let Ok(decoded) = base64url_decode(av) {
                    cache.insert(attr.to_string(), decoded);
                }
            }
        }

        cache
    }

    fn collect_user_attr_versions_from_ug(user_info: &Value) -> HashMap<String, String> {
        let mut versions = HashMap::new();
        let Some(obj) = user_info.as_object() else {
            return versions;
        };

        let attrs = [
            "^!keys",
            "*keyring",
            "*~usk",
            "*~jscd",
            "+puCu255",
            "+puEd255",
            "+sigCu255",
            "+sigPubk",
        ];

        for attr in attrs {
            if let Some(v) = obj
                .get(attr)
                .and_then(|v| v.get("v"))
                .and_then(|v| v.as_str())
            {
                versions.insert(attr.to_string(), v.to_string());
            }
        }

        versions
    }

    fn invalid_tsid_error() -> MegaError {
        MegaError::ApiError {
            code: ApiErrorCode::NotExist as i32,
            message: ApiErrorCode::NotExist.description().to_string(),
        }
    }

    fn build_upgrade_payload(
        password: &str,
        master_key: &[u8; 16],
    ) -> Result<(String, String, String)> {
        let client_random = make_random_key();
        let mut buffer = b"mega.nz".to_vec();
        buffer.resize(200, b'P');
        buffer.extend_from_slice(&client_random);
        let salt = Sha256::digest(&buffer);

        let derived = derive_key_v2(password, salt.as_slice())?;
        let password_key: [u8; 16] = derived[..16].try_into().unwrap();
        let auth_key = &derived[16..32];

        let encrypted_master_key = encrypt_key(master_key, &password_key);

        let mut hasher = Sha256::new();
        hasher.update(auth_key);
        let hashed = hasher.finalize();
        let hak = &hashed[..16];

        Ok((
            base64url_encode(&client_random),
            base64url_encode(&encrypted_master_key),
            base64url_encode(hak),
        ))
    }

    async fn attempt_account_upgrade(
        api: &mut ApiClient,
        password: &str,
        master_key: &[u8; 16],
    ) -> Result<UpgradeOutcome> {
        let (crv, emk, hak) = Self::build_upgrade_payload(password, master_key)?;
        let resp = api
            .request_batch(vec![
                json!({"a": "stp"}),
                json!({"a": "avu", "crv": crv, "emk": emk, "hak": hak}),
            ])
            .await?;

        let arr = resp.as_array().ok_or(MegaError::InvalidResponse)?;
        let avu = arr.get(1).ok_or(MegaError::InvalidResponse)?;
        if let Some(code) = avu.as_i64() {
            if code == 0 {
                return Ok(UpgradeOutcome::Upgraded);
            }
            if code == -8 {
                return Ok(UpgradeOutcome::AlreadyUpgraded);
            }
            if code < 0 {
                return Ok(UpgradeOutcome::Failed);
            }
        }

        Ok(UpgradeOutcome::Upgraded)
    }

    /// Change the current user's password.
    ///
    /// This updates the password on the server by re-encrypting the master key
    /// with a new key derived from the new password and a fresh salt.
    ///
    /// # Arguments
    /// * `new_password` - The new password to set
    ///
    /// # Example
    /// ```no_run
    /// # use megalib::SessionHandle;
    /// # async fn example() -> megalib::error::Result<()> {
    /// let mut session = SessionHandle::login("user@example.com", "old_password").await?;
    /// session.change_password("new_secure_password").await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn change_password(&mut self, new_password: &str) -> Result<()> {
        // 1. Generate new 16-byte random salt
        let salt = make_random_key();
        let salt_b64 = base64url_encode(&salt);

        // 2. Derive new keys (V2)
        let derived = derive_key_v2(new_password, &salt)?;
        let password_key: [u8; 16] = derived[..16].try_into().unwrap();
        let user_hash = base64url_encode(&derived[16..32]);

        // 3. Re-encrypt the master key with the new password key
        let encrypted_master_key = encrypt_key(&self.master_key, &password_key);
        let k_b64 = base64url_encode(&encrypted_master_key);

        // 4. Send 'up' request to update profile
        let response = self
            .api
            .request(json!({
                "a": "up",
                "k": k_b64,
                "uh": user_hash,
                "s": salt_b64
            }))
            .await?;

        // Check for error code if any
        if let Some(err_code) = response.as_i64()
            && err_code < 0
        {
            // Fix: Fully qualified path to ApiErrorCode
            let error_code = crate::api::ApiErrorCode::from(err_code);
            return Err(MegaError::ApiError {
                code: err_code as i32,
                message: error_code.description().to_string(),
            });
        }

        Ok(())
    }

    pub(super) async fn login_with_session(session_b64: &str, proxy: Option<&str>) -> Result<Self> {
        let blob = Session::parse_session_blob(session_b64)?;

        let mut api = match proxy {
            Some(p) => ApiClient::with_proxy(p)?,
            None => ApiClient::new(),
        };

        // Use existing session id to validate the session on the server.
        let mut session_id = blob.session_id.clone();
        api.set_session_id(session_id.clone());

        let sek = make_random_key();
        let sek_b64 = base64url_encode(&sek);
        let mut login_payload = json!({
            "a": "us",
            "sek": &sek_b64
        });
        if let Some(si) = device_id_hash() {
            login_payload["si"] = Value::String(si);
        }

        let login_response = api.request(login_payload).await?;

        let session_key = match login_response.get("sek").and_then(|v| v.as_str()) {
            Some(sek_b64) => {
                let decoded = base64url_decode(sek_b64)?;
                if decoded.len() != 16 {
                    return Err(MegaError::InvalidResponse);
                }
                let mut key = [0u8; 16];
                key.copy_from_slice(&decoded);
                Some(key)
            }
            None => None,
        };

        let mut master_key = blob.master_key;
        if blob.master_key_encrypted {
            let sek = session_key.ok_or(MegaError::InvalidResponse)?;
            master_key = aes128_ecb_decrypt_block(&blob.master_key, &sek);
        }

        let rsa_key = if let Some(privk_b64) = login_response.get("privk").and_then(|v| v.as_str())
        {
            decrypt_private_key(privk_b64, &master_key)?
        } else {
            Self::empty_rsa_key()
        };

        if let Some(tsid) = login_response.get("tsid").and_then(|v| v.as_str()) {
            if !verify_tsid(tsid, &master_key)? {
                return Err(Self::invalid_tsid_error());
            }
            session_id = tsid.to_string();
            api.set_session_id(session_id.clone());
        }

        let user_info = api.request(json!({"a": "ug", "v": 1})).await?;

        let user_handle = user_info["u"]
            .as_str()
            .ok_or(MegaError::InvalidResponse)?
            .to_string();
        let user_email = user_info["email"].as_str().unwrap_or_default().to_string();
        let user_name = user_info["name"].as_str().map(|s| s.to_string());
        let scsn = user_info
            .get("sn")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());
        let user_attr_cache = Self::collect_user_attrs_from_ug(&user_info);
        let user_attr_versions = Self::collect_user_attr_versions_from_ug(&user_info);

        let mut session = Session::new_internal(
            api,
            session_id,
            session_key,
            master_key,
            rsa_key,
            user_email,
            user_name,
            user_handle,
            user_attr_cache,
            user_attr_versions,
            scsn,
        );
        session.install_default_persistence_runtime()?;

        let account_is_v2 = user_info.get("aav").and_then(|v| v.as_i64()) == Some(2);
        if account_is_v2 {
            let _ = session.initialize_account_keys().await;
        }

        let _ = session.load_keys_attribute().await;
        let _ = session.promote_pending_shares().await;
        if session.clear_inuse_flags_for_missing_shares() {
            let _ = session.persist_keys_with_retry().await;
        }

        Ok(session)
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::io::{Read, Write};
    use std::net::{TcpListener, TcpStream};
    use std::path::{Path, PathBuf};
    use std::sync::{Arc, Mutex, OnceLock};
    use std::thread;
    use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

    use base64::Engine;
    use base64::engine::general_purpose;
    use serde_json::{Value, json};

    use super::*;
    use crate::api::ApiClient;
    use crate::crypto::aes::aes128_ecb_encrypt_block;

    struct TestDir {
        path: PathBuf,
    }

    impl TestDir {
        fn new(label: &str) -> Self {
            let unique = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .expect("system clock should be after unix epoch")
                .as_nanos();
            let path = std::env::temp_dir().join(format!(
                "megalib-auth-tests-{}-{}-{}",
                label,
                std::process::id(),
                unique
            ));
            Self { path }
        }

        fn path(&self) -> &Path {
            &self.path
        }
    }

    impl Drop for TestDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.path);
        }
    }

    struct EnvGuard {
        home: Option<std::ffi::OsString>,
        xdg_state_home: Option<std::ffi::OsString>,
        localappdata: Option<std::ffi::OsString>,
    }

    impl EnvGuard {
        fn new(home: &Path) -> Self {
            let xdg_state_home = home.join("xdg-state");
            let localappdata = home.join("AppData").join("Local");

            let guard = Self {
                home: std::env::var_os("HOME"),
                xdg_state_home: std::env::var_os("XDG_STATE_HOME"),
                localappdata: std::env::var_os("LOCALAPPDATA"),
            };

            // SAFETY: tests serialize environment mutation with a process-wide mutex.
            unsafe {
                std::env::set_var("HOME", home);
                std::env::set_var("XDG_STATE_HOME", &xdg_state_home);
                std::env::set_var("LOCALAPPDATA", &localappdata);
            }

            guard
        }
    }

    impl Drop for EnvGuard {
        fn drop(&mut self) {
            // SAFETY: tests serialize environment mutation with a process-wide mutex.
            unsafe {
                match &self.home {
                    Some(value) => std::env::set_var("HOME", value),
                    None => std::env::remove_var("HOME"),
                }
                match &self.xdg_state_home {
                    Some(value) => std::env::set_var("XDG_STATE_HOME", value),
                    None => std::env::remove_var("XDG_STATE_HOME"),
                }
                match &self.localappdata {
                    Some(value) => std::env::set_var("LOCALAPPDATA", value),
                    None => std::env::remove_var("LOCALAPPDATA"),
                }
            }
        }
    }

    struct TestApiServer {
        url: String,
        actions: Arc<Mutex<Vec<String>>>,
        handle: Option<thread::JoinHandle<()>>,
    }

    impl TestApiServer {
        fn start(tsid: String, user_info: Value) -> Self {
            let listener = TcpListener::bind("127.0.0.1:0")
                .expect("test should be able to bind a local API stub");
            listener
                .set_nonblocking(true)
                .expect("test API listener should accept nonblocking mode");
            let url = format!(
                "http://{}/cs",
                listener.local_addr().expect("listener address")
            );
            let actions = Arc::new(Mutex::new(Vec::new()));
            let recorded_actions = Arc::clone(&actions);
            let user_info = user_info.to_string();

            let handle = thread::spawn(move || {
                let mut handled_requests = 0usize;
                let mut idle_deadline = Instant::now() + Duration::from_secs(2);

                loop {
                    match listener.accept() {
                        Ok((mut stream, _)) => {
                            stream
                                .set_nonblocking(false)
                                .expect("accepted test API stream should switch to blocking mode");
                            idle_deadline = Instant::now() + Duration::from_secs(2);
                            let body = read_request_body(&mut stream);
                            let request: Value = serde_json::from_slice(&body)
                                .expect("test API stub should receive valid JSON");
                            let action = request
                                .as_array()
                                .and_then(|items| items.first())
                                .and_then(|item| item.get("a"))
                                .and_then(Value::as_str)
                                .expect("test API stub request should include an action")
                                .to_string();
                            recorded_actions
                                .lock()
                                .expect("recorded actions mutex should not be poisoned")
                                .push(action.clone());
                            handled_requests += 1;

                            let response_body = match action.as_str() {
                                "us" => format!(r#"[{{"tsid":"{tsid}"}}]"#),
                                "ug" => format!("[{user_info}]"),
                                "uga" => "[-9]".to_string(),
                                other => panic!("unexpected API action in auth test: {other}"),
                            };
                            write_response(&mut stream, &response_body);

                            if handled_requests >= 3 {
                                break;
                            }
                        }
                        Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
                            if Instant::now() >= idle_deadline {
                                assert_eq!(
                                    handled_requests, 3,
                                    "auth test API stub expected 3 requests but saw {handled_requests}"
                                );
                                break;
                            }
                            thread::sleep(Duration::from_millis(10));
                        }
                        Err(err) => panic!("auth test API stub accept failed: {err}"),
                    }
                }
            });

            Self {
                url,
                actions,
                handle: Some(handle),
            }
        }

        fn actions(&self) -> Vec<String> {
            self.actions
                .lock()
                .expect("recorded actions mutex should not be poisoned")
                .clone()
        }
    }

    impl Drop for TestApiServer {
        fn drop(&mut self) {
            if let Some(handle) = self.handle.take() {
                handle
                    .join()
                    .expect("auth test API stub thread should finish cleanly");
            }
        }
    }

    fn read_request_body(stream: &mut TcpStream) -> Vec<u8> {
        let mut buffer = Vec::new();
        let mut chunk = [0u8; 1024];
        let header_end = loop {
            let read = stream
                .read(&mut chunk)
                .expect("test API stub should be able to read request bytes");
            assert!(read > 0, "test API stub received an empty HTTP request");
            buffer.extend_from_slice(&chunk[..read]);
            if let Some(end) = find_header_end(&buffer) {
                break end;
            }
        };

        let headers = std::str::from_utf8(&buffer[..header_end])
            .expect("test API stub request headers should be UTF-8");
        let content_length = headers
            .lines()
            .find_map(|line| {
                let (name, value) = line.split_once(':')?;
                if name.eq_ignore_ascii_case("content-length") {
                    Some(
                        value
                            .trim()
                            .parse::<usize>()
                            .expect("content-length header should be numeric"),
                    )
                } else {
                    None
                }
            })
            .unwrap_or(0);

        while buffer.len() < header_end + content_length {
            let read = stream
                .read(&mut chunk)
                .expect("test API stub should be able to read remaining body bytes");
            assert!(read > 0, "test API stub request body ended unexpectedly");
            buffer.extend_from_slice(&chunk[..read]);
        }

        buffer[header_end..header_end + content_length].to_vec()
    }

    fn find_header_end(buffer: &[u8]) -> Option<usize> {
        buffer
            .windows(4)
            .position(|window| window == b"\r\n\r\n")
            .map(|idx| idx + 4)
    }

    fn write_response(stream: &mut TcpStream, body: &str) {
        let response = format!(
            "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
            body.len(),
            body
        );
        stream
            .write_all(response.as_bytes())
            .expect("test API stub should be able to write the response");
        stream
            .flush()
            .expect("test API stub should flush the response");
    }

    fn test_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    fn make_session_blob(master_key: [u8; 16], sid_bytes: &[u8]) -> String {
        let mut blob = Vec::with_capacity(16 + sid_bytes.len());
        blob.extend_from_slice(&master_key);
        blob.extend_from_slice(sid_bytes);
        general_purpose::STANDARD.encode(blob)
    }

    fn make_valid_tsid(master_key: &[u8; 16]) -> String {
        let mut sid = vec![0u8; 43];
        sid[..16].copy_from_slice(b"0123456789ABCDEF");
        sid[16..27].copy_from_slice(b"session-mid");
        let mut challenge = [0u8; 16];
        challenge.copy_from_slice(&sid[..16]);
        let encrypted = aes128_ecb_encrypt_block(&challenge, master_key);
        sid[27..].copy_from_slice(&encrypted);
        base64url_encode(&sid)
    }

    fn default_persistence_root_for_home(home: &Path) -> PathBuf {
        #[cfg(target_os = "windows")]
        {
            return home.join("AppData").join("Local").join("megalib");
        }

        #[cfg(target_os = "macos")]
        {
            home.join("Library")
                .join("Application Support")
                .join("megalib")
        }

        #[cfg(not(any(target_os = "windows", target_os = "macos")))]
        {
            home.join("xdg-state").join("megalib")
        }
    }

    #[test]
    fn load_restores_tree_cache_state_through_authenticated_startup_path() {
        let _lock = test_lock()
            .lock()
            .expect("auth test lock should not be poisoned");
        let dir = TestDir::new("startup-restore");
        let _env = EnvGuard::new(dir.path());
        let root = default_persistence_root_for_home(dir.path());
        let master_key = [0x41; 16];
        let session_blob = make_session_blob(master_key, b"sid-bytes");
        let tsid = make_valid_tsid(&master_key);
        let session_file = dir.path().join("session.txt");

        let mut persisted = Session::test_dummy().install_persistence_runtime_at_for_tests(root);
        persisted.user_handle = "account-handle".to_string();
        persisted.master_key = master_key;
        persisted.scsn = Some("persisted-scsn".to_string());
        persisted.user_alert_lsn = Some("persisted-lsn".to_string());
        persisted.user_alerts = vec![json!({"id": "persisted-alert"})];
        persisted.alerts_catchup_pending = true;
        persisted.nodes = vec![
            crate::fs::Node {
                name: "Root".to_string(),
                handle: "root".to_string(),
                parent_handle: None,
                node_type: crate::fs::NodeType::Root,
                size: 0,
                timestamp: 0,
                key: Vec::new(),
                path: Some("/Root".to_string()),
                link: None,
                file_attr: None,
                share_key: None,
                share_handle: None,
                is_inshare: false,
                is_outshare: false,
                share_access: None,
            },
            crate::fs::Node {
                name: "Documents".to_string(),
                handle: "docs".to_string(),
                parent_handle: Some("root".to_string()),
                node_type: crate::fs::NodeType::Folder,
                size: 0,
                timestamp: 1,
                key: vec![0x11; 16],
                path: Some("/Root/Documents".to_string()),
                link: None,
                file_attr: None,
                share_key: None,
                share_handle: None,
                is_inshare: false,
                is_outshare: true,
                share_access: None,
            },
        ];
        persisted.pending_nodes = vec![json!({"h": "pending", "p": "docs", "t": 0})];
        persisted.outshares.insert(
            "docs".to_string(),
            std::collections::HashSet::from(["EXP".to_string()]),
        );
        persisted.pending_outshares.insert(
            "docs".to_string(),
            std::collections::HashSet::from(["pending-user".to_string()]),
        );
        persisted
            .persist_tree_cache_state()
            .expect("test should seed a disk-backed tree/cache snapshot");

        fs::write(&session_file, &session_blob).expect("test should write the session blob file");

        let server = TestApiServer::start(
            tsid,
            json!({
                "u": "account-handle",
                "email": "restored@example.com",
                "name": "Restored User",
                "sn": "fresh-server-scsn"
            }),
        );
        ApiClient::set_test_api_url_override(Some(server.url.clone()));

        let runtime = tokio::runtime::Runtime::new().expect("auth test runtime should build");
        let loaded = runtime
            .block_on(Session::load(&session_file))
            .expect("startup load should succeed")
            .expect("startup load should return a session");

        ApiClient::set_test_api_url_override(None);

        assert_eq!(loaded.scsn.as_deref(), Some("persisted-scsn"));
        assert_eq!(loaded.user_alert_lsn.as_deref(), Some("persisted-lsn"));
        assert_eq!(loaded.user_alerts.len(), 1);
        assert_eq!(loaded.nodes.len(), 2);
        assert_eq!(
            loaded
                .nodes
                .iter()
                .find(|node| node.handle == "docs")
                .and_then(crate::fs::Node::path),
            Some("/Root/Documents")
        );
        assert_eq!(loaded.pending_nodes.len(), 1);
        assert_eq!(
            loaded.outshares.get("docs"),
            Some(&std::collections::HashSet::from(["EXP".to_string()]))
        );
        assert_eq!(
            loaded.pending_outshares.get("docs"),
            Some(&std::collections::HashSet::from([
                "pending-user".to_string()
            ]))
        );
        assert_eq!(server.actions(), vec!["us", "ug", "uga"]);
    }
}
