Skip to main content

ipfrs_tensorlogic/
temporal_knowledge_graph.rs

1//! Temporal Knowledge Graph — tracks how facts and relationships evolve over time.
2//!
3//! # Overview
4//!
5//! A [`TemporalKnowledgeGraph`] stores nodes and edges along with a timeline of events,
6//! allowing queries that ask "what was true at time T?" as well as full history queries.
7//!
8//! # Quick start
9//!
10//! ```rust
11//! use ipfrs_tensorlogic::temporal_knowledge_graph::{
12//!     TemporalKnowledgeGraph, TkgQuery,
13//! };
14//!
15//! let mut g = TemporalKnowledgeGraph::new();
16//! let alice = g.add_node("Alice".to_string(), 0).expect("example: should succeed in docs");
17//! let bob   = g.add_node("Bob".to_string(),   0).expect("example: should succeed in docs");
18//! let edge  = g.add_edge(alice, bob, "knows".to_string(), 1.0, 0).expect("example: should succeed in docs");
19//!
20//! let snap = g.snapshot_at(10);
21//! assert_eq!(snap.nodes.len(), 2);
22//! assert_eq!(snap.edges.len(), 1);
23//! ```
24
25use std::collections::{BTreeMap, HashMap, VecDeque};
26
27// ─────────────────────────────────────────────────────────────────────────────
28// Internal helpers
29// ─────────────────────────────────────────────────────────────────────────────
30
31/// XorShift64 PRNG — deterministic, no external dependencies.
32#[inline]
33fn xorshift64(state: &mut u64) -> u64 {
34    let mut x = *state;
35    x ^= x << 13;
36    x ^= x >> 7;
37    x ^= x << 17;
38    *state = x;
39    x
40}
41
42/// FNV-1a 64-bit hash.
43#[inline]
44fn fnv1a_64(data: &[u8]) -> u64 {
45    let mut h: u64 = 14_695_981_039_346_656_037;
46    for &b in data {
47        h ^= b as u64;
48        h = h.wrapping_mul(1_099_511_628_211);
49    }
50    h
51}
52
53/// Generate a 16-byte identifier seeded from `seed` mixed with a counter.
54fn gen_id(seed: &mut u64, salt: u64) -> [u8; 16] {
55    let a = xorshift64(seed);
56    let b = fnv1a_64(&(a ^ salt).to_le_bytes());
57    let mut out = [0u8; 16];
58    out[..8].copy_from_slice(&a.to_le_bytes());
59    out[8..].copy_from_slice(&b.to_le_bytes());
60    out
61}
62
63// ─────────────────────────────────────────────────────────────────────────────
64// Public newtypes / identifiers
65// ─────────────────────────────────────────────────────────────────────────────
66
67/// Unique identifier for a node (16 opaque bytes).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
69pub struct NodeId(pub [u8; 16]);
70
71/// Unique identifier for an edge (16 opaque bytes).
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
73pub struct EdgeId(pub [u8; 16]);
74
75// ─────────────────────────────────────────────────────────────────────────────
76// Core data types
77// ─────────────────────────────────────────────────────────────────────────────
78
79/// A node in the temporal knowledge graph.
80#[derive(Debug, Clone)]
81pub struct TkgNode {
82    pub id: NodeId,
83    pub label: String,
84    pub properties: HashMap<String, String>,
85    pub created_at: u64,
86    pub deleted_at: Option<u64>,
87}
88
89impl TkgNode {
90    /// Returns `true` if the node exists (has not been deleted) at time `t`.
91    pub fn alive_at(&self, t: u64) -> bool {
92        self.created_at <= t && self.deleted_at.is_none_or(|d| t < d)
93    }
94}
95
96/// A directed edge in the temporal knowledge graph.
97#[derive(Debug, Clone)]
98pub struct TkgEdge {
99    pub id: EdgeId,
100    pub src: NodeId,
101    pub dst: NodeId,
102    pub relation: String,
103    pub weight: f64,
104    pub valid_from: u64,
105    pub valid_until: Option<u64>,
106}
107
108impl TkgEdge {
109    /// Returns `true` if the edge is valid at time `t`.
110    pub fn valid_at(&self, t: u64) -> bool {
111        self.valid_from <= t && self.valid_until.is_none_or(|v| t < v)
112    }
113}
114
115// ─────────────────────────────────────────────────────────────────────────────
116// Events
117// ─────────────────────────────────────────────────────────────────────────────
118
119/// An event recorded in the graph timeline.
120#[derive(Debug, Clone)]
121pub enum TkgEvent {
122    NodeAdded(NodeId),
123    NodeDeleted(NodeId),
124    EdgeAdded(EdgeId),
125    EdgeDeleted(EdgeId),
126    PropertyChanged {
127        node_id: NodeId,
128        key: String,
129        old_val: Option<String>,
130        new_val: String,
131    },
132}
133
134// ─────────────────────────────────────────────────────────────────────────────
135// Queries
136// ─────────────────────────────────────────────────────────────────────────────
137
138/// Query types supported by [`TemporalKnowledgeGraph`].
139#[derive(Debug, Clone)]
140pub enum TkgQuery {
141    /// All nodes alive at timestamp `t`.
142    NodesAt(u64),
143    /// All edges valid at timestamp `t`.
144    EdgesAt(u64),
145    /// Full event history for a node.
146    NodeHistory(NodeId),
147    /// All edges between `src` and `dst` in the time window `[from_t, to_t]`.
148    EdgesBetween {
149        src: NodeId,
150        dst: NodeId,
151        from_t: u64,
152        to_t: u64,
153    },
154    /// Value of a node property at a specific time.
155    PropertyAt {
156        node_id: NodeId,
157        key: String,
158        at_t: u64,
159    },
160}
161
162/// The result of a [`TkgQuery`].
163#[derive(Debug, Clone)]
164pub enum TkgQueryResult {
165    Nodes(Vec<TkgNode>),
166    Edges(Vec<TkgEdge>),
167    Events(Vec<(u64, TkgEvent)>),
168    Property(Option<String>),
169    Empty,
170}
171
172// ─────────────────────────────────────────────────────────────────────────────
173// Snapshot
174// ─────────────────────────────────────────────────────────────────────────────
175
176/// A point-in-time snapshot of the graph.
177#[derive(Debug, Clone)]
178pub struct TkgSnapshot {
179    pub at: u64,
180    pub nodes: Vec<TkgNode>,
181    pub edges: Vec<TkgEdge>,
182}
183
184// ─────────────────────────────────────────────────────────────────────────────
185// Statistics
186// ─────────────────────────────────────────────────────────────────────────────
187
188/// Aggregate statistics for the graph.
189#[derive(Debug, Clone, Default)]
190pub struct TkgGraphStats {
191    pub total_nodes: usize,
192    pub live_nodes: usize,
193    pub total_edges: usize,
194    pub live_edges: usize,
195    pub total_events: usize,
196    pub first_timestamp: Option<u64>,
197    pub last_timestamp: Option<u64>,
198}
199
200// ─────────────────────────────────────────────────────────────────────────────
201// Merge policy
202// ─────────────────────────────────────────────────────────────────────────────
203
204/// Conflict-resolution policy when merging two graphs.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum TkgMergePolicy {
207    /// Keep this graph's data on conflict.
208    KeepMine,
209    /// Keep the other graph's data on conflict.
210    KeepOther,
211    /// Include data from both graphs (union).
212    UnionAll,
213}
214
215// ─────────────────────────────────────────────────────────────────────────────
216// Error type
217// ─────────────────────────────────────────────────────────────────────────────
218
219/// Errors produced by temporal knowledge graph operations.
220#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
221pub enum TkgError {
222    #[error("node {0:?} not found")]
223    NodeNotFound(NodeId),
224    #[error("edge {0:?} not found")]
225    EdgeNotFound(EdgeId),
226    #[error("node {0:?} already deleted")]
227    NodeAlreadyDeleted(NodeId),
228    #[error("edge {0:?} already deleted")]
229    EdgeAlreadyDeleted(EdgeId),
230    #[error("source node {0:?} does not exist at the given timestamp")]
231    SourceNodeInvalid(NodeId),
232    #[error("destination node {0:?} does not exist at the given timestamp")]
233    DestNodeInvalid(NodeId),
234}
235
236// ─────────────────────────────────────────────────────────────────────────────
237// Main struct
238// ─────────────────────────────────────────────────────────────────────────────
239
240/// A temporal knowledge graph that records how facts and relationships change.
241///
242/// All mutations are timestamped and appended to an internal timeline, enabling
243/// point-in-time queries and full history replay.
244#[derive(Debug, Clone)]
245pub struct TemporalKnowledgeGraph {
246    nodes: HashMap<NodeId, TkgNode>,
247    edges: HashMap<EdgeId, TkgEdge>,
248    /// timestamp → ordered list of events at that tick.
249    timeline: BTreeMap<u64, Vec<TkgEvent>>,
250    /// Monotonically increasing PRNG state for ID generation.
251    rng_state: u64,
252    /// Counter incremented per ID generation to guarantee uniqueness.
253    id_counter: u64,
254}
255
256impl Default for TemporalKnowledgeGraph {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262impl TemporalKnowledgeGraph {
263    /// Create a new, empty temporal knowledge graph.
264    pub fn new() -> Self {
265        Self {
266            nodes: HashMap::new(),
267            edges: HashMap::new(),
268            timeline: BTreeMap::new(),
269            rng_state: 0xcafe_babe_dead_beef,
270            id_counter: 0,
271        }
272    }
273
274    // ─────────────────────────────────────────────── ID generation ─────────
275
276    fn next_node_id(&mut self) -> NodeId {
277        self.id_counter = self.id_counter.wrapping_add(1);
278        NodeId(gen_id(&mut self.rng_state, self.id_counter))
279    }
280
281    fn next_edge_id(&mut self) -> EdgeId {
282        self.id_counter = self.id_counter.wrapping_add(1);
283        let raw = gen_id(&mut self.rng_state, self.id_counter ^ 0xFFFF_FFFF);
284        EdgeId(raw)
285    }
286
287    // ───────────────────────────────────────── Timeline helpers ─────────────
288
289    fn push_event(&mut self, ts: u64, event: TkgEvent) {
290        self.timeline.entry(ts).or_default().push(event);
291    }
292
293    // ─────────────────────────────────────────────── Mutations ──────────────
294
295    /// Add a node with the given `label` at timestamp `ts`.
296    ///
297    /// Returns the new [`NodeId`] on success.
298    pub fn add_node(&mut self, label: String, ts: u64) -> Result<NodeId, TkgError> {
299        let id = self.next_node_id();
300        let node = TkgNode {
301            id,
302            label,
303            properties: HashMap::new(),
304            created_at: ts,
305            deleted_at: None,
306        };
307        self.nodes.insert(id, node);
308        self.push_event(ts, TkgEvent::NodeAdded(id));
309        Ok(id)
310    }
311
312    /// Soft-delete node `id` at timestamp `ts`.
313    pub fn delete_node(&mut self, id: NodeId, ts: u64) -> Result<(), TkgError> {
314        let node = self.nodes.get_mut(&id).ok_or(TkgError::NodeNotFound(id))?;
315        if node.deleted_at.is_some() {
316            return Err(TkgError::NodeAlreadyDeleted(id));
317        }
318        node.deleted_at = Some(ts);
319        self.push_event(ts, TkgEvent::NodeDeleted(id));
320        Ok(())
321    }
322
323    /// Add an edge from `src` to `dst` with the given `relation`, `weight`, and
324    /// start timestamp `valid_from`.
325    ///
326    /// Both endpoint nodes must exist (not deleted) at `valid_from`.
327    pub fn add_edge(
328        &mut self,
329        src: NodeId,
330        dst: NodeId,
331        relation: String,
332        weight: f64,
333        valid_from: u64,
334    ) -> Result<EdgeId, TkgError> {
335        // Validate source node.
336        {
337            let src_node = self
338                .nodes
339                .get(&src)
340                .ok_or(TkgError::SourceNodeInvalid(src))?;
341            if !src_node.alive_at(valid_from) {
342                return Err(TkgError::SourceNodeInvalid(src));
343            }
344        }
345        // Validate destination node.
346        {
347            let dst_node = self.nodes.get(&dst).ok_or(TkgError::DestNodeInvalid(dst))?;
348            if !dst_node.alive_at(valid_from) {
349                return Err(TkgError::DestNodeInvalid(dst));
350            }
351        }
352
353        let id = self.next_edge_id();
354        let edge = TkgEdge {
355            id,
356            src,
357            dst,
358            relation,
359            weight,
360            valid_from,
361            valid_until: None,
362        };
363        self.edges.insert(id, edge);
364        self.push_event(valid_from, TkgEvent::EdgeAdded(id));
365        Ok(id)
366    }
367
368    /// Soft-delete edge `id` at timestamp `ts`.
369    pub fn delete_edge(&mut self, id: EdgeId, ts: u64) -> Result<(), TkgError> {
370        let edge = self.edges.get_mut(&id).ok_or(TkgError::EdgeNotFound(id))?;
371        if edge.valid_until.is_some() {
372            return Err(TkgError::EdgeAlreadyDeleted(id));
373        }
374        edge.valid_until = Some(ts);
375        self.push_event(ts, TkgEvent::EdgeDeleted(id));
376        Ok(())
377    }
378
379    /// Set (or overwrite) a property on node `id` at timestamp `ts`.
380    pub fn set_property(
381        &mut self,
382        id: NodeId,
383        key: String,
384        value: String,
385        ts: u64,
386    ) -> Result<(), TkgError> {
387        let node = self.nodes.get_mut(&id).ok_or(TkgError::NodeNotFound(id))?;
388        let old_val = node.properties.get(&key).cloned();
389        node.properties.insert(key.clone(), value.clone());
390        self.push_event(
391            ts,
392            TkgEvent::PropertyChanged {
393                node_id: id,
394                key,
395                old_val,
396                new_val: value,
397            },
398        );
399        Ok(())
400    }
401
402    // ─────────────────────────────────────────────────── Queries ────────────
403
404    /// Execute a [`TkgQuery`] and return a [`TkgQueryResult`].
405    pub fn query(&self, q: TkgQuery) -> TkgQueryResult {
406        match q {
407            TkgQuery::NodesAt(t) => {
408                let nodes = self
409                    .nodes
410                    .values()
411                    .filter(|n| n.alive_at(t))
412                    .cloned()
413                    .collect();
414                TkgQueryResult::Nodes(nodes)
415            }
416
417            TkgQuery::EdgesAt(t) => {
418                let edges = self
419                    .edges
420                    .values()
421                    .filter(|e| e.valid_at(t))
422                    .cloned()
423                    .collect();
424                TkgQueryResult::Edges(edges)
425            }
426
427            TkgQuery::NodeHistory(node_id) => {
428                let mut events: Vec<(u64, TkgEvent)> = Vec::new();
429                for (&ts, evs) in &self.timeline {
430                    for ev in evs {
431                        let relevant = match ev {
432                            TkgEvent::NodeAdded(id) | TkgEvent::NodeDeleted(id) => *id == node_id,
433                            TkgEvent::PropertyChanged { node_id: nid, .. } => *nid == node_id,
434                            _ => false,
435                        };
436                        if relevant {
437                            events.push((ts, ev.clone()));
438                        }
439                    }
440                }
441                TkgQueryResult::Events(events)
442            }
443
444            TkgQuery::EdgesBetween {
445                src,
446                dst,
447                from_t,
448                to_t,
449            } => {
450                let edges = self
451                    .edges
452                    .values()
453                    .filter(|e| {
454                        e.src == src
455                            && e.dst == dst
456                            && e.valid_from <= to_t
457                            && e.valid_until.is_none_or(|u| u >= from_t)
458                    })
459                    .cloned()
460                    .collect();
461                TkgQueryResult::Edges(edges)
462            }
463
464            TkgQuery::PropertyAt { node_id, key, at_t } => {
465                // Replay property-change events up to `at_t`.
466                let mut current: Option<String> = None;
467                for (&ts, evs) in &self.timeline {
468                    if ts > at_t {
469                        break;
470                    }
471                    for ev in evs {
472                        if let TkgEvent::PropertyChanged {
473                            node_id: nid,
474                            key: k,
475                            new_val,
476                            ..
477                        } = ev
478                        {
479                            if *nid == node_id && k == &key {
480                                current = Some(new_val.clone());
481                            }
482                        }
483                    }
484                }
485                TkgQueryResult::Property(current)
486            }
487        }
488    }
489
490    // ─────────────────────────────────────── Snapshot ───────────────────────
491
492    /// Return a point-in-time snapshot: all nodes and edges valid at `t`.
493    pub fn snapshot_at(&self, t: u64) -> TkgSnapshot {
494        let nodes: Vec<TkgNode> = self
495            .nodes
496            .values()
497            .filter(|n| n.alive_at(t))
498            .cloned()
499            .collect();
500        let edges: Vec<TkgEdge> = self
501            .edges
502            .values()
503            .filter(|e| e.valid_at(t))
504            .cloned()
505            .collect();
506        TkgSnapshot {
507            at: t,
508            nodes,
509            edges,
510        }
511    }
512
513    // ─────────────────────────────────── Temporal path ──────────────────────
514
515    /// BFS to find a path from `src` to `dst` using only edges valid at `at_t`.
516    ///
517    /// Returns `None` if no path exists or if either endpoint does not exist at `at_t`.
518    pub fn temporal_path(&self, src: NodeId, dst: NodeId, at_t: u64) -> Option<Vec<NodeId>> {
519        // Quick sanity checks.
520        let src_node = self.nodes.get(&src)?;
521        if !src_node.alive_at(at_t) {
522            return None;
523        }
524        let dst_node = self.nodes.get(&dst)?;
525        if !dst_node.alive_at(at_t) {
526            return None;
527        }
528
529        if src == dst {
530            return Some(vec![src]);
531        }
532
533        // Build adjacency list for nodes alive at `at_t`.
534        let mut adj: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
535        for edge in self.edges.values() {
536            if edge.valid_at(at_t) {
537                adj.entry(edge.src).or_default().push(edge.dst);
538            }
539        }
540
541        // BFS.
542        let mut visited: HashMap<NodeId, NodeId> = HashMap::new(); // child → parent
543        let mut queue: VecDeque<NodeId> = VecDeque::new();
544        queue.push_back(src);
545        visited.insert(src, src); // sentinel: src's parent is itself
546
547        'bfs: while let Some(cur) = queue.pop_front() {
548            if let Some(neighbours) = adj.get(&cur) {
549                for &next in neighbours {
550                    if visited.contains_key(&next) {
551                        continue;
552                    }
553                    visited.insert(next, cur);
554                    if next == dst {
555                        break 'bfs;
556                    }
557                    queue.push_back(next);
558                }
559            }
560        }
561
562        if !visited.contains_key(&dst) {
563            return None;
564        }
565
566        // Reconstruct path.
567        let mut path = Vec::new();
568        let mut cur = dst;
569        loop {
570            path.push(cur);
571            let parent = *visited.get(&cur)?;
572            if parent == cur {
573                break; // reached src sentinel
574            }
575            cur = parent;
576        }
577        path.reverse();
578        Some(path)
579    }
580
581    // ─────────────────────────────────────────── Merge ──────────────────────
582
583    /// Merge `other` into `self` according to `policy`.
584    ///
585    /// - [`TkgMergePolicy::KeepMine`]  — on ID collision keep `self`'s data.
586    /// - [`TkgMergePolicy::KeepOther`] — on ID collision keep `other`'s data.
587    /// - [`TkgMergePolicy::UnionAll`]  — keep all data; on ID collision prefer `other`.
588    pub fn merge_graphs(&mut self, other: &Self, policy: TkgMergePolicy) {
589        // Merge nodes.
590        for (&id, other_node) in &other.nodes {
591            match policy {
592                TkgMergePolicy::KeepMine => {
593                    self.nodes.entry(id).or_insert_with(|| other_node.clone());
594                }
595                TkgMergePolicy::KeepOther => {
596                    self.nodes.insert(id, other_node.clone());
597                }
598                TkgMergePolicy::UnionAll => {
599                    let entry = self.nodes.entry(id).or_insert_with(|| other_node.clone());
600                    // Union properties.
601                    for (k, v) in &other_node.properties {
602                        entry
603                            .properties
604                            .entry(k.clone())
605                            .or_insert_with(|| v.clone());
606                    }
607                    // Extend deleted_at to the later timestamp if both are deleted.
608                    if let (Some(mine_d), Some(other_d)) = (entry.deleted_at, other_node.deleted_at)
609                    {
610                        entry.deleted_at = Some(mine_d.max(other_d));
611                    } else if other_node.deleted_at.is_some() {
612                        entry.deleted_at = other_node.deleted_at;
613                    }
614                }
615            }
616        }
617
618        // Merge edges.
619        for (&id, other_edge) in &other.edges {
620            match policy {
621                TkgMergePolicy::KeepMine => {
622                    self.edges.entry(id).or_insert_with(|| other_edge.clone());
623                }
624                TkgMergePolicy::KeepOther | TkgMergePolicy::UnionAll => {
625                    self.edges.insert(id, other_edge.clone());
626                }
627            }
628        }
629
630        // Merge timeline.
631        for (&ts, other_evs) in &other.timeline {
632            let entry = self.timeline.entry(ts).or_default();
633            for ev in other_evs {
634                entry.push(ev.clone());
635            }
636        }
637    }
638
639    // ─────────────────────────────────────────── Statistics ─────────────────
640
641    /// Compute aggregate statistics for the graph.
642    pub fn stats(&self) -> TkgGraphStats {
643        let total_events: usize = self.timeline.values().map(|v| v.len()).sum();
644        TkgGraphStats {
645            total_nodes: self.nodes.len(),
646            live_nodes: self
647                .nodes
648                .values()
649                .filter(|n| n.deleted_at.is_none())
650                .count(),
651            total_edges: self.edges.len(),
652            live_edges: self
653                .edges
654                .values()
655                .filter(|e| e.valid_until.is_none())
656                .count(),
657            total_events,
658            first_timestamp: self.timeline.keys().next().copied(),
659            last_timestamp: self.timeline.keys().next_back().copied(),
660        }
661    }
662
663    // ─────────────────────────────────────────── Accessors ──────────────────
664
665    /// Return a reference to the node with the given id, if it exists.
666    pub fn get_node(&self, id: NodeId) -> Option<&TkgNode> {
667        self.nodes.get(&id)
668    }
669
670    /// Return a reference to the edge with the given id, if it exists.
671    pub fn get_edge(&self, id: EdgeId) -> Option<&TkgEdge> {
672        self.edges.get(&id)
673    }
674
675    /// Return all events recorded at exactly timestamp `ts`.
676    pub fn events_at(&self, ts: u64) -> &[TkgEvent] {
677        self.timeline.get(&ts).map(|v| v.as_slice()).unwrap_or(&[])
678    }
679
680    /// Iterate over the timeline in chronological order.
681    pub fn timeline_iter(&self) -> impl Iterator<Item = (u64, &[TkgEvent])> {
682        self.timeline.iter().map(|(&ts, evs)| (ts, evs.as_slice()))
683    }
684
685    /// Number of nodes (including deleted).
686    pub fn node_count(&self) -> usize {
687        self.nodes.len()
688    }
689
690    /// Number of edges (including expired).
691    pub fn edge_count(&self) -> usize {
692        self.edges.len()
693    }
694}
695
696// ─────────────────────────────────────────────────────────────────────────────
697// Tests
698// ─────────────────────────────────────────────────────────────────────────────
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    // ── Helpers ──────────────────────────────────────────────────────────────
705
706    fn make_graph() -> (TemporalKnowledgeGraph, NodeId, NodeId) {
707        let mut g = TemporalKnowledgeGraph::new();
708        let a = g
709            .add_node("Alice".to_string(), 0)
710            .expect("test setup: add_node Alice should succeed");
711        let b = g
712            .add_node("Bob".to_string(), 0)
713            .expect("test setup: add_node Bob should succeed");
714        (g, a, b)
715    }
716
717    // ── ID & PRNG ─────────────────────────────────────────────────────────
718
719    #[test]
720    fn test_xorshift64_nonzero() {
721        let mut state = 1u64;
722        let v = xorshift64(&mut state);
723        assert_ne!(v, 1);
724    }
725
726    #[test]
727    fn test_xorshift64_deterministic() {
728        let mut s1 = 42u64;
729        let mut s2 = 42u64;
730        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
731    }
732
733    #[test]
734    fn test_fnv1a_empty() {
735        let h = fnv1a_64(&[]);
736        assert_eq!(h, 14_695_981_039_346_656_037u64);
737    }
738
739    #[test]
740    fn test_fnv1a_deterministic() {
741        let a = fnv1a_64(b"hello");
742        let b = fnv1a_64(b"hello");
743        assert_eq!(a, b);
744    }
745
746    #[test]
747    fn test_fnv1a_different_inputs() {
748        assert_ne!(fnv1a_64(b"foo"), fnv1a_64(b"bar"));
749    }
750
751    #[test]
752    fn test_gen_id_unique() {
753        let mut state = 0x1234_5678u64;
754        let id1 = gen_id(&mut state, 1);
755        let id2 = gen_id(&mut state, 2);
756        assert_ne!(id1, id2);
757    }
758
759    // ── Node operations ───────────────────────────────────────────────────
760
761    #[test]
762    fn test_add_node_returns_unique_ids() {
763        let mut g = TemporalKnowledgeGraph::new();
764        let a = g
765            .add_node("A".to_string(), 0)
766            .expect("test setup: add_node A should succeed");
767        let b = g
768            .add_node("B".to_string(), 0)
769            .expect("test setup: add_node B should succeed");
770        assert_ne!(a, b);
771    }
772
773    #[test]
774    fn test_add_node_stored() {
775        let mut g = TemporalKnowledgeGraph::new();
776        let id = g
777            .add_node("X".to_string(), 5)
778            .expect("test setup: add_node X should succeed");
779        let node = g
780            .get_node(id)
781            .expect("test setup: node must exist after add_node succeeded");
782        assert_eq!(node.label, "X");
783        assert_eq!(node.created_at, 5);
784        assert!(node.deleted_at.is_none());
785    }
786
787    #[test]
788    fn test_delete_node_sets_deleted_at() {
789        let (mut g, a, _) = make_graph();
790        g.delete_node(a, 10)
791            .expect("test setup: delete_node should succeed");
792        let node = g
793            .get_node(a)
794            .expect("test setup: node must exist after add_node succeeded");
795        assert_eq!(node.deleted_at, Some(10));
796    }
797
798    #[test]
799    fn test_delete_node_not_found() {
800        let mut g = TemporalKnowledgeGraph::new();
801        let fake = NodeId([0u8; 16]);
802        assert!(matches!(
803            g.delete_node(fake, 0),
804            Err(TkgError::NodeNotFound(_))
805        ));
806    }
807
808    #[test]
809    fn test_delete_node_twice_errors() {
810        let (mut g, a, _) = make_graph();
811        g.delete_node(a, 5)
812            .expect("test setup: first deletion of node a must succeed");
813        assert!(matches!(
814            g.delete_node(a, 10),
815            Err(TkgError::NodeAlreadyDeleted(_))
816        ));
817    }
818
819    #[test]
820    fn test_node_alive_at() {
821        let node = TkgNode {
822            id: NodeId([0; 16]),
823            label: "x".to_string(),
824            properties: HashMap::new(),
825            created_at: 5,
826            deleted_at: Some(15),
827        };
828        assert!(!node.alive_at(4));
829        assert!(node.alive_at(5));
830        assert!(node.alive_at(14));
831        assert!(!node.alive_at(15));
832    }
833
834    #[test]
835    fn test_node_alive_no_deletion() {
836        let node = TkgNode {
837            id: NodeId([0; 16]),
838            label: "x".to_string(),
839            properties: HashMap::new(),
840            created_at: 0,
841            deleted_at: None,
842        };
843        assert!(node.alive_at(u64::MAX));
844    }
845
846    // ── Edge operations ───────────────────────────────────────────────────
847
848    #[test]
849    fn test_add_edge_stored() {
850        let (mut g, a, b) = make_graph();
851        let eid = g
852            .add_edge(a, b, "knows".to_string(), 0.9, 0)
853            .expect("test setup: add edge a→b with relation 'knows'");
854        let edge = g
855            .get_edge(eid)
856            .expect("test setup: get edge by id after successful add");
857        assert_eq!(edge.src, a);
858        assert_eq!(edge.dst, b);
859        assert_eq!(edge.relation, "knows");
860        assert!((edge.weight - 0.9).abs() < 1e-10);
861    }
862
863    #[test]
864    fn test_add_edge_invalid_src() {
865        let mut g = TemporalKnowledgeGraph::new();
866        let fake = NodeId([0u8; 16]);
867        let b = g
868            .add_node("B".to_string(), 0)
869            .expect("test setup: add node B to graph");
870        assert!(matches!(
871            g.add_edge(fake, b, "r".to_string(), 1.0, 0),
872            Err(TkgError::SourceNodeInvalid(_))
873        ));
874    }
875
876    #[test]
877    fn test_add_edge_invalid_dst() {
878        let mut g = TemporalKnowledgeGraph::new();
879        let a = g
880            .add_node("A".to_string(), 0)
881            .expect("test setup: add node A to graph");
882        let fake = NodeId([0u8; 16]);
883        assert!(matches!(
884            g.add_edge(a, fake, "r".to_string(), 1.0, 0),
885            Err(TkgError::DestNodeInvalid(_))
886        ));
887    }
888
889    #[test]
890    fn test_add_edge_on_deleted_src_fails() {
891        let (mut g, a, b) = make_graph();
892        g.delete_node(a, 5)
893            .expect("test setup: delete node a before attempting edge add");
894        assert!(matches!(
895            g.add_edge(a, b, "r".to_string(), 1.0, 10),
896            Err(TkgError::SourceNodeInvalid(_))
897        ));
898    }
899
900    #[test]
901    fn test_delete_edge_sets_valid_until() {
902        let (mut g, a, b) = make_graph();
903        let eid = g
904            .add_edge(a, b, "r".to_string(), 1.0, 0)
905            .expect("test setup: add edge a→b with relation 'r'");
906        g.delete_edge(eid, 20)
907            .expect("test setup: delete edge at timestamp 20");
908        let edge = g
909            .get_edge(eid)
910            .expect("test setup: get edge after deletion to inspect valid_until");
911        assert_eq!(edge.valid_until, Some(20));
912    }
913
914    #[test]
915    fn test_delete_edge_not_found() {
916        let mut g = TemporalKnowledgeGraph::new();
917        let fake = EdgeId([0u8; 16]);
918        assert!(matches!(
919            g.delete_edge(fake, 0),
920            Err(TkgError::EdgeNotFound(_))
921        ));
922    }
923
924    #[test]
925    fn test_delete_edge_twice_errors() {
926        let (mut g, a, b) = make_graph();
927        let eid = g
928            .add_edge(a, b, "r".to_string(), 1.0, 0)
929            .expect("test setup: add edge for double-delete test");
930        g.delete_edge(eid, 5)
931            .expect("test setup: first deletion of edge must succeed");
932        assert!(matches!(
933            g.delete_edge(eid, 10),
934            Err(TkgError::EdgeAlreadyDeleted(_))
935        ));
936    }
937
938    #[test]
939    fn test_edge_valid_at() {
940        let edge = TkgEdge {
941            id: EdgeId([0; 16]),
942            src: NodeId([0; 16]),
943            dst: NodeId([1; 16]),
944            relation: "r".to_string(),
945            weight: 1.0,
946            valid_from: 10,
947            valid_until: Some(20),
948        };
949        assert!(!edge.valid_at(9));
950        assert!(edge.valid_at(10));
951        assert!(edge.valid_at(19));
952        assert!(!edge.valid_at(20));
953    }
954
955    // ── Properties ────────────────────────────────────────────────────────
956
957    #[test]
958    fn test_set_property_stores_value() {
959        let (mut g, a, _) = make_graph();
960        g.set_property(a, "age".to_string(), "30".to_string(), 1)
961            .expect("test setup: set property 'age' to '30' on node a");
962        let node = g
963            .get_node(a)
964            .expect("test setup: get node a after setting property");
965        assert_eq!(node.properties.get("age").map(String::as_str), Some("30"));
966    }
967
968    #[test]
969    fn test_set_property_overwrite() {
970        let (mut g, a, _) = make_graph();
971        g.set_property(a, "age".to_string(), "30".to_string(), 1)
972            .expect("test setup: set initial property 'age' to '30'");
973        g.set_property(a, "age".to_string(), "31".to_string(), 2)
974            .expect("test setup: overwrite property 'age' to '31'");
975        let node = g
976            .get_node(a)
977            .expect("test setup: get node a after property overwrite");
978        assert_eq!(node.properties.get("age").map(String::as_str), Some("31"));
979    }
980
981    #[test]
982    fn test_set_property_node_not_found() {
983        let mut g = TemporalKnowledgeGraph::new();
984        let fake = NodeId([0u8; 16]);
985        assert!(matches!(
986            g.set_property(fake, "k".to_string(), "v".to_string(), 0),
987            Err(TkgError::NodeNotFound(_))
988        ));
989    }
990
991    // ── Timeline & events ─────────────────────────────────────────────────
992
993    #[test]
994    fn test_events_at_returns_events() {
995        let (g, a, _) = make_graph();
996        let evs = g.events_at(0);
997        assert_eq!(evs.len(), 2);
998        let has_a = evs
999            .iter()
1000            .any(|e| matches!(e, TkgEvent::NodeAdded(id) if *id == a));
1001        assert!(has_a);
1002    }
1003
1004    #[test]
1005    fn test_events_at_empty_timestamp() {
1006        let g = TemporalKnowledgeGraph::new();
1007        assert!(g.events_at(999).is_empty());
1008    }
1009
1010    #[test]
1011    fn test_timeline_iter_ordered() {
1012        let mut g = TemporalKnowledgeGraph::new();
1013        g.add_node("A".to_string(), 10)
1014            .expect("test setup: add node A at timestamp 10");
1015        g.add_node("B".to_string(), 5)
1016            .expect("test setup: add node B at timestamp 5");
1017        g.add_node("C".to_string(), 20)
1018            .expect("test setup: add node C at timestamp 20");
1019        let ts: Vec<u64> = g.timeline_iter().map(|(t, _)| t).collect();
1020        assert_eq!(ts, vec![5, 10, 20]);
1021    }
1022
1023    // ── Queries ───────────────────────────────────────────────────────────
1024
1025    #[test]
1026    fn test_query_nodes_at() {
1027        let (mut g, a, _) = make_graph();
1028        g.delete_node(a, 5)
1029            .expect("test setup: delete node a at timestamp 5");
1030        if let TkgQueryResult::Nodes(nodes) = g.query(TkgQuery::NodesAt(3)) {
1031            assert_eq!(nodes.len(), 2);
1032        } else {
1033            panic!("wrong variant");
1034        }
1035        if let TkgQueryResult::Nodes(nodes) = g.query(TkgQuery::NodesAt(10)) {
1036            assert_eq!(nodes.len(), 1);
1037        } else {
1038            panic!("wrong variant");
1039        }
1040    }
1041
1042    #[test]
1043    fn test_query_edges_at() {
1044        let (mut g, a, b) = make_graph();
1045        let eid = g
1046            .add_edge(a, b, "r".to_string(), 1.0, 0)
1047            .expect("test setup: add edge a→b at timestamp 0");
1048        g.delete_edge(eid, 10)
1049            .expect("test setup: delete edge at timestamp 10");
1050        if let TkgQueryResult::Edges(edges) = g.query(TkgQuery::EdgesAt(5)) {
1051            assert_eq!(edges.len(), 1);
1052        } else {
1053            panic!("wrong variant");
1054        }
1055        if let TkgQueryResult::Edges(edges) = g.query(TkgQuery::EdgesAt(15)) {
1056            assert_eq!(edges.len(), 0);
1057        } else {
1058            panic!("wrong variant");
1059        }
1060    }
1061
1062    #[test]
1063    fn test_query_node_history() {
1064        let (mut g, a, _) = make_graph();
1065        g.set_property(a, "x".to_string(), "1".to_string(), 2)
1066            .expect("test setup: set property 'x' to '1' at timestamp 2");
1067        g.set_property(a, "x".to_string(), "2".to_string(), 4)
1068            .expect("test setup: set property 'x' to '2' at timestamp 4");
1069        g.delete_node(a, 6)
1070            .expect("test setup: delete node a at timestamp 6 to generate delete event");
1071        if let TkgQueryResult::Events(events) = g.query(TkgQuery::NodeHistory(a)) {
1072            // NodeAdded + 2 PropertyChanged + NodeDeleted = 4
1073            assert_eq!(events.len(), 4);
1074        } else {
1075            panic!("wrong variant");
1076        }
1077    }
1078
1079    #[test]
1080    fn test_query_edges_between() {
1081        let (mut g, a, b) = make_graph();
1082        g.add_edge(a, b, "r".to_string(), 1.0, 5)
1083            .expect("test setup: add edge a→b with relation 'r' at timestamp 5");
1084        g.add_edge(a, b, "s".to_string(), 0.5, 15)
1085            .expect("test setup: add edge a→b with relation 's' at timestamp 15");
1086        if let TkgQueryResult::Edges(edges) = g.query(TkgQuery::EdgesBetween {
1087            src: a,
1088            dst: b,
1089            from_t: 0,
1090            to_t: 10,
1091        }) {
1092            assert_eq!(edges.len(), 1);
1093        } else {
1094            panic!("wrong variant");
1095        }
1096    }
1097
1098    #[test]
1099    fn test_query_edges_between_full_range() {
1100        let (mut g, a, b) = make_graph();
1101        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1102            .expect("test setup: add first edge a→b with relation 'r'");
1103        g.add_edge(a, b, "s".to_string(), 0.5, 0)
1104            .expect("test setup: add second edge a→b with relation 's'");
1105        if let TkgQueryResult::Edges(edges) = g.query(TkgQuery::EdgesBetween {
1106            src: a,
1107            dst: b,
1108            from_t: 0,
1109            to_t: u64::MAX,
1110        }) {
1111            assert_eq!(edges.len(), 2);
1112        } else {
1113            panic!("wrong variant");
1114        }
1115    }
1116
1117    #[test]
1118    fn test_query_property_at_returns_correct_version() {
1119        let (mut g, a, _) = make_graph();
1120        g.set_property(a, "k".to_string(), "v1".to_string(), 5)
1121            .expect("test setup: set property 'k' to 'v1' at timestamp 5");
1122        g.set_property(a, "k".to_string(), "v2".to_string(), 10)
1123            .expect("test setup: set property 'k' to 'v2' at timestamp 10");
1124        if let TkgQueryResult::Property(Some(val)) = g.query(TkgQuery::PropertyAt {
1125            node_id: a,
1126            key: "k".to_string(),
1127            at_t: 7,
1128        }) {
1129            assert_eq!(val, "v1");
1130        } else {
1131            panic!("expected Some(v1)");
1132        }
1133        if let TkgQueryResult::Property(Some(val)) = g.query(TkgQuery::PropertyAt {
1134            node_id: a,
1135            key: "k".to_string(),
1136            at_t: 10,
1137        }) {
1138            assert_eq!(val, "v2");
1139        } else {
1140            panic!("expected Some(v2)");
1141        }
1142    }
1143
1144    #[test]
1145    fn test_query_property_at_none_before_set() {
1146        let (mut g, a, _) = make_graph();
1147        g.set_property(a, "k".to_string(), "v".to_string(), 10)
1148            .expect("test setup: set property 'k' to 'v' at timestamp 10");
1149        if let TkgQueryResult::Property(val) = g.query(TkgQuery::PropertyAt {
1150            node_id: a,
1151            key: "k".to_string(),
1152            at_t: 5,
1153        }) {
1154            assert!(val.is_none());
1155        } else {
1156            panic!("wrong variant");
1157        }
1158    }
1159
1160    // ── Snapshot ──────────────────────────────────────────────────────────
1161
1162    #[test]
1163    fn test_snapshot_at_basic() {
1164        let (mut g, a, b) = make_graph();
1165        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1166            .expect("test setup: add edge a→b for basic snapshot test");
1167        let snap = g.snapshot_at(5);
1168        assert_eq!(snap.at, 5);
1169        assert_eq!(snap.nodes.len(), 2);
1170        assert_eq!(snap.edges.len(), 1);
1171    }
1172
1173    #[test]
1174    fn test_snapshot_excludes_deleted_nodes() {
1175        let (mut g, a, _) = make_graph();
1176        g.delete_node(a, 3)
1177            .expect("test setup: delete node a at timestamp 3 before snapshot at 5");
1178        let snap = g.snapshot_at(5);
1179        assert_eq!(snap.nodes.len(), 1);
1180    }
1181
1182    #[test]
1183    fn test_snapshot_excludes_expired_edges() {
1184        let (mut g, a, b) = make_graph();
1185        let eid = g
1186            .add_edge(a, b, "r".to_string(), 1.0, 0)
1187            .expect("test setup: add edge for snapshot expired-edge test");
1188        g.delete_edge(eid, 5)
1189            .expect("test setup: delete edge at timestamp 5 before snapshot at 10");
1190        let snap = g.snapshot_at(10);
1191        assert_eq!(snap.edges.len(), 0);
1192    }
1193
1194    #[test]
1195    fn test_snapshot_empty_graph() {
1196        let g = TemporalKnowledgeGraph::new();
1197        let snap = g.snapshot_at(100);
1198        assert!(snap.nodes.is_empty());
1199        assert!(snap.edges.is_empty());
1200    }
1201
1202    // ── Temporal path ─────────────────────────────────────────────────────
1203
1204    #[test]
1205    fn test_temporal_path_direct() {
1206        let (mut g, a, b) = make_graph();
1207        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1208            .expect("test setup: add direct edge a→b for path test");
1209        let path = g
1210            .temporal_path(a, b, 5)
1211            .expect("test setup: direct path from a to b must exist at timestamp 5");
1212        assert_eq!(path, vec![a, b]);
1213    }
1214
1215    #[test]
1216    fn test_temporal_path_self() {
1217        let (g, a, _) = make_graph();
1218        let path = g
1219            .temporal_path(a, a, 0)
1220            .expect("test setup: self-path from a to a must always exist");
1221        assert_eq!(path, vec![a]);
1222    }
1223
1224    #[test]
1225    fn test_temporal_path_multi_hop() {
1226        let mut g = TemporalKnowledgeGraph::new();
1227        let a = g
1228            .add_node("A".to_string(), 0)
1229            .expect("test setup: add node A for multi-hop path test");
1230        let b = g
1231            .add_node("B".to_string(), 0)
1232            .expect("test setup: add node B for multi-hop path test");
1233        let c = g
1234            .add_node("C".to_string(), 0)
1235            .expect("test setup: add node C for multi-hop path test");
1236        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1237            .expect("test setup: add edge A→B for multi-hop path");
1238        g.add_edge(b, c, "r".to_string(), 1.0, 0)
1239            .expect("test setup: add edge B→C for multi-hop path");
1240        let path = g
1241            .temporal_path(a, c, 5)
1242            .expect("test setup: multi-hop path A→B→C must exist at timestamp 5");
1243        assert_eq!(path, vec![a, b, c]);
1244    }
1245
1246    #[test]
1247    fn test_temporal_path_no_path() {
1248        let (g, a, b) = make_graph();
1249        // No edges added.
1250        assert!(g.temporal_path(a, b, 5).is_none());
1251    }
1252
1253    #[test]
1254    fn test_temporal_path_expired_edge() {
1255        let (mut g, a, b) = make_graph();
1256        let eid = g
1257            .add_edge(a, b, "r".to_string(), 1.0, 0)
1258            .expect("test setup: add edge that will be expired for path test");
1259        g.delete_edge(eid, 5)
1260            .expect("test setup: delete edge at timestamp 5 to expire it");
1261        // Path exists at t=3 but not t=10.
1262        assert!(g.temporal_path(a, b, 3).is_some());
1263        assert!(g.temporal_path(a, b, 10).is_none());
1264    }
1265
1266    #[test]
1267    fn test_temporal_path_deleted_node() {
1268        let (mut g, a, b) = make_graph();
1269        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1270            .expect("test setup: add edge a→b before deleting node b");
1271        g.delete_node(b, 5)
1272            .expect("test setup: delete destination node b at timestamp 5");
1273        // dst deleted at t=10 → no path.
1274        assert!(g.temporal_path(a, b, 10).is_none());
1275    }
1276
1277    #[test]
1278    fn test_temporal_path_unknown_src() {
1279        let g = TemporalKnowledgeGraph::new();
1280        let fake = NodeId([0u8; 16]);
1281        let fake2 = NodeId([1u8; 16]);
1282        assert!(g.temporal_path(fake, fake2, 0).is_none());
1283    }
1284
1285    // ── Merge ─────────────────────────────────────────────────────────────
1286
1287    #[test]
1288    fn test_merge_keep_mine() {
1289        let (mut g1, a, _) = make_graph();
1290        g1.set_property(a, "x".to_string(), "mine".to_string(), 1)
1291            .expect("test setup: set property 'x' to 'mine' on node a in g1");
1292
1293        let mut g2 = TemporalKnowledgeGraph::new();
1294        // Insert node with same id manually by building a compatible graph.
1295        // We'll just add a new node in g2 and merge; property should not override.
1296        g2.nodes.insert(
1297            a,
1298            TkgNode {
1299                id: a,
1300                label: "Alice-other".to_string(),
1301                properties: {
1302                    let mut m = HashMap::new();
1303                    m.insert("x".to_string(), "other".to_string());
1304                    m
1305                },
1306                created_at: 0,
1307                deleted_at: None,
1308            },
1309        );
1310
1311        g1.merge_graphs(&g2, TkgMergePolicy::KeepMine);
1312        assert_eq!(
1313            g1.get_node(a)
1314                .expect("test setup: get node a from g1 after KeepMine merge")
1315                .properties
1316                .get("x")
1317                .map(String::as_str),
1318            Some("mine")
1319        );
1320    }
1321
1322    #[test]
1323    fn test_merge_keep_other() {
1324        let (mut g1, a, _) = make_graph();
1325        g1.set_property(a, "x".to_string(), "mine".to_string(), 1)
1326            .expect("test setup: set property 'x' to 'mine' on node a before KeepOther merge");
1327
1328        let mut g2 = TemporalKnowledgeGraph::new();
1329        g2.nodes.insert(
1330            a,
1331            TkgNode {
1332                id: a,
1333                label: "Alice-other".to_string(),
1334                properties: {
1335                    let mut m = HashMap::new();
1336                    m.insert("x".to_string(), "other".to_string());
1337                    m
1338                },
1339                created_at: 0,
1340                deleted_at: None,
1341            },
1342        );
1343
1344        g1.merge_graphs(&g2, TkgMergePolicy::KeepOther);
1345        assert_eq!(
1346            g1.get_node(a)
1347                .expect("test setup: get node a from g1 after KeepOther merge")
1348                .properties
1349                .get("x")
1350                .map(String::as_str),
1351            Some("other")
1352        );
1353    }
1354
1355    #[test]
1356    fn test_merge_union_all_new_node() {
1357        let (mut g1, _, _) = make_graph();
1358        let mut g2 = TemporalKnowledgeGraph::new();
1359        let c = g2
1360            .add_node("Charlie".to_string(), 0)
1361            .expect("test setup: add Charlie node to g2 for union merge test");
1362
1363        g1.merge_graphs(&g2, TkgMergePolicy::UnionAll);
1364        assert!(g1.get_node(c).is_some());
1365    }
1366
1367    #[test]
1368    fn test_merge_timeline_combined() {
1369        let mut g1 = TemporalKnowledgeGraph::new();
1370        g1.add_node("A".to_string(), 5)
1371            .expect("test setup: add node A at timestamp 5 in g1");
1372
1373        let mut g2 = TemporalKnowledgeGraph::new();
1374        g2.add_node("B".to_string(), 10)
1375            .expect("test setup: add node B at timestamp 10 in g2");
1376
1377        g1.merge_graphs(&g2, TkgMergePolicy::UnionAll);
1378        assert!(!g1.events_at(5).is_empty());
1379        assert!(!g1.events_at(10).is_empty());
1380    }
1381
1382    #[test]
1383    fn test_merge_edges_keep_mine() {
1384        let (mut g1, a, b) = make_graph();
1385        let eid = g1
1386            .add_edge(a, b, "mine".to_string(), 1.0, 0)
1387            .expect("test setup: add edge with relation 'mine' in g1 before merge");
1388
1389        let mut g2 = TemporalKnowledgeGraph::new();
1390        g2.edges.insert(
1391            eid,
1392            TkgEdge {
1393                id: eid,
1394                src: a,
1395                dst: b,
1396                relation: "other".to_string(),
1397                weight: 0.5,
1398                valid_from: 0,
1399                valid_until: None,
1400            },
1401        );
1402
1403        g1.merge_graphs(&g2, TkgMergePolicy::KeepMine);
1404        assert_eq!(
1405            g1.get_edge(eid)
1406                .expect("test setup: get edge from g1 after KeepMine merge")
1407                .relation,
1408            "mine"
1409        );
1410    }
1411
1412    // ── Statistics ────────────────────────────────────────────────────────
1413
1414    #[test]
1415    fn test_stats_basic() {
1416        let (mut g, a, b) = make_graph();
1417        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1418            .expect("test setup: add edge a→b for stats test");
1419        let s = g.stats();
1420        assert_eq!(s.total_nodes, 2);
1421        assert_eq!(s.live_nodes, 2);
1422        assert_eq!(s.total_edges, 1);
1423        assert_eq!(s.live_edges, 1);
1424    }
1425
1426    #[test]
1427    fn test_stats_after_deletion() {
1428        let (mut g, a, b) = make_graph();
1429        let eid = g
1430            .add_edge(a, b, "r".to_string(), 1.0, 0)
1431            .expect("test setup: add edge for stats-after-deletion test");
1432        g.delete_node(a, 5)
1433            .expect("test setup: delete node a at timestamp 5 for stats test");
1434        g.delete_edge(eid, 5)
1435            .expect("test setup: delete edge at timestamp 5 for stats test");
1436        let s = g.stats();
1437        assert_eq!(s.live_nodes, 1);
1438        assert_eq!(s.live_edges, 0);
1439    }
1440
1441    #[test]
1442    fn test_stats_timestamps() {
1443        let mut g = TemporalKnowledgeGraph::new();
1444        g.add_node("A".to_string(), 3)
1445            .expect("test setup: add node A at timestamp 3");
1446        g.add_node("B".to_string(), 7)
1447            .expect("test setup: add node B at timestamp 7");
1448        let s = g.stats();
1449        assert_eq!(s.first_timestamp, Some(3));
1450        assert_eq!(s.last_timestamp, Some(7));
1451    }
1452
1453    #[test]
1454    fn test_stats_empty_graph() {
1455        let g = TemporalKnowledgeGraph::new();
1456        let s = g.stats();
1457        assert_eq!(s.total_nodes, 0);
1458        assert_eq!(s.total_events, 0);
1459        assert!(s.first_timestamp.is_none());
1460    }
1461
1462    // ── Node/Edge counts ──────────────────────────────────────────────────
1463
1464    #[test]
1465    fn test_node_count() {
1466        let (g, _, _) = make_graph();
1467        assert_eq!(g.node_count(), 2);
1468    }
1469
1470    #[test]
1471    fn test_edge_count() {
1472        let (mut g, a, b) = make_graph();
1473        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1474            .expect("test setup: add single edge for edge count test");
1475        assert_eq!(g.edge_count(), 1);
1476    }
1477
1478    // ── Default / Clone ───────────────────────────────────────────────────
1479
1480    #[test]
1481    fn test_default_empty() {
1482        let g: TemporalKnowledgeGraph = Default::default();
1483        assert_eq!(g.node_count(), 0);
1484        assert_eq!(g.edge_count(), 0);
1485    }
1486
1487    #[test]
1488    fn test_clone_independent() {
1489        let (mut g, a, b) = make_graph();
1490        let mut g2 = g.clone();
1491        g.add_edge(a, b, "r".to_string(), 1.0, 0)
1492            .expect("test setup: add edge a→b for clone test");
1493        assert_eq!(g.edge_count(), 1);
1494        assert_eq!(g2.edge_count(), 0);
1495        // Modifications to g2 don't affect g.
1496        g2.delete_node(a, 99)
1497            .expect("test setup: delete node a in cloned graph to verify independence");
1498        assert!(g
1499            .get_node(a)
1500            .expect("test setup: get node a from original graph after clone modification")
1501            .deleted_at
1502            .is_none());
1503    }
1504
1505    // ── Error display ─────────────────────────────────────────────────────
1506
1507    #[test]
1508    fn test_error_display_node_not_found() {
1509        let e = TkgError::NodeNotFound(NodeId([0u8; 16]));
1510        assert!(e.to_string().contains("not found"));
1511    }
1512
1513    #[test]
1514    fn test_error_display_edge_not_found() {
1515        let e = TkgError::EdgeNotFound(EdgeId([0u8; 16]));
1516        assert!(e.to_string().contains("not found"));
1517    }
1518
1519    // ── Edge uniqueness ───────────────────────────────────────────────────
1520
1521    #[test]
1522    fn test_multiple_edges_same_pair() {
1523        let (mut g, a, b) = make_graph();
1524        let e1 = g
1525            .add_edge(a, b, "r1".to_string(), 1.0, 0)
1526            .expect("test setup: add first edge with relation 'r1'");
1527        let e2 = g
1528            .add_edge(a, b, "r2".to_string(), 0.5, 0)
1529            .expect("test setup: add second edge with relation 'r2'");
1530        assert_ne!(e1, e2);
1531        assert_eq!(g.edge_count(), 2);
1532    }
1533
1534    // ── Large graph BFS ───────────────────────────────────────────────────
1535
1536    #[test]
1537    fn test_temporal_path_long_chain() {
1538        let mut g = TemporalKnowledgeGraph::new();
1539        let n = 20usize;
1540        let mut ids = Vec::with_capacity(n);
1541        for i in 0..n {
1542            ids.push(
1543                g.add_node(format!("N{i}"), 0)
1544                    .expect("test setup: add node N{i} in long-chain test"),
1545            );
1546        }
1547        for i in 0..n - 1 {
1548            g.add_edge(ids[i], ids[i + 1], "next".to_string(), 1.0, 0)
1549                .expect("test setup: add chain edge from node i to node i+1");
1550        }
1551        let path = g
1552            .temporal_path(ids[0], ids[n - 1], 0)
1553            .expect("test setup: long chain path from first to last node must exist");
1554        assert_eq!(path.len(), n);
1555        assert_eq!(path[0], ids[0]);
1556        assert_eq!(path[n - 1], ids[n - 1]);
1557    }
1558
1559    // ── NodeId / EdgeId ordering ──────────────────────────────────────────
1560
1561    #[test]
1562    fn test_node_id_ordering() {
1563        let a = NodeId([0u8; 16]);
1564        let b = NodeId([1u8; 16]);
1565        assert!(a < b);
1566    }
1567
1568    #[test]
1569    fn test_edge_id_equality() {
1570        let a = EdgeId([42u8; 16]);
1571        let b = EdgeId([42u8; 16]);
1572        assert_eq!(a, b);
1573    }
1574}