rmcl 0.4.0

A fully featured Minecraft TUI launcher
use super::*;

#[test]
fn migration_backs_up_and_renames_instance_directories() {
    let temp = tempfile::tempdir().unwrap();
    let instances = temp.path().join("instances");
    let meta = temp.path().join("meta");
    let config = temp.path().join("config.toml");
    let instance = instances.join("Example");
    fs::create_dir_all(instance.join(".minecraft/saves/world")).unwrap();
    fs::create_dir_all(instance.join(".rmcl/config-sync/local-config/config")).unwrap();
    fs::write(instance.join(".minecraft/saves/world/level.dat"), b"world").unwrap();
    fs::write(
        instance.join(".rmcl/config-sync/local-config/options.txt"),
        b"options",
    )
    .unwrap();
    fs::write(&config, b"[paths]").unwrap();
    fs::create_dir_all(meta.join("config-sync/profiles/main")).unwrap();
    fs::write(
        meta.join("config-sync/profiles/main/options.txt"),
        b"profile",
    )
    .unwrap();

    let mut progress = Vec::new();
    let backup = run(&instances, &meta, &config, |update| progress.push(update)).unwrap();

    assert_eq!(
        fs::read(instance.join("minecraft/saves/world/level.dat")).unwrap(),
        b"world"
    );
    assert_eq!(
        fs::read(instance.join("rmcl/content/config/options.txt")).unwrap(),
        b"options"
    );
    assert!(backup.join("instances/Example/.minecraft").exists());
    assert!(backup.join("profiles/legacy/main/options.txt").exists());
    assert!(
        backup
            .file_name()
            .unwrap()
            .to_string_lossy()
            .starts_with("backup-")
    );
    assert!(!meta.join("config-sync").exists());
    assert!(progress.iter().any(|update| {
        update.item_total.is_some_and(|total| total > 0)
            && update.item_current.is_some_and(|current| current > 0)
    }));
    assert_eq!(
        marker_version(&MetadataPaths::new(&meta).layout_marker()),
        Some(LAYOUT_VERSION)
    );
    assert!(cache_rebuild_pending(&meta));
    assert_eq!(run(&instances, &meta, &config, |_| {}).unwrap(), backup);
    finish_cache_rebuild(&meta).unwrap();
    assert!(!cache_rebuild_pending(&meta));
}

#[test]
fn migration_resumes_from_the_recorded_journal() {
    let temp = tempfile::tempdir().unwrap();
    let instances = temp.path().join("instances");
    let meta = temp.path().join("meta");
    let config = temp.path().join("config.toml");
    let done = instances.join("Done");
    let pending = instances.join("Pending");
    fs::create_dir_all(done.join("minecraft")).unwrap();
    fs::create_dir_all(pending.join(".minecraft/saves")).unwrap();

    let metadata = MetadataPaths::new(&meta);
    fs::create_dir_all(metadata.state()).unwrap();
    fs::create_dir_all(metadata.backups()).unwrap();
    let backup = metadata.backups().join("backup-resume");
    fs::create_dir_all(&backup).unwrap();
    write_json_atomic(
        &metadata.migration_journal(),
        &MigrationJournal {
            version: LAYOUT_VERSION,
            backup_dir: backup.clone(),
            completed: vec!["backup".to_owned(), "instance:Done".to_owned()],
        },
    )
    .unwrap();

    assert_eq!(run(&instances, &meta, &config, |_| {}).unwrap(), backup);
    assert!(done.join("minecraft").exists());
    assert!(pending.join("minecraft/saves").exists());
    assert!(!metadata.migration_journal().exists());
}

