//! Runner-related data models.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// A GitLab CI/CD runner.
///
/// Runners are agents that execute CI/CD jobs. They can be shared across
/// projects or specific to a project/group.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Runner {
    /// Unique runner identifier
    pub id: u64,

    /// Runner description
    pub description: String,

    /// IP address of the runner
    #[serde(default)]
    pub ip_address: Option<String>,

    /// Whether the runner is active
    pub active: bool,

    /// Whether the runner is currently online
    #[serde(default)]
    pub online: bool,

    /// Current status of the runner
    pub status: RunnerStatus,

    /// Type of runner (instance, group, or project)
    pub runner_type: RunnerType,

    /// Whether the runner is shared across projects
    #[serde(default)]
    pub is_shared: bool,

    /// Runner name
    #[serde(default)]
    pub name: Option<String>,

    /// Tags associated with the runner
    #[serde(default)]
    pub tag_list: Vec<String>,

    /// Version of GitLab Runner software
    #[serde(default)]
    pub version: Option<String>,

    /// Runner architecture (e.g., "amd64", "arm64")
    #[serde(default)]
    pub architecture: Option<String>,

    /// Runner platform (e.g., "linux", "windows", "darwin")
    #[serde(default)]
    pub platform: Option<String>,

    /// Last time the runner contacted GitLab
    #[serde(default)]
    pub contacted_at: Option<DateTime<Utc>>,

    /// Maximum timeout for jobs on this runner
    #[serde(default)]
    pub maximum_timeout: Option<u64>,

    /// Whether jobs can run without tags
    #[serde(default)]
    pub run_untagged: bool,

    /// Whether the runner is locked to specific projects
    #[serde(default)]
    pub locked: bool,

    /// Access level (not_protected or ref_protected)
    #[serde(default)]
    pub access_level: Option<String>,
}

/// Status of a runner.
///
/// Indicates whether a runner is available to execute jobs.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum RunnerStatus {
    /// Runner is online and available
    Online,

    /// Runner is offline or unreachable
    Offline,

    /// Runner is paused and won't accept new jobs
    Paused,

    /// Runner status is not determined
    #[serde(other)]
    Unknown,
}

impl RunnerStatus {
    /// Returns `true` if the runner is available to execute jobs.
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_gitlab::models::RunnerStatus;
    ///
    /// assert!(RunnerStatus::Online.is_available());
    /// assert!(!RunnerStatus::Offline.is_available());
    /// assert!(!RunnerStatus::Paused.is_available());
    /// ```
    pub fn is_available(self) -> bool {
        self == Self::Online
    }
}

impl std::fmt::Display for RunnerStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Online => write!(f, "online"),
            Self::Offline => write!(f, "offline"),
            Self::Paused => write!(f, "paused"),
            Self::Unknown => write!(f, "unknown"),
        }
    }
}

/// Type of runner.
///
/// Runners can be scoped to different levels of GitLab's hierarchy.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum RunnerType {
    /// Instance-level runner (available to all projects)
    #[serde(rename = "instance_type")]
    Instance,

    /// Group-level runner (available to all projects in a group)
    #[serde(rename = "group_type")]
    Group,

    /// Project-level runner (specific to one project)
    #[serde(rename = "project_type")]
    Project,
}

impl std::fmt::Display for RunnerType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Instance => write!(f, "instance"),
            Self::Group => write!(f, "group"),
            Self::Project => write!(f, "project"),
        }
    }
}

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

    #[test]
    fn test_runner_status_is_available() {
        assert!(RunnerStatus::Online.is_available());
        assert!(!RunnerStatus::Offline.is_available());
        assert!(!RunnerStatus::Paused.is_available());
    }

    #[test]
    fn test_runner_status_display() {
        assert_eq!(RunnerStatus::Online.to_string(), "online");
        assert_eq!(RunnerStatus::Offline.to_string(), "offline");
        assert_eq!(RunnerStatus::Paused.to_string(), "paused");
    }

    #[test]
    fn test_runner_type_display() {
        assert_eq!(RunnerType::Instance.to_string(), "instance");
        assert_eq!(RunnerType::Group.to_string(), "group");
        assert_eq!(RunnerType::Project.to_string(), "project");
    }

    #[test]
    fn test_runner_status_serialization() {
        let status = RunnerStatus::Online;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, "\"online\"");

        let deserialized: RunnerStatus = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, RunnerStatus::Online);
    }

    #[test]
    fn test_runner_type_serialization() {
        let runner_type = RunnerType::Instance;
        let json = serde_json::to_string(&runner_type).unwrap();
        assert_eq!(json, "\"instance_type\"");

        let deserialized: RunnerType = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, RunnerType::Instance);
    }
}
