ito-core 0.1.26

Core functionality and business logic for Ito
Documentation
use super::*;
use std::fs;
use tempfile::TempDir;

fn make_dirs() -> (TempDir, PathBuf, PathBuf) {
    let tmp = TempDir::new().expect("tempdir");
    let ito = tmp.path().join(".ito");
    let worktree_ito = tmp.path().join("worktree").join(".ito");
    fs::create_dir_all(&ito).unwrap();
    fs::create_dir_all(&worktree_ito).unwrap();
    (tmp, ito, worktree_ito)
}

#[test]
#[cfg(unix)]
fn create_dir_link_creates_symlink() {
    let tmp = TempDir::new().unwrap();
    let src = tmp.path().join("real_dir");
    let dst = tmp.path().join("link");
    fs::create_dir_all(&src).unwrap();

    create_dir_link(&src, &dst).expect("symlink creation should succeed");

    assert!(dst.exists(), "link path should resolve");
    let target = fs::read_link(&dst).expect("should be a symlink");
    assert_eq!(target, src);
}

#[test]
#[cfg(unix)]
fn create_dir_link_fails_when_dst_exists() {
    let tmp = TempDir::new().unwrap();
    let src = tmp.path().join("real_dir");
    let dst = tmp.path().join("existing");
    fs::create_dir_all(&src).unwrap();
    fs::create_dir_all(&dst).unwrap();

    let result = create_dir_link(&src, &dst);
    assert!(result.is_err(), "should fail when dst already exists");
}

#[test]
#[cfg(unix)]
fn wire_creates_symlinks_for_all_dirs() {
    let (_tmp, ito, worktree_ito) = make_dirs();

    wire_coordination_symlinks(&ito, &worktree_ito).expect("wire should succeed");

    for dir in COORDINATION_DIRS {
        let link = ito.join(dir);
        assert!(link.exists(), "link '{dir}' should exist");
        let target = fs::read_link(&link).expect("should be a symlink");
        assert_eq!(
            target,
            worktree_ito.join(dir),
            "link '{dir}' points at wrong target"
        );
    }
}

#[test]
#[cfg(unix)]
fn wire_is_idempotent() {
    let (_tmp, ito, worktree_ito) = make_dirs();

    wire_coordination_symlinks(&ito, &worktree_ito).expect("first wire");
    wire_coordination_symlinks(&ito, &worktree_ito).expect("second wire should be idempotent");

    for dir in COORDINATION_DIRS {
        let link = ito.join(dir);
        let target = fs::read_link(&link).expect("should still be a symlink");
        assert_eq!(target, worktree_ito.join(dir));
    }
}

#[test]
#[cfg(unix)]
fn wire_migrates_real_dir_content() {
    let (_tmp, ito, worktree_ito) = make_dirs();
    let changes_dir = ito.join("changes");
    fs::create_dir_all(&changes_dir).unwrap();
    let sentinel = changes_dir.join("sentinel.md");
    fs::write(&sentinel, "hello").unwrap();

    wire_coordination_symlinks(&ito, &worktree_ito).expect("wire should succeed");

    let link = ito.join("changes");
    assert!(fs::read_link(&link).is_ok(), "changes should be a symlink");
    let migrated = worktree_ito.join("changes").join("sentinel.md");
    assert!(migrated.exists(), "sentinel.md should be in the worktree");
    assert_eq!(fs::read_to_string(&migrated).unwrap(), "hello");
    let via_link = link.join("sentinel.md");
    assert!(
        via_link.exists(),
        "sentinel.md should be accessible via symlink"
    );
}

#[test]
#[cfg(unix)]
fn wire_handles_empty_real_dir() {
    let (_tmp, ito, worktree_ito) = make_dirs();
    let specs_dir = ito.join("specs");
    fs::create_dir_all(&specs_dir).unwrap();

    wire_coordination_symlinks(&ito, &worktree_ito).expect("wire should succeed");

    let link = ito.join("specs");
    assert!(fs::read_link(&link).is_ok(), "specs should be a symlink");
}

#[test]
fn gitignore_entries_added_when_missing() {
    let tmp = TempDir::new().unwrap();
    let project_root = tmp.path();

    update_gitignore_for_symlinks(project_root).expect("should succeed");

    let content = fs::read_to_string(project_root.join(".gitignore")).unwrap();
    for dir in COORDINATION_DIRS {
        assert!(content.contains(&format!(".ito/{dir}")));
    }
}

