// SPDX-License-Identifier: Apache-2.0
// Sealed with Magistamp.

//! An example job execution framework.
//!
//! It includes:
//! - A `Job` trait for defining asynchronous tasks.
//! - A `JobRunner` struct for collecting and executing multiple jobs.
//! - Concrete implementations of jobs (`FileJob`, `SleepJob`, `MathJob`).
//!
//! Jobs are run concurrently using Tokio, and results are gathered
//! with proper error handling.

use std::io::Write;
use std::pin::Pin;

use tokio::time::{sleep, Duration};



/// Run a predefined set of jobs asynchronously.
///
/// This function creates a `JobRunner`, adds three different jobs,
/// and executes them concurrently. Results are printed to stdout,
/// and any errors are reported to stderr.
pub async fn run_jobs()
{
   let mut runner: JobRunner = JobRunner::new();

   runner.add_job(FileJob::new());
   runner.add_job(SleepJob::new());
   runner.add_job(MathJob::new());

   for result in runner.run_all().await.iter()
   {
      match &result.1
      {
         Ok(msg) =>
         {
            println!("{}:  {}", result.0, msg);
         }

         Err(error) =>
         {
            eprintln!("{}:  {}", result.0, error);
         }
      }
   }
}


/// A Job will return this after running. The traits need to be Send
/// because they maybe sent between threads.
type JobResult = Result<String, Box<dyn std::error::Error + Send>>;

/// The Future task that Jobs will return that can later be run.
/// Here we Pin a heap allocated future so that it can be referenced safely.
/// The output is then set to the desired output of the function.
/// This is the may way to do 'async dyn traits'.
type PinnedFuture<'a> =
   Pin<Box<dyn core::future::Future<Output = JobResult> + Send + 'a>>;



/// An async trait that can be used as a dyn trait.
pub trait Job: Send + Sync
{
   /// Retrieve the name of the Job.
   fn name(&self) -> &str;

   /// Run the Job.
   ///
   /// This function needs to be async.
   /// Here we Pin a heap allocated future so that it can be referenced safely.
   /// The output is then set to the desired output of the function.
   ///
   /// Inside the implementation all you need to do is:
   /// ```ignore
   /// Box::pin(async move
   /// {
   ///    // Place your functions code here.
   /// })
   /// ```
   fn run<'a>(&self) -> PinnedFuture<'a>
      where Self: Sync + 'a;
}


/// A struct to hold and execute multiple jobs.
pub struct JobRunner
{
   jobs: Vec<Box<dyn Job>>
}

impl JobRunner
{
   /// Create a new JobRunner with an empty job list.
   pub fn new() -> Self
   {
      JobRunner
      {
         jobs: Vec::new()
      }
   }

   /// Add a new job to the runner.
   pub fn add_job(&mut self, job: impl Job + 'static)
   {
      self.jobs.push(Box::new(job));
   }

   /// Run all added jobs concurrently and collect their results.
   ///
   /// Uses Tokio to spawn concurrent tasks for each job. After
   /// completion, all jobs are cleared from the runner.
   pub async fn run_all(&mut self) -> Vec<(String, JobResult)>
   {
      let tasks: tokio::task::JoinSet<(String, JobResult)> =
         self.jobs
             .iter()
             .map(|j| {
                let name = j.name().to_string();
                let fut = j.run();

                async move { (name, fut.await) }
             })
             .collect();

      let result = tasks.join_all().await;

      self.jobs.clear();

      result
   }
}


/// A dummy job simulating a file operation.
pub struct FileJob {}

impl FileJob
{
   /// Create a new FileJob instance.
   fn new() -> Self
   {
      FileJob
      {
      }
   }
}

/// Implementation of the Job trait for FileJob.
impl Job for FileJob
{
   /// Retrieve the name of the Job.
   fn name(&self) -> &str
   {
      "File Job"
   }

