//! Job-related data models.

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

/// A CI/CD job within a pipeline.
///
/// Jobs are individual units of work in a pipeline, such as building,
/// testing, or deploying code.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Job {
    /// Unique job identifier
    pub id: u64,

    /// Job name as defined in .gitlab-ci.yml
    pub name: String,

    /// Current status of the job
    pub status: JobStatus,

    /// Pipeline stage the job belongs to
    pub stage: String,

    /// Git reference (branch or tag) the job ran on
    #[serde(rename = "ref")]
    pub ref_name: String,

    /// Git commit SHA the job ran on
    #[serde(default)]
    pub sha: Option<String>,

    /// When the job was created
    pub created_at: DateTime<Utc>,

    /// When the job started running
    pub started_at: Option<DateTime<Utc>>,

    /// When the job finished
    pub finished_at: Option<DateTime<Utc>>,

    /// Duration of job execution in seconds
    pub duration: Option<f64>,

    /// Time spent queued before execution in seconds
    pub queued_duration: Option<f64>,

    /// Web URL to view the job in GitLab UI
    pub web_url: String,

    /// User who triggered the job
    #[serde(default)]
    pub user: Option<JobUser>,

    /// Runner that executed the job
    #[serde(default)]
    pub runner: Option<JobRunner>,

    /// Whether the job can be retried
    #[serde(default)]
    pub allow_failure: bool,

    /// Tag list for the job
    #[serde(default)]
    pub tag_list: Vec<String>,

    /// Artifacts associated with this job
    #[serde(default)]
    pub artifacts: Vec<Artifact>,

    /// Coverage percentage for this job
    #[serde(default)]
    pub coverage: Option<f64>,
}

/// User information associated with a job.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JobUser {
    /// User ID
    pub id: u64,

    /// Username
    pub username: String,

    /// Display name
    pub name: String,

    /// User state
    pub state: String,

    /// URL to user's avatar
    pub avatar_url: Option<String>,

    /// Web URL to user's profile
    pub web_url: String,
}

/// Runner information for a job.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JobRunner {
    /// Runner ID
    pub id: u64,

    /// Runner description
    pub description: String,

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

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

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

/// Artifact produced by a job.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Artifact {
    /// Artifact filename
    pub filename: String,

    /// Artifact size in bytes
    pub size: u64,

    /// Artifact file type
    #[serde(default)]
    pub file_type: Option<String>,

    /// Artifact file format
    #[serde(default)]
    pub file_format: Option<String>,
}

/// Status of a CI/CD job.
///
/// Represents the various states a GitLab job can be in during its lifecycle.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum JobStatus {
    /// Job created but not yet running
    Created,

    /// Job is waiting to be picked up by a runner
    Pending,

    /// Job is currently running
    Running,

    /// Job completed successfully
    Success,

    /// Job failed
    Failed,

    /// Job was canceled
    Canceled,

    /// Job was skipped
    Skipped,

    /// Job is waiting for manual action
    Manual,

    /// Job is scheduled to run
    Scheduled,
}

impl JobStatus {
    /// Returns `true` if the job is in a terminal state (completed, won't change).
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_gitlab::models::JobStatus;
    ///
    /// assert!(JobStatus::Success.is_finished());
    /// assert!(JobStatus::Failed.is_finished());
    /// assert!(!JobStatus::Running.is_finished());
    /// ```
    pub fn is_finished(self) -> bool {
        matches!(
            self,
            Self::Success | Self::Failed | Self::Canceled | Self::Skipped
        )
    }

    /// Returns `true` if the job is currently active (running or pending).
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_gitlab::models::JobStatus;
    ///
    /// assert!(JobStatus::Running.is_active());
    /// assert!(JobStatus::Pending.is_active());
    /// assert!(!JobStatus::Success.is_active());
    /// ```
    pub fn is_active(self) -> bool {
        matches!(self, Self::Created | Self::Pending | Self::Running)
    }

    /// Returns `true` if the job succeeded.
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_gitlab::models::JobStatus;
    ///
    /// assert!(JobStatus::Success.is_successful());
    /// assert!(!JobStatus::Failed.is_successful());
    /// ```
    pub fn is_successful(self) -> bool {
        self == Self::Success
    }

    /// Returns `true` if the job failed.
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_gitlab::models::JobStatus;
    ///
    /// assert!(JobStatus::Failed.is_failed());
    /// assert!(!JobStatus::Success.is_failed());
    /// ```
    pub fn is_failed(self) -> bool {
        self == Self::Failed
    }
}

impl std::fmt::Display for JobStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Created => write!(f, "created"),
            Self::Pending => write!(f, "pending"),
            Self::Running => write!(f, "running"),
            Self::Success => write!(f, "success"),
            Self::Failed => write!(f, "failed"),
            Self::Canceled => write!(f, "canceled"),
            Self::Skipped => write!(f, "skipped"),
            Self::Manual => write!(f, "manual"),
            Self::Scheduled => write!(f, "scheduled"),
        }
    }
}

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

    #[test]
    fn test_job_status_is_finished() {
        assert!(JobStatus::Success.is_finished());
        assert!(JobStatus::Failed.is_finished());
        assert!(JobStatus::Canceled.is_finished());
        assert!(JobStatus::Skipped.is_finished());

        assert!(!JobStatus::Running.is_finished());
        assert!(!JobStatus::Pending.is_finished());
    }

    #[test]
    fn test_job_status_is_active() {
        assert!(JobStatus::Running.is_active());
        assert!(JobStatus::Pending.is_active());
        assert!(JobStatus::Created.is_active());

        assert!(!JobStatus::Success.is_active());
        assert!(!JobStatus::Failed.is_active());
    }

    #[test]
    fn test_job_status_display() {
        assert_eq!(JobStatus::Success.to_string(), "success");
        assert_eq!(JobStatus::Failed.to_string(), "failed");
        assert_eq!(JobStatus::Running.to_string(), "running");
    }

    #[test]
    fn test_job_status_serialization() {
        let status = JobStatus::Success;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, "\"success\"");

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