1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
16pub enum JobPriority {
17 Low = 0,
19 #[default]
21 Normal = 1,
22 High = 2,
24 Critical = 3,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum JobStatus {
31 Queued,
33 Running,
35 Completed,
37 Failed,
39 Cancelled,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct SimulationJob {
46 pub id: Uuid,
48 pub name: String,
50 pub priority: JobPriority,
52 pub status: JobStatus,
54 pub created_at: DateTime<Utc>,
56 pub started_at: Option<DateTime<Utc>>,
58 pub completed_at: Option<DateTime<Utc>>,
60 pub webhook_url: Option<String>,
62 pub config: serde_json::Value,
64 pub error: Option<String>,
66}
67
68impl SimulationJob {
69 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 pub fn with_priority(mut self, priority: JobPriority) -> Self {
87 self.priority = priority;
88 self
89 }
90
91 pub fn with_webhook(mut self, url: impl Into<String>) -> Self {
93 self.webhook_url = Some(url.into());
94 self
95 }
96
97 pub fn start(&mut self) {
99 self.status = JobStatus::Running;
100 self.started_at = Some(Utc::now());
101 }
102
103 pub fn complete(&mut self) {
105 self.status = JobStatus::Completed;
106 self.completed_at = Some(Utc::now());
107 }
108
109 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 pub fn cancel(&mut self) {
118 self.status = JobStatus::Cancelled;
119 self.completed_at = Some(Utc::now());
120 }
121
122 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#[derive(Debug)]
133struct JobWrapper {
134 job: SimulationJob,
135 sequence: u64, }
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 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#[derive(Debug)]
164pub struct JobQueue {
165 queue: BinaryHeap<JobWrapper>,
166 jobs: HashMap<Uuid, SimulationJob>,
167 sequence_counter: u64,
168}
169
170impl JobQueue {
171 pub fn new() -> Self {
173 Self {
174 queue: BinaryHeap::new(),
175 jobs: HashMap::new(),
176 sequence_counter: 0,
177 }
178 }
179
180 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 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 pub fn get(&self, id: &Uuid) -> Option<&SimulationJob> {
205 self.jobs.get(id)
206 }
207
208 pub fn get_mut(&mut self, id: &Uuid) -> Option<&mut SimulationJob> {
210 self.jobs.get_mut(id)
211 }
212
213 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 pub fn all_jobs(&self) -> Vec<&SimulationJob> {
226 self.jobs.values().collect()
227 }
228
229 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 pub fn queue_size(&self) -> usize {
239 self.queue.len()
240 }
241
242 pub fn total_jobs(&self) -> usize {
244 self.jobs.len()
245 }
246
247 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#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct SimulationResult {
263 pub id: Uuid,
265 pub job_id: Uuid,
267 pub metrics: SimulationMetrics,
269 pub metadata: HashMap<String, serde_json::Value>,
271 pub stored_at: DateTime<Utc>,
273}
274
275impl SimulationResult {
276 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 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#[derive(Debug)]
296pub struct ResultStorage {
297 results: HashMap<Uuid, SimulationResult>,
298 storage_path: Option<PathBuf>,
299}
300
301impl ResultStorage {
302 pub fn new() -> Self {
304 Self {
305 results: HashMap::new(),
306 storage_path: None,
307 }
308 }
309
310 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 pub fn store(&mut self, result: SimulationResult) -> SimResult<Uuid> {
320 let id = result.id;
321
322 self.results.insert(id, result.clone());
324
325 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 pub fn get(&self, id: &Uuid) -> Option<&SimulationResult> {
338 self.results.get(id)
339 }
340
341 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 pub fn all_results(&self) -> Vec<&SimulationResult> {
351 self.results.values().collect()
352 }
353
354 pub fn delete(&mut self, id: &Uuid) -> bool {
356 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 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 pub fn clear(&mut self) {
393 self.results.clear();
394 }
395
396 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#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct WebhookNotification {
411 pub job_id: Uuid,
413 pub status: JobStatus,
415 pub result_id: Option<Uuid>,
417 pub error: Option<String>,
419 pub timestamp: DateTime<Utc>,
421}
422
423impl WebhookNotification {
424 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 pub fn with_result_id(mut self, result_id: Uuid) -> Self {
437 self.result_id = Some(result_id);
438 self
439 }
440
441 pub fn with_error(mut self, error: impl Into<String>) -> Self {
443 self.error = Some(error.into());
444 self
445 }
446}
447
448#[derive(Debug)]
450pub struct WebhookDelivery {
451 pending: VecDeque<(String, WebhookNotification)>,
452 delivered: Vec<(String, WebhookNotification, DateTime<Utc>)>,
453}
454
455impl WebhookDelivery {
456 pub fn new() -> Self {
458 Self {
459 pending: VecDeque::new(),
460 delivered: Vec::new(),
461 }
462 }
463
464 pub fn queue(&mut self, url: String, notification: WebhookNotification) {
466 self.pending.push_back((url, notification));
467 }
468
469 pub fn pop_pending(&mut self) -> Option<(String, WebhookNotification)> {
471 self.pending.pop_front()
472 }
473
474 pub fn mark_delivered(&mut self, url: String, notification: WebhookNotification) {
476 self.delivered.push((url, notification, Utc::now()));
477 }
478
479 pub fn pending_count(&self) -> usize {
481 self.pending.len()
482 }
483
484 pub fn delivered_count(&self) -> usize {
486 self.delivered.len()
487 }
488
489 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#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct SimulationComparison {
504 pub id: Uuid,
506 pub result_ids: Vec<Uuid>,
508 pub metrics: HashMap<String, serde_json::Value>,
510 pub created_at: DateTime<Utc>,
512}
513
514impl SimulationComparison {
515 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 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#[derive(Debug)]
533pub struct ComparisonAPI {
534 comparisons: HashMap<Uuid, SimulationComparison>,
535}
536
537impl ComparisonAPI {
538 pub fn new() -> Self {
540 Self {
541 comparisons: HashMap::new(),
542 }
543 }
544
545 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 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 let results: Vec<_> = result_ids.iter().filter_map(|id| storage.get(id)).collect();
567
568 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 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 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 pub fn get(&self, id: &Uuid) -> Option<&SimulationComparison> {
598 self.comparisons.get(id)
599 }
600
601 pub fn all_comparisons(&self) -> Vec<&SimulationComparison> {
603 self.comparisons.values().collect()
604 }
605
606 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#[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 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 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 pub fn submit_job(&self, job: SimulationJob) -> Uuid {
650 let mut queue = self.job_queue.lock().unwrap();
651 queue.submit(job)
652 }
653
654 pub fn get_next_job(&self) -> Option<SimulationJob> {
656 let mut queue = self.job_queue.lock().unwrap();
657 queue.pop()
658 }
659
660 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 pub fn cancel_job(&self, id: &Uuid) -> bool {
668 let mut queue = self.job_queue.lock().unwrap();
669 queue.cancel(id)
670 }
671
672 pub fn complete_job(&self, job_id: Uuid, metrics: SimulationMetrics) -> SimResult<Uuid> {
674 {
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 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 {
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 pub fn fail_job(&self, job_id: Uuid, error: impl Into<String>) -> SimResult<()> {
707 let error_msg = error.into();
708
709 {
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 {
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 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 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 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 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 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 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 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#[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 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}