rmcl 0.6.0

A fully featured Minecraft TUI launcher
// SPDX-FileCopyrightText: 2026 Constantin Bauer
// SPDX-License-Identifier: GPL-3.0-only

use super::*;

impl AccountStore {
    pub(crate) fn empty_for_test(path: PathBuf) -> Self {
        Self {
            accounts: Vec::new(),
            path,
            load_error: None,
        }
    }
}

#[test]
fn offline_uuid_has_valid_v3_shape() {
    let uuid = offline_uuid("Steve");
    let parts: Vec<&str> = uuid.split('-').collect();
    assert_eq!(parts.len(), 5, "UUID must have 5 dash-separated parts");
    assert_eq!(parts[0].len(), 8);
    assert_eq!(parts[1].len(), 4);
    assert_eq!(parts[2].len(), 4);
    assert_eq!(parts[3].len(), 4);
    assert_eq!(parts[4].len(), 12);
    assert!(parts[2].starts_with('3'));
    let first_nibble = u8::from_str_radix(&parts[3][..1], 16).unwrap();
    assert!((0x8..=0xb).contains(&first_nibble));
}

#[test]
fn offline_uuid_is_pinned_for_known_names() {
    assert_eq!(
        offline_uuid("Steve"),
        "7e0a3689-ed7c-347c-87cc-3689ed7c47cc"
    );
    assert_eq!(offline_uuid("Alex"), "a13d42a3-454f-3e39-a392-42a3454fe392");
}

#[test]
fn offline_uuid_different_for_different_names() {
    assert_ne!(offline_uuid("Steve"), offline_uuid("Alex"));
}

#[test]
fn create_offline_account_fields() {
    let acc = create_offline_account("TestPlayer");
    assert_eq!(acc.username, "TestPlayer");
    assert_eq!(acc.account_type, AccountType::Offline);
    assert!(!acc.active);
    assert!(acc.refresh_token.is_none());
    assert_eq!(acc.uuid, offline_uuid("TestPlayer"));
}

fn make_store(dir: &std::path::Path) -> AccountStore {
    AccountStore {
        accounts: Vec::new(),
        path: dir.join("accounts.json"),
        load_error: None,
    }
}

fn microsoft_account(name: &str) -> Account {
    Account {
        uuid: format!("00000000-0000-0000-0000-{:012}", name.len()),
        username: name.to_owned(),
        account_type: AccountType::Microsoft,
        active: false,
        refresh_token: Some("refresh".to_owned()),
        cached_mc_token: None,
        cached_mc_token_expires_at: None,
    }
}

#[test]
fn store_add_first_becomes_active() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    assert_eq!(store.accounts.len(), 1);
    assert!(store.accounts[0].active);
}

#[test]
fn store_add_second_stays_inactive() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.add(create_offline_account("Bob")).unwrap();
    assert_eq!(store.accounts.len(), 2);
    assert!(store.accounts[0].active);
    assert!(!store.accounts[1].active);
}

#[test]
fn store_add_duplicate_uuid_replaces() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    let mut dup = create_offline_account("Alice");
    dup.username = "AliceRenamed".to_owned();
    dup.uuid = store.accounts[0].uuid.clone();
    store.add(dup).unwrap();
    assert_eq!(store.accounts.len(), 1);
    assert_eq!(store.accounts[0].username, "AliceRenamed");
}

#[test]
fn store_active_account_none_when_empty() {
    let tmp = tempfile::tempdir().unwrap();
    let store = make_store(tmp.path());
    assert!(store.active_account().is_none());
}

#[test]
fn store_has_microsoft_account_when_one_exists() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Offline")).unwrap();
    assert!(!store.has_microsoft_account());

    store.add(microsoft_account("Owner")).unwrap();
    assert!(store.has_microsoft_account());
}

#[test]
fn store_active_account_returns_active() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.add(create_offline_account("Bob")).unwrap();
    let active = store.active_account().unwrap();
    assert_eq!(active.username, "Alice");
}

#[test]
fn store_set_active_changes_active() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.add(create_offline_account("Bob")).unwrap();
    store.set_active(1).unwrap();
    assert!(!store.accounts[0].active);
    assert!(store.accounts[1].active);
}

