solverforge_core/solver/
change.rs

1//! ProblemChange API for real-time planning.
2//!
3//! This module provides the ability to dynamically add, remove, or modify
4//! planning entities and problem facts during solving.
5//!
6//! # Architecture
7//!
8//! Problem changes are queued and applied between solver moves. When a problem
9//! change is applied:
10//!
11//! 1. The solver clones the current best solution
12//! 2. The problem change is applied via the ProblemChangeDirector
13//! 3. Variable listeners are triggered
14//! 4. The score is recalculated
15//! 5. Solving resumes from the modified state
16//!
17//! # Example
18//!
19//! ```ignore
20//! use solverforge_core::solver::change::{ProblemChange, ProblemChangeDirector};
21//!
22//! struct AddEntityChange {
23//!     entity_id: String,
24//!     entity_data: serde_json::Value,
25//! }
26//!
27//! impl ProblemChange for AddEntityChange {
28//!     fn do_change(&self, solution: &mut serde_json::Value, director: &mut dyn ProblemChangeDirector) {
29//!         director.add_entity(&self.entity_id, self.entity_data.clone(), |entities| {
30//!             entities.as_array_mut().unwrap().push(self.entity_data.clone());
31//!         });
32//!     }
33//! }
34//! ```
35
36use serde::{Deserialize, Serialize};
37use std::fmt::Debug;
38
39/// A problem change represents a modification to the planning solution during solving.
40///
41/// Problem changes allow real-time planning by dynamically adding, removing, or modifying
42/// planning entities and problem facts while the solver is running.
43///
44/// All modifications to the solution must be performed through the `ProblemChangeDirector`
45/// to ensure that variable listeners are properly notified and the score is correctly updated.
46pub trait ProblemChange: Send + Sync + Debug {
47    /// Apply the change to the working solution.
48    ///
49    /// # Arguments
50    ///
51    /// * `solution` - The working solution to modify (as JSON value)
52    /// * `director` - The director through which all modifications must be made
53    fn do_change(&self, solution: &mut serde_json::Value, director: &mut dyn ProblemChangeDirector);
54
55    /// Convert to a serializable DTO for transmission to the solver service.
56    fn to_dto(&self) -> ProblemChangeDto;
57}
58
59/// Type alias for consumer functions used by ProblemChangeDirector.
60pub type ChangeConsumer = Box<dyn FnOnce(&mut serde_json::Value) + Send>;
61
62/// Director for applying problem changes to the working solution.
63///
64/// All modifications to the solution during a problem change must go through
65/// this director to ensure proper tracking and variable listener notification.
66pub trait ProblemChangeDirector: Send {
67    /// Add a new planning entity to the solution.
68    ///
69    /// # Arguments
70    ///
71    /// * `entity_id` - Unique identifier for the entity (typically the @PlanningId value)
72    /// * `entity` - The entity data as JSON
73    /// * `consumer` - Function that adds the entity to the appropriate collection in the solution
74    fn add_entity(&mut self, entity_id: &str, entity: serde_json::Value, consumer: ChangeConsumer);
75
76    /// Remove a planning entity from the solution.
77    ///
78    /// The entity is looked up by its ID and the working copy is passed to the consumer.
79    ///
80    /// # Arguments
81    ///
82    /// * `entity_id` - The ID of the entity to remove
83    /// * `consumer` - Function that removes the entity from the appropriate collection
84    fn remove_entity(&mut self, entity_id: &str, consumer: ChangeConsumer);
85
86    /// Change a planning variable on an entity.
87    ///
88    /// # Arguments
89    ///
90    /// * `entity_id` - The ID of the entity to modify
91    /// * `variable_name` - Name of the planning variable to change
92    /// * `consumer` - Function that updates the variable value
93    fn change_variable(&mut self, entity_id: &str, variable_name: &str, consumer: ChangeConsumer);
94
95    /// Add a new problem fact to the solution.
96    ///
97    /// # Arguments
98    ///
99    /// * `fact_id` - Unique identifier for the problem fact
100    /// * `fact` - The problem fact data as JSON
101    /// * `consumer` - Function that adds the fact to the appropriate collection
102    fn add_problem_fact(
103        &mut self,
104        fact_id: &str,
105        fact: serde_json::Value,
106        consumer: ChangeConsumer,
107    );
108
109    /// Remove a problem fact from the solution.
110    ///
111    /// # Arguments
112    ///
113    /// * `fact_id` - The ID of the problem fact to remove
114    /// * `consumer` - Function that removes the fact from the appropriate collection
115    fn remove_problem_fact(&mut self, fact_id: &str, consumer: ChangeConsumer);
116
117    /// Change a property on an entity or problem fact.
118    ///
119    /// # Arguments
120    ///
121    /// * `object_id` - The ID of the entity or problem fact
122    /// * `consumer` - Function that updates the property
123    fn change_problem_property(&mut self, object_id: &str, consumer: ChangeConsumer);
124
125    /// Look up a working object by its external ID.
126    ///
127    /// Returns a clone of the working object, or an error if not found.
128    ///
129    /// # Arguments
130    ///
131    /// * `external_id` - The ID of the object to look up
132    fn look_up_working_object_or_fail(
133        &self,
134        external_id: &str,
135    ) -> Result<serde_json::Value, ProblemChangeError>;
136
137    /// Look up a working object by its external ID.
138    ///
139    /// Returns `None` if the object is not found, rather than failing.
140    ///
141    /// # Arguments
142    ///
143    /// * `external_id` - The ID of the object to look up
144    fn look_up_working_object(&self, external_id: &str) -> Option<serde_json::Value>;
145
146    /// Trigger variable listeners for changes made so far.
147    ///
148    /// This is called automatically after the entire problem change is processed,
149    /// but can be called manually to trigger listeners mid-change.
150    fn update_shadow_variables(&mut self);
151}
152
153/// Error type for problem change operations.
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub enum ProblemChangeError {
156    /// The requested object was not found in the working solution.
157    ObjectNotFound { id: String },
158    /// The object type is not supported for lookup.
159    UnsupportedType { type_name: String },
160    /// A validation error occurred during the change.
161    ValidationError { message: String },
162    /// The change could not be applied.
163    ApplicationError { message: String },
164}
165
166impl std::fmt::Display for ProblemChangeError {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            ProblemChangeError::ObjectNotFound { id } => {
170                write!(f, "Object not found: {}", id)
171            }
172            ProblemChangeError::UnsupportedType { type_name } => {
173                write!(f, "Unsupported type for lookup: {}", type_name)
174            }
175            ProblemChangeError::ValidationError { message } => {
176                write!(f, "Validation error: {}", message)
177            }
178            ProblemChangeError::ApplicationError { message } => {
179                write!(f, "Application error: {}", message)
180            }
181        }
182    }
183}
184
185impl std::error::Error for ProblemChangeError {}
186
187/// Serializable DTO for transmitting problem changes to the solver service.
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(tag = "type")]
190pub enum ProblemChangeDto {
191    /// Add a planning entity.
192    #[serde(rename = "addEntity")]
193    AddEntity {
194        /// The entity type/class name.
195        entity_class: String,
196        /// Unique identifier for the entity.
197        entity_id: String,
198        /// The entity data as JSON.
199        entity: serde_json::Value,
200        /// The field path in the solution where entities are stored.
201        collection_path: String,
202    },
203
204    /// Remove a planning entity.
205    #[serde(rename = "removeEntity")]
206    RemoveEntity {
207        /// The entity type/class name.
208        entity_class: String,
209        /// Unique identifier of the entity to remove.
210        entity_id: String,
211        /// The field path in the solution where entities are stored.
212        collection_path: String,
213    },
214
215    /// Change a planning variable.
216    #[serde(rename = "changeVariable")]
217    ChangeVariable {
218        /// The entity type/class name.
219        entity_class: String,
220        /// Unique identifier of the entity.
221        entity_id: String,
222        /// Name of the planning variable to change.
223        variable_name: String,
224        /// The new value for the variable (null to unassign).
225        new_value: serde_json::Value,
226    },
227
228    /// Add a problem fact.
229    #[serde(rename = "addProblemFact")]
230    AddProblemFact {
231        /// The problem fact type/class name.
232        fact_class: String,
233        /// Unique identifier for the fact.
234        fact_id: String,
235        /// The problem fact data as JSON.
236        fact: serde_json::Value,
237        /// The field path in the solution where facts are stored.
238        collection_path: String,
239    },
240
241    /// Remove a problem fact.
242    #[serde(rename = "removeProblemFact")]
243    RemoveProblemFact {
244        /// The problem fact type/class name.
245        fact_class: String,
246        /// Unique identifier of the fact to remove.
247        fact_id: String,
248        /// The field path in the solution where facts are stored.
249        collection_path: String,
250    },
251
252    /// Change a property on an entity or problem fact.
253    #[serde(rename = "changeProblemProperty")]
254    ChangeProblemProperty {
255        /// The object type/class name.
256        object_class: String,
257        /// Unique identifier of the object.
258        object_id: String,
259        /// Name of the property to change.
260        property_name: String,
261        /// The new value for the property.
262        new_value: serde_json::Value,
263    },
264
265    /// A batch of problem changes to apply atomically.
266    #[serde(rename = "batch")]
267    Batch {
268        /// The changes to apply in order.
269        changes: Vec<ProblemChangeDto>,
270    },
271}
272
273impl ProblemChangeDto {
274    /// Create an AddEntity change.
275    pub fn add_entity(
276        entity_class: impl Into<String>,
277        entity_id: impl Into<String>,
278        entity: serde_json::Value,
279        collection_path: impl Into<String>,
280    ) -> Self {
281        ProblemChangeDto::AddEntity {
282            entity_class: entity_class.into(),
283            entity_id: entity_id.into(),
284            entity,
285            collection_path: collection_path.into(),
286        }
287    }
288
289    /// Create a RemoveEntity change.
290    pub fn remove_entity(
291        entity_class: impl Into<String>,
292        entity_id: impl Into<String>,
293        collection_path: impl Into<String>,
294    ) -> Self {
295        ProblemChangeDto::RemoveEntity {
296            entity_class: entity_class.into(),
297            entity_id: entity_id.into(),
298            collection_path: collection_path.into(),
299        }
300    }
301
302    /// Create a ChangeVariable change.
303    pub fn change_variable(
304        entity_class: impl Into<String>,
305        entity_id: impl Into<String>,
306        variable_name: impl Into<String>,
307        new_value: serde_json::Value,
308    ) -> Self {
309        ProblemChangeDto::ChangeVariable {
310            entity_class: entity_class.into(),
311            entity_id: entity_id.into(),
312            variable_name: variable_name.into(),
313            new_value,
314        }
315    }
316
317    /// Create an AddProblemFact change.
318    pub fn add_problem_fact(
319        fact_class: impl Into<String>,
320        fact_id: impl Into<String>,
321        fact: serde_json::Value,
322        collection_path: impl Into<String>,
323    ) -> Self {
324        ProblemChangeDto::AddProblemFact {
325            fact_class: fact_class.into(),
326            fact_id: fact_id.into(),
327            fact,
328            collection_path: collection_path.into(),
329        }
330    }
331
332    /// Create a RemoveProblemFact change.
333    pub fn remove_problem_fact(
334        fact_class: impl Into<String>,
335        fact_id: impl Into<String>,
336        collection_path: impl Into<String>,
337    ) -> Self {
338        ProblemChangeDto::RemoveProblemFact {
339            fact_class: fact_class.into(),
340            fact_id: fact_id.into(),
341            collection_path: collection_path.into(),
342        }
343    }
344
345    /// Create a ChangeProblemProperty change.
346    pub fn change_problem_property(
347        object_class: impl Into<String>,
348        object_id: impl Into<String>,
349        property_name: impl Into<String>,
350        new_value: serde_json::Value,
351    ) -> Self {
352        ProblemChangeDto::ChangeProblemProperty {
353            object_class: object_class.into(),
354            object_id: object_id.into(),
355            property_name: property_name.into(),
356            new_value,
357        }
358    }
359
360    /// Create a batch of changes.
361    pub fn batch(changes: Vec<ProblemChangeDto>) -> Self {
362        ProblemChangeDto::Batch { changes }
363    }
364}
365
366/// Default implementation of ProblemChangeDirector for local use.
367///
368/// This implementation tracks changes and maintains an index of working objects
369/// for lookup operations.
370#[derive(Debug)]
371pub struct DefaultProblemChangeDirector {
372    /// Index of object IDs to their JSON values.
373    object_index: std::collections::HashMap<String, serde_json::Value>,
374    /// Record of changes made.
375    changes: Vec<ChangeRecord>,
376    /// Whether shadow variables need updating.
377    shadow_variables_dirty: bool,
378}
379
380/// Record of a change made through the director.
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub enum ChangeRecord {
383    /// A planning entity was added.
384    EntityAdded {
385        /// The ID of the added entity.
386        id: String,
387    },
388    /// A planning entity was removed.
389    EntityRemoved {
390        /// The ID of the removed entity.
391        id: String,
392    },
393    /// A planning variable was changed.
394    VariableChanged {
395        /// The ID of the entity whose variable changed.
396        entity_id: String,
397        /// The name of the variable that changed.
398        variable: String,
399    },
400    /// A problem fact was added.
401    FactAdded {
402        /// The ID of the added fact.
403        id: String,
404    },
405    /// A problem fact was removed.
406    FactRemoved {
407        /// The ID of the removed fact.
408        id: String,
409    },
410    /// A property on an entity or fact was changed.
411    PropertyChanged {
412        /// The ID of the object whose property changed.
413        object_id: String,
414    },
415}
416
417impl DefaultProblemChangeDirector {
418    /// Create a new director with the given working solution.
419    ///
420    /// # Arguments
421    ///
422    /// * `solution` - The working solution JSON
423    /// * `id_field` - The field name used for object IDs (typically "id")
424    pub fn new(solution: &serde_json::Value, id_field: &str) -> Self {
425        let mut object_index = std::collections::HashMap::new();
426        Self::index_objects(solution, id_field, &mut object_index);
427
428        Self {
429            object_index,
430            changes: Vec::new(),
431            shadow_variables_dirty: false,
432        }
433    }
434
435    /// Recursively index all objects with IDs in the solution.
436    fn index_objects(
437        value: &serde_json::Value,
438        id_field: &str,
439        index: &mut std::collections::HashMap<String, serde_json::Value>,
440    ) {
441        match value {
442            serde_json::Value::Object(map) => {
443                if let Some(serde_json::Value::String(id)) = map.get(id_field) {
444                    index.insert(id.clone(), value.clone());
445                }
446                for v in map.values() {
447                    Self::index_objects(v, id_field, index);
448                }
449            }
450            serde_json::Value::Array(arr) => {
451                for v in arr {
452                    Self::index_objects(v, id_field, index);
453                }
454            }
455            _ => {}
456        }
457    }
458
459    /// Get the changes recorded by this director.
460    pub fn changes(&self) -> &[ChangeRecord] {
461        &self.changes
462    }
463
464    /// Check if any changes were made.
465    pub fn has_changes(&self) -> bool {
466        !self.changes.is_empty()
467    }
468
469    /// Update the object index with a new or modified object.
470    pub fn update_index(&mut self, id: String, value: serde_json::Value) {
471        self.object_index.insert(id, value);
472    }
473
474    /// Remove an object from the index.
475    pub fn remove_from_index(&mut self, id: &str) {
476        self.object_index.remove(id);
477    }
478}
479
480impl ProblemChangeDirector for DefaultProblemChangeDirector {
481    fn add_entity(&mut self, entity_id: &str, entity: serde_json::Value, consumer: ChangeConsumer) {
482        self.object_index
483            .insert(entity_id.to_string(), entity.clone());
484        self.changes.push(ChangeRecord::EntityAdded {
485            id: entity_id.to_string(),
486        });
487        self.shadow_variables_dirty = true;
488
489        // The consumer is expected to add the entity to a mutable reference to the solution
490        // In practice, this is called with a reference to the appropriate collection
491        let mut entity_copy = entity;
492        consumer(&mut entity_copy);
493    }
494
495    fn remove_entity(&mut self, entity_id: &str, consumer: ChangeConsumer) {
496        if let Some(mut entity) = self.object_index.remove(entity_id) {
497            consumer(&mut entity);
498            self.changes.push(ChangeRecord::EntityRemoved {
499                id: entity_id.to_string(),
500            });
501            self.shadow_variables_dirty = true;
502        }
503    }
504
505    fn change_variable(&mut self, entity_id: &str, variable_name: &str, consumer: ChangeConsumer) {
506        if let Some(entity) = self.object_index.get_mut(entity_id) {
507            consumer(entity);
508            self.changes.push(ChangeRecord::VariableChanged {
509                entity_id: entity_id.to_string(),
510                variable: variable_name.to_string(),
511            });
512            self.shadow_variables_dirty = true;
513        }
514    }
515
516    fn add_problem_fact(
517        &mut self,
518        fact_id: &str,
519        fact: serde_json::Value,
520        consumer: ChangeConsumer,
521    ) {
522        self.object_index.insert(fact_id.to_string(), fact.clone());
523        self.changes.push(ChangeRecord::FactAdded {
524            id: fact_id.to_string(),
525        });
526
527        let mut fact_copy = fact;
528        consumer(&mut fact_copy);
529    }
530
531    fn remove_problem_fact(&mut self, fact_id: &str, consumer: ChangeConsumer) {
532        if let Some(mut fact) = self.object_index.remove(fact_id) {
533            consumer(&mut fact);
534            self.changes.push(ChangeRecord::FactRemoved {
535                id: fact_id.to_string(),
536            });
537        }
538    }
539
540    fn change_problem_property(&mut self, object_id: &str, consumer: ChangeConsumer) {
541        if let Some(object) = self.object_index.get_mut(object_id) {
542            consumer(object);
543            self.changes.push(ChangeRecord::PropertyChanged {
544                object_id: object_id.to_string(),
545            });
546        }
547    }
548
549    fn look_up_working_object_or_fail(
550        &self,
551        external_id: &str,
552    ) -> Result<serde_json::Value, ProblemChangeError> {
553        self.object_index.get(external_id).cloned().ok_or_else(|| {
554            ProblemChangeError::ObjectNotFound {
555                id: external_id.to_string(),
556            }
557        })
558    }
559
560    fn look_up_working_object(&self, external_id: &str) -> Option<serde_json::Value> {
561        self.object_index.get(external_id).cloned()
562    }
563
564    fn update_shadow_variables(&mut self) {
565        // In the actual implementation, this would trigger variable listeners
566        // For now, we just mark that shadow variables have been updated
567        self.shadow_variables_dirty = false;
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use serde_json::json;
575
576    // Test ProblemChangeError display
577    #[test]
578    fn test_error_display_object_not_found() {
579        let err = ProblemChangeError::ObjectNotFound {
580            id: "entity1".to_string(),
581        };
582        assert_eq!(format!("{}", err), "Object not found: entity1");
583    }
584
585    #[test]
586    fn test_error_display_unsupported_type() {
587        let err = ProblemChangeError::UnsupportedType {
588            type_name: "UnknownType".to_string(),
589        };
590        assert_eq!(
591            format!("{}", err),
592            "Unsupported type for lookup: UnknownType"
593        );
594    }
595
596    #[test]
597    fn test_error_display_validation() {
598        let err = ProblemChangeError::ValidationError {
599            message: "ID cannot be empty".to_string(),
600        };
601        assert_eq!(format!("{}", err), "Validation error: ID cannot be empty");
602    }
603
604    #[test]
605    fn test_error_display_application() {
606        let err = ProblemChangeError::ApplicationError {
607            message: "Failed to apply change".to_string(),
608        };
609        assert_eq!(
610            format!("{}", err),
611            "Application error: Failed to apply change"
612        );
613    }
614
615    // Test ProblemChangeDto constructors
616    #[test]
617    fn test_add_entity_dto() {
618        let dto = ProblemChangeDto::add_entity(
619            "Lesson",
620            "lesson1",
621            json!({"id": "lesson1", "subject": "Math"}),
622            "lessons",
623        );
624
625        match dto {
626            ProblemChangeDto::AddEntity {
627                entity_class,
628                entity_id,
629                entity,
630                collection_path,
631            } => {
632                assert_eq!(entity_class, "Lesson");
633                assert_eq!(entity_id, "lesson1");
634                assert_eq!(entity["subject"], "Math");
635                assert_eq!(collection_path, "lessons");
636            }
637            _ => panic!("Expected AddEntity"),
638        }
639    }
640
641    #[test]
642    fn test_remove_entity_dto() {
643        let dto = ProblemChangeDto::remove_entity("Lesson", "lesson1", "lessons");
644
645        match dto {
646            ProblemChangeDto::RemoveEntity {
647                entity_class,
648                entity_id,
649                collection_path,
650            } => {
651                assert_eq!(entity_class, "Lesson");
652                assert_eq!(entity_id, "lesson1");
653                assert_eq!(collection_path, "lessons");
654            }
655            _ => panic!("Expected RemoveEntity"),
656        }
657    }
658
659    #[test]
660    fn test_change_variable_dto() {
661        let dto = ProblemChangeDto::change_variable("Lesson", "lesson1", "room", json!("Room101"));
662
663        match dto {
664            ProblemChangeDto::ChangeVariable {
665                entity_class,
666                entity_id,
667                variable_name,
668                new_value,
669            } => {
670                assert_eq!(entity_class, "Lesson");
671                assert_eq!(entity_id, "lesson1");
672                assert_eq!(variable_name, "room");
673                assert_eq!(new_value, json!("Room101"));
674            }
675            _ => panic!("Expected ChangeVariable"),
676        }
677    }
678
679    #[test]
680    fn test_change_variable_dto_null() {
681        let dto =
682            ProblemChangeDto::change_variable("Lesson", "lesson1", "room", serde_json::Value::Null);
683
684        match dto {
685            ProblemChangeDto::ChangeVariable { new_value, .. } => {
686                assert!(new_value.is_null());
687            }
688            _ => panic!("Expected ChangeVariable"),
689        }
690    }
691
692    #[test]
693    fn test_add_problem_fact_dto() {
694        let dto = ProblemChangeDto::add_problem_fact(
695            "Room",
696            "room1",
697            json!({"id": "room1", "capacity": 30}),
698            "rooms",
699        );
700
701        match dto {
702            ProblemChangeDto::AddProblemFact {
703                fact_class,
704                fact_id,
705                fact,
706                collection_path,
707            } => {
708                assert_eq!(fact_class, "Room");
709                assert_eq!(fact_id, "room1");
710                assert_eq!(fact["capacity"], 30);
711                assert_eq!(collection_path, "rooms");
712            }
713            _ => panic!("Expected AddProblemFact"),
714        }
715    }
716
717    #[test]
718    fn test_remove_problem_fact_dto() {
719        let dto = ProblemChangeDto::remove_problem_fact("Room", "room1", "rooms");
720
721        match dto {
722            ProblemChangeDto::RemoveProblemFact {
723                fact_class,
724                fact_id,
725                collection_path,
726            } => {
727                assert_eq!(fact_class, "Room");
728                assert_eq!(fact_id, "room1");
729                assert_eq!(collection_path, "rooms");
730            }
731            _ => panic!("Expected RemoveProblemFact"),
732        }
733    }
734
735    #[test]
736    fn test_change_problem_property_dto() {
737        let dto = ProblemChangeDto::change_problem_property("Room", "room1", "capacity", json!(50));
738
739        match dto {
740            ProblemChangeDto::ChangeProblemProperty {
741                object_class,
742                object_id,
743                property_name,
744                new_value,
745            } => {
746                assert_eq!(object_class, "Room");
747                assert_eq!(object_id, "room1");
748                assert_eq!(property_name, "capacity");
749                assert_eq!(new_value, json!(50));
750            }
751            _ => panic!("Expected ChangeProblemProperty"),
752        }
753    }
754
755    #[test]
756    fn test_batch_dto() {
757        let changes = vec![
758            ProblemChangeDto::add_entity("Lesson", "lesson1", json!({"id": "lesson1"}), "lessons"),
759            ProblemChangeDto::change_variable("Lesson", "lesson1", "room", json!("Room101")),
760        ];
761
762        let dto = ProblemChangeDto::batch(changes);
763
764        match dto {
765            ProblemChangeDto::Batch { changes } => {
766                assert_eq!(changes.len(), 2);
767            }
768            _ => panic!("Expected Batch"),
769        }
770    }
771
772    // Test DTO serialization
773    #[test]
774    fn test_add_entity_dto_serialization() {
775        let dto = ProblemChangeDto::add_entity(
776            "Lesson",
777            "lesson1",
778            json!({"id": "lesson1", "subject": "Math"}),
779            "lessons",
780        );
781
782        let json = serde_json::to_string(&dto).unwrap();
783        assert!(json.contains(r#""type":"addEntity""#));
784        assert!(json.contains(r#""entity_class":"Lesson""#));
785        assert!(json.contains(r#""entity_id":"lesson1""#));
786
787        let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
788        assert_eq!(dto, parsed);
789    }
790
791    #[test]
792    fn test_remove_entity_dto_serialization() {
793        let dto = ProblemChangeDto::remove_entity("Lesson", "lesson1", "lessons");
794
795        let json = serde_json::to_string(&dto).unwrap();
796        assert!(json.contains(r#""type":"removeEntity""#));
797
798        let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
799        assert_eq!(dto, parsed);
800    }
801
802    #[test]
803    fn test_change_variable_dto_serialization() {
804        let dto = ProblemChangeDto::change_variable("Lesson", "lesson1", "room", json!("Room101"));
805
806        let json = serde_json::to_string(&dto).unwrap();
807        assert!(json.contains(r#""type":"changeVariable""#));
808        assert!(json.contains(r#""variable_name":"room""#));
809
810        let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
811        assert_eq!(dto, parsed);
812    }
813
814    #[test]
815    fn test_batch_dto_serialization() {
816        let dto = ProblemChangeDto::batch(vec![
817            ProblemChangeDto::add_entity("Lesson", "l1", json!({"id": "l1"}), "lessons"),
818            ProblemChangeDto::remove_entity("Lesson", "l2", "lessons"),
819        ]);
820
821        let json = serde_json::to_string(&dto).unwrap();
822        assert!(json.contains(r#""type":"batch""#));
823        assert!(json.contains(r#""changes""#));
824
825        let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
826        assert_eq!(dto, parsed);
827    }
828
829    // Test DefaultProblemChangeDirector
830    #[test]
831    fn test_director_new_indexes_objects() {
832        let solution = json!({
833            "lessons": [
834                {"id": "lesson1", "subject": "Math"},
835                {"id": "lesson2", "subject": "English"}
836            ],
837            "rooms": [
838                {"id": "room1", "capacity": 30}
839            ]
840        });
841
842        let director = DefaultProblemChangeDirector::new(&solution, "id");
843
844        assert!(director.look_up_working_object("lesson1").is_some());
845        assert!(director.look_up_working_object("lesson2").is_some());
846        assert!(director.look_up_working_object("room1").is_some());
847        assert!(director.look_up_working_object("nonexistent").is_none());
848    }
849
850    #[test]
851    fn test_director_look_up_or_fail() {
852        let solution = json!({
853            "entities": [{"id": "e1", "value": 10}]
854        });
855
856        let director = DefaultProblemChangeDirector::new(&solution, "id");
857
858        let result = director.look_up_working_object_or_fail("e1");
859        assert!(result.is_ok());
860        assert_eq!(result.unwrap()["value"], 10);
861
862        let result = director.look_up_working_object_or_fail("nonexistent");
863        assert!(result.is_err());
864        match result.unwrap_err() {
865            ProblemChangeError::ObjectNotFound { id } => {
866                assert_eq!(id, "nonexistent");
867            }
868            _ => panic!("Expected ObjectNotFound error"),
869        }
870    }
871
872    #[test]
873    fn test_director_add_entity() {
874        let solution = json!({"entities": []});
875        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
876
877        let new_entity = json!({"id": "e1", "value": 42});
878        director.add_entity("e1", new_entity.clone(), Box::new(|_| {}));
879
880        assert!(director.look_up_working_object("e1").is_some());
881        assert!(director.has_changes());
882
883        let changes = director.changes();
884        assert_eq!(changes.len(), 1);
885        matches!(&changes[0], ChangeRecord::EntityAdded { id } if id == "e1");
886    }
887
888    #[test]
889    fn test_director_remove_entity() {
890        let solution = json!({
891            "entities": [{"id": "e1", "value": 42}]
892        });
893        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
894
895        assert!(director.look_up_working_object("e1").is_some());
896
897        director.remove_entity("e1", Box::new(|_| {}));
898
899        assert!(director.look_up_working_object("e1").is_none());
900        assert!(director.has_changes());
901    }
902
903    #[test]
904    fn test_director_change_variable() {
905        let solution = json!({
906            "entities": [{"id": "e1", "room": null}]
907        });
908        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
909
910        director.change_variable(
911            "e1",
912            "room",
913            Box::new(|entity| {
914                entity["room"] = json!("Room101");
915            }),
916        );
917
918        let entity = director.look_up_working_object("e1").unwrap();
919        assert_eq!(entity["room"], json!("Room101"));
920        assert!(director.has_changes());
921    }
922
923    #[test]
924    fn test_director_add_problem_fact() {
925        let solution = json!({"facts": []});
926        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
927
928        let fact = json!({"id": "f1", "name": "Fact1"});
929        director.add_problem_fact("f1", fact, Box::new(|_| {}));
930
931        assert!(director.look_up_working_object("f1").is_some());
932        assert!(director.has_changes());
933    }
934
935    #[test]
936    fn test_director_remove_problem_fact() {
937        let solution = json!({
938            "facts": [{"id": "f1", "name": "Fact1"}]
939        });
940        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
941
942        director.remove_problem_fact("f1", Box::new(|_| {}));
943
944        assert!(director.look_up_working_object("f1").is_none());
945        assert!(director.has_changes());
946    }
947
948    #[test]
949    fn test_director_change_problem_property() {
950        let solution = json!({
951            "facts": [{"id": "f1", "capacity": 30}]
952        });
953        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
954
955        director.change_problem_property(
956            "f1",
957            Box::new(|obj| {
958                obj["capacity"] = json!(50);
959            }),
960        );
961
962        let fact = director.look_up_working_object("f1").unwrap();
963        assert_eq!(fact["capacity"], json!(50));
964        assert!(director.has_changes());
965    }
966
967    #[test]
968    fn test_director_update_shadow_variables() {
969        let solution = json!({"entities": []});
970        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
971
972        director.add_entity("e1", json!({"id": "e1"}), Box::new(|_| {}));
973        assert!(director.shadow_variables_dirty);
974
975        director.update_shadow_variables();
976        assert!(!director.shadow_variables_dirty);
977    }
978
979    #[test]
980    fn test_director_no_changes_initially() {
981        let solution = json!({"entities": []});
982        let director = DefaultProblemChangeDirector::new(&solution, "id");
983
984        assert!(!director.has_changes());
985        assert!(director.changes().is_empty());
986    }
987
988    #[test]
989    fn test_director_multiple_changes() {
990        let solution = json!({
991            "entities": [{"id": "e1", "room": null}],
992            "rooms": [{"id": "r1", "capacity": 30}]
993        });
994        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
995
996        director.add_entity("e2", json!({"id": "e2", "room": null}), Box::new(|_| {}));
997        director.change_variable(
998            "e1",
999            "room",
1000            Box::new(|e| {
1001                e["room"] = json!("r1");
1002            }),
1003        );
1004        director.remove_problem_fact("r1", Box::new(|_| {}));
1005
1006        assert_eq!(director.changes().len(), 3);
1007    }
1008
1009    #[test]
1010    fn test_director_nested_objects() {
1011        let solution = json!({
1012            "schedule": {
1013                "lessons": [
1014                    {
1015                        "id": "l1",
1016                        "teacher": {"id": "t1", "name": "Alice"}
1017                    }
1018                ]
1019            }
1020        });
1021
1022        let director = DefaultProblemChangeDirector::new(&solution, "id");
1023
1024        // Both lesson and nested teacher should be indexed
1025        assert!(director.look_up_working_object("l1").is_some());
1026        assert!(director.look_up_working_object("t1").is_some());
1027    }
1028
1029    #[test]
1030    fn test_director_update_and_remove_from_index() {
1031        let solution = json!({"entities": []});
1032        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
1033
1034        director.update_index("manual1".to_string(), json!({"id": "manual1", "value": 1}));
1035        assert!(director.look_up_working_object("manual1").is_some());
1036
1037        director.remove_from_index("manual1");
1038        assert!(director.look_up_working_object("manual1").is_none());
1039    }
1040
1041    // Test error serialization
1042    #[test]
1043    fn test_error_serialization() {
1044        let err = ProblemChangeError::ObjectNotFound {
1045            id: "test".to_string(),
1046        };
1047        let json = serde_json::to_string(&err).unwrap();
1048        let parsed: ProblemChangeError = serde_json::from_str(&json).unwrap();
1049        assert_eq!(err, parsed);
1050    }
1051
1052    // Test edge cases
1053    #[test]
1054    fn test_director_remove_nonexistent_entity() {
1055        let solution = json!({"entities": []});
1056        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
1057
1058        // Should not panic, just do nothing
1059        director.remove_entity("nonexistent", Box::new(|_| {}));
1060        assert!(!director.has_changes());
1061    }
1062
1063    #[test]
1064    fn test_director_change_variable_nonexistent() {
1065        let solution = json!({"entities": []});
1066        let mut director = DefaultProblemChangeDirector::new(&solution, "id");
1067
1068        // Should not panic, just do nothing
1069        director.change_variable("nonexistent", "room", Box::new(|_| {}));
1070        assert!(!director.has_changes());
1071    }
1072
1073    #[test]
1074    fn test_dto_with_complex_entity() {
1075        let complex_entity = json!({
1076            "id": "lesson1",
1077            "subject": "Advanced Mathematics",
1078            "teacher": {"id": "t1", "name": "Dr. Smith"},
1079            "students": [
1080                {"id": "s1", "name": "Alice"},
1081                {"id": "s2", "name": "Bob"}
1082            ],
1083            "schedule": {
1084                "day": "Monday",
1085                "period": 3
1086            }
1087        });
1088
1089        let dto =
1090            ProblemChangeDto::add_entity("Lesson", "lesson1", complex_entity.clone(), "lessons");
1091
1092        let json = serde_json::to_string(&dto).unwrap();
1093        let parsed: ProblemChangeDto = serde_json::from_str(&json).unwrap();
1094
1095        match parsed {
1096            ProblemChangeDto::AddEntity { entity, .. } => {
1097                assert_eq!(entity, complex_entity);
1098            }
1099            _ => panic!("Expected AddEntity"),
1100        }
1101    }
1102
1103    #[test]
1104    fn test_director_empty_solution() {
1105        let solution = json!({});
1106        let director = DefaultProblemChangeDirector::new(&solution, "id");
1107
1108        assert!(director.look_up_working_object("any").is_none());
1109    }
1110
1111    #[test]
1112    fn test_director_array_at_root() {
1113        let solution = json!([
1114            {"id": "e1", "value": 1},
1115            {"id": "e2", "value": 2}
1116        ]);
1117
1118        let director = DefaultProblemChangeDirector::new(&solution, "id");
1119
1120        assert!(director.look_up_working_object("e1").is_some());
1121        assert!(director.look_up_working_object("e2").is_some());
1122    }
1123
1124    #[test]
1125    fn test_director_different_id_field() {
1126        let solution = json!({
1127            "entities": [
1128                {"uuid": "abc-123", "name": "Entity1"},
1129                {"uuid": "def-456", "name": "Entity2"}
1130            ]
1131        });
1132
1133        let director = DefaultProblemChangeDirector::new(&solution, "uuid");
1134
1135        assert!(director.look_up_working_object("abc-123").is_some());
1136        assert!(director.look_up_working_object("def-456").is_some());
1137        assert!(director.look_up_working_object("Entity1").is_none());
1138    }
1139}