#[test]
fn stale_partial_backup_is_replaced() {
    let temp = tempfile::tempdir().unwrap();
    let instances = temp.path().join("instances");
    let backup = temp.path().join("backups/backup-test");
    let partial = backup.with_extension("partial");
    fs::create_dir_all(instances.join("Example")).unwrap();
    fs::write(instances.join("Example/data.txt"), b"current").unwrap();
    fs::create_dir_all(&partial).unwrap();
    fs::write(partial.join("stale.txt"), b"stale").unwrap();

    backup_user_data(
        &instances,
        &temp.path().join("missing-config.toml"),
        &temp.path().join("meta"),
        &backup,
        |_, _, _| {},
    )
    .unwrap();

    assert_eq!(
        fs::read(backup.join("instances/Example/data.txt")).unwrap(),
        b"current"
    );
    assert!(!backup.join("stale.txt").exists());
    assert!(!partial.exists());
}

#[test]
fn backup_rejects_overlap_and_insufficient_space() {
    let temp = tempfile::tempdir().unwrap();
    let instances = temp.path().join("instances");
    fs::create_dir_all(&instances).unwrap();

    let overlap =
        validate_backup_destination(&instances, &instances.join("backup"), 0).unwrap_err();
    assert!(matches!(overlap, MigrationError::BackupOverlap(_)));

    let outside = temp.path().join("backups/backup");
    let insufficient = validate_backup_destination(&instances, &outside, u64::MAX).unwrap_err();
    assert!(matches!(
        insufficient,
        MigrationError::InsufficientSpace { .. }
    ));
}

#[test]
fn migration_need_follows_legacy_data_marker_and_pending_rebuild() {
    let temp = tempfile::tempdir().unwrap();
    let instances = temp.path().join("instances");
    let meta = temp.path().join("meta");
    fs::create_dir_all(instances.join("Example/.minecraft")).unwrap();

    assert!(is_needed(&instances, &meta));
    initialize_new_layout(&meta).unwrap();
    assert!(!is_needed(&instances, &meta));
    fs::write(MetadataPaths::new(&meta).layout_marker(), b"invalid marker").unwrap();
    assert!(is_needed(&instances, &meta));
    initialize_new_layout(&meta).unwrap();
    fs::write(
        MetadataPaths::new(&meta).cache_rebuild_pending(),
        LAYOUT_VERSION.to_string(),
    )
    .unwrap();
    assert!(is_needed(&instances, &meta));
}

#[cfg(unix)]
#[test]
fn backup_preserves_symlinks() {
    let temp = tempfile::tempdir().unwrap();
    let source = temp.path().join("source");
    let destination = temp.path().join("destination");
    fs::create_dir_all(&source).unwrap();
    fs::write(source.join("target.txt"), b"target").unwrap();
    std::os::unix::fs::symlink("target.txt", source.join("link.txt")).unwrap();

    copy_dir_recursive_with_progress(&source, &destination, &mut |_, _| {}).unwrap();

    assert_eq!(
        fs::read_link(destination.join("link.txt")).unwrap(),
        PathBuf::from("target.txt")
    );
}

#[test]
fn conflicting_visible_and_hidden_directories_are_not_merged() {
    let temp = tempfile::tempdir().unwrap();
    let instance = temp.path().join("Example");
    fs::create_dir_all(instance.join(".minecraft")).unwrap();
    fs::create_dir_all(instance.join("minecraft")).unwrap();
    let error = migrate_instance(&instance, "Example").unwrap_err();
    assert!(matches!(error, MigrationError::PathConflict { .. }));
}

#[test]
fn merge_conflicts_do_not_overwrite_existing_profiles() {
    let temp = tempfile::tempdir().unwrap();
    let source = temp.path().join("legacy");
    let destination = temp.path().join("current");
    fs::create_dir_all(source.join("nested")).unwrap();
    fs::create_dir_all(destination.join("nested")).unwrap();
    fs::write(source.join("nested/options.txt"), b"legacy").unwrap();
    fs::write(destination.join("nested/options.txt"), b"current").unwrap();

    let error = move_or_merge(&source, &destination).unwrap_err();

    assert!(matches!(error, MigrationError::MergeConflict { .. }));
    assert_eq!(
        fs::read(destination.join("nested/options.txt")).unwrap(),
        b"current"
    );
    assert_eq!(
        fs::read(source.join("nested/options.txt")).unwrap(),
        b"legacy"
    );
}