Skip to main content

legalis_sim/
api.rs

1//! Simulation-as-a-Service API
2//!
3//! This module provides an API for running simulations as a service,
4//! including job queuing, result storage, and webhook notifications.
5
6use crate::{SimResult, SimulationMetrics};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::{BinaryHeap, HashMap, VecDeque};
10use std::path::PathBuf;
11use std::sync::{Arc, Mutex};
12use uuid::Uuid;
13
14/// Priority level for simulation jobs
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
16pub enum JobPriority {
17    /// Low priority (background jobs)
18    Low = 0,
19    /// Normal priority (default)
20    #[default]
21    Normal = 1,
22    /// High priority (urgent jobs)
23    High = 2,
24    /// Critical priority (immediate execution)
25    Critical = 3,
26}
27
28/// Status of a simulation job
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum JobStatus {
31    /// Job is queued and waiting for execution
32    Queued,
33    /// Job is currently running
34    Running,
35    /// Job completed successfully
36    Completed,
37    /// Job failed with an error
38    Failed,
39    /// Job was cancelled
40    Cancelled,
41}
42
43/// A simulation job in the queue
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct SimulationJob {
46    /// Unique job identifier
47    pub id: Uuid,
48    /// Job name/description
49    pub name: String,
50    /// Job priority
51    pub priority: JobPriority,
52    /// Job status
53    pub status: JobStatus,
54    /// Time job was created
55    pub created_at: DateTime<Utc>,
56    /// Time job started running (if applicable)
57    pub started_at: Option<DateTime<Utc>>,
58    /// Time job completed (if applicable)
59    pub completed_at: Option<DateTime<Utc>>,
60    /// Webhook URL to call when job completes
61    pub webhook_url: Option<String>,
62    /// Job configuration (JSON serialized)
63    pub config: serde_json::Value,
64    /// Error message (if failed)
65    pub error: Option<String>,
66}
67
68impl SimulationJob {
69    /// Create a new simulation job
70    pub fn new(name: impl Into<String>, config: serde_json::Value) -> Self {
71        Self {
72            id: Uuid::new_v4(),
73            name: name.into(),
74            priority: JobPriority::default(),
75            status: JobStatus::Queued,
76            created_at: Utc::now(),
77            started_at: None,
78            completed_at: None,
79            webhook_url: None,
80            config,
81            error: None,
82        }
83    }
84
85    /// Set the job priority
86    pub fn with_priority(mut self, priority: JobPriority) -> Self {
87        self.priority = priority;
88        self
89    }
90
91    /// Set the webhook URL
92    pub fn with_webhook(mut self, url: impl Into<String>) -> Self {
93        self.webhook_url = Some(url.into());
94        self
95    }
96
97    /// Mark job as started
98    pub fn start(&mut self) {
99        self.status = JobStatus::Running;
100        self.started_at = Some(Utc::now());
101    }
102
103    /// Mark job as completed
104    pub fn complete(&mut self) {
105        self.status = JobStatus::Completed;
106        self.completed_at = Some(Utc::now());
107    }
108
109    /// Mark job as failed with an error message
110    pub fn fail(&mut self, error: impl Into<String>) {
111        self.status = JobStatus::Failed;
112        self.completed_at = Some(Utc::now());
113        self.error = Some(error.into());
114    }
115
116    /// Mark job as cancelled
117    pub fn cancel(&mut self) {
118        self.status = JobStatus::Cancelled;
119        self.completed_at = Some(Utc::now());
120    }
121
122    /// Calculate job duration in milliseconds
123    pub fn duration_ms(&self) -> Option<i64> {
124        match (self.started_at, self.completed_at) {
125            (Some(start), Some(end)) => Some((end - start).num_milliseconds()),
126            _ => None,
127        }
128    }
129}
130
131// Internal wrapper for priority queue ordering
132#[derive(Debug)]
133struct JobWrapper {
134    job: SimulationJob,
135    sequence: u64, // For stable ordering within same priority
136}
137
138impl PartialEq for JobWrapper {
139    fn eq(&self, other: &Self) -> bool {
140        self.job.priority == other.job.priority && self.sequence == other.sequence
141    }
142}
143
144impl Eq for JobWrapper {}
145
146impl PartialOrd for JobWrapper {
147    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
148        Some(self.cmp(other))
149    }
150}
151
152impl Ord for JobWrapper {
153    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
154        // Higher priority first, then lower sequence (earlier submission)
155        match self.job.priority.cmp(&other.job.priority) {
156            std::cmp::Ordering::Equal => other.sequence.cmp(&self.sequence),
157            other => other,
158        }
159    }
160}
161
162/// Job queue for managing simulation jobs
163#[derive(Debug)]
164pub struct JobQueue {
165    queue: BinaryHeap<JobWrapper>,
166    jobs: HashMap<Uuid, SimulationJob>,
167    sequence_counter: u64,
168}
169
170impl JobQueue {
171    /// Create a new job queue
172    pub fn new() -> Self {
173        Self {
174            queue: BinaryHeap::new(),
175            jobs: HashMap::new(),
176            sequence_counter: 0,
177        }
178    }
179
180    /// Submit a new job to the queue
181    pub fn submit(&mut self, job: SimulationJob) -> Uuid {
182        let id = job.id;
183        let wrapper = JobWrapper {
184            job: job.clone(),
185            sequence: self.sequence_counter,
186        };
187        self.sequence_counter += 1;
188        self.queue.push(wrapper);
189        self.jobs.insert(id, job);
190        id
191    }
192
193    /// Get the next job from the queue (highest priority)
194    pub fn pop(&mut self) -> Option<SimulationJob> {
195        self.queue.pop().map(|wrapper| {
196            let mut job = wrapper.job;
197            job.start();
198            self.jobs.insert(job.id, job.clone());
199            job
200        })
201    }
202
203    /// Get a job by ID
204    pub fn get(&self, id: &Uuid) -> Option<&SimulationJob> {
205        self.jobs.get(id)
206    }
207
208    /// Get a mutable reference to a job by ID
209    pub fn get_mut(&mut self, id: &Uuid) -> Option<&mut SimulationJob> {
210        self.jobs.get_mut(id)
211    }
212
213    /// Cancel a job by ID
214    pub fn cancel(&mut self, id: &Uuid) -> bool {
215        if let Some(job) = self.jobs.get_mut(id)
216            && job.status == JobStatus::Queued
217        {
218            job.cancel();
219            return true;
220        }
221        false
222    }
223
224    /// Get all jobs
225    pub fn all_jobs(&self) -> Vec<&SimulationJob> {
226        self.jobs.values().collect()
227    }
228
229    /// Get jobs by status
230    pub fn jobs_by_status(&self, status: JobStatus) -> Vec<&SimulationJob> {
231        self.jobs
232            .values()
233            .filter(|job| job.status == status)
234            .collect()
235    }
236
237    /// Get queue size
238    pub fn queue_size(&self) -> usize {
239        self.queue.len()
240    }
241
242    /// Get total job count
243    pub fn total_jobs(&self) -> usize {
244        self.jobs.len()
245    }
246
247    /// Clear completed jobs
248    pub fn clear_completed(&mut self) {
249        self.jobs
250            .retain(|_, job| job.status != JobStatus::Completed);
251    }
252}
253
254impl Default for JobQueue {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260/// Storage for simulation results
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct SimulationResult {
263    /// Unique result identifier
264    pub id: Uuid,
265    /// Associated job ID
266    pub job_id: Uuid,
267    /// Simulation metrics
268    pub metrics: SimulationMetrics,
269    /// Additional metadata
270    pub metadata: HashMap<String, serde_json::Value>,
271    /// Timestamp when result was stored
272    pub stored_at: DateTime<Utc>,
273}
274
275impl SimulationResult {
276    /// Create a new simulation result
277    pub fn new(job_id: Uuid, metrics: SimulationMetrics) -> Self {
278        Self {
279            id: Uuid::new_v4(),
280            job_id,
281            metrics,
282            metadata: HashMap::new(),
283            stored_at: Utc::now(),
284        }
285    }
286
287    /// Add metadata to the result
288    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
289        self.metadata.insert(key.into(), value);
290        self
291    }
292}
293
294/// Result storage system
295#[derive(Debug)]
296pub struct ResultStorage {
297    results: HashMap<Uuid, SimulationResult>,
298    storage_path: Option<PathBuf>,
299}
300
301impl ResultStorage {
302    /// Create a new result storage
303    pub fn new() -> Self {
304        Self {
305            results: HashMap::new(),
306            storage_path: None,
307        }
308    }
309
310    /// Create result storage with file persistence
311    pub fn with_path(path: impl Into<PathBuf>) -> Self {
312        Self {
313            results: HashMap::new(),
314            storage_path: Some(path.into()),
315        }
316    }
317
318    /// Store a simulation result
319    pub fn store(&mut self, result: SimulationResult) -> SimResult<Uuid> {
320        let id = result.id;
321
322        // Store in memory
323        self.results.insert(id, result.clone());
324
325        // Persist to disk if path is configured
326        if let Some(ref path) = self.storage_path {
327            let file_path = path.join(format!("{}.json", id));
328            let json = serde_json::to_string_pretty(&result)?;
329            std::fs::create_dir_all(path)?;
330            std::fs::write(&file_path, json)?;
331        }
332
333        Ok(id)
334    }
335
336    /// Get a result by ID
337    pub fn get(&self, id: &Uuid) -> Option<&SimulationResult> {
338        self.results.get(id)
339    }
340
341    /// Get all results for a job
342    pub fn get_by_job(&self, job_id: &Uuid) -> Vec<&SimulationResult> {
343        self.results
344            .values()
345            .filter(|r| r.job_id == *job_id)
346            .collect()
347    }
348
349    /// Get all results
350    pub fn all_results(&self) -> Vec<&SimulationResult> {
351        self.results.values().collect()
352    }
353
354    /// Delete a result by ID
355    pub fn delete(&mut self, id: &Uuid) -> bool {
356        // Delete from disk if path is configured
357        if let Some(ref path) = self.storage_path {
358            let file_path = path.join(format!("{}.json", id));
359            let _ = std::fs::remove_file(file_path);
360        }
361
362        self.results.remove(id).is_some()
363    }
364
365    /// Load results from disk
366    pub fn load_all(&mut self) -> SimResult<usize> {
367        if let Some(ref path) = self.storage_path {
368            if !path.exists() {
369                return Ok(0);
370            }
371
372            let mut count = 0;
373            for entry in std::fs::read_dir(path)? {
374                let entry = entry?;
375                let path = entry.path();
376
377                if path.extension().and_then(|s| s.to_str()) == Some("json") {
378                    let json = std::fs::read_to_string(&path)?;
379                    let result: SimulationResult = serde_json::from_str(&json)?;
380                    self.results.insert(result.id, result);
381                    count += 1;
382                }
383            }
384
385            Ok(count)
386        } else {
387            Ok(0)
388        }
389    }
390
391    /// Clear all results
392    pub fn clear(&mut self) {
393        self.results.clear();
394    }
395
396    /// Get result count
397    pub fn count(&self) -> usize {
398        self.results.len()
399    }
400}
401
402impl Default for ResultStorage {
403    fn default() -> Self {
404        Self::new()
405    }
406}
407
408/// Webhook notification system
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct WebhookNotification {
411    /// Job ID
412    pub job_id: Uuid,
413    /// Job status
414    pub status: JobStatus,
415    /// Result ID (if completed successfully)
416    pub result_id: Option<Uuid>,
417    /// Error message (if failed)
418    pub error: Option<String>,
419    /// Timestamp
420    pub timestamp: DateTime<Utc>,
421}
422
423impl WebhookNotification {
424    /// Create a new webhook notification
425    pub fn new(job_id: Uuid, status: JobStatus) -> Self {
426        Self {
427            job_id,
428            status,
429            result_id: None,
430            error: None,
431            timestamp: Utc::now(),
432        }
433    }
434
435    /// Set result ID
436    pub fn with_result_id(mut self, result_id: Uuid) -> Self {
437        self.result_id = Some(result_id);
438        self
439    }
440
441    /// Set error message
442    pub fn with_error(mut self, error: impl Into<String>) -> Self {
443        self.error = Some(error.into());
444        self
445    }
446}
447
448/// Webhook delivery system
449#[derive(Debug)]
450pub struct WebhookDelivery {
451    pending: VecDeque<(String, WebhookNotification)>,
452    delivered: Vec<(String, WebhookNotification, DateTime<Utc>)>,
453}
454
455impl WebhookDelivery {
456    /// Create a new webhook delivery system
457    pub fn new() -> Self {
458        Self {
459            pending: VecDeque::new(),
460            delivered: Vec::new(),
461        }
462    }
463
464    /// Queue a webhook notification
465    pub fn queue(&mut self, url: String, notification: WebhookNotification) {
466        self.pending.push_back((url, notification));
467    }
468
469    /// Get next pending webhook
470    pub fn pop_pending(&mut self) -> Option<(String, WebhookNotification)> {
471        self.pending.pop_front()
472    }
473
474    /// Mark a webhook as delivered
475    pub fn mark_delivered(&mut self, url: String, notification: WebhookNotification) {
476        self.delivered.push((url, notification, Utc::now()));
477    }
478
479    /// Get pending count
480    pub fn pending_count(&self) -> usize {
481        self.pending.len()
482    }
483
484    /// Get delivered count
485    pub fn delivered_count(&self) -> usize {
486        self.delivered.len()
487    }
488
489    /// Get all delivered webhooks
490    pub fn get_delivered(&self) -> &[(String, WebhookNotification, DateTime<Utc>)] {
491        &self.delivered
492    }
493}
494
495impl Default for WebhookDelivery {
496    fn default() -> Self {
497        Self::new()
498    }
499}
500
501/// Comparison API for comparing simulation results
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct SimulationComparison {
504    /// Comparison ID
505    pub id: Uuid,
506    /// Result IDs being compared
507    pub result_ids: Vec<Uuid>,
508    /// Comparison metrics
509    pub metrics: HashMap<String, serde_json::Value>,
510    /// Timestamp
511    pub created_at: DateTime<Utc>,
512}
513
514impl SimulationComparison {
515    /// Create a new comparison
516    pub fn new(result_ids: Vec<Uuid>) -> Self {
517        Self {
518            id: Uuid::new_v4(),
519            result_ids,
520            metrics: HashMap::new(),
521            created_at: Utc::now(),
522        }
523    }
524
525    /// Add a metric to the comparison
526    pub fn add_metric(&mut self, key: impl Into<String>, value: serde_json::Value) {
527        self.metrics.insert(key.into(), value);
528    }
529}
530
531/// API for comparing simulation results
532#[derive(Debug)]
533pub struct ComparisonAPI {
534    comparisons: HashMap<Uuid, SimulationComparison>,
535}
536
537impl ComparisonAPI {
538    /// Create a new comparison API
539    pub fn new() -> Self {
540        Self {
541            comparisons: HashMap::new(),
542        }
543    }
544
545    /// Compare two or more simulation results
546    pub fn compare(&mut self, storage: &ResultStorage, result_ids: Vec<Uuid>) -> SimResult<Uuid> {
547        if result_ids.len() < 2 {
548            return Err(crate::SimulationError::InvalidConfiguration(
549                "Need at least 2 results to compare".to_string(),
550            ));
551        }
552
553        // Verify all results exist
554        for id in &result_ids {
555            if storage.get(id).is_none() {
556                return Err(crate::SimulationError::ExecutionError(format!(
557                    "Result {} not found",
558                    id
559                )));
560            }
561        }
562
563        let mut comparison = SimulationComparison::new(result_ids.clone());
564
565        // Calculate comparison metrics
566        let results: Vec<_> = result_ids.iter().filter_map(|id| storage.get(id)).collect();
567
568        // Compare total applications
569        let applications: Vec<_> = results
570            .iter()
571            .map(|r| r.metrics.total_applications)
572            .collect();
573        comparison.add_metric("total_applications", serde_json::json!(applications));
574
575        // Compare deterministic vs discretion counts
576        let deterministic: Vec<_> = results
577            .iter()
578            .map(|r| r.metrics.deterministic_count)
579            .collect();
580        comparison.add_metric("deterministic_count", serde_json::json!(deterministic));
581
582        let discretion: Vec<_> = results.iter().map(|r| r.metrics.discretion_count).collect();
583        comparison.add_metric("discretion_count", serde_json::json!(discretion));
584
585        // Compare averages
586        if !applications.is_empty() {
587            let avg = applications.iter().sum::<usize>() as f64 / applications.len() as f64;
588            comparison.add_metric("avg_applications", serde_json::json!(avg));
589        }
590
591        let id = comparison.id;
592        self.comparisons.insert(id, comparison);
593        Ok(id)
594    }
595
596    /// Get a comparison by ID
597    pub fn get(&self, id: &Uuid) -> Option<&SimulationComparison> {
598        self.comparisons.get(id)
599    }
600
601    /// Get all comparisons
602    pub fn all_comparisons(&self) -> Vec<&SimulationComparison> {
603        self.comparisons.values().collect()
604    }
605
606    /// Delete a comparison
607    pub fn delete(&mut self, id: &Uuid) -> bool {
608        self.comparisons.remove(id).is_some()
609    }
610}
611
612impl Default for ComparisonAPI {
613    fn default() -> Self {
614        Self::new()
615    }
616}
617
618/// Simulation-as-a-Service API
619#[derive(Debug)]
620pub struct SimulationAPI {
621    job_queue: Arc<Mutex<JobQueue>>,
622    result_storage: Arc<Mutex<ResultStorage>>,
623    webhook_delivery: Arc<Mutex<WebhookDelivery>>,
624    comparison_api: Arc<Mutex<ComparisonAPI>>,
625}
626
627impl SimulationAPI {
628    /// Create a new simulation API
629    pub fn new() -> Self {
630        Self {
631            job_queue: Arc::new(Mutex::new(JobQueue::new())),
632            result_storage: Arc::new(Mutex::new(ResultStorage::new())),
633            webhook_delivery: Arc::new(Mutex::new(WebhookDelivery::new())),
634            comparison_api: Arc::new(Mutex::new(ComparisonAPI::new())),
635        }
636    }
637
638    /// Create API with persistent storage
639    pub fn with_storage(path: impl Into<PathBuf>) -> Self {
640        Self {
641            job_queue: Arc::new(Mutex::new(JobQueue::new())),
642            result_storage: Arc::new(Mutex::new(ResultStorage::with_path(path))),
643            webhook_delivery: Arc::new(Mutex::new(WebhookDelivery::new())),
644            comparison_api: Arc::new(Mutex::new(ComparisonAPI::new())),
645        }
646    }
647
648    /// Submit a simulation job
649    pub fn submit_job(&self, job: SimulationJob) -> Uuid {
650        let mut queue = self.job_queue.lock().unwrap();
651        queue.submit(job)
652    }
653
654    /// Get next job from queue
655    pub fn get_next_job(&self) -> Option<SimulationJob> {
656        let mut queue = self.job_queue.lock().unwrap();
657        queue.pop()
658    }
659
660    /// Get job status
661    pub fn get_job(&self, id: &Uuid) -> Option<SimulationJob> {
662        let queue = self.job_queue.lock().unwrap();
663        queue.get(id).cloned()
664    }
665
666    /// Cancel a job
667    pub fn cancel_job(&self, id: &Uuid) -> bool {
668        let mut queue = self.job_queue.lock().unwrap();
669        queue.cancel(id)
670    }
671
672    /// Complete a job and store result
673    pub fn complete_job(&self, job_id: Uuid, metrics: SimulationMetrics) -> SimResult<Uuid> {
674        // Update job status
675        {
676            let mut queue = self.job_queue.lock().unwrap();
677            if let Some(job) = queue.get_mut(&job_id) {
678                job.complete();
679            }
680        }
681
682        // Store result
683        let result = SimulationResult::new(job_id, metrics);
684        let result_id = {
685            let mut storage = self.result_storage.lock().unwrap();
686            storage.store(result)?
687        };
688
689        // Queue webhook notification
690        {
691            let queue = self.job_queue.lock().unwrap();
692            if let Some(job) = queue.get(&job_id)
693                && let Some(ref webhook_url) = job.webhook_url
694            {
695                let notification = WebhookNotification::new(job_id, JobStatus::Completed)
696                    .with_result_id(result_id);
697                let mut delivery = self.webhook_delivery.lock().unwrap();
698                delivery.queue(webhook_url.clone(), notification);
699            }
700        }
701
702        Ok(result_id)
703    }
704
705    /// Fail a job
706    pub fn fail_job(&self, job_id: Uuid, error: impl Into<String>) -> SimResult<()> {
707        let error_msg = error.into();
708
709        // Update job status
710        {
711            let mut queue = self.job_queue.lock().unwrap();
712            if let Some(job) = queue.get_mut(&job_id) {
713                job.fail(error_msg.clone());
714            }
715        }
716
717        // Queue webhook notification
718        {
719            let queue = self.job_queue.lock().unwrap();
720            if let Some(job) = queue.get(&job_id)
721                && let Some(ref webhook_url) = job.webhook_url
722            {
723                let notification =
724                    WebhookNotification::new(job_id, JobStatus::Failed).with_error(error_msg);
725                let mut delivery = self.webhook_delivery.lock().unwrap();
726                delivery.queue(webhook_url.clone(), notification);
727            }
728        }
729
730        Ok(())
731    }
732
733    /// Get simulation result
734    pub fn get_result(&self, id: &Uuid) -> Option<SimulationResult> {
735        let storage = self.result_storage.lock().unwrap();
736        storage.get(id).cloned()
737    }
738
739    /// Get all results for a job
740    pub fn get_job_results(&self, job_id: &Uuid) -> Vec<SimulationResult> {
741        let storage = self.result_storage.lock().unwrap();
742        storage.get_by_job(job_id).into_iter().cloned().collect()
743    }
744
745    /// Compare simulation results
746    pub fn compare_results(&self, result_ids: Vec<Uuid>) -> SimResult<Uuid> {
747        let storage = self.result_storage.lock().unwrap();
748        let mut comparison_api = self.comparison_api.lock().unwrap();
749        comparison_api.compare(&storage, result_ids)
750    }
751
752    /// Get comparison
753    pub fn get_comparison(&self, id: &Uuid) -> Option<SimulationComparison> {
754        let comparison_api = self.comparison_api.lock().unwrap();
755        comparison_api.get(id).cloned()
756    }
757
758    /// Get next pending webhook
759    pub fn get_next_webhook(&self) -> Option<(String, WebhookNotification)> {
760        let mut delivery = self.webhook_delivery.lock().unwrap();
761        delivery.pop_pending()
762    }
763
764    /// Mark webhook as delivered
765    pub fn mark_webhook_delivered(&self, url: String, notification: WebhookNotification) {
766        let mut delivery = self.webhook_delivery.lock().unwrap();
767        delivery.mark_delivered(url, notification);
768    }
769
770    /// Get queue statistics
771    pub fn get_queue_stats(&self) -> QueueStats {
772        let queue = self.job_queue.lock().unwrap();
773        QueueStats {
774            total_jobs: queue.total_jobs(),
775            queued: queue.jobs_by_status(JobStatus::Queued).len(),
776            running: queue.jobs_by_status(JobStatus::Running).len(),
777            completed: queue.jobs_by_status(JobStatus::Completed).len(),
778            failed: queue.jobs_by_status(JobStatus::Failed).len(),
779            cancelled: queue.jobs_by_status(JobStatus::Cancelled).len(),
780        }
781    }
782}
783
784impl Default for SimulationAPI {
785    fn default() -> Self {
786        Self::new()
787    }
788}
789
790/// Queue statistics
791#[derive(Debug, Clone, Serialize, Deserialize)]
792pub struct QueueStats {
793    pub total_jobs: usize,
794    pub queued: usize,
795    pub running: usize,
796    pub completed: usize,
797    pub failed: usize,
798    pub cancelled: usize,
799}
800
801#[cfg(test)]
802mod tests {
803    use super::*;
804
805    #[test]
806    fn test_job_priority() {
807        assert!(JobPriority::Critical > JobPriority::High);
808        assert!(JobPriority::High > JobPriority::Normal);
809        assert!(JobPriority::Normal > JobPriority::Low);
810    }
811
812    #[test]
813    fn test_job_creation() {
814        let config = serde_json::json!({"test": "data"});
815        let job = SimulationJob::new("test job", config.clone());
816
817        assert_eq!(job.name, "test job");
818        assert_eq!(job.status, JobStatus::Queued);
819        assert_eq!(job.priority, JobPriority::Normal);
820        assert_eq!(job.config, config);
821    }
822
823    #[test]
824    fn test_job_with_priority() {
825        let config = serde_json::json!({});
826        let job = SimulationJob::new("test", config).with_priority(JobPriority::High);
827
828        assert_eq!(job.priority, JobPriority::High);
829    }
830
831    #[test]
832    fn test_job_with_webhook() {
833        let config = serde_json::json!({});
834        let job = SimulationJob::new("test", config).with_webhook("https://example.com/webhook");
835
836        assert_eq!(
837            job.webhook_url,
838            Some("https://example.com/webhook".to_string())
839        );
840    }
841
842    #[test]
843    fn test_job_lifecycle() {
844        let config = serde_json::json!({});
845        let mut job = SimulationJob::new("test", config);
846
847        assert_eq!(job.status, JobStatus::Queued);
848        assert!(job.started_at.is_none());
849
850        job.start();
851        assert_eq!(job.status, JobStatus::Running);
852        assert!(job.started_at.is_some());
853
854        job.complete();
855        assert_eq!(job.status, JobStatus::Completed);
856        assert!(job.completed_at.is_some());
857        assert!(job.duration_ms().is_some());
858    }
859
860    #[test]
861    fn test_job_failure() {
862        let config = serde_json::json!({});
863        let mut job = SimulationJob::new("test", config);
864
865        job.start();
866        job.fail("Test error");
867
868        assert_eq!(job.status, JobStatus::Failed);
869        assert_eq!(job.error, Some("Test error".to_string()));
870    }
871
872    #[test]
873    fn test_job_queue_submit() {
874        let mut queue = JobQueue::new();
875        let config = serde_json::json!({});
876        let job = SimulationJob::new("test", config);
877        let id = job.id;
878
879        let submitted_id = queue.submit(job);
880        assert_eq!(id, submitted_id);
881        assert_eq!(queue.total_jobs(), 1);
882    }
883
884    #[test]
885    fn test_job_queue_priority() {
886        let mut queue = JobQueue::new();
887
888        let low = SimulationJob::new("low", serde_json::json!({})).with_priority(JobPriority::Low);
889        let high =
890            SimulationJob::new("high", serde_json::json!({})).with_priority(JobPriority::High);
891        let normal =
892            SimulationJob::new("normal", serde_json::json!({})).with_priority(JobPriority::Normal);
893
894        queue.submit(low);
895        queue.submit(high.clone());
896        queue.submit(normal);
897
898        // Should get high priority first
899        let next = queue.pop().unwrap();
900        assert_eq!(next.name, "high");
901        assert_eq!(next.status, JobStatus::Running);
902    }
903
904    #[test]
905    fn test_job_queue_cancel() {
906        let mut queue = JobQueue::new();
907        let config = serde_json::json!({});
908        let job = SimulationJob::new("test", config);
909        let id = queue.submit(job);
910
911        assert!(queue.cancel(&id));
912
913        let cancelled = queue.get(&id).unwrap();
914        assert_eq!(cancelled.status, JobStatus::Cancelled);
915    }
916
917    #[test]
918    fn test_result_storage() {
919        let mut storage = ResultStorage::new();
920        let metrics = SimulationMetrics::default();
921        let job_id = Uuid::new_v4();
922        let result = SimulationResult::new(job_id, metrics);
923
924        let id = storage.store(result).unwrap();
925        assert!(storage.get(&id).is_some());
926        assert_eq!(storage.count(), 1);
927    }
928
929    #[test]
930    fn test_result_storage_get_by_job() {
931        let mut storage = ResultStorage::new();
932        let job_id = Uuid::new_v4();
933
934        let result1 = SimulationResult::new(job_id, SimulationMetrics::default());
935        let result2 = SimulationResult::new(job_id, SimulationMetrics::default());
936
937        storage.store(result1).unwrap();
938        storage.store(result2).unwrap();
939
940        let results = storage.get_by_job(&job_id);
941        assert_eq!(results.len(), 2);
942    }
943
944    #[test]
945    fn test_webhook_notification() {
946        let job_id = Uuid::new_v4();
947        let result_id = Uuid::new_v4();
948
949        let notification =
950            WebhookNotification::new(job_id, JobStatus::Completed).with_result_id(result_id);
951
952        assert_eq!(notification.job_id, job_id);
953        assert_eq!(notification.status, JobStatus::Completed);
954        assert_eq!(notification.result_id, Some(result_id));
955    }
956
957    #[test]
958    fn test_webhook_delivery() {
959        let mut delivery = WebhookDelivery::new();
960        let notification = WebhookNotification::new(Uuid::new_v4(), JobStatus::Completed);
961
962        delivery.queue("https://example.com".to_string(), notification.clone());
963        assert_eq!(delivery.pending_count(), 1);
964
965        let (url, notif) = delivery.pop_pending().unwrap();
966        assert_eq!(url, "https://example.com");
967        assert_eq!(delivery.pending_count(), 0);
968
969        delivery.mark_delivered(url, notif);
970        assert_eq!(delivery.delivered_count(), 1);
971    }
972
973    #[test]
974    fn test_comparison_api() {
975        let mut storage = ResultStorage::new();
976        let mut api = ComparisonAPI::new();
977
978        let job_id1 = Uuid::new_v4();
979        let job_id2 = Uuid::new_v4();
980
981        let result1 = SimulationResult::new(job_id1, SimulationMetrics::default());
982        let result2 = SimulationResult::new(job_id2, SimulationMetrics::default());
983
984        let id1 = storage.store(result1).unwrap();
985        let id2 = storage.store(result2).unwrap();
986
987        let comparison_id = api.compare(&storage, vec![id1, id2]).unwrap();
988        let comparison = api.get(&comparison_id).unwrap();
989
990        assert_eq!(comparison.result_ids.len(), 2);
991        assert!(comparison.metrics.contains_key("total_applications"));
992    }
993
994    #[test]
995    fn test_comparison_api_requires_two_results() {
996        let storage = ResultStorage::new();
997        let mut api = ComparisonAPI::new();
998
999        let result = api.compare(&storage, vec![Uuid::new_v4()]);
1000        assert!(result.is_err());
1001    }
1002
1003    #[test]
1004    fn test_simulation_api() {
1005        let api = SimulationAPI::new();
1006        let config = serde_json::json!({});
1007        let job = SimulationJob::new("test", config);
1008
1009        api.submit_job(job);
1010
1011        let stats = api.get_queue_stats();
1012        assert_eq!(stats.total_jobs, 1);
1013        assert_eq!(stats.queued, 1);
1014    }
1015
1016    #[test]
1017    fn test_simulation_api_complete_job() {
1018        let api = SimulationAPI::new();
1019        let config = serde_json::json!({});
1020        let job = SimulationJob::new("test", config);
1021        let job_id = job.id;
1022
1023        api.submit_job(job);
1024
1025        let metrics = SimulationMetrics::default();
1026        let result_id = api.complete_job(job_id, metrics).unwrap();
1027
1028        assert!(api.get_result(&result_id).is_some());
1029
1030        let stats = api.get_queue_stats();
1031        assert_eq!(stats.completed, 1);
1032    }
1033
1034    #[test]
1035    fn test_simulation_api_fail_job() {
1036        let api = SimulationAPI::new();
1037        let config = serde_json::json!({});
1038        let job = SimulationJob::new("test", config);
1039
1040        let job_id = api.submit_job(job);
1041        api.fail_job(job_id, "Test error").unwrap();
1042
1043        let stats = api.get_queue_stats();
1044        assert_eq!(stats.failed, 1);
1045    }
1046
1047    #[test]
1048    fn test_simulation_api_with_webhook() {
1049        let api = SimulationAPI::new();
1050        let config = serde_json::json!({});
1051        let job = SimulationJob::new("test", config).with_webhook("https://example.com/webhook");
1052        let job_id = job.id;
1053
1054        api.submit_job(job);
1055
1056        let metrics = SimulationMetrics::default();
1057        api.complete_job(job_id, metrics).unwrap();
1058
1059        let webhook = api.get_next_webhook();
1060        assert!(webhook.is_some());
1061
1062        let (url, notification) = webhook.unwrap();
1063        assert_eq!(url, "https://example.com/webhook");
1064        assert_eq!(notification.status, JobStatus::Completed);
1065    }
1066
1067    #[test]
1068    fn test_simulation_api_compare_results() {
1069        let api = SimulationAPI::new();
1070
1071        let config = serde_json::json!({});
1072        let job1 = SimulationJob::new("test1", config.clone());
1073        let job2 = SimulationJob::new("test2", config);
1074        let job_id1 = job1.id;
1075        let job_id2 = job2.id;
1076
1077        api.submit_job(job1);
1078        api.submit_job(job2);
1079
1080        let result_id1 = api
1081            .complete_job(job_id1, SimulationMetrics::default())
1082            .unwrap();
1083        let result_id2 = api
1084            .complete_job(job_id2, SimulationMetrics::default())
1085            .unwrap();
1086
1087        let comparison_id = api.compare_results(vec![result_id1, result_id2]).unwrap();
1088        let comparison = api.get_comparison(&comparison_id).unwrap();
1089
1090        assert_eq!(comparison.result_ids.len(), 2);
1091    }
1092}