geist_supervisor 0.1.28

Generic OTA supervisor for field devices
Documentation
use crate::config::Config;
use crate::services::ota::BackupManager;
use serde::Serialize;
use std::path::Path;
use std::process::Command;
use tracing::{debug, warn};

/// System version and metadata information
#[derive(Debug, Clone, Serialize)]
pub struct SystemInfo {
    pub binary_version: String,
    pub git_hash: String,
    pub build_date: String,
    pub target_platform: String,
    pub deployment_mode: String,
    pub deployment_size_mb: u64,
    pub binary_location: String,
    pub supervisor_version: String,
    pub required_dependencies: Vec<String>,
    pub ota_metadata: OtaMetadata,
}

/// OTA-specific metadata
#[derive(Debug, Clone, Serialize)]
pub struct OtaMetadata {
    pub min_supervisor_version: String,
    pub service_name: String,
    pub assets_included: bool,
    pub health_check_endpoint: String,
    pub health_check_timeout: u32,
    pub rollback_compatible_versions: Vec<String>,
}

/// Service for handling version and system information
pub struct VersionService {
    service_name: String,
}

impl VersionService {
    pub fn new() -> Self {
        Self {
            service_name: "geist_supervisor".to_string(),
        }
    }

    pub fn with_service_name(service_name: String) -> Self {
        Self { service_name }
    }

    /// Get comprehensive system information
    pub fn get_system_info(&self, binary_path: &Path) -> SystemInfo {
        let current_version = Config::format_version(&Config::get_current_version());
        let supervisor_version = format!("v{}", Config::PKG_VERSION);
        let git_hash = self.get_git_hash().unwrap_or_else(|| "unknown".to_string());
        let build_date = self
            .get_build_date()
            .unwrap_or_else(|| "unknown".to_string());
        let deployment_mode = self.detect_deployment_mode();
        let deployment_size_mb = self.get_binary_size_mb(binary_path);
        let ota_metadata = self.get_ota_metadata(&current_version);

        SystemInfo {
            binary_version: current_version,
            git_hash,
            build_date,
            target_platform: std::env::consts::OS.to_string(),
            deployment_mode,
            deployment_size_mb,
            binary_location: binary_path.to_string_lossy().to_string(),
            supervisor_version,
            required_dependencies: vec![],
            ota_metadata,
        }
    }

    /// Git hash baked in at compile time by build.rs
    pub fn get_git_hash(&self) -> Option<String> {
        let hash = env!("GIT_HASH");
        if hash == "unknown" {
            None
        } else {
            Some(hash.to_string())
        }
    }

    /// Build date baked in at compile time by build.rs
    pub fn get_build_date(&self) -> Option<String> {
        let date = env!("BUILD_DATE");
        if date == "unknown" {
            None
        } else {
            Some(date.to_string())
        }
    }

    /// Detect deployment mode based on various indicators
    pub fn detect_deployment_mode(&self) -> String {
        if std::env::var("GEIST_PRODUCTION").is_ok() {
            debug!("Production mode detected from GEIST_PRODUCTION env var");
            return "production".to_string();
        }

        // Check if we're in a systemd environment (likely production)
        if std::env::var("INVOCATION_ID").is_ok() || std::env::var("SERVICE_RESULT").is_ok() {
            debug!("Production mode detected from systemd environment");
            return "production".to_string();
        }

        // Check if installed in /opt (production deployment)
        let prod_path = format!("/opt/{}", self.service_name);
        if Path::new(&prod_path).exists() {
            debug!("Production mode detected from {} path", prod_path);
            return "production".to_string();
        }

        debug!("Development mode detected");
        "development".to_string()
    }

    /// Get binary size in MB
    pub fn get_binary_size_mb(&self, binary_path: &Path) -> u64 {
        match std::fs::metadata(binary_path) {
            Ok(metadata) => {
                let size_mb = metadata.len() / (1024 * 1024);
                debug!("Binary size: {} MB", size_mb);
                size_mb
            }
            Err(e) => {
                warn!(
                    "Failed to get binary metadata for {}: {}",
                    binary_path.display(),
                    e
                );
                0
            }
        }
    }

    /// Get OTA metadata
    pub fn get_ota_metadata(&self, current_version: &str) -> OtaMetadata {
        OtaMetadata {
            min_supervisor_version: "v0.1.0".to_string(),
            service_name: self.service_name.clone(),
            assets_included: true,
            health_check_endpoint: "/health".to_string(),
            health_check_timeout: 30,
            rollback_compatible_versions: self.get_rollback_compatible_versions(current_version),
        }
    }

