partiri-cli 0.1.6

partiri CLI — Deploy and manage services on Partiri Cloud
use std::path::PathBuf;

use inquire::{Confirm, Text};
use owo_colors::OwoColorize;

use crate::error::Result;
use crate::output::print_success;

/// Path to the credentials file where the API key is stored.
/// Returns `~/.config/partiri/key`.
pub fn credentials_path() -> Option<PathBuf> {
    let base = dirs::config_dir().or_else(dirs::home_dir)?;
    Some(base.join("partiri").join("key"))
}

/// Read the stored API key from the credentials file.
pub fn read_key() -> Option<String> {
    let path = credentials_path()?;
    std::fs::read_to_string(&path)
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

pub fn run() -> Result<()> {
    println!("\n{}\n", "  partiri auth".bold().cyan());

    // ── Bail early if we can't determine where to save ───────────────────────
    let creds = credentials_path()
        .ok_or("Could not determine config directory. Set $HOME or $XDG_CONFIG_HOME.")?;

    // ── Show current status ───────────────────────────────────────────────────
    let current = read_key();
    match current {
        Some(ref existing) => {
            let masked = mask_key(existing);
            println!("  Current key: {}", masked.bold());
            let update = Confirm::new("Replace it with a new key?")
                .with_default(false)
                .prompt()
                .map_err(|_| "Cancelled.")?;
            if !update {
                return Ok(());
            }
        }
        None => {
            println!(
                "  {}",
                "No API key configured. Get one from https://partiri.cloud/settings/api-keys"
                    .dimmed()
            );
        }
    }

    // ── Prompt for key ────────────────────────────────────────────────────────
    let key = Text::new("Paste your API key:")
        .prompt()
        .map_err(|_| "Cancelled.")?;

    let key = key.trim().to_string();
    if key.is_empty() {
        return Err("API key cannot be empty.".into());
    }
    if key.len() < 64 {
        return Err("Api Key does not look right. Check that you pasted the full key.".into());
    }

    // ── Write to credentials file ────────────────────────────────────────────
    save_credentials_file(&creds, &key)
        .map_err(|e| format!("Failed to write credentials to {}: {e}", creds.display()))?;

    // Verify the save by reading back
    let saved = std::fs::read_to_string(&creds)
        .map(|s| s.trim().to_string())
        .unwrap_or_default();
    if saved != key {
        return Err(
            "Key file was written but content does not match. Check file permissions.".into(),
        );
    }
    print_success(&format!("Key saved to {}.", creds.display()));

    Ok(())
}

fn save_credentials_file(path: &PathBuf, key: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    #[cfg(unix)]
    {
        use std::io::Write;
        use std::os::unix::fs::OpenOptionsExt;
        let mut file = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .mode(0o600)
            .open(path)?;
        file.write_all(key.as_bytes())?;
    }
    #[cfg(not(unix))]
    {
        std::fs::write(path, key)?;
    }
    Ok(())
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

fn mask_key(key: &str) -> String {
    let chars: Vec<char> = key.chars().collect();
    if chars.len() <= 4 {
        return "****".to_string();
    }
    let head: String = chars[..4].iter().collect();
    let tail: String = chars[chars.len() - 4..].iter().collect();
    format!("{head}{tail}")
}

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

    // ─── mask_key ─────────────────────────────────────────────────────────────

    #[test]
    fn mask_key_empty_returns_stars() {
        assert_eq!(mask_key(""), "****");
    }

    #[test]
    fn mask_key_three_chars_returns_stars() {
        assert_eq!(mask_key("abc"), "****");
    }

    #[test]
    fn mask_key_exactly_four_returns_stars() {
        assert_eq!(mask_key("abcd"), "****");
    }

    #[test]
    fn mask_key_five_chars_shows_overlap() {
        let result = mask_key("abcde");
        assert!(
            result.starts_with("abcd"),
            "should start with first 4 chars"
        );
        assert!(result.ends_with("bcde"), "should end with last 4 chars");
    }

    #[test]
    fn mask_key_long_shows_first_and_last_four() {
        let result = mask_key("abcdefghij");
        assert!(result.starts_with("abcd"), "should start with first 4");
        assert!(result.ends_with("ghij"), "should end with last 4");
        // Middle characters should not appear
        assert!(!result.contains("efgh"));
    }

    #[test]
    fn mask_key_contains_ellipsis_separator() {
        let result = mask_key("abcdefghij");
        // The separator is the Unicode ellipsis U+2026
        assert!(
            result.contains('\u{2026}'),
            "should use ellipsis separator: {}",
            result
        );
    }

    // ─── credentials_path ─────────────────────────────────────────────────────

    #[test]
    fn credentials_path_returns_some() {
        assert!(credentials_path().is_some());
    }

    #[test]
    fn credentials_path_ends_with_partiri_key() {
        let path = credentials_path().unwrap();
        assert!(
            path.ends_with("partiri/key"),
            "expected …/partiri/key, got {:?}",
            path
        );
    }

    // ─── save_credentials_file ────────────────────────────────────────────────

    #[test]
    fn save_credentials_creates_dirs_and_file() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("partiri").join("key");
        save_credentials_file(&path, "my-secret-key").unwrap();
        let content = std::fs::read_to_string(&path).unwrap();
        assert_eq!(content, "my-secret-key");
    }

    #[test]
    fn save_credentials_overwrites_existing() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("partiri").join("key");
        save_credentials_file(&path, "old-key").unwrap();
        save_credentials_file(&path, "new-key").unwrap();
        let content = std::fs::read_to_string(&path).unwrap();
        assert_eq!(content, "new-key");
    }
}