optirs_core/research/
peer_review.rs

1// Peer review tools and anonymous review systems
2//
3// This module provides tools for managing peer review processes,
4// including anonymous reviews, reviewer assignment, and review quality assessment.
5
6use crate::error::{OptimError, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Peer review system manager
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PeerReviewSystem {
14    /// Review sessions
15    pub sessions: HashMap<String, ReviewSession>,
16    /// Reviewer pool
17    pub reviewers: HashMap<String, Reviewer>,
18    /// Review assignments
19    pub assignments: Vec<ReviewAssignment>,
20    /// Review quality metrics
21    pub quality_metrics: Vec<ReviewQualityMetric>,
22}
23
24/// Review session for a paper or project
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ReviewSession {
27    /// Session ID
28    pub id: String,
29    /// Paper/project ID
30    pub submission_id: String,
31    /// Review type
32    pub review_type: ReviewType,
33    /// Session status
34    pub status: ReviewSessionStatus,
35    /// Review criteria
36    pub criteria: Vec<ReviewCriterion>,
37    /// Deadline
38    pub deadline: DateTime<Utc>,
39    /// Reviews collected
40    pub reviews: Vec<PeerReview>,
41    /// Meta-review
42    pub meta_review: Option<MetaReview>,
43    /// Discussion thread
44    pub discussion: Vec<ReviewDiscussion>,
45}
46
47/// Types of peer review
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub enum ReviewType {
50    /// Single-blind review
51    SingleBlind,
52    /// Double-blind review
53    DoubleBlind,
54    /// Open review
55    Open,
56    /// Post-publication review
57    PostPublication,
58    /// Internal review
59    Internal,
60}
61
62/// Review session status
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub enum ReviewSessionStatus {
65    /// Waiting for reviewers
66    WaitingForReviewers,
67    /// Reviews in progress
68    InProgress,
69    /// Reviews complete
70    ReviewsComplete,
71    /// Meta-review in progress
72    MetaReviewInProgress,
73    /// Session complete
74    Complete,
75    /// Session cancelled
76    Cancelled,
77}
78
79/// Review criterion
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ReviewCriterion {
82    /// Criterion name
83    pub name: String,
84    /// Description
85    pub description: String,
86    /// Score range
87    pub score_range: (f64, f64),
88    /// Weight in overall score
89    pub weight: f64,
90    /// Required for review
91    pub required: bool,
92}
93
94/// Individual peer review
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct PeerReview {
97    /// Review ID
98    pub id: String,
99    /// Anonymous reviewer ID
100    pub reviewer_id: String,
101    /// Overall recommendation
102    pub recommendation: ReviewRecommendation,
103    /// Scores per criterion
104    pub criterion_scores: HashMap<String, f64>,
105    /// Overall score
106    pub overall_score: f64,
107    /// Confidence level
108    pub confidence: f64,
109    /// Written review
110    pub written_review: WrittenReview,
111    /// Review status
112    pub status: ReviewStatus,
113    /// Submission timestamp
114    pub submitted_at: Option<DateTime<Utc>>,
115    /// Time spent on review (minutes)
116    pub time_spent_minutes: Option<u32>,
117}
118
119/// Review recommendations
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
121pub enum ReviewRecommendation {
122    /// Strong accept
123    StrongAccept,
124    /// Accept
125    Accept,
126    /// Weak accept
127    WeakAccept,
128    /// Borderline accept
129    BorderlineAccept,
130    /// Borderline reject
131    BorderlineReject,
132    /// Weak reject
133    WeakReject,
134    /// Reject
135    Reject,
136    /// Strong reject
137    StrongReject,
138}
139
140/// Written review components
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct WrittenReview {
143    /// Summary
144    pub summary: String,
145    /// Strengths
146    pub strengths: Vec<String>,
147    /// Weaknesses
148    pub weaknesses: Vec<String>,
149    /// Detailed comments
150    pub detailed_comments: String,
151    /// Questions for authors
152    pub questions: Vec<String>,
153    /// Minor issues
154    pub minor_issues: Vec<String>,
155    /// Suggestions for improvement
156    pub suggestions: Vec<String>,
157    /// Comments for committee only
158    pub committee_comments: Option<String>,
159}
160
161/// Review status
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163pub enum ReviewStatus {
164    /// Assigned but not started
165    Assigned,
166    /// In progress
167    InProgress,
168    /// Draft completed
169    Draft,
170    /// Submitted
171    Submitted,
172    /// Revision requested
173    RevisionRequested,
174    /// Declined
175    Declined,
176    /// Overdue
177    Overdue,
178}
179
180/// Meta-review (review of reviews)
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct MetaReview {
183    /// Meta-reviewer ID
184    pub meta_reviewer_id: String,
185    /// Summary of individual reviews
186    pub review_summary: String,
187    /// Final recommendation
188    pub final_recommendation: ReviewRecommendation,
189    /// Justification
190    pub justification: String,
191    /// Review quality assessment
192    pub review_quality: Vec<ReviewQualityAssessment>,
193    /// Areas of agreement
194    pub areas_of_agreement: Vec<String>,
195    /// Areas of disagreement
196    pub areas_of_disagreement: Vec<String>,
197    /// Decision rationale
198    pub decision_rationale: String,
199}
200
201/// Assessment of individual review quality
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ReviewQualityAssessment {
204    /// Review ID
205    pub review_id: String,
206    /// Quality dimensions
207    pub quality_scores: HashMap<String, f64>,
208    /// Overall quality score
209    pub overall_quality: f64,
210    /// Helpfulness to authors
211    pub helpfulness: f64,
212    /// Comments on review quality
213    pub comments: String,
214}
215
216/// Review discussion thread
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ReviewDiscussion {
219    /// Post ID
220    pub id: String,
221    /// Author (anonymous)
222    pub author: String,
223    /// Post content
224    pub content: String,
225    /// Reply to post ID
226    pub reply_to: Option<String>,
227    /// Post timestamp
228    pub posted_at: DateTime<Utc>,
229    /// Post type
230    pub post_type: DiscussionPostType,
231}
232
233/// Types of discussion posts
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235pub enum DiscussionPostType {
236    /// Question
237    Question,
238    /// Answer
239    Answer,
240    /// Clarification
241    Clarification,
242    /// Disagreement
243    Disagreement,
244    /// Consensus
245    Consensus,
246    /// Moderator message
247    ModeratorMessage,
248}
249
250/// Reviewer information
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct Reviewer {
253    /// Anonymous reviewer ID
254    pub id: String,
255    /// Expertise areas
256    pub expertise_areas: Vec<String>,
257    /// Experience level
258    pub experience_level: ExperienceLevel,
259    /// Review history
260    pub review_history: ReviewerHistory,
261    /// Availability
262    pub availability: ReviewerAvailability,
263    /// Quality metrics
264    pub quality_metrics: ReviewerQualityMetrics,
265    /// Preferences
266    pub preferences: ReviewerPreferences,
267}
268
269/// Reviewer experience levels
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
271pub enum ExperienceLevel {
272    /// Expert reviewer
273    Expert,
274    /// Senior reviewer
275    Senior,
276    /// Experienced reviewer
277    Experienced,
278    /// Junior reviewer
279    Junior,
280    /// Novice reviewer
281    Novice,
282}
283
284/// Reviewer history and statistics
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ReviewerHistory {
287    /// Total reviews completed
288    pub total_reviews: usize,
289    /// Reviews in last 12 months
290    pub reviews_last_year: usize,
291    /// Average review time (days)
292    pub avg_review_time_days: f64,
293    /// On-time submission rate
294    pub on_time_rate: f64,
295    /// Average review quality score
296    pub avg_quality_score: f64,
297    /// Review acceptance rate
298    pub review_acceptance_rate: f64,
299}
300
301/// Reviewer availability
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct ReviewerAvailability {
304    /// Currently available
305    pub available: bool,
306    /// Maximum reviews per month
307    pub max_reviews_per_month: u32,
308    /// Current review load
309    pub current_load: u32,
310    /// Unavailable periods
311    pub unavailable_periods: Vec<(DateTime<Utc>, DateTime<Utc>)>,
312    /// Preferred review types
313    pub preferred_types: Vec<ReviewType>,
314}
315
316/// Reviewer quality metrics
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ReviewerQualityMetrics {
319    /// Thoroughness score
320    pub thoroughness: f64,
321    /// Constructiveness score
322    pub constructiveness: f64,
323    /// Timeliness score
324    pub timeliness: f64,
325    /// Expertise match score
326    pub expertise_match: f64,
327    /// Overall reviewer score
328    pub overall_score: f64,
329}
330
331/// Reviewer preferences
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct ReviewerPreferences {
334    /// Preferred paper types
335    pub preferred_paper_types: Vec<String>,
336    /// Avoid paper types
337    pub avoid_paper_types: Vec<String>,
338    /// Maximum review length preference
339    pub max_review_length: Option<u32>,
340    /// Anonymous review preference
341    pub anonymous_preference: bool,
342    /// Notification preferences
343    pub notification_preferences: NotificationPreferences,
344}
345
346/// Notification preferences for reviewers
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct NotificationPreferences {
349    /// Email notifications
350    pub email: bool,
351    /// Reminder frequency (days)
352    pub reminder_frequency: u32,
353    /// Deadline notifications
354    pub deadline_notifications: bool,
355    /// Discussion notifications
356    pub discussion_notifications: bool,
357}
358
359/// Review assignment
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ReviewAssignment {
362    /// Assignment ID
363    pub id: String,
364    /// Session ID
365    pub session_id: String,
366    /// Reviewer ID
367    pub reviewer_id: String,
368    /// Assignment date
369    pub assigned_at: DateTime<Utc>,
370    /// Due date
371    pub due_date: DateTime<Utc>,
372    /// Assignment status
373    pub status: AssignmentStatus,
374    /// Assignment method
375    pub assignment_method: AssignmentMethod,
376    /// Expertise match score
377    pub expertise_match: f64,
378}
379
380/// Assignment status
381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
382pub enum AssignmentStatus {
383    /// Pending acceptance
384    Pending,
385    /// Accepted
386    Accepted,
387    /// Declined
388    Declined,
389    /// Completed
390    Completed,
391    /// Overdue
392    Overdue,
393    /// Cancelled
394    Cancelled,
395}
396
397/// Assignment methods
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399pub enum AssignmentMethod {
400    /// Manual assignment
401    Manual,
402    /// Automatic based on expertise
403    AutomaticExpertise,
404    /// Automatic load balancing
405    AutomaticLoadBalancing,
406    /// Hybrid assignment
407    Hybrid,
408    /// Self-assignment
409    SelfAssignment,
410}
411
412/// Review quality metric
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct ReviewQualityMetric {
415    /// Metric name
416    pub name: String,
417    /// Description
418    pub description: String,
419    /// Value range
420    pub value_range: (f64, f64),
421    /// Higher is better
422    pub higher_is_better: bool,
423    /// Calculation method
424    pub calculation_method: String,
425}
426
427impl Default for PeerReviewSystem {
428    fn default() -> Self {
429        Self::new()
430    }
431}
432
433impl PeerReviewSystem {
434    /// Create a new peer review system
435    pub fn new() -> Self {
436        Self {
437            sessions: HashMap::new(),
438            reviewers: HashMap::new(),
439            assignments: Vec::new(),
440            quality_metrics: Self::create_default_quality_metrics(),
441        }
442    }
443
444    /// Create a new review session
445    pub fn create_review_session(
446        &mut self,
447        submission_id: &str,
448        review_type: ReviewType,
449        criteria: Vec<ReviewCriterion>,
450        deadline: DateTime<Utc>,
451    ) -> String {
452        let session_id = uuid::Uuid::new_v4().to_string();
453        let session = ReviewSession {
454            id: session_id.clone(),
455            submission_id: submission_id.to_string(),
456            review_type,
457            status: ReviewSessionStatus::WaitingForReviewers,
458            criteria,
459            deadline,
460            reviews: Vec::new(),
461            meta_review: None,
462            discussion: Vec::new(),
463        };
464
465        self.sessions.insert(session_id.clone(), session);
466        session_id
467    }
468
469    /// Assign reviewers to a session
470    pub fn assign_reviewers(
471        &mut self,
472        session_id: &str,
473        reviewer_ids: &[String],
474        assignment_method: AssignmentMethod,
475    ) -> Result<Vec<String>> {
476        if !self.sessions.contains_key(session_id) {
477            return Err(OptimError::InvalidConfig("Session not found".to_string()));
478        }
479
480        let mut assignment_ids = Vec::new();
481        let now = Utc::now();
482        let session = self.sessions.get(session_id).unwrap();
483
484        for reviewer_id in reviewer_ids {
485            if !self.reviewers.contains_key(reviewer_id) {
486                continue; // Skip unknown reviewers
487            }
488
489            let assignment_id = uuid::Uuid::new_v4().to_string();
490            let assignment = ReviewAssignment {
491                id: assignment_id.clone(),
492                session_id: session_id.to_string(),
493                reviewer_id: reviewer_id.clone(),
494                assigned_at: now,
495                due_date: session.deadline,
496                status: AssignmentStatus::Pending,
497                assignment_method: assignment_method.clone(),
498                expertise_match: self.calculate_expertise_match(reviewer_id, session_id),
499            };
500
501            self.assignments.push(assignment);
502            assignment_ids.push(assignment_id);
503        }
504
505        // Update session status
506        if let Some(session) = self.sessions.get_mut(session_id) {
507            session.status = ReviewSessionStatus::InProgress;
508        }
509
510        Ok(assignment_ids)
511    }
512
513    /// Submit a peer review
514    pub fn submit_review(
515        &mut self,
516        session_id: &str,
517        reviewer_id: &str,
518        review: PeerReview,
519    ) -> Result<()> {
520        let session = self
521            .sessions
522            .get_mut(session_id)
523            .ok_or_else(|| OptimError::InvalidConfig("Session not found".to_string()))?;
524
525        // Verify reviewer is assigned
526        let assignment = self
527            .assignments
528            .iter_mut()
529            .find(|a| a.session_id == session_id && a.reviewer_id == reviewer_id)
530            .ok_or_else(|| {
531                OptimError::InvalidConfig("Reviewer not assigned to this session".to_string())
532            })?;
533
534        // Update assignment status
535        assignment.status = AssignmentStatus::Completed;
536
537        // Add review to session
538        session.reviews.push(review);
539
540        // Check if all reviews are complete
541        let total_assignments = self
542            .assignments
543            .iter()
544            .filter(|a| a.session_id == session_id)
545            .count();
546
547        if session.reviews.len() == total_assignments {
548            session.status = ReviewSessionStatus::ReviewsComplete;
549        }
550
551        Ok(())
552    }
553
554    /// Generate meta-review
555    pub fn generate_meta_review(&mut self, session_id: &str, meta_reviewer_id: &str) -> Result<()> {
556        // First, check session status and create meta review with immutable access
557        let meta_review = {
558            let session = self
559                .sessions
560                .get(session_id)
561                .ok_or_else(|| OptimError::InvalidConfig("Session not found".to_string()))?;
562
563            if session.status != ReviewSessionStatus::ReviewsComplete {
564                return Err(OptimError::InvalidConfig(
565                    "Not all reviews are complete".to_string(),
566                ));
567            }
568
569            self.create_meta_review(session, meta_reviewer_id)
570        };
571
572        // Now update the session with mutable access
573        let session = self.sessions.get_mut(session_id).unwrap(); // Safe because we just checked it exists
574        session.meta_review = Some(meta_review);
575        session.status = ReviewSessionStatus::Complete;
576
577        Ok(())
578    }
579
580    /// Calculate reviewer workload
581    pub fn calculate_reviewer_workload(&self, reviewer_id: &str) -> u32 {
582        self.assignments
583            .iter()
584            .filter(|a| {
585                a.reviewer_id == reviewer_id
586                    && matches!(
587                        a.status,
588                        AssignmentStatus::Pending | AssignmentStatus::Accepted
589                    )
590            })
591            .count() as u32
592    }
593
594    /// Get available reviewers for expertise area
595    pub fn get_available_reviewers(&self, expertise_area: &str) -> Vec<&Reviewer> {
596        self.reviewers
597            .values()
598            .filter(|r| {
599                r.availability.available
600                    && r.expertise_areas
601                        .iter()
602                        .any(|area| area.to_lowercase().contains(&expertise_area.to_lowercase()))
603                    && r.availability.current_load < r.availability.max_reviews_per_month
604            })
605            .collect()
606    }
607
608    /// Calculate review quality score
609    pub fn calculate_review_quality(&self, review: &PeerReview) -> f64 {
610        let mut quality_score = 0.0;
611        let mut total_weight = 0.0;
612
613        // Length and detail assessment
614        let review_length = review.written_review.detailed_comments.len()
615            + review
616                .written_review
617                .strengths
618                .iter()
619                .map(|s| s.len())
620                .sum::<usize>()
621            + review
622                .written_review
623                .weaknesses
624                .iter()
625                .map(|s| s.len())
626                .sum::<usize>();
627
628        let length_score = ((review_length as f64).ln() / 10.0).min(1.0);
629        quality_score += length_score * 0.3;
630        total_weight += 0.3;
631
632        // Number of specific points
633        let specific_points = review.written_review.strengths.len()
634            + review.written_review.weaknesses.len()
635            + review.written_review.suggestions.len();
636
637        let specificity_score = (specific_points as f64 / 10.0).min(1.0);
638        quality_score += specificity_score * 0.4;
639        total_weight += 0.4;
640
641        // Confidence level
642        quality_score += review.confidence * 0.3;
643        total_weight += 0.3;
644
645        quality_score / total_weight
646    }
647
648    fn calculate_expertise_match(&self, reviewer_id: &str, sessionid: &str) -> f64 {
649        // Simplified expertise matching
650        // In practice, you'd use more sophisticated matching algorithms
651        if let Some(reviewer) = self.reviewers.get(reviewer_id) {
652            if reviewer.expertise_areas.is_empty() {
653                0.5 // Default moderate match
654            } else {
655                0.8 // Good match if has expertise areas
656            }
657        } else {
658            0.0
659        }
660    }
661
662    fn create_meta_review(&self, session: &ReviewSession, meta_reviewer_id: &str) -> MetaReview {
663        let review_summary = format!("Meta-review of {} reviews", session.reviews.len());
664
665        // Calculate consensus
666        let recommendations: Vec<_> = session.reviews.iter().map(|r| &r.recommendation).collect();
667
668        let final_recommendation = self.determine_consensus_recommendation(&recommendations);
669
670        // Assess review quality
671        let review_quality: Vec<_> = session
672            .reviews
673            .iter()
674            .map(|review| {
675                let quality_score = self.calculate_review_quality(review);
676                ReviewQualityAssessment {
677                    review_id: review.id.clone(),
678                    quality_scores: HashMap::new(),
679                    overall_quality: quality_score,
680                    helpfulness: quality_score * 0.9, // Simplified
681                    comments: if quality_score > 0.7 {
682                        "High quality review".to_string()
683                    } else {
684                        "Review could be more detailed".to_string()
685                    },
686                }
687            })
688            .collect();
689
690        MetaReview {
691            meta_reviewer_id: meta_reviewer_id.to_string(),
692            review_summary,
693            final_recommendation,
694            justification: "Based on consensus of reviewer recommendations".to_string(),
695            review_quality,
696            areas_of_agreement: vec!["Technical quality assessment".to_string()],
697            areas_of_disagreement: vec!["Significance of contribution".to_string()],
698            decision_rationale: "Decision based on majority reviewer consensus".to_string(),
699        }
700    }
701
702    fn determine_consensus_recommendation(
703        &self,
704        recommendations: &[&ReviewRecommendation],
705    ) -> ReviewRecommendation {
706        // Simplified consensus algorithm - use majority vote
707        let mut counts = HashMap::new();
708        for rec in recommendations {
709            *counts.entry(rec).or_insert(0) += 1;
710        }
711
712        counts
713            .into_iter()
714            .max_by_key(|(_, count)| *count)
715            .map(|(rec, _)| (*rec).clone())
716            .unwrap_or(ReviewRecommendation::BorderlineReject)
717    }
718
719    fn create_default_quality_metrics() -> Vec<ReviewQualityMetric> {
720        vec![
721            ReviewQualityMetric {
722                name: "Thoroughness".to_string(),
723                description: "How comprehensive and detailed the review is".to_string(),
724                value_range: (0.0, 1.0),
725                higher_is_better: true,
726                calculation_method: "Based on review length and number of specific points"
727                    .to_string(),
728            },
729            ReviewQualityMetric {
730                name: "Constructiveness".to_string(),
731                description: "How helpful the review is for improving the work".to_string(),
732                value_range: (0.0, 1.0),
733                higher_is_better: true,
734                calculation_method: "Based on number of suggestions and actionable feedback"
735                    .to_string(),
736            },
737            ReviewQualityMetric {
738                name: "Timeliness".to_string(),
739                description: "How promptly the review was submitted".to_string(),
740                value_range: (0.0, 1.0),
741                higher_is_better: true,
742                calculation_method: "Based on submission time relative to deadline".to_string(),
743            },
744        ]
745    }
746}
747
748impl Default for ReviewerHistory {
749    fn default() -> Self {
750        Self {
751            total_reviews: 0,
752            reviews_last_year: 0,
753            avg_review_time_days: 14.0,
754            on_time_rate: 1.0,
755            avg_quality_score: 0.7,
756            review_acceptance_rate: 0.9,
757        }
758    }
759}
760
761impl Default for ReviewerAvailability {
762    fn default() -> Self {
763        Self {
764            available: true,
765            max_reviews_per_month: 5,
766            current_load: 0,
767            unavailable_periods: Vec::new(),
768            preferred_types: vec![ReviewType::DoubleBlind],
769        }
770    }
771}
772
773impl Default for ReviewerQualityMetrics {
774    fn default() -> Self {
775        Self {
776            thoroughness: 0.7,
777            constructiveness: 0.7,
778            timeliness: 0.8,
779            expertise_match: 0.7,
780            overall_score: 0.7,
781        }
782    }
783}
784
785impl Default for NotificationPreferences {
786    fn default() -> Self {
787        Self {
788            email: true,
789            reminder_frequency: 7,
790            deadline_notifications: true,
791            discussion_notifications: false,
792        }
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799
800    #[test]
801    fn test_peer_review_system_creation() {
802        let system = PeerReviewSystem::new();
803        assert!(system.sessions.is_empty());
804        assert!(system.reviewers.is_empty());
805        assert!(!system.quality_metrics.is_empty());
806    }
807
808    #[test]
809    fn test_create_review_session() {
810        let mut system = PeerReviewSystem::new();
811
812        let criteria = vec![ReviewCriterion {
813            name: "Technical Quality".to_string(),
814            description: "Assessment of technical merit".to_string(),
815            score_range: (1.0, 5.0),
816            weight: 0.4,
817            required: true,
818        }];
819
820        let deadline = Utc::now() + chrono::Duration::days(14);
821        let session_id =
822            system.create_review_session("paper123", ReviewType::DoubleBlind, criteria, deadline);
823
824        assert!(system.sessions.contains_key(&session_id));
825        let session = &system.sessions[&session_id];
826        assert_eq!(session.submission_id, "paper123");
827        assert_eq!(session.review_type, ReviewType::DoubleBlind);
828    }
829
830    #[test]
831    fn test_reviewer_workload_calculation() {
832        let mut system = PeerReviewSystem::new();
833
834        // Add some assignments
835        system.assignments.push(ReviewAssignment {
836            id: "assign1".to_string(),
837            session_id: "session1".to_string(),
838            reviewer_id: "reviewer1".to_string(),
839            assigned_at: Utc::now(),
840            due_date: Utc::now() + chrono::Duration::days(14),
841            status: AssignmentStatus::Pending,
842            assignment_method: AssignmentMethod::Manual,
843            expertise_match: 0.8,
844        });
845
846        let workload = system.calculate_reviewer_workload("reviewer1");
847        assert_eq!(workload, 1);
848
849        let workload = system.calculate_reviewer_workload("reviewer2");
850        assert_eq!(workload, 0);
851    }
852}