osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use std::sync::Mutex;

use super::{
    ClipboardError, ClipboardService,
    backend::{
        ClipboardCommand, OSC52_MAX_BYTES_DEFAULT, base64_encode, base64_encoded_len,
        copy_via_command, osc52_enabled, osc52_max_bytes, osc52_payload, platform_backends,
    },
};

fn env_lock() -> &'static Mutex<()> {
    crate::tests::env_lock()
}

fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
    env_lock()
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

fn set_path_for_test(value: Option<&str>) {
    let key = "PATH";
    match value {
        Some(value) => unsafe { std::env::set_var(key, value) },
        None => unsafe { std::env::remove_var(key) },
    }
}

fn set_env_for_test(key: &str, value: Option<&str>) {
    match value {
        Some(value) => unsafe { std::env::set_var(key, value) },
        None => unsafe { std::env::remove_var(key) },
    }
}

#[test]
fn base64_encoder_matches_known_values() {
    assert_eq!(base64_encode(b""), "");
    assert_eq!(base64_encode(b"f"), "Zg==");
    assert_eq!(base64_encode(b"fo"), "Zm8=");
    assert_eq!(base64_encode(b"foo"), "Zm9v");
    assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
}

#[test]
fn base64_length_and_env_helpers_behave_predictably() {
    let _guard = acquire_env_lock();
    assert_eq!(base64_encoded_len(0), 0);
    assert_eq!(base64_encoded_len(1), 4);
    assert_eq!(base64_encoded_len(4), 8);

    let osc52_original = std::env::var("OSC52").ok();
    let max_original = std::env::var("OSC52_MAX_BYTES").ok();

    set_env_for_test("OSC52", Some("off"));
    assert!(!osc52_enabled());
    set_env_for_test("OSC52", Some("yes"));
    assert!(osc52_enabled());

    set_env_for_test("OSC52_MAX_BYTES", Some("4096"));
    assert_eq!(osc52_max_bytes(), 4096);
    set_env_for_test("OSC52_MAX_BYTES", Some("0"));
    assert_eq!(osc52_max_bytes(), OSC52_MAX_BYTES_DEFAULT);

    set_env_for_test("OSC52", osc52_original.as_deref());
    set_env_for_test("OSC52_MAX_BYTES", max_original.as_deref());
}

#[test]
fn clipboard_error_display_covers_backend_spawn_and_status_cases() {
    assert_eq!(
        ClipboardError::NoBackendAvailable {
            attempts: vec!["osc52".to_string(), "xclip".to_string()],
        }
        .to_string(),
        "no clipboard backend available (tried: osc52, xclip)"
    );
    assert_eq!(
        ClipboardError::SpawnFailed {
            command: "xclip".to_string(),
            reason: "missing".to_string(),
        }
        .to_string(),
        "failed to start clipboard command `xclip`: missing"
    );
    assert_eq!(
        ClipboardError::CommandFailed {
            command: "xclip".to_string(),
            status: 7,
            stderr: "no display".to_string(),
        }
        .to_string(),
        "clipboard command `xclip` failed with status 7: no display"
    );
    assert_eq!(
        ClipboardError::Io("broken pipe".to_string()).to_string(),
        "clipboard I/O error: broken pipe"
    );
}

#[test]
fn command_backend_reports_success_and_failure() {
    let _guard = acquire_env_lock();
    copy_via_command(
        ClipboardCommand {
            command: "/bin/sh",
            args: &["-c", "cat >/dev/null"],
        },
        "hello",
    )
    .expect("shell sink should succeed");

    let err = copy_via_command(
        ClipboardCommand {
            command: "/bin/sh",
            args: &["-c", "echo nope >&2; exit 7"],
        },
        "hello",
    )
    .expect_err("non-zero clipboard command should fail");

    assert!(matches!(
        err,
        ClipboardError::CommandFailed {
            status: 7,
            ref stderr,
            ..
        } if stderr.contains("nope")
    ));
}

#[test]
fn platform_backends_prefers_wayland_when_present() {
    let _guard = acquire_env_lock();
    let original = std::env::var("WAYLAND_DISPLAY").ok();
    set_env_for_test("WAYLAND_DISPLAY", Some("wayland-0"));
    let backends = platform_backends();
    set_env_for_test("WAYLAND_DISPLAY", original.as_deref());

    if cfg!(target_os = "windows") || cfg!(target_os = "macos") {
        assert!(!backends.is_empty());
    } else {
        assert_eq!(backends[0].command, "wl-copy");
    }
}

#[test]
fn copy_without_osc52_reports_no_backend_when_path_is_empty() {
    let _guard = acquire_env_lock();
    let key = "PATH";
    let original = std::env::var(key).ok();
    set_path_for_test(Some(""));

    let service = ClipboardService::new().with_osc52(false);
    let result = service.copy_text("hello");

    if let Some(value) = original {
        set_path_for_test(Some(&value));
    } else {
        set_path_for_test(None);
    }

    match result {
        Err(ClipboardError::NoBackendAvailable { attempts }) => {
            assert!(!attempts.is_empty());
        }
        Err(ClipboardError::SpawnFailed { .. }) => {}
        other => panic!("unexpected result: {other:?}"),
    }
}

