biovault 0.1.30

A bioinformatics data vault CLI tool
Documentation
use crate::config::Config;
use crate::error::Error;
use anyhow::Context;

pub async fn show() -> crate::error::Result<()> {
    let config = Config::load()?;
    let config_path = Config::get_config_path()?;

    println!("BioVault Configuration");
    println!("----------------------");
    println!("Config file: {}", config_path.display());
    println!("Email: {}", config.email);

    if let Some(ref syftbox_path) = config.syftbox_config {
        println!("SyftBox config: {}", syftbox_path);
    } else {
        println!("SyftBox config: ~/.syftbox/config.json (default)");
    }

    match config.get_syftbox_data_dir() {
        Ok(data_dir) => println!("SyftBox data dir: {}", data_dir.display()),
        Err(e) => println!("SyftBox data dir: <error: {}>", e),
    }

    Ok(())
}

pub async fn set_email(email: String) -> crate::error::Result<()> {
    let mut config = Config::load()?;
    let config_path = Config::get_config_path()?;

    config.email = email.clone();
    config.save(&config_path)?;

    println!("✓ Email updated to: {}", email);
    Ok(())
}

pub async fn set_syftbox(path: Option<String>) -> crate::error::Result<()> {
    let mut config = Config::load()?;
    let config_path = Config::get_config_path()?;

    if let Some(path) = path {
        let syftbox_path = std::path::PathBuf::from(&path);
        if !syftbox_path.exists() {
            return Err(Error::SyftBoxConfigMissing(
                syftbox_path.display().to_string(),
            ));
        }

        let content = std::fs::read_to_string(&syftbox_path).with_context(|| {
            format!("Failed to read SyftBox config: {}", syftbox_path.display())
        })?;

        let _: serde_json::Value = serde_json::from_str(&content).with_context(|| {
            format!("Invalid JSON in SyftBox config: {}", syftbox_path.display())
        })?;

        config.syftbox_config = Some(path.clone());
        config.save(&config_path)?;

        println!("✓ SyftBox config path updated to: {}", path);
    } else {
        config.syftbox_config = None;
        config.save(&config_path)?;

        println!("✓ SyftBox config path reset to default: ~/.syftbox/config.json");
    }

    match config.get_syftbox_data_dir() {
        Ok(data_dir) => println!("  Data directory: {}", data_dir.display()),
        Err(e) => println!("  Warning: {}", e),
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[tokio::test]
    #[serial_test::serial]
    async fn test_config_operations() -> crate::error::Result<()> {
        let temp_dir = TempDir::new()?;
        crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));

        let config_dir = temp_dir.path().join(".biovault");
        fs::create_dir_all(&config_dir)?;

        let initial_config = Config {
            email: "initial@example.com".to_string(),
            syftbox_config: None,
            version: None,
        };
        initial_config.save(config_dir.join("config.yaml"))?;

        set_email("new@example.com".to_string()).await?;

        let updated_config = Config::load()?;
        assert_eq!(updated_config.email, "new@example.com");

        let syftbox_dir = temp_dir.path().join(".syftbox");
        fs::create_dir_all(&syftbox_dir)?;
        let syftbox_config_path = syftbox_dir.join("config.json");

        let syftbox_data = serde_json::json!({
            "data_dir": "/test/data"
        });
        fs::write(
            &syftbox_config_path,
            serde_json::to_string_pretty(&syftbox_data)?,
        )?;

        set_syftbox(Some(syftbox_config_path.to_string_lossy().to_string())).await?;

        let final_config = Config::load()?;
        assert!(final_config.syftbox_config.is_some());

        crate::config::clear_test_biovault_home();
        Ok(())
    }

    #[tokio::test]
    #[serial_test::serial]
    async fn test_syftbox_reset_and_errors() -> crate::error::Result<()> {
        let tmp = TempDir::new()?;
        crate::config::set_test_biovault_home(tmp.path().join(".bv"));
        fs::create_dir_all(tmp.path().join(".bv")).unwrap();
        // Seed config
        let cfg = Config {
            email: "e@example".into(),
            syftbox_config: None,
            version: None,
        };
        cfg.save(tmp.path().join(".bv/config.yaml"))?;

        // set_syftbox(None) resets to default
        set_syftbox(None).await?;
        let cfg2 = Config::load()?;
        assert!(cfg2.syftbox_config.is_none());

        // Missing file path returns error
        let err = set_syftbox(Some(
            tmp.path()
                .join("no/such.json")
                .to_string_lossy()
                .to_string(),
        ))
        .await;
        assert!(err.is_err());

        // Invalid JSON returns error
        let bad = tmp.path().join("bad.json");
        fs::write(&bad, "not json").unwrap();
        let err2 = set_syftbox(Some(bad.to_string_lossy().to_string())).await;
        assert!(err2.is_err());
        crate::config::clear_test_biovault_home();
        Ok(())
    }

    #[tokio::test]
    #[serial_test::serial]
    async fn test_show_config() -> crate::error::Result<()> {
        let temp_dir = TempDir::new()?;
        crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));

        let config_dir = temp_dir.path().join(".biovault");
        fs::create_dir_all(&config_dir)?;

        let config = Config {
            email: "show@example.com".to_string(),
            syftbox_config: Some("/custom/syftbox.json".to_string()),
            version: Some("1.0.0".to_string()),
        };
        config.save(config_dir.join("config.yaml"))?;

        // This function prints to stdout, so we just verify it doesn't panic
        let result = show().await;
        assert!(result.is_ok());

        crate::config::clear_test_biovault_home();
        Ok(())
    }

    #[tokio::test]
    #[serial_test::serial]
    async fn test_email_validation() -> crate::error::Result<()> {
        let temp_dir = TempDir::new()?;
        crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));

        let config_dir = temp_dir.path().join(".biovault");
        fs::create_dir_all(&config_dir)?;

        // Create initial config
        let initial_config = Config {
            email: "initial@example.com".to_string(),
            syftbox_config: None,
            version: None,
        };
        initial_config.save(config_dir.join("config.yaml"))?;

        // Set various email formats
        set_email("user@example.com".to_string()).await?;
        let config = Config::load()?;
        assert_eq!(config.email, "user@example.com");

        set_email("user.name+tag@example.co.uk".to_string()).await?;
        let config = Config::load()?;
        assert_eq!(config.email, "user.name+tag@example.co.uk");

        crate::config::clear_test_biovault_home();
        Ok(())
    }

    #[test]
    fn test_show_without_config() {
        // Test that show handles missing config gracefully
        let temp_dir = TempDir::new().unwrap();
        crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));

        // Don't create any config file
        let runtime = tokio::runtime::Runtime::new().unwrap();
        let _result = runtime.block_on(show());

        // Should either succeed with defaults or fail gracefully
        // We're just making sure it doesn't panic

        crate::config::clear_test_biovault_home();
    }
}