   /// Run the File Job.
   fn run<'a>(&self) -> PinnedFuture<'a>
      where Self: Sync + 'a
   {
      Box::pin(async move {
         let mut write_target = std::env::temp_dir();
         write_target.push("file_job");
         write_target.set_extension("txt");

         let mut file = std::fs::File::create(write_target)
            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
         file.write_all(b"Jason is an awesome programmer!!")
            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;

         Ok(String::from("File written"))
      })
   }
}


/// A dummy job simulating a sleep or delay operation.
pub struct SleepJob {}

impl SleepJob
{
   /// Create a new SleepJob instance.
   fn new() -> Self
   {
      SleepJob
      {
      }
   }
}

/// Implementation of the Job trait for SleepJob.
impl Job for SleepJob
{
   /// Retrieve the name of the Job.
   fn name(&self) -> &str
   {
      "Sleep Job"
   }

   /// Run the Sleep Job.
   fn run<'a>(&self) -> PinnedFuture<'a>
      where Self: Sync + 'a
   {
      Box::pin(async move 
      {
         sleep(Duration::from_millis(500)).await;
         Ok(String::from("Zzzzzzzzzzz"))
      })
   }
}


/// A dummy job simulating a math operation.
pub struct MathJob {}

impl MathJob
{
   /// Create a new MathJob instance.
   fn new() -> Self
   {
      MathJob
      {
      }
   }
}

/// Implementation of the Job trait for MathJob.
impl Job for MathJob
{
   /// Retrieve the name of the Job.
   fn name(&self) -> &str
   {
      "Math Job"
   }

   /// Run the Math Job.
   fn run<'a>(&self) -> PinnedFuture<'a>
      where Self: Sync + 'a
   {
      Box::pin(async move {
         let _ans = 10 * 4 + 2;
         Ok(String::from("Math stuff"))
      })
   }
}




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

   #[tokio::test]
   async fn test_file_job()
   {
      let job = FileJob::new();
      assert_eq!(job.name(), "File Job");

      let result = job.run().await;
      assert!(result.is_ok());
      assert_eq!(result.unwrap(), "File written");
   }

   #[tokio::test]
   async fn test_sleep_job()
   {
      let job = SleepJob::new();
      assert_eq!(job.name(), "Sleep Job");

      let start = Instant::now();
      let result = job.run().await;
      let elapsed = start.elapsed();

      assert!(result.is_ok());
      assert_eq!(result.unwrap(), "Zzzzzzzzzzz");
      assert!(elapsed >= Duration::from_millis(500), "Sleep duration too short");
   }

   #[tokio::test]
   async fn test_math_job()
   {
      let job = MathJob::new();
      assert_eq!(job.name(), "Math Job");

      let result = job.run().await;
      assert!(result.is_ok());
      assert_eq!(result.unwrap(), "Math stuff");
   }

   #[tokio::test]
   async fn test_job_runner_executes_all_jobs()
   {
      let mut runner = JobRunner::new();
      runner.add_job(FileJob::new());
      runner.add_job(SleepJob::new());
      runner.add_job(MathJob::new());

      let mut results = runner.run_all().await;

      // Ensure jobs are cleared after run
      assert!(runner.jobs.is_empty());

      // Sort results by name for consistent testing
      results.sort_by(|a, b| a.0.cmp(&b.0));

      assert_eq!(results.len(), 3);

      assert_eq!(results[0].0, "File Job");
      assert_eq!(results[0].1.as_ref().unwrap(), "File written");

      assert_eq!(results[1].0, "Math Job");
      assert_eq!(results[1].1.as_ref().unwrap(), "Math stuff");

      assert_eq!(results[2].0, "Sleep Job");
      assert_eq!(results[2].1.as_ref().unwrap(), "Zzzzzzzzzzz");
   }

   #[tokio::test]
   async fn test_job_runner_with_no_jobs()
   {
      let mut runner = JobRunner::new();
      let results = runner.run_all().await;
      assert!(results.is_empty());
   }
}