#[test]
fn command_backend_reports_spawn_failure_for_missing_binary() {
    let err = copy_via_command(
        ClipboardCommand {
            command: "/definitely/missing/clipboard-bin",
            args: &[],
        },
        "hello",
    )
    .expect_err("missing binary should fail to spawn");
    assert!(matches!(err, ClipboardError::SpawnFailed { .. }));
}

#[test]
fn platform_backends_include_x11_fallbacks_without_wayland() {
    let _guard = acquire_env_lock();
    let original = std::env::var("WAYLAND_DISPLAY").ok();
    set_env_for_test("WAYLAND_DISPLAY", None);
    let backends = platform_backends();
    set_env_for_test("WAYLAND_DISPLAY", original.as_deref());

    if !(cfg!(target_os = "windows") || cfg!(target_os = "macos")) {
        let names = backends
            .iter()
            .map(|backend| backend.command)
            .collect::<Vec<_>>();
        assert!(names.contains(&"xclip"));
        assert!(names.contains(&"xsel"));
    }
}

#[test]
fn command_failure_without_stderr_uses_short_display() {
    let err = ClipboardError::CommandFailed {
        command: "xclip".to_string(),
        status: 9,
        stderr: String::new(),
    };
    assert_eq!(
        err.to_string(),
        "clipboard command `xclip` failed with status 9"
    );
}

#[test]
fn osc52_helpers_respect_env_toggles_and_defaults() {
    let _guard = acquire_env_lock();
    let original_enabled = std::env::var("OSC52").ok();
    let original_max = std::env::var("OSC52_MAX_BYTES").ok();

    set_env_for_test("OSC52", Some("off"));
    assert!(!osc52_enabled());
    set_env_for_test("OSC52", Some("FALSE"));
    assert!(!osc52_enabled());
    set_env_for_test("OSC52", None);
    assert!(osc52_enabled());

    set_env_for_test("OSC52_MAX_BYTES", Some("2048"));
    assert_eq!(osc52_max_bytes(), 2048);
    set_env_for_test("OSC52_MAX_BYTES", Some("0"));
    assert_eq!(osc52_max_bytes(), OSC52_MAX_BYTES_DEFAULT);
    set_env_for_test("OSC52_MAX_BYTES", Some("wat"));
    assert_eq!(osc52_max_bytes(), OSC52_MAX_BYTES_DEFAULT);

    set_env_for_test("OSC52", original_enabled.as_deref());
    set_env_for_test("OSC52_MAX_BYTES", original_max.as_deref());
}

#[test]
fn clipboard_service_builders_toggle_osc52_preference() {
    let default = ClipboardService::new();
    assert!(default.prefer_osc52);

    let disabled = ClipboardService::new().with_osc52(false);
    assert!(!disabled.prefer_osc52);
}

#[test]
fn clipboard_plan_prefers_osc52_only_when_tty_and_payload_fit_unit() {
    let _guard = acquire_env_lock();
    let osc52_original = std::env::var("OSC52").ok();
    let max_original = std::env::var("OSC52_MAX_BYTES").ok();
    set_env_for_test("OSC52", Some("yes"));
    set_env_for_test("OSC52_MAX_BYTES", Some("128"));

    let service = ClipboardService::new();
    let tty_plan = service.plan_copy("hello", true);
    assert!(tty_plan.use_osc52);
    assert_eq!(tty_plan.attempts, vec!["osc52".to_string()]);

    let non_tty_plan = service.plan_copy("hello", false);
    assert!(!non_tty_plan.use_osc52);

    set_env_for_test("OSC52_MAX_BYTES", Some("4"));
    let oversized_plan = service.plan_copy("hello", true);
    assert!(!oversized_plan.use_osc52);
    assert!(oversized_plan.attempts[0].starts_with("osc52 (payload"));

    set_env_for_test("OSC52", osc52_original.as_deref());
    set_env_for_test("OSC52_MAX_BYTES", max_original.as_deref());
}

#[test]
fn clipboard_command_loop_reports_success_and_real_failures_unit() {
    let service = ClipboardService::new();
    service
        .copy_with_commands(
            "hello",
            Vec::new(),
            vec![ClipboardCommand {
                command: "/bin/sh",
                args: &["-c", "cat >/dev/null"],
            }],
        )
        .expect("shell sink should succeed");

    let err = service
        .copy_with_commands(
            "hello",
            vec!["osc52".to_string()],
            vec![ClipboardCommand {
                command: "/bin/sh",
                args: &["-c", "echo nope >&2; exit 7"],
            }],
        )
        .expect_err("non-zero clipboard command should fail");

    assert!(matches!(
        err,
        ClipboardError::CommandFailed {
            status: 7,
            ref stderr,
            ..
        } if stderr.contains("nope")
    ));
}

#[test]
fn osc52_payload_wraps_encoded_text_without_touching_stdout_unit() {
    assert_eq!(osc52_payload("ping"), "\u{1b}]52;c;cGluZw==\u{7}");
}