#[test]
fn gitignore_no_duplicates_on_second_call() {
    let tmp = TempDir::new().unwrap();
    let project_root = tmp.path();

    update_gitignore_for_symlinks(project_root).expect("first call");
    update_gitignore_for_symlinks(project_root).expect("second call");

    let content = fs::read_to_string(project_root.join(".gitignore")).unwrap();
    for dir in COORDINATION_DIRS {
        let entry = format!(".ito/{dir}");
        let count = content.lines().filter(|l| l.trim() == entry).count();
        assert_eq!(
            count, 1,
            ".ito/{dir} should appear exactly once, found {count}"
        );
    }
}

#[test]
fn gitignore_preserves_existing_content() {
    let tmp = TempDir::new().unwrap();
    let project_root = tmp.path();
    let gitignore_path = project_root.join(".gitignore");

    fs::write(&gitignore_path, "target/\n*.log\n").unwrap();

    update_gitignore_for_symlinks(project_root).expect("should succeed");

    let content = fs::read_to_string(&gitignore_path).unwrap();
    assert!(content.contains("target/"));
    assert!(content.contains("*.log"));
}

#[test]
fn gitignore_skips_already_present_entries() {
    let tmp = TempDir::new().unwrap();
    let project_root = tmp.path();
    let gitignore_path = project_root.join(".gitignore");

    fs::write(&gitignore_path, ".ito/changes\n.ito/specs\n").unwrap();

    update_gitignore_for_symlinks(project_root).expect("should succeed");

    let content = fs::read_to_string(&gitignore_path).unwrap();
    let changes_count = content
        .lines()
        .filter(|l| l.trim() == ".ito/changes")
        .count();
    let specs_count = content.lines().filter(|l| l.trim() == ".ito/specs").count();
    assert_eq!(changes_count, 1);
    assert_eq!(specs_count, 1);
    assert!(content.contains(".ito/modules"));
    assert!(content.contains(".ito/workflows"));
    assert!(content.contains(".ito/audit"));
}

#[test]
fn gitignore_created_when_absent() {
    let tmp = TempDir::new().unwrap();
    let project_root = tmp.path();

    assert!(!project_root.join(".gitignore").exists());
    update_gitignore_for_symlinks(project_root).expect("should succeed");
    assert!(project_root.join(".gitignore").exists());
}

#[test]
#[cfg(unix)]
fn remove_restores_real_dirs_with_content() {
    let (_tmp, ito, worktree_ito) = make_dirs();

    wire_coordination_symlinks(&ito, &worktree_ito).expect("wire");
    let via_link = ito.join("changes").join("task.md");
    fs::write(&via_link, "task content").unwrap();

    remove_coordination_symlinks(&ito, &worktree_ito).expect("remove");

    let changes = ito.join("changes");
    assert!(changes.is_dir(), "changes should be a real directory");
    assert!(
        fs::read_link(&changes).is_err(),
        "changes should not be a symlink"
    );
    let restored = changes.join("task.md");
    assert!(restored.exists(), "task.md should be restored");
    assert_eq!(fs::read_to_string(&restored).unwrap(), "task content");
}

#[test]
#[cfg(unix)]
fn remove_is_noop_for_real_dirs() {
    let (_tmp, ito, worktree_ito) = make_dirs();
    for dir in COORDINATION_DIRS {
        fs::create_dir_all(ito.join(dir)).unwrap();
    }

    remove_coordination_symlinks(&ito, &worktree_ito).expect("remove");

    for dir in COORDINATION_DIRS {
        let path = ito.join(dir);
        assert!(path.is_dir(), "{dir} should still be a real directory");
        assert!(
            fs::read_link(&path).is_err(),
            "{dir} should not be a symlink"
        );
    }
}

#[test]
#[cfg(unix)]
fn remove_is_noop_when_dirs_absent() {
    let (_tmp, ito, worktree_ito) = make_dirs();
    remove_coordination_symlinks(&ito, &worktree_ito).expect("remove on empty ito dir");
}