    /// Get rollback compatible versions from available backups
    pub fn get_rollback_compatible_versions(&self, current_version: &str) -> Vec<String> {
        let mut versions = vec![current_version.to_string()];

        if let Ok(backup_manager) = BackupManager::new() {
            if let Ok(backups) = backup_manager.list_backups() {
                for backup_name in &backups {
                    // Extract version from backup filename: "binary.backup.TIMESTAMP"
                    // The version info isn't in the filename, but backup availability
                    // means we can rollback to that point
                    versions.push(backup_name.clone());
                }
            }
        }

        versions
    }

    /// Check if git is available
    pub fn is_git_available(&self) -> bool {
        Command::new("git")
            .args(["--version"])
            .output()
            .map(|output| output.status.success())
            .unwrap_or(false)
    }

    /// Get current version with proper formatting
    pub fn get_current_version(&self) -> String {
        Config::format_version(&Config::get_current_version())
    }

    /// Get supervisor version
    pub fn get_supervisor_version(&self) -> String {
        format!("v{}", Config::PKG_VERSION)
    }
}

impl Default for VersionService {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_version_service_creation() {
        let service = VersionService::new();
        assert_eq!(
            service.get_supervisor_version(),
            format!("v{}", Config::PKG_VERSION)
        );
    }

    #[test]
    fn test_version_service_with_service_name() {
        let service = VersionService::with_service_name("orb".to_string());
        let metadata = service.get_ota_metadata("v1.0.0");
        assert_eq!(metadata.service_name, "orb");
    }

    #[test]
    fn test_get_current_version() {
        let service = VersionService::new();
        let version = service.get_current_version();
        // Should start with 'v' due to formatting
        assert!(version.starts_with('v') || version == "unknown");
    }

    #[test]
    fn test_detect_deployment_mode_development() {
        let service = VersionService::new();
        // In test environment, should detect as development
        let mode = service.detect_deployment_mode();
        // Could be either development or production depending on test environment
        assert!(mode == "development" || mode == "production");
    }

    #[test]
    fn test_get_binary_size_mb_nonexistent() {
        let service = VersionService::new();
        let nonexistent_path = PathBuf::from("/nonexistent/path");
        let size = service.get_binary_size_mb(&nonexistent_path);
        assert_eq!(size, 0);
    }

    #[test]
    fn test_get_binary_size_mb_existing() {
        let service = VersionService::new();
        let temp_dir = TempDir::new().unwrap();
        let test_file = temp_dir.path().join("test_binary");

        // Create a file with known content
        std::fs::write(&test_file, "test content").unwrap();

        let size = service.get_binary_size_mb(&test_file);
        // Should be 0 MB for such a small file
        assert_eq!(size, 0);
    }

    #[test]
    fn test_get_ota_metadata() {
        let service = VersionService::with_service_name("my_app".to_string());
        let metadata = service.get_ota_metadata("v1.0.0");

        assert_eq!(metadata.service_name, "my_app");
        assert_eq!(metadata.health_check_endpoint, "/health");
        assert_eq!(metadata.health_check_timeout, 30);
        assert!(metadata.assets_included);
    }

    #[test]
    fn test_get_rollback_compatible_versions() {
        let service = VersionService::new();
        let versions = service.get_rollback_compatible_versions("v1.0.0");

        assert!(!versions.is_empty());
        assert_eq!(versions[0], "v1.0.0");
    }

    #[test]
    fn test_get_system_info() {
        let service = VersionService::new();
        let temp_dir = TempDir::new().unwrap();
        let test_binary = temp_dir.path().join("test_binary");
        std::fs::write(&test_binary, "test").unwrap();

        let info = service.get_system_info(&test_binary);

        assert!(!info.binary_version.is_empty());
        assert!(!info.supervisor_version.is_empty());
        assert_eq!(info.target_platform, std::env::consts::OS);
        assert!(info.deployment_mode == "development" || info.deployment_mode == "production");
        assert_eq!(info.binary_location, test_binary.to_string_lossy());
        assert!(info.required_dependencies.is_empty());
    }

    #[test]
    fn test_git_hash_with_no_git() {
        let service = VersionService::new();
        let hash = service.get_git_hash();
        if let Some(h) = hash {
            assert!(!h.is_empty());
        }
    }

    #[test]
    fn test_build_date_with_no_env() {
        let service = VersionService::new();
        let date = service.get_build_date();
        if let Some(d) = date {
            assert!(!d.is_empty());
        }
    }

    #[test]
    fn test_is_git_available() {
        let service = VersionService::new();
        let _ = service.is_git_available();
    }
}