clync 0.4.1

Encrypted sync for Claude Code across machines
use super::*;
use crate::config::{
    EncryptionConfig, StorageConfig, SyncConfig, SyncTargets, default_lfs_threshold,
};

fn test_dir(name: &str) -> std::path::PathBuf {
    let dir = std::env::temp_dir()
        .join("clync-sync-test")
        .join(name)
        .join(format!("{}", std::process::id()));
    if dir.exists() {
        std::fs::remove_dir_all(&dir).ok();
    }
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

fn make_config(encrypted: bool) -> Config {
    Config {
        sync: SyncConfig {
            claude_dir: std::path::PathBuf::from("/tmp/fake-claude"),
            include_companion_dirs: false,
            clone_base: None,
            storage: StorageConfig::Git {
                path: std::path::PathBuf::from("/tmp/fake-store"),
                auto_push: false,
                lfs_threshold: default_lfs_threshold(),
            },
        },
        encryption: if encrypted {
            EncryptionConfig::KeyFile {
                path: std::path::PathBuf::from("/tmp/fake-key"),
            }
        } else {
            EncryptionConfig::None
        },
        targets: SyncTargets::default(),
    }
}

#[test]
fn session_filename_encrypted() {
    assert_eq!(session_filename("abc-123", true), "abc-123.jsonl.age");
}

#[test]
fn session_filename_unencrypted() {
    assert_eq!(session_filename("abc-123", false), "abc-123.jsonl");
}

#[test]
fn manifest_filename_encrypted() {
    let config = make_config(true);
    assert_eq!(manifest_filename(&config), "manifest.json.age");
}

#[test]
fn manifest_filename_unencrypted() {
    let config = make_config(false);
    assert_eq!(manifest_filename(&config), "manifest.json");
}

#[test]
fn is_encrypted_check() {
    assert!(is_encrypted(&make_config(true)));
    assert!(!is_encrypted(&make_config(false)));
}

#[test]
fn tar_untar_roundtrip() {
    let dir = test_dir("tar_roundtrip");
    let src = dir.join("source");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("file1.txt"), "hello").unwrap();
    std::fs::write(src.join("file2.txt"), "world").unwrap();

    let tar_data = tar_directory(&src).unwrap();
    assert!(!tar_data.is_empty());

    let dst = dir.join("restored").join("source");
    untar_directory(&tar_data, &dst).unwrap();

    assert_eq!(
        std::fs::read_to_string(dst.join("file1.txt")).unwrap(),
        "hello"
    );
    assert_eq!(
        std::fs::read_to_string(dst.join("file2.txt")).unwrap(),
        "world"
    );

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn tar_untar_nested_dirs() {
    let dir = test_dir("tar_nested");
    let src = dir.join("nested");
    std::fs::create_dir_all(src.join("sub")).unwrap();
    std::fs::write(src.join("sub").join("deep.txt"), "deep content").unwrap();

    let tar_data = tar_directory(&src).unwrap();
    let dst = dir.join("out").join("nested");
    untar_directory(&tar_data, &dst).unwrap();

    assert_eq!(
        std::fs::read_to_string(dst.join("sub").join("deep.txt")).unwrap(),
        "deep content"
    );

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn tar_untar_overwrites_existing() {
    let dir = test_dir("tar_overwrite");
    let src = dir.join("src_dir");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("file.txt"), "new content").unwrap();

    let tar_data = tar_directory(&src).unwrap();

    let dst = dir.join("dst").join("src_dir");
    std::fs::create_dir_all(&dst).unwrap();
    std::fs::write(dst.join("file.txt"), "old content").unwrap();

    untar_directory(&tar_data, &dst).unwrap();
    assert_eq!(
        std::fs::read_to_string(dst.join("file.txt")).unwrap(),
        "new content"
    );

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn push_session_writes_to_store() {
    let dir = test_dir("push_session_basic");
    let store_dir = dir.join("store");
    std::fs::create_dir_all(&store_dir).unwrap();
    let store = crate::store::folder::FolderStore::new(store_dir);

    let jsonl_path = dir.join("test-session.jsonl");
    std::fs::write(&jsonl_path, b"session content").unwrap();

    let session = LocalSession {
        uuid: "abc-123".into(),
        project_dir_name: "project-a".into(),
        jsonl_path,
        companion_dir: None,
        entry: crate::manifest::SessionEntry {
            uuid: "abc-123".into(),
            project_path: "project-a".into(),
            mtime: 0,
            size: 15,
            content_hash: 12345,
            has_companion: false,
            last_pushed_by: "test".into(),
            remote_url: None,
        },
    };

    let cipher = crate::crypto::Cipher::Plaintext;
    push_session(&session, &cipher, &store, false, false).unwrap();

    assert!(store.exists("sessions/abc-123.jsonl"));
    assert_eq!(
        store.read_file("sessions/abc-123.jsonl").unwrap(),
        b"session content"
    );

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn push_session_with_companion_dir() {
    let dir = test_dir("push_session_companion");
    let store_dir = dir.join("store");
    std::fs::create_dir_all(&store_dir).unwrap();
    let store = crate::store::folder::FolderStore::new(store_dir);

    let jsonl_path = dir.join("sess-456.jsonl");
    std::fs::write(&jsonl_path, b"session data").unwrap();

    let companion = dir.join("sess-456");
    std::fs::create_dir_all(&companion).unwrap();
    std::fs::write(companion.join("tool-output.txt"), b"tool result").unwrap();

    let session = LocalSession {
        uuid: "sess-456".into(),
        project_dir_name: "project-b".into(),
        jsonl_path,
        companion_dir: Some(companion),
        entry: crate::manifest::SessionEntry {
            uuid: "sess-456".into(),
            project_path: "project-b".into(),
            mtime: 0,
            size: 12,
            content_hash: 99999,
            has_companion: true,
            last_pushed_by: "test".into(),
            remote_url: None,
        },
    };

    let cipher = crate::crypto::Cipher::Plaintext;
    push_session(&session, &cipher, &store, false, true).unwrap();

    assert!(store.exists("sessions/sess-456.jsonl"));
    assert!(store.exists("sessions/sess-456.dir.tar.gz"));

    let tar_data = store.read_file("sessions/sess-456.dir.tar.gz").unwrap();
    let restore_dir = dir.join("restored");
    std::fs::create_dir_all(&restore_dir).unwrap();
    untar_directory(&tar_data, &restore_dir.join("sess-456")).unwrap();
    assert!(
        restore_dir
            .join("sess-456")
            .join("tool-output.txt")
            .exists()
    );

    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn push_session_encrypted_uses_age_extension() {
    let dir = test_dir("push_session_enc");
    let store_dir = dir.join("store");
    std::fs::create_dir_all(&store_dir).unwrap();
    let store = crate::store::folder::FolderStore::new(store_dir);

    let jsonl_path = dir.join("enc-uuid.jsonl");
    std::fs::write(&jsonl_path, b"data").unwrap();

    let session = LocalSession {
        uuid: "enc-uuid".into(),
        project_dir_name: "proj".into(),
        jsonl_path,
        companion_dir: None,
        entry: crate::manifest::SessionEntry {
            uuid: "enc-uuid".into(),
            project_path: "proj".into(),
            mtime: 0,
            size: 4,
            content_hash: 0,
            has_companion: false,
            last_pushed_by: "test".into(),
            remote_url: None,
        },
    };

    let cipher = crate::crypto::Cipher::Plaintext;
    push_session(&session, &cipher, &store, true, false).unwrap();
    assert!(store.exists("sessions/enc-uuid.jsonl.age"));

    std::fs::remove_dir_all(&dir).ok();
}