#[test]
fn store_set_active_out_of_bounds_keeps_selection() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.add(create_offline_account("Bob")).unwrap();
    store.set_active(99).unwrap();
    assert!(
        store.accounts[0].active,
        "existing selection must survive a bad index"
    );
    assert!(!store.accounts[1].active);
}

#[test]
fn store_add_replacing_active_keeps_active() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.add(create_offline_account("Bob")).unwrap();
    let mut dup = create_offline_account("Alice");
    dup.username = "AliceRenamed".to_owned();
    store.add(dup).unwrap();
    assert!(
        store.accounts.iter().any(|a| a.active),
        "re-adding the active account must not orphan the selection"
    );
    let active = store.active_account().unwrap();
    assert_eq!(active.username, "AliceRenamed");
}

#[test]
fn store_remove_activates_first_remaining() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.add(create_offline_account("Bob")).unwrap();
    store.remove(0).unwrap();
    assert_eq!(store.accounts.len(), 1);
    assert_eq!(store.accounts[0].username, "Bob");
    assert!(store.accounts[0].active);
}

#[test]
fn store_remove_out_of_bounds_noop() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.remove(5).unwrap();
    assert_eq!(store.accounts.len(), 1);
}

#[test]
fn store_save_and_reload() {
    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(create_offline_account("Alice")).unwrap();
    store.add(create_offline_account("Bob")).unwrap();
    store.save().unwrap();

    let reloaded = AccountStore {
        accounts: serde_json::from_str(
            &std::fs::read_to_string(tmp.path().join("accounts.json")).unwrap(),
        )
        .unwrap(),
        path: tmp.path().join("accounts.json"),
        load_error: None,
    };
    assert_eq!(reloaded.accounts.len(), 2);
    assert_eq!(reloaded.accounts[0].username, "Alice");
    assert!(reloaded.accounts[0].active);
}

#[cfg(unix)]
#[test]
fn account_file_remains_owner_only_after_replacement() {
    use std::os::unix::fs::PermissionsExt;

    let tmp = tempfile::tempdir().unwrap();
    let mut store = make_store(tmp.path());
    store.add(microsoft_account("Owner")).unwrap();
    let path = tmp.path().join("accounts.json");
    assert_eq!(
        std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
        0o600
    );

    std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
    store.add(create_offline_account("Player")).unwrap();
    assert_eq!(
        std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
        0o600
    );
}

#[test]
fn invalid_account_file_cannot_be_overwritten_by_a_new_account() {
    let tmp = tempfile::tempdir().unwrap();
    let path = tmp.path().join("accounts.json");
    std::fs::write(&path, b"{broken credentials").unwrap();
    let mut store = AccountStore::load_from(path.clone());
    assert!(store.check_loaded().is_err());

    assert!(store.add(microsoft_account("Owner")).is_err());
    assert!(store.accounts.is_empty());
    assert_eq!(std::fs::read(path).unwrap(), b"{broken credentials");
}

#[test]
fn stale_account_mutations_preserve_new_credentials_and_selection() {
    let tmp = tempfile::tempdir().unwrap();
    let mut first = make_store(tmp.path());
    first.add(microsoft_account("Owner")).unwrap();
    first.add(create_offline_account("Player")).unwrap();
    let mut stale = AccountStore::load_from(first.path.clone());
    let uuid = first.accounts[0].uuid.clone();
    first
        .update_credentials(
            &uuid,
            Some("new-refresh".to_owned()),
            "new-token",
            Some(12345),
        )
        .unwrap();
    first.add(create_offline_account("New")).unwrap();
    stale.set_active(1).unwrap();
    assert_eq!(stale.accounts.len(), 3);
    assert_eq!(
        stale.accounts[0].refresh_token.as_deref(),
        Some("new-refresh")
    );
    first
        .update_credentials(&uuid, None, "newer-token", Some(23456))
        .unwrap();
    assert_eq!(first.active_account().unwrap().username, "Player");
    assert_eq!(
        first.accounts[0].cached_mc_token.as_deref(),
        Some("newer-token")
    );
}