quickseal 0.1.4

Create Kubernetes SealedSecrets with a single secure command
Documentation
//! Interactive prompt mode for creating sealed secrets
//!
//! Provides a guided, user-friendly interface for users who prefer
//! step-by-step prompts over command-line flags.

use crate::models::{SecretKeyValue, SecretRequest};
use crate::ValidationError;
use dialoguer::{Confirm, Input, Password};
use std::collections::HashMap;

/// Run interactive mode to collect secret information from user
///
/// This function guides the user through creating a sealed secret
/// by prompting for each required and optional field.
///
/// # Returns
///
/// Returns a `SecretRequest` with all user-provided information,
/// or an error if the user cancels (Ctrl+C) or provides invalid input.
///
/// # Errors
///
/// Returns error if:
/// - User cancels with Ctrl+C
/// - Input validation fails
/// - I/O error occurs during prompting
pub fn interactive_create() -> Result<SecretRequest, anyhow::Error> {
    eprintln!("\n=== Interactive Sealed Secret Creation ===\n");

    // Prompt for secret name
    let name: String = Input::new()
        .with_prompt("Secret name (lowercase alphanumeric, hyphens, dots)")
        .validate_with(|input: &String| -> Result<(), &str> {
            crate::models::validate_secret_name(input).map_err(|_| {
                "Invalid secret name. Must follow Kubernetes naming rules:\n  \
                 - lowercase alphanumeric, hyphens, or dots\n  \
                 - must start and end with alphanumeric\n  \
                 - max 253 characters"
            })
        })
        .interact_text()?;

    // Prompt for namespace
    let namespace: String = Input::new()
        .with_prompt("Namespace (lowercase alphanumeric with hyphens)")
        .validate_with(|input: &String| -> Result<(), &str> {
            crate::models::validate_namespace(input).map_err(|_| {
                "Invalid namespace. Must follow Kubernetes naming rules:\n  \
                 - lowercase alphanumeric with hyphens\n  \
                 - must start and end with alphanumeric\n  \
                 - max 63 characters"
            })
        })
        .interact_text()?;

    // Collect key-value pairs
    eprintln!("\n--- Secret Data ---");
    eprintln!("Enter key-value pairs (leave key empty to finish)\n");

    let mut data = Vec::new();
    loop {
        let key: String = Input::new()
            .with_prompt("Secret key")
            .allow_empty(true)
            .interact_text()?;

        if key.is_empty() {
            if data.is_empty() {
                eprintln!("⚠️  At least one key-value pair is required");
                continue;
            }
            break;
        }

        // Validate key
        if let Err(e) = crate::models::validate_key_name(&key) {
            eprintln!("⚠️  Invalid key: {}", e);
            eprintln!("   Keys must start with letter or underscore, followed by alphanumeric or underscores\n");
            continue;
        }

        // Check for duplicate keys
        if data.iter().any(|kv: &SecretKeyValue| kv.key == key) {
            eprintln!("⚠️  Key '{}' already exists\n", key);
            continue;
        }

        // Prompt for value (masked)
        let value: String = Password::new()
            .with_prompt(format!("Secret value for '{}'", key))
            .interact()?;

        if value.is_empty() {
            eprintln!("⚠️  Value cannot be empty\n");
            continue;
        }

        data.push(SecretKeyValue::new(key, value)?);
        eprintln!("✓ Added\n");
    }

    // Optional: Add labels
    eprintln!("\n--- Labels (Optional) ---");
    let add_labels = Confirm::new()
        .with_prompt("Add labels to the secret?")
        .default(false)
        .interact()?;

    let mut labels = HashMap::new();
    if add_labels {
        eprintln!("Enter labels (leave key empty to finish)\n");
        loop {
            let key: String = Input::new()
                .with_prompt("Label key")
                .allow_empty(true)
                .interact_text()?;

            if key.is_empty() {
                break;
            }

            let value: String = Input::new()
                .with_prompt(format!("Label value for '{}'", key))
                .interact_text()?;

            labels.insert(key, value);
            eprintln!("✓ Added\n");
        }
    }

    // Optional: Add annotations
    eprintln!("\n--- Annotations (Optional) ---");
    let add_annotations = Confirm::new()
        .with_prompt("Add annotations to the secret?")
        .default(false)
        .interact()?;

    let mut annotations = HashMap::new();
    if add_annotations {
        eprintln!("Enter annotations (leave key empty to finish)\n");
        loop {
            let key: String = Input::new()
                .with_prompt("Annotation key")
                .allow_empty(true)
                .interact_text()?;

            if key.is_empty() {
                break;
            }

            let value: String = Input::new()
                .with_prompt(format!("Annotation value for '{}'", key))
                .interact_text()?;

            annotations.insert(key, value);
            eprintln!("✓ Added\n");
        }
    }

    // Display summary and confirm
    eprintln!("\n=== Summary ===");
    eprintln!("Name: {}", name);
    eprintln!("Namespace: {}", namespace);
    eprintln!(
        "Keys: {}",
        data.iter()
            .map(|kv| kv.key.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    );

    if !labels.is_empty() {
        eprintln!(
            "Labels: {}",
            labels
                .iter()
                .map(|(k, v)| format!("{}={}", k, v))
                .collect::<Vec<_>>()
                .join(", ")
        );
    } else {
        eprintln!("Labels: none");
    }

    if !annotations.is_empty() {
        eprintln!(
            "Annotations: {}",
            annotations
                .iter()
                .map(|(k, v)| format!("{}={}", k, v))
                .collect::<Vec<_>>()
                .join(", ")
        );
    } else {
        eprintln!("Annotations: none");
    }

    eprintln!();
    let confirmed = Confirm::new()
        .with_prompt("Create SealedSecret with this configuration?")
        .default(true)
        .interact()?;

    if !confirmed {
        return Err(anyhow::anyhow!("Cancelled by user"));
    }

    // Create SecretRequest
    SecretRequest::new(name, namespace, labels, annotations, data)
        .map_err(|e: ValidationError| anyhow::anyhow!("Validation error: {}", e))
}

#[cfg(test)]
mod tests {
    // Note: Interactive tests require user input simulation
    // These tests verify the structure is correct
    // Full integration tests should use expect-style testing

    #[test]
    fn test_interactive_create_function_exists() {
        // Verify the function signature compiles
        // Real testing requires input simulation tools like expectrl
        // This is a placeholder test to verify the module structure
    }
}