mt_net/
lib.rs

1pub use ros_pointcloud2::PointCloud2Msg;
2pub use ros2_interfaces_jazzy_rkyv::nav_msgs::msg::Odometry;
3pub use ros2_interfaces_jazzy_rkyv::sensor_msgs::msg::{Imu, PointCloud2};
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7pub const COMPARE_NODE_NAME: &str = "#minot";
8
9#[derive(Clone, Debug, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
10pub enum Qos {
11    Sensor,
12    #[default]
13    SystemDefault,
14    Custom(QosProfile),
15}
16
17#[derive(Deserialize, Debug, Clone, Copy)]
18#[serde(untagged)]
19pub enum HistoryValue {
20    Numeric(i32),
21    String(HistoryPolicy),
22}
23
24#[derive(Deserialize, Debug, Clone, Copy)]
25#[serde(rename_all = "snake_case")]
26pub enum HistoryPolicy {
27    KeepLast,
28    KeepAll,
29}
30
31#[derive(Deserialize, Debug, Clone, Copy)]
32#[serde(untagged)]
33pub enum ReliabilityValue {
34    Numeric(i32),
35    String(ReliabilityPolicy),
36}
37
38#[derive(Deserialize, Debug, Clone, Copy)]
39#[serde(rename_all = "snake_case")]
40pub enum ReliabilityPolicy {
41    Reliable,
42    BestEffort,
43}
44
45#[derive(Deserialize, Debug, Clone, Copy)]
46#[serde(untagged)]
47pub enum DurabilityValue {
48    Numeric(i32),
49    String(DurabilityPolicy),
50}
51
52#[derive(Deserialize, Debug, Clone, Copy)]
53#[serde(rename_all = "snake_case")]
54pub enum DurabilityPolicy {
55    Volatile,
56    TransientLocal,
57}
58
59#[derive(Deserialize, Debug, Clone, Copy)]
60#[serde(untagged)]
61pub enum LivelinessValue {
62    Numeric(i32),
63    String(LivelinessPolicy),
64}
65
66#[derive(Deserialize, Debug, Clone, Copy)]
67#[serde(rename_all = "snake_case")]
68pub enum LivelinessPolicy {
69    Automatic,
70    ManualByTopic,
71}
72
73fn deserialize_history<'de, D>(deserializer: D) -> Result<String, D::Error>
74where
75    D: serde::Deserializer<'de>,
76{
77    let value = HistoryValue::deserialize(deserializer)?;
78    Ok(match value {
79        HistoryValue::Numeric(1) => "keep_last".to_string(),
80        HistoryValue::Numeric(2) => "keep_all".to_string(),
81        HistoryValue::Numeric(_) => "keep_last".to_string(), // default
82        HistoryValue::String(HistoryPolicy::KeepLast) => "keep_last".to_string(),
83        HistoryValue::String(HistoryPolicy::KeepAll) => "keep_all".to_string(),
84    })
85}
86
87fn deserialize_reliability<'de, D>(deserializer: D) -> Result<String, D::Error>
88where
89    D: serde::Deserializer<'de>,
90{
91    let value = ReliabilityValue::deserialize(deserializer)?;
92    Ok(match value {
93        ReliabilityValue::Numeric(1) => "reliable".to_string(),
94        ReliabilityValue::Numeric(2) => "best_effort".to_string(),
95        ReliabilityValue::Numeric(_) => "reliable".to_string(), // default
96        ReliabilityValue::String(ReliabilityPolicy::Reliable) => "reliable".to_string(),
97        ReliabilityValue::String(ReliabilityPolicy::BestEffort) => "best_effort".to_string(),
98    })
99}
100
101fn deserialize_durability<'de, D>(deserializer: D) -> Result<String, D::Error>
102where
103    D: serde::Deserializer<'de>,
104{
105    let value = DurabilityValue::deserialize(deserializer)?;
106    Ok(match value {
107        DurabilityValue::Numeric(1) => "transient_local".to_string(),
108        DurabilityValue::Numeric(2) => "volatile".to_string(),
109        DurabilityValue::Numeric(_) => "volatile".to_string(), // default
110        DurabilityValue::String(DurabilityPolicy::TransientLocal) => "transient_local".to_string(),
111        DurabilityValue::String(DurabilityPolicy::Volatile) => "volatile".to_string(),
112    })
113}
114
115fn deserialize_liveliness<'de, D>(deserializer: D) -> Result<String, D::Error>
116where
117    D: serde::Deserializer<'de>,
118{
119    let value = LivelinessValue::deserialize(deserializer)?;
120    Ok(match value {
121        LivelinessValue::Numeric(1) => "automatic".to_string(),
122        LivelinessValue::Numeric(3) => "manual_by_topic".to_string(),
123        LivelinessValue::Numeric(_) => "automatic".to_string(), // default
124        LivelinessValue::String(LivelinessPolicy::Automatic) => "automatic".to_string(),
125        LivelinessValue::String(LivelinessPolicy::ManualByTopic) => "manual_by_topic".to_string(),
126    })
127}
128
129#[derive(
130    rkyv::Archive, Deserialize, Debug, Serialize, Clone, rkyv::Deserialize, rkyv::Serialize,
131)]
132pub struct QosProfile {
133    #[serde(deserialize_with = "deserialize_history")]
134    pub history: String,
135    pub depth: i32,
136    #[serde(deserialize_with = "deserialize_reliability")]
137    pub reliability: String,
138    #[serde(deserialize_with = "deserialize_durability")]
139    pub durability: String,
140    pub deadline: QosTime,
141    pub lifespan: QosTime,
142    #[serde(deserialize_with = "deserialize_liveliness")]
143    pub liveliness: String,
144    pub liveliness_lease_duration: QosTime,
145    pub avoid_ros_namespace_conventions: bool,
146}
147
148#[derive(
149    Deserialize, Serialize, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, Copy,
150)]
151pub struct QosTime {
152    pub sec: u64,
153    pub nsec: u64,
154}
155
156#[allow(clippy::large_enum_variant)]
157#[derive(Clone, Debug, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
158pub enum SensorTypeMapped {
159    Lidar(PointCloud2),
160    Imu(Imu),
161    Odometry(Odometry),
162    Any(Vec<u8>),
163}
164
165#[derive(Clone, Debug, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
166pub struct BagMsg {
167    pub topic: String,
168    pub msg_type: String,
169    pub data: SensorTypeMapped,
170    pub qos: Option<Qos>,
171}
172
173#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
174pub enum RatPubRegisterKind {
175    Publish,
176    Subscribe,
177}
178
179#[derive(PartialEq, Eq, Debug, Clone)]
180pub struct Rules {
181    store: HashMap<Variable, Vec<VariableHuman>>,
182    pub cache: HashMap<Variable, HashSet<(Client, RatPubRegisterKind)>>,
183    id_counter: u32,
184}
185
186pub type Client = String;
187pub type Variable = String;
188
189impl Default for Rules {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl Rules {
196    pub fn new() -> Self {
197        Self {
198            store: HashMap::new(),
199            cache: HashMap::new(),
200            id_counter: 0,
201        }
202    }
203
204    pub fn insert(&mut self, variable: String, strategies: Vec<VariableHuman>) {
205        match self.store.get_mut(&variable) {
206            Some(el) => {
207                el.extend(strategies);
208            }
209            None => {
210                self.store.insert(variable, strategies);
211            }
212        }
213    }
214
215    pub fn new_id(&mut self) -> u32 {
216        self.id_counter += 1;
217        self.id_counter
218    }
219
220    /// Removes a client (ship) from the rules and cache.
221    /// - If the client is the ship performing a Shoot, Catch, Sail, or None action, the entire rule is removed but if there is another partner like in catch or shoot, the partner will be put into the cache with a subscriber/publisher state based on if was receiving it (subscriber) or sending it and the removing client wanted to catch it (publisher)
222    /// - If the client is a target in a Shoot action, it is removed from the target list. If the list becomes empty, the Shoot rule is removed, AND the shooter ship is registered as a publisher in the cache.
223    /// - Any cache registrations for the client are removed.
224    pub fn remove_client(&mut self, client: &str) {
225        // Process store rules
226        let mut variables_to_remove_from_store = Vec::new();
227        let mut cache_registrations_from_store: HashMap<
228            Variable,
229            HashSet<(Client, RatPubRegisterKind)>,
230        > = HashMap::new();
231        let mut store_updates: HashMap<Variable, Vec<VariableHuman>> = HashMap::new();
232
233        for (variable, rules) in self.store.iter_mut() {
234            let mut rules_to_keep_in_variable = Vec::new();
235            let mut variable_has_remaining_rules = false;
236
237            for rule in rules.drain(..) {
238                // Use drain to efficiently iterate and remove
239                if rule.ship == client {
240                    // Case 1: Client is the main ship performing an action. Remove the rule.
241                    match &rule.strategy {
242                        Some(ActionPlan::Shoot { target, id: _ }) => {
243                            // The client was the shooter, the targets were effectively subscribers
244                            if !target.is_empty() {
245                                cache_registrations_from_store
246                                    .entry(variable.clone())
247                                    .or_default()
248                                    .extend(
249                                        target
250                                            .iter()
251                                            .cloned()
252                                            .map(|t| (t, RatPubRegisterKind::Subscribe)),
253                                    );
254                            }
255                        }
256                        Some(ActionPlan::Catch { source, id: _ }) => {
257                            // The client was the catcher, the source was the publisher
258                            cache_registrations_from_store
259                                .entry(variable.clone())
260                                .or_default()
261                                .insert((source.clone(), RatPubRegisterKind::Publish));
262                        }
263                        _ => {
264                            // Sail, None, or no strategy - no partners to register
265                        }
266                    }
267                } else if let Some(ActionPlan::Shoot { target, id }) = &rule.strategy {
268                    // Case 2: Rule is a Shoot action. Check if client is a target.
269                    let initial_target_count = target.len();
270                    let new_target = target
271                        .iter()
272                        .filter(|t| t != &client)
273                        .cloned()
274                        .collect::<Vec<_>>();
275
276                    if new_target.len() < initial_target_count {
277                        // The client was in the target list and has been removed.
278                        if new_target.is_empty() {
279                            // Target list is now empty, remove the Shoot rule.
280                            // The shooter ship is registered as a publisher.
281                            cache_registrations_from_store
282                                .entry(variable.clone())
283                                .or_default()
284                                .insert((rule.ship.clone(), RatPubRegisterKind::Publish));
285                        } else {
286                            // Target list is not empty, keep the rule with the updated target list.
287                            rules_to_keep_in_variable.push(VariableHuman {
288                                ship: rule.ship.clone(),
289                                strategy: Some(ActionPlan::Shoot {
290                                    target: new_target,
291                                    id: *id,
292                                }),
293                            });
294                            variable_has_remaining_rules = true;
295                        }
296                    } else {
297                        // Client was not in the target list, keep the rule as is.
298                        rules_to_keep_in_variable.push(rule);
299                        variable_has_remaining_rules = true;
300                    }
301                } else if let Some(ActionPlan::Catch { source, id: _ }) = &rule.strategy {
302                    // Case 3: Rule is a Catch action. Check if client is the source.
303                    if source == client {
304                        // The client being removed is the source of this Catch rule. Remove the rule.
305                        // The ship performing the Catch is registered as a subscriber.
306                        cache_registrations_from_store
307                            .entry(variable.clone())
308                            .or_default()
309                            .insert((rule.ship.clone(), RatPubRegisterKind::Subscribe));
310                    } else {
311                        // Client is not the source, keep the rule as is.
312                        rules_to_keep_in_variable.push(rule);
313                        variable_has_remaining_rules = true;
314                    }
315                } else {
316                    // Case 4: Rule does not involve the client, keep it.
317                    rules_to_keep_in_variable.push(rule);
318                    variable_has_remaining_rules = true;
319                }
320            }
321
322            // Decide whether to update or remove the variable entry in the store
323            if variable_has_remaining_rules {
324                store_updates.insert(variable.clone(), rules_to_keep_in_variable);
325            } else {
326                variables_to_remove_from_store.push(variable.clone());
327            }
328        }
329
330        // Apply store updates and removals
331        for (variable, updated_rules) in store_updates {
332            self.store.insert(variable, updated_rules);
333        }
334        for var in variables_to_remove_from_store {
335            self.store.remove(&var);
336        }
337
338        // Add collected cache registrations from store processing to the main cache
339        for (variable, registrations) in cache_registrations_from_store {
340            self.cache
341                .entry(variable)
342                .or_default()
343                .extend(registrations);
344        }
345
346        // Process cache registrations
347        let mut variables_to_remove_from_cache = Vec::new();
348        for (variable, registrations) in self.cache.iter_mut() {
349            let initial_count = registrations.len();
350            registrations.retain(|(c, _)| c != client);
351            if registrations.is_empty() && initial_count > 0 {
352                variables_to_remove_from_cache.push(variable.clone());
353            }
354        }
355
356        // Remove variables from the cache that have no remaining registrations
357        for var in variables_to_remove_from_cache {
358            self.cache.remove(&var);
359        }
360
361        // After removing clients from both store and cache, resolve the cache
362        // to potentially generate new rules from remaining cache entries.
363        self.resolve_cache();
364    }
365
366    pub fn clear(&mut self) {
367        self.store.clear();
368    }
369
370    pub fn raw(&self) -> &std::collections::HashMap<String, Vec<VariableHuman>> {
371        &self.store
372    }
373
374    /// Resolves cache entries into rules in the store based on existing rules and new registrations.
375    /// Generates only Shoot actions, never Catch actions.
376    /// - If a variable exists in the store AND has existing Shoot rules,
377    ///   cache registrations for that variable are used to update/regenerate
378    ///   the Shoot rules based on the combined set of publishers and subscribers
379    ///   from the store and cache. The cache entry is removed.
380    /// - If a variable is NOT in the store, OR is in the store but lacks Shoot rules,
381    ///   it requires a publisher/subscriber pair *within the cache* to generate new
382    ///   Shoot rules. These are added (either to a new entry or by extending
383    ///   a non-Shoot entry in the store). If no pair exists in the cache,
384    ///   the cache entry remains.
385    pub fn resolve_cache(&mut self) {
386        // Collect variables from cache keys to avoid borrowing issues while modifying
387        let vars_to_process: Vec<Variable> = self.cache.keys().cloned().collect();
388
389        for var_name in vars_to_process {
390            // Get and remove cache registrations for this variable
391            if let Some(registrations) = self.cache.remove(&var_name) {
392                let mut new_pubs: HashSet<String> = HashSet::new();
393                let mut new_subs: HashSet<String> = HashSet::new();
394
395                for (client, kind) in registrations.iter() {
396                    match kind {
397                        RatPubRegisterKind::Publish => {
398                            new_pubs.insert(client.clone());
399                        }
400                        RatPubRegisterKind::Subscribe => {
401                            new_subs.insert(client.clone());
402                        }
403                    }
404                }
405
406                // Temporarily remove existing rules from the store
407                let existing_rules_option = self.store.remove(&var_name);
408
409                // Check if the existing store entry (if any) contains Shoot rules
410                // We now only care about existing Shoot rules for the first case
411                let had_existing_shoot = &existing_rules_option.as_ref().and_then(|rules| {
412                    rules
413                        .iter()
414                        .find(|vh| matches!(vh.strategy, Some(ActionPlan::Shoot { .. })))
415                        .map(|v| match v.strategy.as_ref() {
416                            // TODO cleanup
417                            Some(ap) => match ap {
418                                ActionPlan::Sail => unreachable!(),
419                                ActionPlan::Shoot { target: _, id } => id,
420                                ActionPlan::Catch { .. } => unreachable!(),
421                            },
422                            None => unreachable!(),
423                        })
424                });
425
426                let mut generated_rules: Vec<VariableHuman> = Vec::new();
427
428                match had_existing_shoot {
429                    Some(id) => {
430                        // Case 1: Variable was in store AND had Shoot rules.
431                        // Combine clients from existing rules and new cache registrations, then regenerate Shoot rules.
432                        let mut all_pubs: HashSet<String> = HashSet::new();
433                        let mut all_subs: HashSet<String> = HashSet::new();
434                        let mut other_vh: Vec<VariableHuman> = Vec::new();
435                        let id = **id;
436                        if let Some(existing_rules) = existing_rules_option {
437                            for vh in existing_rules {
438                                match vh.strategy {
439                                    Some(ActionPlan::Shoot { target, id: _ }) => {
440                                        all_pubs.insert(vh.ship);
441                                        // Also collect existing subscribers from targets of existing shoots
442                                        all_subs.extend(target.into_iter());
443                                    }
444                                    // Treat existing Catch rules' ships as subscribers for combining lists
445                                    Some(ActionPlan::Catch { source: _, id: _ }) => {
446                                        all_subs.insert(vh.ship);
447                                    }
448                                    _ => {
449                                        other_vh.push(vh);
450                                    }
451                                }
452                            }
453                        }
454                        // Add new clients from cache registrations
455                        all_pubs.extend(new_pubs.into_iter());
456                        all_subs.extend(new_subs.into_iter());
457
458                        let mut sorted_all_pubs: Vec<String> = all_pubs.into_iter().collect();
459                        sorted_all_pubs.sort();
460                        let mut sorted_all_subs: Vec<String> = all_subs.into_iter().collect();
461                        sorted_all_subs.sort();
462                        generated_rules.extend(other_vh);
463
464                        if !sorted_all_pubs.is_empty() && !sorted_all_subs.is_empty() {
465                            // Add Shoot rules for all publishers targeting all subscribers
466                            for pub_client in &sorted_all_pubs {
467                                generated_rules.push(VariableHuman {
468                                    ship: pub_client.clone(),
469                                    strategy: Some(ActionPlan::Shoot {
470                                        target: sorted_all_subs.clone(),
471                                        id,
472                                    }),
473                                });
474                            }
475                        }
476
477                        // Re-insert the generated rules into the store
478                        self.store.insert(var_name.clone(), generated_rules);
479                    }
480                    None => {
481                        // Case 2: Variable was NOT in store, OR was in store but only had non-Shoot rules.
482                        // Require a publisher/subscriber pair *in the cache* to generate rules.
483                        if !new_pubs.is_empty() && !new_subs.is_empty() {
484                            // Generate Shoot rules (new_pubs -> new_subs) from the cache pair
485                            let mut sorted_new_pubs: Vec<String> = new_pubs.into_iter().collect();
486                            sorted_new_pubs.sort();
487                            let mut sorted_new_subs: Vec<String> = new_subs.into_iter().collect();
488                            sorted_new_subs.sort();
489
490                            let mut pair_generated_rules: Vec<VariableHuman> = Vec::new();
491                            let pair_id = self.new_id();
492                            for pub_client in &sorted_new_pubs {
493                                pair_generated_rules.push(VariableHuman {
494                                    ship: pub_client.clone(),
495                                    strategy: Some(ActionPlan::Shoot {
496                                        target: sorted_new_subs.clone(),
497                                        id: pair_id,
498                                    }),
499                                });
500                            }
501
502                            if let Some(mut existing_rules) = existing_rules_option {
503                                // Extend the existing non-Shoot rules with the newly generated pair Shoot rules
504                                existing_rules.extend(pair_generated_rules);
505                                self.store.insert(var_name.clone(), existing_rules);
506                            } else {
507                                // Insert a new entry with the generated pair Shoot rules
508                                self.store.insert(var_name.clone(), pair_generated_rules);
509                            }
510                        } else {
511                            // Case 3: No pair in cache, and no Shoot in store rules.
512                            // Put the original store rules back (if any) and keep the cache entry.
513                            if let Some(existing_rules) = existing_rules_option {
514                                self.store.insert(var_name.clone(), existing_rules);
515                            }
516                            // Put original cache registrations back as they weren't processed
517                            self.cache.insert(var_name.clone(), registrations);
518                        }
519                    }
520                }
521            }
522        }
523    }
524
525    pub fn register(&mut self, var: String, client: String, kind: RatPubRegisterKind) {
526        match self.cache.get_mut(&var) {
527            Some(el) => {
528                el.insert((client, kind));
529            }
530            None => {
531                let mut set = HashSet::new();
532                set.insert((client, kind));
533                self.cache.insert(var, set);
534            }
535        }
536
537        self.resolve_cache();
538    }
539}
540
541#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Clone, Debug, PartialEq, Eq)]
542pub struct VariableHuman {
543    pub ship: String,
544    pub strategy: Option<ActionPlan>,
545}
546
547#[derive(
548    rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone, Default, PartialEq, Eq,
549)]
550pub enum ActionPlan {
551    #[default]
552    Sail,
553    Shoot {
554        target: Vec<String>,
555        id: u32,
556    },
557    Catch {
558        source: String,
559        id: u32,
560    },
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566
567    use std::collections::{HashMap, HashSet};
568
569    // Helper function to sort VariableHuman vectors for consistent comparison
570    fn sort_variable_humans(humans: &mut [VariableHuman]) {
571        // Sort the outer vector based on ship and then strategy debug representation
572        humans.sort_by(|a, b| {
573            a.ship
574                .cmp(&b.ship)
575                .then(format!("{:?}", a.strategy).cmp(&format!("{:?}", b.strategy)))
576        });
577        // Also sort the target vector inside Shoot strategies for deterministic comparison
578        for human in humans.iter_mut() {
579            if let Some(ActionPlan::Shoot { target, id: _ }) = &mut human.strategy {
580                target.sort();
581            }
582        }
583    }
584    fn create_rules() -> Rules {
585        Rules::new()
586    }
587
588    #[test]
589    fn test_remove_client_main_ship_no_partner() {
590        let mut rules = create_rules();
591        rules.insert(
592            "var1".to_string(),
593            vec![VariableHuman {
594                ship: "client1".to_string(),
595                strategy: Some(ActionPlan::Sail),
596            }],
597        );
598
599        rules.remove_client("client1");
600
601        // client1's rule should be removed
602        assert_eq!(rules.store.len(), 0);
603    }
604
605    #[test]
606    fn test_remove_client_main_ship_catch() {
607        let mut rules = create_rules();
608        rules.insert(
609            "var1".to_string(),
610            vec![VariableHuman {
611                ship: "client1".to_string(),
612                strategy: Some(ActionPlan::Catch {
613                    source: "client2".to_string(),
614                    id: 0,
615                }),
616            }],
617        );
618
619        rules.remove_client("client1");
620
621        // client1's rule should be removed from store
622        assert_eq!(rules.store.len(), 0);
623        // client2 should be registered as a publisher in cache and resolved into a shoot rule
624        assert_eq!(rules.cache.len(), 1); // Cache should be resolved
625        let generated_rules = rules.cache.get("var1").unwrap();
626        assert_eq!(generated_rules.len(), 1); // resolve_cache requires a pub/sub pair in cache if no existing shoot rule
627
628        // Let's re-run with an existing subscriber in cache for the resolution to work
629        let mut rules = create_rules();
630        rules.insert(
631            "var1".to_string(),
632            vec![VariableHuman {
633                ship: "client1".to_string(),
634                strategy: Some(ActionPlan::Catch {
635                    source: "client2".to_string(),
636                    id: 0,
637                }),
638            }],
639        );
640        rules
641            .cache
642            .entry("var1".to_string())
643            .or_default()
644            .insert(("client3".to_string(), RatPubRegisterKind::Subscribe));
645
646        rules.remove_client("client1");
647        assert_eq!(rules.store.len(), 1);
648        assert!(rules.store.contains_key("var1"));
649        let generated_rules = rules.store.get("var1").unwrap();
650        assert_eq!(generated_rules.len(), 1);
651        assert_eq!(generated_rules[0].ship, "client2");
652        assert_eq!(
653            generated_rules[0].strategy,
654            Some(ActionPlan::Shoot {
655                target: vec!["client3".to_string()],
656                id: 1
657            })
658        );
659        assert_eq!(rules.cache.len(), 0); // Cache should be resolved
660    }
661
662    #[test]
663    fn test_remove_client_main_ship_shoot() {
664        let mut rules = create_rules();
665        rules.insert(
666            "var1".to_string(),
667            vec![VariableHuman {
668                ship: "client1".to_string(),
669                strategy: Some(ActionPlan::Shoot {
670                    target: vec!["client2".to_string(), "client3".to_string()],
671                    id: 0,
672                }),
673            }],
674        );
675
676        rules.remove_client("client1");
677
678        // client1's rule should be removed from store
679        assert_eq!(rules.store.len(), 0);
680        // client2 and client3 should be registered as subscribers in cache and resolved
681        let generated_rules = rules.cache.get("var1").unwrap();
682        assert_eq!(generated_rules.len(), 2); // Cache should be resolved
683    }
684
685    #[test]
686    fn test_remove_client_target_in_shoot_partial_removal() {
687        let mut rules = create_rules();
688        rules.insert(
689            "var1".to_string(),
690            vec![
691                VariableHuman {
692                    ship: "shooter1".to_string(),
693                    strategy: Some(ActionPlan::Shoot {
694                        target: vec!["client_to_remove".to_string(), "client2".to_string()],
695                        id: 0,
696                    }),
697                },
698                VariableHuman {
699                    ship: "shooter2".to_string(),
700                    strategy: Some(ActionPlan::Shoot {
701                        target: vec!["client2".to_string(), "client_to_remove".to_string()],
702                        id: 0,
703                    }),
704                },
705            ],
706        );
707
708        rules.remove_client("client_to_remove");
709
710        // client_to_remove should be removed from target lists
711        assert_eq!(rules.store.len(), 1);
712        assert!(rules.store.contains_key("var1"));
713        let remaining_rules = rules.store.get("var1").unwrap();
714        assert_eq!(remaining_rules.len(), 2);
715
716        let rule1 = &remaining_rules[0];
717        let rule2 = &remaining_rules[1];
718
719        // Order might not be guaranteed, check both possibilities
720        if rule1.ship == "shooter1" {
721            assert_eq!(
722                rule1.strategy,
723                Some(ActionPlan::Shoot {
724                    target: vec!["client2".to_string()],
725                    id: 0
726                })
727            );
728            assert_eq!(rule2.ship, "shooter2");
729            assert_eq!(
730                rule2.strategy,
731                Some(ActionPlan::Shoot {
732                    target: vec!["client2".to_string()],
733                    id: 0
734                })
735            );
736        } else {
737            assert_eq!(rule1.ship, "shooter2");
738            assert_eq!(
739                rule1.strategy,
740                Some(ActionPlan::Shoot {
741                    target: vec!["client2".to_string()],
742                    id: 0
743                })
744            );
745            assert_eq!(rule2.ship, "shooter1");
746            assert_eq!(
747                rule2.strategy,
748                Some(ActionPlan::Shoot {
749                    target: vec!["client2".to_string()],
750                    id: 0
751                })
752            );
753        }
754
755        assert_eq!(rules.cache.len(), 0); // No rules were fully removed, no new cache registrations from store
756    }
757
758    #[test]
759    fn test_remove_client_target_in_shoot_full_removal() {
760        let mut rules = create_rules();
761        rules.insert(
762            "var1".to_string(),
763            vec![VariableHuman {
764                ship: "shooter1".to_string(),
765                strategy: Some(ActionPlan::Shoot {
766                    target: vec!["client_to_remove".to_string()],
767                    id: 0,
768                }),
769            }],
770        );
771
772        rules.remove_client("client_to_remove");
773
774        assert_eq!(rules.cache.len(), 1);
775        assert_eq!(rules.store.len(), 0);
776
777        // Let's re-run with an existing subscriber in cache for the resolution to work
778        let mut rules = create_rules();
779        rules.insert(
780            "var1".to_string(),
781            vec![VariableHuman {
782                ship: "shooter1".to_string(),
783                strategy: Some(ActionPlan::Shoot {
784                    target: vec!["client_to_remove".to_string()],
785                    id: 0,
786                }),
787            }],
788        );
789        rules
790            .cache
791            .entry("var1".to_string())
792            .or_default()
793            .insert(("client2".to_string(), RatPubRegisterKind::Subscribe));
794
795        rules.remove_client("client_to_remove");
796
797        assert_eq!(rules.store.len(), 1);
798        assert!(rules.store.contains_key("var1"));
799        let generated_rules = rules.store.get("var1").unwrap();
800        assert_eq!(generated_rules.len(), 1);
801        assert_eq!(generated_rules[0].ship, "shooter1");
802        assert_eq!(
803            generated_rules[0].strategy,
804            Some(ActionPlan::Shoot {
805                target: vec!["client2".to_string()],
806                id: 1
807            })
808        );
809        assert_eq!(rules.cache.len(), 0); // Cache should be resolved
810    }
811
812    #[test]
813    fn test_remove_client_from_cache_partial_removal() {
814        let mut rules = create_rules();
815        rules
816            .cache
817            .entry("var1".to_string())
818            .or_default()
819            .insert(("client_to_remove".to_string(), RatPubRegisterKind::Publish));
820        rules
821            .cache
822            .entry("var1".to_string())
823            .or_default()
824            .insert(("client2".to_string(), RatPubRegisterKind::Subscribe));
825        rules.cache.entry("var2".to_string()).or_default().insert((
826            "client_to_remove".to_string(),
827            RatPubRegisterKind::Subscribe,
828        ));
829        rules
830            .cache
831            .entry("var2".to_string())
832            .or_default()
833            .insert(("client3".to_string(), RatPubRegisterKind::Publish));
834
835        rules.remove_client("client_to_remove");
836
837        // client_to_remove's cache entries should be removed
838        // var1 cache: should only have client2 (Subscribe)
839        // var2 cache: should only have client3 (Publish)
840        // The cache should then be resolved.
841
842        assert_eq!(rules.cache.len(), 2); // Cache should be resolved
843        assert_eq!(rules.store.len(), 0);
844    }
845
846    #[test]
847    fn test_remove_client_from_cache_full_removal() {
848        let mut rules = create_rules();
849        rules
850            .cache
851            .entry("var1".to_string())
852            .or_default()
853            .insert(("client_to_remove".to_string(), RatPubRegisterKind::Publish));
854
855        rules.remove_client("client_to_remove");
856
857        // var1 cache should be empty and removed
858        assert_eq!(rules.cache.len(), 0);
859        assert_eq!(rules.store.len(), 0); // No cache entries to resolve into store rules
860    }
861
862    #[test]
863    fn test_remove_client_involved_in_multiple_rules_and_variables() {
864        let mut rules = create_rules();
865        rules.insert(
866            "var1".to_string(),
867            vec![
868                VariableHuman {
869                    ship: "client_to_remove".to_string(),
870                    strategy: Some(ActionPlan::Sail),
871                },
872                VariableHuman {
873                    ship: "shooter1".to_string(),
874                    strategy: Some(ActionPlan::Shoot {
875                        target: vec!["client_to_remove".to_string(), "client2".to_string()],
876                        id: 0,
877                    }),
878                },
879            ],
880        );
881        rules.insert(
882            "var2".to_string(),
883            vec![VariableHuman {
884                ship: "client3".to_string(),
885                strategy: Some(ActionPlan::Catch {
886                    source: "client_to_remove".to_string(),
887                    id: 0,
888                }),
889            }],
890        );
891        rules.cache.entry("var1".to_string()).or_default().insert((
892            "client_to_remove".to_string(),
893            RatPubRegisterKind::Subscribe,
894        ));
895        rules
896            .cache
897            .entry("var3".to_string())
898            .or_default()
899            .insert(("client_to_remove".to_string(), RatPubRegisterKind::Publish));
900        rules
901            .cache
902            .entry("var3".to_string())
903            .or_default()
904            .insert(("client4".to_string(), RatPubRegisterKind::Subscribe));
905
906        rules.remove_client("client_to_remove");
907
908        assert_eq!(rules.cache.len(), 2); // Cache should be resolved
909
910        assert_eq!(rules.store.len(), 1);
911        assert!(rules.store.contains_key("var1"));
912
913        let var1_rules = rules.store.get("var1").unwrap();
914        assert_eq!(var1_rules.len(), 1);
915        assert_eq!(var1_rules[0].ship, "shooter1");
916        assert_eq!(
917            var1_rules[0].strategy,
918            Some(ActionPlan::Shoot {
919                target: vec!["client2".to_string()],
920                id: 0
921            })
922        );
923    }
924
925    #[test]
926    fn test_remove_client_not_present() {
927        let mut rules = create_rules();
928        rules.insert(
929            "var1".to_string(),
930            vec![VariableHuman {
931                ship: "client1".to_string(),
932                strategy: Some(ActionPlan::Sail),
933            }],
934        );
935        rules
936            .cache
937            .entry("var2".to_string())
938            .or_default()
939            .insert(("client2".to_string(), RatPubRegisterKind::Publish));
940
941        let initial_store = rules.store.clone();
942        let initial_cache = rules.cache.clone();
943
944        rules.remove_client("client_not_present");
945
946        // No changes should occur
947        assert_eq!(rules.store, initial_store);
948        assert_eq!(rules.cache, initial_cache);
949    }
950    #[test]
951    fn test_resolve_cache_basic_pair() {
952        let mut rules = Rules::new();
953        rules.register(
954            "position".to_string(),
955            "ShipA".to_string(),
956            RatPubRegisterKind::Publish,
957        );
958        rules.register(
959            "position".to_string(),
960            "ShipB".to_string(),
961            RatPubRegisterKind::Subscribe,
962        );
963
964        rules.resolve_cache();
965
966        // Check the store
967        let mut expected_store: HashMap<Variable, Vec<VariableHuman>> = {
968            let mut map = HashMap::new();
969            let var_humans = vec![VariableHuman {
970                ship: "ShipA".to_string(),
971                strategy: Some(ActionPlan::Shoot {
972                    target: vec!["ShipB".to_string()],
973                    id: 1,
974                }),
975            }];
976            map.insert("position".to_string(), var_humans);
977            map
978        };
979
980        let mut actual_store = rules.store.clone();
981
982        // Sort both actual and expected for comparison
983        if let Some(humans) = actual_store.get_mut("position") {
984            sort_variable_humans(humans);
985        }
986        if let Some(humans) = expected_store.get_mut("position") {
987            sort_variable_humans(humans);
988        }
989
990        assert_eq!(actual_store, expected_store);
991
992        // Check the cache
993        assert!(rules.cache.is_empty());
994    }
995
996    #[test]
997    fn test_resolve_cache_multiple_pubs_subs() {
998        let mut rules = Rules::new();
999        rules.register(
1000            "velocity".to_string(),
1001            "ShipX".to_string(),
1002            RatPubRegisterKind::Publish,
1003        );
1004        rules.register(
1005            "velocity".to_string(),
1006            "ShipY".to_string(),
1007            RatPubRegisterKind::Publish,
1008        );
1009        rules.register(
1010            "velocity".to_string(),
1011            "ShipZ".to_string(),
1012            RatPubRegisterKind::Subscribe,
1013        );
1014        rules.register(
1015            "velocity".to_string(),
1016            "ShipW".to_string(),
1017            RatPubRegisterKind::Subscribe,
1018        );
1019
1020        rules.resolve_cache();
1021
1022        // Check the store
1023        let expected_store: HashMap<Variable, Vec<VariableHuman>> = {
1024            let mut map = HashMap::new();
1025            // Define the expected rules (order doesn't matter initially, will sort later)
1026            let var_humans = vec![
1027                // Publishers shooting at all subscribers (targets should be sorted by resolve_cache)
1028                VariableHuman {
1029                    ship: "ShipX".to_string(),
1030                    strategy: Some(ActionPlan::Shoot {
1031                        target: vec!["ShipW".to_string(), "ShipZ".to_string()],
1032                        id: 1,
1033                    }),
1034                },
1035                VariableHuman {
1036                    ship: "ShipY".to_string(),
1037                    strategy: Some(ActionPlan::Shoot {
1038                        target: vec!["ShipW".to_string(), "ShipZ".to_string()],
1039                        id: 1,
1040                    }),
1041                },
1042            ];
1043            map.insert("velocity".to_string(), var_humans);
1044            map
1045        };
1046
1047        // Clone and sort the actual store for comparison
1048        let mut actual_store_sorted = rules.store.clone();
1049        if let Some(humans) = actual_store_sorted.get_mut("velocity") {
1050            sort_variable_humans(humans); // Sorts outer vec and inner targets
1051        }
1052
1053        // Clone and sort the expected store for comparison
1054        let mut expected_store_sorted = expected_store.clone();
1055        if let Some(humans) = expected_store_sorted.get_mut("velocity") {
1056            sort_variable_humans(humans); // Sorts outer vec and inner targets
1057        }
1058
1059        assert_eq!(actual_store_sorted, expected_store_sorted);
1060
1061        // Check the cache
1062        assert!(rules.cache.is_empty());
1063    }
1064
1065    #[test]
1066    fn test_resolve_cache_no_pair_pubs_only() {
1067        let mut rules = Rules::new();
1068        rules.register(
1069            "fuel".to_string(),
1070            "ShipA".to_string(),
1071            RatPubRegisterKind::Publish,
1072        );
1073        rules.register(
1074            "fuel".to_string(),
1075            "ShipC".to_string(),
1076            RatPubRegisterKind::Publish,
1077        );
1078
1079        rules.resolve_cache();
1080
1081        // Check the store - should be unchanged as 'fuel' was not in store and no pair formed
1082        assert!(rules.store.is_empty());
1083
1084        // Check the cache - entries should still be there
1085        let expected_cache: HashMap<Variable, HashSet<(Client, RatPubRegisterKind)>> = {
1086            let mut map = HashMap::new();
1087            let mut set = HashSet::new();
1088            set.insert(("ShipA".to_string(), RatPubRegisterKind::Publish));
1089            set.insert(("ShipC".to_string(), RatPubRegisterKind::Publish));
1090            map.insert("fuel".to_string(), set);
1091            map
1092        };
1093        assert_eq!(rules.cache, expected_cache);
1094    }
1095
1096    #[test]
1097    fn test_resolve_cache_no_pair_subs_only() {
1098        let mut rules = Rules::new();
1099        rules.register(
1100            "cargo".to_string(),
1101            "ShipB".to_string(),
1102            RatPubRegisterKind::Subscribe,
1103        );
1104        rules.register(
1105            "cargo".to_string(),
1106            "ShipD".to_string(),
1107            RatPubRegisterKind::Subscribe,
1108        );
1109
1110        rules.resolve_cache();
1111
1112        // Check the store - should be unchanged as 'cargo' was not in store and no pair formed
1113        assert!(rules.store.is_empty());
1114
1115        // Check the cache - entries should still be there
1116        let expected_cache: HashMap<Variable, HashSet<(Client, RatPubRegisterKind)>> = {
1117            let mut map = HashMap::new();
1118            let mut set = HashSet::new();
1119            set.insert(("ShipB".to_string(), RatPubRegisterKind::Subscribe));
1120            set.insert(("ShipD".to_string(), RatPubRegisterKind::Subscribe));
1121            map.insert("cargo".to_string(), set);
1122            map
1123        };
1124        assert_eq!(rules.cache, expected_cache);
1125    }
1126
1127    #[test]
1128    fn test_resolve_cache_mixed_variables() {
1129        let mut rules = Rules::new();
1130        // Variable with a pair (should be resolved and added to store)
1131        rules.register(
1132            "shields".to_string(),
1133            "ShipF".to_string(),
1134            RatPubRegisterKind::Publish,
1135        );
1136        rules.register(
1137            "shields".to_string(),
1138            "ShipG".to_string(),
1139            RatPubRegisterKind::Subscribe,
1140        );
1141
1142        // Variable without a pair (should remain in cache)
1143        rules.register(
1144            "waypoints".to_string(),
1145            "ShipH".to_string(),
1146            RatPubRegisterKind::Publish,
1147        );
1148
1149        rules.resolve_cache();
1150
1151        // Check the store - only 'shields' should be present, generated from the pair
1152        let mut expected_store: HashMap<Variable, Vec<VariableHuman>> = {
1153            let mut map = HashMap::new();
1154            let var_humans = vec![VariableHuman {
1155                ship: "ShipF".to_string(),
1156                strategy: Some(ActionPlan::Shoot {
1157                    target: vec!["ShipG".to_string()],
1158                    id: 1,
1159                }),
1160            }];
1161            map.insert("shields".to_string(), var_humans);
1162            map
1163        };
1164
1165        let mut actual_store = rules.store.clone();
1166
1167        // Sort both actual and expected for comparison
1168        if let Some(humans) = actual_store.get_mut("shields") {
1169            sort_variable_humans(humans);
1170        }
1171        if let Some(humans) = expected_store.get_mut("shields") {
1172            sort_variable_humans(humans);
1173        }
1174
1175        assert_eq!(actual_store, expected_store);
1176
1177        // Check the cache - only 'waypoints' should remain
1178        let expected_cache: HashMap<Variable, HashSet<(Client, RatPubRegisterKind)>> = {
1179            let mut map = HashMap::new();
1180            let mut set = HashSet::new();
1181            set.insert(("ShipH".to_string(), RatPubRegisterKind::Publish));
1182            map.insert("waypoints".to_string(), set);
1183            map
1184        };
1185        assert_eq!(rules.cache, expected_cache);
1186    }
1187
1188    #[test]
1189    fn test_resolve_cache_state_after_mixed_plus_new_subs() {
1190        let mut rules = Rules::new();
1191
1192        // Set up the state expected after test_resolve_cache_mixed_variables
1193        // Store should have 'shields' rules
1194        let shields_rules = vec![VariableHuman {
1195            ship: "ShipF".to_string(),
1196            strategy: Some(ActionPlan::Shoot {
1197                target: vec!["ShipG".to_string()],
1198                id: 0,
1199            }),
1200        }];
1201        rules.insert("shields".to_string(), shields_rules.clone());
1202
1203        // Cache should have 'waypoints' pub-only registration
1204        rules.register(
1205            "waypoints".to_string(),
1206            "ShipH".to_string(),
1207            RatPubRegisterKind::Publish,
1208        );
1209
1210        // Add new cache entries: subs-only for a new topic 'status'
1211        rules.register(
1212            "status".to_string(),
1213            "ShipI".to_string(),
1214            RatPubRegisterKind::Subscribe,
1215        );
1216        rules.register(
1217            "status".to_string(),
1218            "ShipJ".to_string(),
1219            RatPubRegisterKind::Subscribe,
1220        );
1221
1222        // Clone initial states to compare against after resolution
1223        let initial_store = rules.store.clone();
1224        let initial_cache = rules.cache.clone();
1225
1226        rules.resolve_cache();
1227
1228        // Expected: Store remains unchanged ('status' cache didn't form pair, 'waypoints' still no sub)
1229        // We need to sort the initial store for comparison as insert order isn't guaranteed
1230        let mut initial_store_sorted = initial_store.clone();
1231        if let Some(humans) = initial_store_sorted.get_mut("shields") {
1232            sort_variable_humans(humans);
1233        }
1234        let mut actual_store_sorted = rules.store.clone();
1235        if let Some(humans) = actual_store_sorted.get_mut("shields") {
1236            sort_variable_humans(humans);
1237        }
1238
1239        assert_eq!(actual_store_sorted, initial_store_sorted);
1240
1241        // Expected: Cache remains unchanged ('status' and 'waypoints' entries persist)
1242        assert_eq!(rules.cache, initial_cache);
1243    }
1244
1245    #[test]
1246    fn test_resolve_cache_state_after_prev_plus_new_pub() {
1247        let mut rules = Rules::new();
1248
1249        // Set up the state expected after the previous test
1250        // Store should have 'shields' rules
1251        let shields_rules = vec![VariableHuman {
1252            ship: "ShipF".to_string(),
1253            strategy: Some(ActionPlan::Shoot {
1254                target: vec!["ShipG".to_string()],
1255                id: 0,
1256            }),
1257        }];
1258        rules.insert("shields".to_string(), shields_rules.clone());
1259
1260        // Cache should have 'waypoints' pub-only registration
1261        rules.register(
1262            "waypoints".to_string(),
1263            "ShipH".to_string(),
1264            RatPubRegisterKind::Publish,
1265        );
1266        // Cache should have 'status' subs-only registrations
1267        rules.register(
1268            "status".to_string(),
1269            "ShipI".to_string(),
1270            RatPubRegisterKind::Subscribe,
1271        );
1272        rules.register(
1273            "status".to_string(),
1274            "ShipJ".to_string(),
1275            RatPubRegisterKind::Subscribe,
1276        );
1277
1278        // Add the new cache entry: publisher for 'status'
1279        rules.register(
1280            "status".to_string(),
1281            "ShipK".to_string(),
1282            RatPubRegisterKind::Publish,
1283        );
1284
1285        rules.resolve_cache();
1286
1287        // Check the store - Should contain original 'shields' rules + new 'status' rules from the pair
1288        let mut expected_store: HashMap<Variable, Vec<VariableHuman>> = HashMap::new();
1289        expected_store.insert("shields".to_string(), shields_rules); // Original shields rules
1290
1291        // Expected 'status' rules (ShipK publishes, ShipI and ShipJ subscribe)
1292        let status_rules = vec![VariableHuman {
1293            ship: "ShipK".to_string(),
1294            strategy: Some(ActionPlan::Shoot {
1295                target: vec!["ShipI".to_string(), "ShipJ".to_string()],
1296                id: 1,
1297            }),
1298        }];
1299        expected_store.insert("status".to_string(), status_rules);
1300
1301        let mut actual_store_sorted = rules.store.clone();
1302        if let Some(humans) = actual_store_sorted.get_mut("shields") {
1303            sort_variable_humans(humans);
1304        }
1305        if let Some(humans) = actual_store_sorted.get_mut("status") {
1306            sort_variable_humans(humans);
1307        }
1308
1309        let mut expected_store_sorted = expected_store.clone();
1310        if let Some(humans) = expected_store_sorted.get_mut("shields") {
1311            sort_variable_humans(humans);
1312        }
1313        if let Some(humans) = expected_store_sorted.get_mut("status") {
1314            sort_variable_humans(humans);
1315        }
1316
1317        assert_eq!(actual_store_sorted, expected_store_sorted);
1318
1319        // Check the cache - 'waypoints' should remain, 'status' should be gone
1320        let expected_cache: HashMap<Variable, HashSet<(Client, RatPubRegisterKind)>> = {
1321            let mut map = HashMap::new();
1322            let mut set = HashSet::new();
1323            set.insert(("ShipH".to_string(), RatPubRegisterKind::Publish));
1324            map.insert("waypoints".to_string(), set);
1325            map
1326        };
1327        assert_eq!(rules.cache, expected_cache);
1328    }
1329
1330    #[test]
1331    fn test_resolve_cache_empty_cache() {
1332        let mut rules = Rules::new();
1333        // Add some initial data to store to ensure it's not affected
1334        rules.insert(
1335            "initial".to_string(),
1336            vec![VariableHuman {
1337                ship: "ShipI".to_string(),
1338                strategy: Some(ActionPlan::Sail),
1339            }],
1340        );
1341
1342        rules.resolve_cache();
1343
1344        // Store should be unchanged
1345        let expected_store: HashMap<Variable, Vec<VariableHuman>> = {
1346            let mut map = HashMap::new();
1347            map.insert(
1348                "initial".to_string(),
1349                vec![VariableHuman {
1350                    ship: "ShipI".to_string(),
1351                    strategy: Some(ActionPlan::Sail),
1352                }],
1353            );
1354            map
1355        };
1356        assert_eq!(rules.store, expected_store);
1357
1358        // Cache should remain empty
1359        assert!(rules.cache.is_empty());
1360    }
1361
1362    #[test]
1363    fn test_resolve_cache_store_with_rules_add_sub() {
1364        let mut rules = Rules::new();
1365        let var_name = "data_stream".to_string();
1366
1367        // Initial Shoot/Catch rules in store
1368        rules.insert(
1369            var_name.clone(),
1370            vec![
1371                VariableHuman {
1372                    ship: "ShipA".to_string(),
1373                    strategy: Some(ActionPlan::Shoot {
1374                        target: vec!["ShipB".to_string()],
1375                        id: 0,
1376                    }),
1377                },
1378                VariableHuman {
1379                    ship: "MonitorShip".to_string(),
1380                    strategy: Some(ActionPlan::Sail),
1381                }, // Existing Sail rule
1382            ],
1383        );
1384
1385        // Add registration to cache: new subscriber
1386        rules.register(
1387            var_name.clone(),
1388            "ShipC".to_string(),
1389            RatPubRegisterKind::Subscribe,
1390        );
1391
1392        rules.resolve_cache();
1393
1394        // Check the store - Should have updated Shoot for ShipA and new Catch for ShipC
1395        let expected_store: HashMap<Variable, Vec<VariableHuman>> = {
1396            let mut map = HashMap::new();
1397            // Combined unique pubs: ShipA (from store)
1398            // Combined unique subs: ShipB (from store), ShipC (from cache)
1399            let var_humans = vec![
1400                VariableHuman {
1401                    ship: "MonitorShip".to_string(),
1402                    strategy: Some(ActionPlan::Sail),
1403                }, // Keep existing Sail rule
1404                VariableHuman {
1405                    ship: "ShipA".to_string(),
1406                    strategy: Some(ActionPlan::Shoot {
1407                        target: vec!["ShipB".to_string(), "ShipC".to_string()],
1408                        id: 0,
1409                    }),
1410                }, // Updated target list
1411            ];
1412            map.insert(var_name.clone(), var_humans);
1413            map
1414        };
1415
1416        let mut actual_store_sorted = rules.store.clone();
1417        if let Some(humans) = actual_store_sorted.get_mut(&var_name) {
1418            sort_variable_humans(humans);
1419        }
1420
1421        let mut expected_store_sorted = expected_store.clone();
1422        if let Some(humans) = expected_store_sorted.get_mut(&var_name) {
1423            sort_variable_humans(humans);
1424        }
1425
1426        assert_eq!(actual_store_sorted, expected_store_sorted);
1427
1428        // Check the cache - entry for 'data_stream' should be removed
1429        assert!(!rules.cache.contains_key(&var_name));
1430        assert!(rules.cache.is_empty()); // Assuming no other cache entries
1431    }
1432
1433    #[test]
1434    fn test_resolve_cache_store_with_rules_add_pub() {
1435        let mut rules = Rules::new();
1436        let var_name = "data_stream".to_string();
1437
1438        // Initial Shoot/Catch rules in store
1439        rules.insert(
1440            var_name.clone(),
1441            vec![
1442                VariableHuman {
1443                    ship: "ShipA".to_string(),
1444                    strategy: Some(ActionPlan::Shoot {
1445                        target: vec!["ShipB".to_string()],
1446                        id: 0,
1447                    }),
1448                },
1449                VariableHuman {
1450                    ship: "MonitorShip".to_string(),
1451                    strategy: Some(ActionPlan::Sail),
1452                }, // Existing Sail rule
1453            ],
1454        );
1455
1456        // Add registration to cache: new subscriber
1457        rules.register(
1458            var_name.clone(),
1459            "ShipC".to_string(),
1460            RatPubRegisterKind::Publish,
1461        );
1462
1463        rules.resolve_cache();
1464
1465        let expected_store: HashMap<Variable, Vec<VariableHuman>> = {
1466            let mut map = HashMap::new();
1467            let var_humans = vec![
1468                VariableHuman {
1469                    ship: "MonitorShip".to_string(),
1470                    strategy: Some(ActionPlan::Sail),
1471                }, // Keep existing Sail rule
1472                VariableHuman {
1473                    ship: "ShipA".to_string(),
1474                    strategy: Some(ActionPlan::Shoot {
1475                        target: vec!["ShipB".to_string()],
1476                        id: 0,
1477                    }),
1478                }, // Existing Catch rule remains
1479                VariableHuman {
1480                    ship: "ShipC".to_string(),
1481                    strategy: Some(ActionPlan::Shoot {
1482                        target: vec!["ShipB".to_string()],
1483                        id: 0,
1484                    }),
1485                },
1486            ];
1487            map.insert(var_name.clone(), var_humans);
1488            map
1489        };
1490
1491        let mut actual_store_sorted = rules.store.clone();
1492        if let Some(humans) = actual_store_sorted.get_mut(&var_name) {
1493            sort_variable_humans(humans);
1494        }
1495
1496        let mut expected_store_sorted = expected_store.clone();
1497        if let Some(humans) = expected_store_sorted.get_mut(&var_name) {
1498            sort_variable_humans(humans);
1499        }
1500
1501        assert_eq!(actual_store_sorted, expected_store_sorted);
1502
1503        // Check the cache - entry for 'data_stream' should be removed
1504        assert!(!rules.cache.contains_key(&var_name));
1505        assert!(rules.cache.is_empty()); // Assuming no other cache entries
1506    }
1507
1508    #[test]
1509    fn test_resolve_cache_store_with_rules_add_pub_sub() {
1510        let mut rules = Rules::new();
1511        let var_name = "telemetry".to_string();
1512
1513        // Initial Shoot/Catch rules in store
1514        rules.insert(
1515            var_name.clone(),
1516            vec![VariableHuman {
1517                ship: "BaseStation".to_string(),
1518                strategy: Some(ActionPlan::Shoot {
1519                    target: vec!["Rover1".to_string()],
1520                    id: 0,
1521                }),
1522            }],
1523        );
1524
1525        // Add registrations to cache: new publisher and new subscriber
1526        rules.register(
1527            var_name.clone(),
1528            "BaseStationBackup".to_string(),
1529            RatPubRegisterKind::Publish,
1530        );
1531        rules.register(
1532            var_name.clone(),
1533            "Rover2".to_string(),
1534            RatPubRegisterKind::Subscribe,
1535        );
1536
1537        rules.resolve_cache();
1538
1539        // Check the store - Should update all Shoot/Catch rules based on all 4 ships
1540        let expected_store: HashMap<Variable, Vec<VariableHuman>> = {
1541            let mut map = HashMap::new();
1542            // Combined unique pubs: BaseStation (store), BaseStationBackup (cache)
1543            // Combined unique subs: Rover1 (store), Rover2 (cache)
1544            let all_subs = vec!["Rover1".to_string(), "Rover2".to_string()]; // Already sorted
1545
1546            let var_humans = vec![
1547                // Shoot rules for all pubs targeting all subs
1548                VariableHuman {
1549                    ship: "BaseStation".to_string(),
1550                    strategy: Some(ActionPlan::Shoot {
1551                        target: all_subs.clone(),
1552                        id: 0,
1553                    }),
1554                },
1555                VariableHuman {
1556                    ship: "BaseStationBackup".to_string(),
1557                    strategy: Some(ActionPlan::Shoot {
1558                        target: all_subs.clone(),
1559                        id: 0,
1560                    }),
1561                },
1562            ];
1563            map.insert(var_name.clone(), var_humans);
1564            map
1565        };
1566
1567        let mut actual_store_sorted = rules.store.clone();
1568        if let Some(humans) = actual_store_sorted.get_mut(&var_name) {
1569            sort_variable_humans(humans);
1570        }
1571
1572        let mut expected_store_sorted = expected_store.clone();
1573        if let Some(humans) = expected_store_sorted.get_mut(&var_name) {
1574            sort_variable_humans(humans);
1575        }
1576
1577        assert_eq!(actual_store_sorted, expected_store_sorted);
1578
1579        // Check the cache - entry for 'telemetry' should be removed
1580        assert!(!rules.cache.contains_key(&var_name));
1581        assert!(rules.cache.is_empty()); // Assuming no other cache entries
1582    }
1583}