#[test]
fn health_embedded_returns_embedded() {
    let tmp = TempDir::new().unwrap();
    let ito = tmp.path().join(".ito");
    let worktree_ito = tmp.path().join("worktree").join(".ito");
    fs::create_dir_all(&ito).unwrap();

    let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Embedded);
    assert_eq!(status, CoordinationHealthStatus::Embedded);
}

#[test]
fn health_worktree_missing_when_dir_absent() {
    let tmp = TempDir::new().unwrap();
    let ito = tmp.path().join(".ito");
    let worktree_ito = tmp.path().join("nonexistent").join(".ito");
    fs::create_dir_all(&ito).unwrap();

    let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
    assert_eq!(
        status,
        CoordinationHealthStatus::WorktreeMissing {
            expected_path: worktree_ito.clone()
        }
    );
}

#[test]
#[cfg(unix)]
fn health_healthy_when_all_symlinks_correct() {
    let (_tmp, ito, worktree_ito) = make_dirs();

    wire_coordination_symlinks(&ito, &worktree_ito).expect("wire");

    let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);
    assert_eq!(status, CoordinationHealthStatus::Healthy);
}

#[test]
fn health_missing_link_is_not_wired() {
    let (_tmp, ito, worktree_ito) = make_dirs();

    let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);

    let CoordinationHealthStatus::NotWired { dirs } = status else {
        panic!("expected NotWired, got {status:?}");
    };
    assert!(dirs.contains(&ito.join("changes")));
}

#[test]
#[cfg(unix)]
fn health_broken_symlinks_when_target_missing() {
    let (_tmp, ito, worktree_ito) = make_dirs();

    wire_coordination_symlinks(&ito, &worktree_ito).expect("wire");
    let target = worktree_ito.join("changes");
    fs::remove_dir_all(&target).unwrap();

    let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);

    let CoordinationHealthStatus::BrokenSymlinks { broken } = status else {
        panic!("expected BrokenSymlinks, got {status:?}");
    };
    assert_eq!(broken.len(), 1);
    assert_eq!(broken[0].0, ito.join("changes"));
}

#[test]
#[cfg(unix)]
fn health_not_wired_when_real_dirs_present() {
    let (_tmp, ito, worktree_ito) = make_dirs();
    for dir in COORDINATION_DIRS {
        fs::create_dir_all(ito.join(dir)).unwrap();
    }

    let status = check_coordination_health(&ito, &worktree_ito, &CoordinationStorage::Worktree);

    let CoordinationHealthStatus::NotWired { dirs } = status else {
        panic!("expected NotWired, got {status:?}");
    };
    assert_eq!(dirs.len(), COORDINATION_DIRS.len());
}

#[test]
fn format_message_healthy_is_none() {
    assert!(format_health_message(&CoordinationHealthStatus::Healthy).is_none());
}

#[test]
fn format_message_embedded_is_none() {
    assert!(format_health_message(&CoordinationHealthStatus::Embedded).is_none());
}

#[test]
fn format_message_worktree_missing_contains_path_and_hint() {
    let path = PathBuf::from("/some/path/.ito");
    let msg = format_health_message(&CoordinationHealthStatus::WorktreeMissing {
        expected_path: path.clone(),
    })
    .expect("should produce a message");

    assert!(msg.contains(&path.display().to_string()));
    assert!(msg.contains("ito init"));
}

#[test]
fn format_message_broken_symlinks_contains_paths_and_hint() {
    let link = PathBuf::from("/project/.ito/changes");
    let target = PathBuf::from("../worktree/.ito/changes");
    let msg = format_health_message(&CoordinationHealthStatus::BrokenSymlinks {
        broken: vec![(link.clone(), target.clone())],
    })
    .expect("should produce a message");

    assert!(msg.contains(&link.display().to_string()));
    assert!(msg.contains(&target.display().to_string()));
    assert!(msg.contains("ito init"));
}

#[test]
fn format_message_not_wired_contains_dir_and_hint() {
    let dir = PathBuf::from("/project/.ito/specs");
    let msg = format_health_message(&CoordinationHealthStatus::NotWired {
        dirs: vec![dir.clone()],
    })
    .expect("should produce a message");

    assert!(msg.contains(&dir.display().to_string()));
    assert!(msg.contains("ito init"));
}