sklink 0.2.3

Install skills into platform directories via a local store and symlinks
use std::path::Path;
use std::process::Command;

fn bin_path() -> std::path::PathBuf {
    std::path::PathBuf::from(env!("CARGO_BIN_EXE_sklink"))
}

fn setup_temp_home() -> tempfile::TempDir {
    tempfile::tempdir().unwrap()
}

fn store_skill_dir(home: &Path, skill: &str) -> std::path::PathBuf {
    home.join(".config/sklink/skills").join(skill)
}

fn other_dir(home: &Path, name: &str) -> std::path::PathBuf {
    home.join("other").join(name)
}

#[test]
fn cli_output_creates_default_dir_and_symlinks_skill() {
    let home = setup_temp_home();
    let project = tempfile::tempdir().unwrap();
    let skill = "software-engineer";

    let stored = store_skill_dir(home.path(), skill);
    std::fs::create_dir_all(&stored).unwrap();
    std::fs::write(stored.join("SKILL.md"), "x").unwrap();

    let out = Command::new(bin_path())
        .env("HOME", home.path())
        .current_dir(project.path())
        .args(["-o", skill])
        .output()
        .unwrap();

    assert!(
        out.status.success(),
        "stdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );

    let link_path = project.path().join(".agent/skills").join(skill);
    let meta = std::fs::symlink_metadata(&link_path).unwrap();
    assert!(meta.file_type().is_symlink());
    let actual = std::fs::canonicalize(&link_path).unwrap();
    let expected = std::fs::canonicalize(&stored).unwrap();
    assert_eq!(actual, expected);
}

#[test]
fn cli_output_export_copies_instead_of_symlink() {
    let home = setup_temp_home();
    let project = tempfile::tempdir().unwrap();
    let skill = "software-engineer";

    let stored = store_skill_dir(home.path(), skill);
    std::fs::create_dir_all(&stored).unwrap();
    std::fs::write(stored.join("SKILL.md"), "x").unwrap();

    let out = Command::new(bin_path())
        .env("HOME", home.path())
        .current_dir(project.path())
        .args(["-o", skill, "--export"])
        .output()
        .unwrap();

    assert!(
        out.status.success(),
        "stdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );

    let path = project
        .path()
        .join(".agent/skills")
        .join(skill)
        .join("SKILL.md");
    let meta = std::fs::symlink_metadata(&path).unwrap();
    assert!(meta.is_file());
    assert!(!meta.file_type().is_symlink());
}

#[test]
fn cli_output_symlink_fails_when_dest_is_regular_file() {
    let home = setup_temp_home();
    let project = tempfile::tempdir().unwrap();
    let skill = "software-engineer";

    let stored = store_skill_dir(home.path(), skill);
    std::fs::create_dir_all(&stored).unwrap();

    let dest_dir = project.path().join(".agent/skills");
    std::fs::create_dir_all(&dest_dir).unwrap();
    std::fs::write(dest_dir.join(skill), "not a symlink").unwrap();

    let out = Command::new(bin_path())
        .env("HOME", home.path())
        .current_dir(project.path())
        .args(["-o", skill])
        .output()
        .unwrap();

    assert!(!out.status.success());
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(stderr.contains("path exists and is not a symlink"));
}

#[test]
fn cli_output_symlink_fails_when_dest_symlink_points_elsewhere() {
    let home = setup_temp_home();
    let project = tempfile::tempdir().unwrap();
    let skill = "software-engineer";

    let stored = store_skill_dir(home.path(), skill);
    std::fs::create_dir_all(&stored).unwrap();

    let dest_dir = project.path().join(".agent/skills");
    std::fs::create_dir_all(&dest_dir).unwrap();
    let other = other_dir(home.path(), "other-skill");
    std::fs::create_dir_all(&other).unwrap();
    std::os::unix::fs::symlink(&other, dest_dir.join(skill)).unwrap();

    let out = Command::new(bin_path())
        .env("HOME", home.path())
        .current_dir(project.path())
        .args(["-o", skill])
        .output()
        .unwrap();

    assert!(!out.status.success());
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(stderr.contains("symlink points to a different target"));
}

#[test]
fn cli_output_export_fails_when_dest_exists() {
    let home = setup_temp_home();
    let project = tempfile::tempdir().unwrap();
    let skill = "software-engineer";

    let stored = store_skill_dir(home.path(), skill);
    std::fs::create_dir_all(&stored).unwrap();

    let dest_dir = project.path().join(".agent/skills");
    std::fs::create_dir_all(dest_dir.join(skill)).unwrap();

    let out = Command::new(bin_path())
        .env("HOME", home.path())
        .current_dir(project.path())
        .args(["-o", skill, "--export"])
        .output()
        .unwrap();

    assert!(!out.status.success());
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(stderr.contains("output path already exists"));
}

#[test]
fn cli_output_respects_dir_option() {
    let home = setup_temp_home();
    let project = tempfile::tempdir().unwrap();
    let skill = "software-engineer";

    let stored = store_skill_dir(home.path(), skill);
    std::fs::create_dir_all(&stored).unwrap();
    std::fs::write(stored.join("SKILL.md"), "x").unwrap();

    let out = Command::new(bin_path())
        .env("HOME", home.path())
        .current_dir(project.path())
        .args(["-o", skill, "--dir", "custom/skills"])
        .output()
        .unwrap();

    assert!(
        out.status.success(),
        "stdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );

    let link_path = project.path().join("custom/skills").join(skill);
    let meta = std::fs::symlink_metadata(&link_path).unwrap();
    assert!(meta.file_type().is_symlink());
    let actual = std::fs::canonicalize(&link_path).unwrap();
    let expected = std::fs::canonicalize(&stored).unwrap();
    assert_eq!(actual, expected);
}

#[test]
fn cli_output_accepts_long_option() {
    let home = setup_temp_home();
    let project = tempfile::tempdir().unwrap();
    let skill = "software-engineer";

    let stored = store_skill_dir(home.path(), skill);
    std::fs::create_dir_all(&stored).unwrap();
    std::fs::write(stored.join("SKILL.md"), "x").unwrap();

    let out = Command::new(bin_path())
        .env("HOME", home.path())
        .current_dir(project.path())
        .args(["--output", skill])
        .output()
        .unwrap();

    assert!(
        out.status.success(),
        "stdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
}