//! Dependency graph for tracking relationships between bindings.

use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
use tuppu_persistence::WorkspaceId;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(pub u64);

impl NodeId {
    pub fn new(id: u64) -> Self { Self(id) }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EdgeId(pub u64);

impl EdgeId {
    pub fn new(id: u64) -> Self { Self(id) }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BindingKey {
    pub workspace: WorkspaceId,
    pub name: String,
}

impl BindingKey {
    pub fn new(workspace: WorkspaceId, name: impl Into<String>) -> Self {
        Self { workspace, name: name.into() }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum RelationshipKind {
    #[default]
    Dependency,
    DerivedFrom,
    Constraint,
    Reference,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Node {
    pub id: NodeId,
    pub workspace_id: WorkspaceId,
    pub binding_name: String,
    pub dirty: bool,
}

impl Node {
    pub fn new(id: NodeId, workspace_id: WorkspaceId, binding_name: impl Into<String>) -> Self {
        Self { id, workspace_id, binding_name: binding_name.into(), dirty: false }
    }
    pub fn key(&self) -> BindingKey {
        BindingKey::new(self.workspace_id, &self.binding_name)
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Edge {
    pub id: EdgeId,
    pub from: NodeId,
    pub to: NodeId,
    pub kind: RelationshipKind,
    pub transform: Option<String>,
}

impl Edge {
    pub fn new(id: EdgeId, from: NodeId, to: NodeId) -> Self {
        Self { id, from, to, kind: RelationshipKind::Dependency, transform: None }
    }
    pub fn with_kind(mut self, kind: RelationshipKind) -> Self {
        self.kind = kind; self
    }
    pub fn with_transform(mut self, t: impl Into<String>) -> Self {
        self.transform = Some(t.into()); self
    }
}

#[derive(Debug, Clone, thiserror::Error)]
pub enum GraphError {
    #[error("node not found: {0:?}")]
    NodeNotFound(NodeId),
    #[error("edge not found: {0:?}")]
    EdgeNotFound(EdgeId),
    #[error("would create cycle: {path:?}")]
    WouldCreateCycle { path: Vec<NodeId> },
    #[error("duplicate edge from {from:?} to {to:?}")]
    DuplicateEdge { from: NodeId, to: NodeId },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PropagationStrategy { #[default] Eager, Lazy, Batched }

#[derive(Debug, Clone)]
pub struct PropagationConfig {
    pub strategy: PropagationStrategy,
    pub max_depth: Option<usize>,
    pub skip_unchanged: bool,
}

impl Default for PropagationConfig {
    fn default() -> Self {
        Self { strategy: PropagationStrategy::Eager, max_depth: None, skip_unchanged: true }
    }
}

#[derive(Debug, Clone, Default)]
pub struct RelationshipGraph {
    nodes: HashMap<NodeId, Node>,
    edges: HashMap<EdgeId, Edge>,
    node_index: HashMap<BindingKey, NodeId>,
    outgoing: HashMap<NodeId, Vec<EdgeId>>,
    incoming: HashMap<NodeId, Vec<EdgeId>>,
    next_node_id: u64,
    next_edge_id: u64,
}

impl RelationshipGraph {
    pub fn new() -> Self { Self::default() }
    pub fn node_count(&self) -> usize { self.nodes.len() }
    pub fn edge_count(&self) -> usize { self.edges.len() }

    pub fn get_or_create_node(&mut self, key: BindingKey) -> NodeId {
        if let Some(&id) = self.node_index.get(&key) { return id; }
        let id = NodeId::new(self.next_node_id);
        self.next_node_id += 1;
        let node = Node::new(id, key.workspace, &key.name);
        self.nodes.insert(id, node);
        self.node_index.insert(key, id);
        self.outgoing.insert(id, Vec::new());
        self.incoming.insert(id, Vec::new());
        id
    }

    pub fn get_node(&self, id: NodeId) -> Option<&Node> { self.nodes.get(&id) }
    pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut Node> { self.nodes.get_mut(&id) }
    pub fn find_node(&self, key: &BindingKey) -> Option<NodeId> { self.node_index.get(key).copied() }
    pub fn get_edge(&self, id: EdgeId) -> Option<&Edge> { self.edges.get(&id) }

    pub fn add_dependency(&mut self, from: NodeId, to: NodeId) -> Result<EdgeId, GraphError> {
        self.add_edge(from, to, RelationshipKind::Dependency, None)
    }

    pub fn add_edge(&mut self, from: NodeId, to: NodeId, kind: RelationshipKind, transform: Option<String>) -> Result<EdgeId, GraphError> {
        if !self.nodes.contains_key(&from) { return Err(GraphError::NodeNotFound(from)); }
        if !self.nodes.contains_key(&to) { return Err(GraphError::NodeNotFound(to)); }
        if self.has_edge(from, to) { return Err(GraphError::DuplicateEdge { from, to }); }
        if self.would_create_cycle(from, to) {
            let path = self.find_path(to, from).unwrap_or_default();
            return Err(GraphError::WouldCreateCycle { path });
        }
        let id = EdgeId::new(self.next_edge_id);
        self.next_edge_id += 1;
        let mut edge = Edge::new(id, from, to).with_kind(kind);
        if let Some(t) = transform { edge = edge.with_transform(t); }
        self.edges.insert(id, edge);
        self.outgoing.entry(from).or_default().push(id);
        self.incoming.entry(to).or_default().push(id);
        Ok(id)
    }

    pub fn has_edge(&self, from: NodeId, to: NodeId) -> bool {
        self.outgoing.get(&from).map(|e| e.iter().any(|&eid| self.edges.get(&eid).map(|e| e.to == to).unwrap_or(false))).unwrap_or(false)
    }

    pub fn remove_node(&mut self, id: NodeId) {
        if let Some(out) = self.outgoing.remove(&id) {
            for eid in out {
                if let Some(e) = self.edges.remove(&eid) {
                    if let Some(inc) = self.incoming.get_mut(&e.to) { inc.retain(|&x| x != eid); }
                }
            }
        }
        if let Some(inc) = self.incoming.remove(&id) {
            for eid in inc {
                if let Some(e) = self.edges.remove(&eid) {
                    if let Some(out) = self.outgoing.get_mut(&e.from) { out.retain(|&x| x != eid); }
                }
            }
        }
        if let Some(n) = self.nodes.remove(&id) { self.node_index.remove(&n.key()); }
    }

    pub fn remove_edge(&mut self, id: EdgeId) -> Result<Edge, GraphError> {
        let e = self.edges.remove(&id).ok_or(GraphError::EdgeNotFound(id))?;
        if let Some(out) = self.outgoing.get_mut(&e.from) { out.retain(|&x| x != id); }
        if let Some(inc) = self.incoming.get_mut(&e.to) { inc.retain(|&x| x != id); }
        Ok(e)
    }

    pub fn get_dependents(&self, id: NodeId) -> Vec<NodeId> {
        self.incoming.get(&id).map(|e| e.iter().filter_map(|&eid| self.edges.get(&eid).map(|e| e.from)).collect()).unwrap_or_default()
    }

    pub fn get_dependencies(&self, id: NodeId) -> Vec<NodeId> {
        self.outgoing.get(&id).map(|e| e.iter().filter_map(|&eid| self.edges.get(&eid).map(|e| e.to)).collect()).unwrap_or_default()
    }

    pub fn edges_for_node(&self, id: NodeId) -> Vec<&Edge> {
        let mut r = Vec::new();
        if let Some(out) = self.outgoing.get(&id) { for &eid in out { if let Some(e) = self.edges.get(&eid) { r.push(e); } } }
        if let Some(inc) = self.incoming.get(&id) { for &eid in inc { if let Some(e) = self.edges.get(&eid) { r.push(e); } } }
        r
    }

    pub fn nodes_in_workspace(&self, ws: WorkspaceId) -> Vec<NodeId> {
        self.nodes.iter().filter(|(_, n)| n.workspace_id == ws).map(|(&id, _)| id).collect()
    }

    pub fn has_cross_workspace_edges(&self) -> bool {
        self.edges.values().any(|e| {
            match (self.nodes.get(&e.from), self.nodes.get(&e.to)) {
                (Some(f), Some(t)) => f.workspace_id != t.workspace_id,
                _ => false,
            }
        })
    }

    pub fn would_create_cycle(&self, from: NodeId, to: NodeId) -> bool { self.can_reach(to, from) }

    pub fn detect_cycle(&self) -> Option<Vec<NodeId>> {
        let mut visited = HashSet::new();
        let mut rec = HashSet::new();
        let mut path = Vec::new();
        for &n in self.nodes.keys() {
            if self.cycle_dfs(n, &mut visited, &mut rec, &mut path) { return Some(path); }
        }
        None
    }

    fn cycle_dfs(&self, n: NodeId, visited: &mut HashSet<NodeId>, rec: &mut HashSet<NodeId>, path: &mut Vec<NodeId>) -> bool {
        if rec.contains(&n) { path.push(n); return true; }
        if visited.contains(&n) { return false; }
        visited.insert(n); rec.insert(n); path.push(n);
        for d in self.get_dependencies(n) { if self.cycle_dfs(d, visited, rec, path) { return true; } }
        path.pop(); rec.remove(&n);
        false
    }

    pub fn can_reach(&self, from: NodeId, to: NodeId) -> bool {
        if from == to { return true; }
        let mut visited = HashSet::new();
        let mut q = VecDeque::new();
        q.push_back(from);
        while let Some(c) = q.pop_front() {
            if c == to { return true; }
            if visited.contains(&c) { continue; }
            visited.insert(c);
            for d in self.get_dependencies(c) { q.push_back(d); }
        }
        false
    }

    pub fn find_path(&self, from: NodeId, to: NodeId) -> Option<Vec<NodeId>> {
        let mut visited = HashSet::new();
        let mut q = VecDeque::new();
        let mut parent: HashMap<NodeId, NodeId> = HashMap::new();
        q.push_back(from); visited.insert(from);
        while let Some(c) = q.pop_front() {
            if c == to {
                let mut p = vec![to];
                let mut n = to;
                while let Some(&pr) = parent.get(&n) { p.push(pr); n = pr; }
                p.reverse();
                return Some(p);
            }
            for d in self.get_dependencies(c) {
                if !visited.contains(&d) { visited.insert(d); parent.insert(d, c); q.push_back(d); }
            }
        }
        None
    }

    pub fn topological_order(&self) -> Vec<NodeId> {
        let mut deg: HashMap<NodeId, usize> = self.nodes.keys().map(|&id| (id, 0)).collect();
        for e in self.edges.values() { *deg.entry(e.from).or_insert(0) += 1; }
        let mut q: VecDeque<_> = deg.iter().filter(|(_, &d)| d == 0).map(|(&id, _)| id).collect();
        let mut r = Vec::new();
        while let Some(n) = q.pop_front() {
            r.push(n);
            for d in self.get_dependents(n) {
                if let Some(dg) = deg.get_mut(&d) { *dg = dg.saturating_sub(1); if *dg == 0 { q.push_back(d); } }
            }
        }
        r
    }

    pub fn affected_nodes(&self, changed: &[NodeId]) -> Vec<NodeId> {
        let mut aff = HashSet::new();
        let mut q = VecDeque::new();
        for &id in changed { q.push_back(id); }
        while let Some(n) = q.pop_front() {
            if aff.contains(&n) { continue; }
            aff.insert(n);
            for d in self.get_dependents(n) { q.push_back(d); }
        }
        self.topological_order().into_iter().filter(|n| aff.contains(n)).collect()
    }

    pub fn clear(&mut self) {
        self.nodes.clear(); self.edges.clear(); self.node_index.clear();
        self.outgoing.clear(); self.incoming.clear();
    }

    pub fn subgraph(&self, nodes: &[NodeId]) -> Self {
        let set: HashSet<_> = nodes.iter().copied().collect();
        let mut sub = Self::new();
        for &id in nodes {
            if let Some(n) = self.nodes.get(&id) {
                let nid = sub.get_or_create_node(n.key());
                if let Some(nn) = sub.get_node_mut(nid) { nn.dirty = n.dirty; }
            }
        }
        for e in self.edges.values() {
            if set.contains(&e.from) && set.contains(&e.to) {
                if let (Some(f), Some(t)) = (self.nodes.get(&e.from), self.nodes.get(&e.to)) {
                    if let (Some(fid), Some(tid)) = (sub.find_node(&f.key()), sub.find_node(&t.key())) {
                        let _ = sub.add_edge(fid, tid, e.kind, e.transform.clone());
                    }
                }
            }
        }
        sub
    }

    pub fn merge(&mut self, other: &Self) -> HashMap<NodeId, NodeId> {
        let mut map = HashMap::new();
        for (old, n) in &other.nodes { map.insert(*old, self.get_or_create_node(n.key())); }
        for e in other.edges.values() {
            if let (Some(&f), Some(&t)) = (map.get(&e.from), map.get(&e.to)) {
                let _ = self.add_edge(f, t, e.kind, e.transform.clone());
            }
        }
        map
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn mk(n: &str) -> BindingKey { BindingKey::new(WorkspaceId::new(), n) }

    #[test] fn node_creation() { let mut g = RelationshipGraph::new(); g.get_or_create_node(mk("x")); assert_eq!(g.node_count(), 1); }
    #[test] fn edge_creation() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert_eq!(g.edge_count(), 1); }
    #[test] fn get_or_create_idempotent() { let mut g = RelationshipGraph::new(); let k = mk("x"); let i1 = g.get_or_create_node(k.clone()); let i2 = g.get_or_create_node(k); assert_eq!(i1, i2); }
    #[test] fn add_dependency_ok() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert!(g.has_edge(a, b)); }
    #[test] fn remove_node_ok() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); g.remove_node(a); assert_eq!(g.node_count(), 1); assert_eq!(g.edge_count(), 0); }
    #[test] fn get_dependents_ok() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert_eq!(g.get_dependents(b).len(), 1); }
    #[test] fn get_dependencies_ok() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert_eq!(g.get_dependencies(a).len(), 1); }
    #[test] fn node_index_ok() { let mut g = RelationshipGraph::new(); let k = mk("x"); let id = g.get_or_create_node(k.clone()); assert_eq!(g.find_node(&k), Some(id)); }
    #[test] fn edge_transform() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let eid = g.add_edge(a, b, RelationshipKind::Dependency, Some("x".into())).unwrap(); assert!(g.get_edge(eid).unwrap().transform.is_some()); }
    #[test] fn cross_ws() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(BindingKey::new(WorkspaceId::new(), "a")); let b = g.get_or_create_node(BindingKey::new(WorkspaceId::new(), "b")); g.add_dependency(a, b).unwrap(); assert!(g.has_cross_workspace_edges()); }
    #[test] fn nodes_in_ws() { let mut g = RelationshipGraph::new(); let ws = WorkspaceId::new(); g.get_or_create_node(BindingKey::new(ws, "a")); g.get_or_create_node(BindingKey::new(ws, "b")); assert_eq!(g.nodes_in_workspace(ws).len(), 2); }
    #[test] fn edges_for() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert_eq!(g.edges_for_node(a).len(), 1); }
    #[test] fn dup_edge() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert!(g.add_dependency(a, b).is_err()); }
    #[test] fn graph_clone() { let mut g = RelationshipGraph::new(); g.get_or_create_node(mk("a")); let c = g.clone(); assert_eq!(c.node_count(), 1); }
    #[test] fn graph_clear() { let mut g = RelationshipGraph::new(); g.get_or_create_node(mk("a")); g.clear(); assert_eq!(g.node_count(), 0); }
    #[test] fn no_cycle_linear() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert!(g.detect_cycle().is_none()); }
    #[test] fn no_cycle_diamond() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); let d = g.get_or_create_node(mk("d")); g.add_dependency(a, b).unwrap(); g.add_dependency(a, c).unwrap(); g.add_dependency(b, d).unwrap(); g.add_dependency(c, d).unwrap(); assert!(g.detect_cycle().is_none()); }
    #[test] fn cycle_direct() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert!(g.add_dependency(b, a).is_err()); }
    #[test] fn cycle_indirect() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); g.add_dependency(a, b).unwrap(); g.add_dependency(b, c).unwrap(); assert!(g.add_dependency(c, a).is_err()); }
    #[test] fn self_loop() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); assert!(g.add_dependency(a, a).is_err()); }
    #[test] fn would_cycle_true() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert!(g.would_create_cycle(b, a)); }
    #[test] fn would_cycle_false() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); assert!(!g.would_create_cycle(a, b)); }
    #[test] fn cycle_path() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); if let Err(GraphError::WouldCreateCycle { path }) = g.add_dependency(b, a) { assert!(!path.is_empty()); } }
    #[test] fn topo_linear() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); let o = g.topological_order(); assert!(o.iter().position(|&x| x == b) < o.iter().position(|&x| x == a)); }
    #[test] fn topo_diamond() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); let d = g.get_or_create_node(mk("d")); g.add_dependency(a, b).unwrap(); g.add_dependency(a, c).unwrap(); g.add_dependency(b, d).unwrap(); g.add_dependency(c, d).unwrap(); let o = g.topological_order(); assert!(o.iter().position(|&x| x == d) < o.iter().position(|&x| x == a)); }
    #[test] fn topo_fanout() { let mut g = RelationshipGraph::new(); let r = g.get_or_create_node(mk("r")); for i in 0..10 { let c = g.get_or_create_node(mk(&format!("c{}", i))); g.add_dependency(c, r).unwrap(); } let o = g.topological_order(); assert_eq!(o[0], r); }
    #[test] fn affected_single() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(b, a).unwrap(); let af = g.affected_nodes(&[a]); assert!(af.contains(&b)); }
    #[test] fn affected_chain() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); g.add_dependency(b, a).unwrap(); g.add_dependency(c, b).unwrap(); assert_eq!(g.affected_nodes(&[a]).len(), 3); }
    #[test] fn affected_diamond() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); let d = g.get_or_create_node(mk("d")); g.add_dependency(b, a).unwrap(); g.add_dependency(c, a).unwrap(); g.add_dependency(d, b).unwrap(); g.add_dependency(d, c).unwrap(); assert_eq!(g.affected_nodes(&[a]).len(), 4); }
    #[test] fn reach_direct() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); assert!(g.can_reach(a, b)); }
    #[test] fn reach_indirect() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); g.add_dependency(a, b).unwrap(); g.add_dependency(b, c).unwrap(); assert!(g.can_reach(a, c)); }
    #[test] fn deep_chain() { let mut g = RelationshipGraph::new(); let mut p = g.get_or_create_node(mk("n0")); for i in 1..100 { let c = g.get_or_create_node(mk(&format!("n{}", i))); g.add_dependency(c, p).unwrap(); p = c; } assert_eq!(g.node_count(), 100); }
    #[test] fn wide_fanout() { let mut g = RelationshipGraph::new(); let r = g.get_or_create_node(mk("r")); for i in 0..100 { let d = g.get_or_create_node(mk(&format!("d{}", i))); g.add_dependency(d, r).unwrap(); } assert_eq!(g.get_dependents(r).len(), 100); }
    #[test] fn diamond() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); let d = g.get_or_create_node(mk("d")); g.add_dependency(a, b).unwrap(); g.add_dependency(a, c).unwrap(); g.add_dependency(b, d).unwrap(); g.add_dependency(c, d).unwrap(); assert_eq!(g.edge_count(), 4); }
    #[test] fn rm_cascade() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); g.add_dependency(a, b).unwrap(); g.add_dependency(b, c).unwrap(); g.remove_node(b); assert_eq!(g.edge_count(), 0); }
    #[test] fn many_ws() { let mut g = RelationshipGraph::new(); for _ in 0..10 { let ws = WorkspaceId::new(); for j in 0..5 { g.get_or_create_node(BindingKey::new(ws, format!("v{}", j))); } } assert_eq!(g.node_count(), 50); }
    // Note: RelationshipGraph uses tuppu-persistence's SerializedDependencyGraph for actual serialization
    #[test] fn merge_ok() { let mut g1 = RelationshipGraph::new(); g1.get_or_create_node(mk("a")); let mut g2 = RelationshipGraph::new(); g2.get_or_create_node(mk("b")); g1.merge(&g2); assert_eq!(g1.node_count(), 2); }
    #[test] fn subgraph_ok() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); g.add_dependency(a, b).unwrap(); let s = g.subgraph(&[a, b]); assert_eq!(s.edge_count(), 1); }
    #[test] fn reloc() { let mut g = RelationshipGraph::new(); let ws1 = WorkspaceId::new(); let a = g.get_or_create_node(BindingKey::new(ws1, "a")); g.remove_node(a); let _a2 = g.get_or_create_node(BindingKey::new(WorkspaceId::new(), "a")); assert_eq!(g.node_count(), 1); }
    #[test] fn strat_eager() { let c = PropagationConfig { strategy: PropagationStrategy::Eager, ..Default::default() }; assert_eq!(c.strategy, PropagationStrategy::Eager); }
    #[test] fn strat_lazy() { let c = PropagationConfig { strategy: PropagationStrategy::Lazy, ..Default::default() }; assert_eq!(c.strategy, PropagationStrategy::Lazy); }
    #[test] fn strat_batch() { let c = PropagationConfig { strategy: PropagationStrategy::Batched, ..Default::default() }; assert_eq!(c.strategy, PropagationStrategy::Batched); }
    #[test] fn max_depth() { let c = PropagationConfig { max_depth: Some(5), ..Default::default() }; assert_eq!(c.max_depth, Some(5)); }
    #[test] fn prop_order() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); g.add_dependency(b, a).unwrap(); g.add_dependency(c, b).unwrap(); let af = g.affected_nodes(&[a]); assert!(af.iter().position(|&x| x == a) < af.iter().position(|&x| x == c)); }
    #[test] fn skip_unchanged() { let c = PropagationConfig { skip_unchanged: true, ..Default::default() }; assert!(c.skip_unchanged); }
    #[test] fn partial() { let mut g = RelationshipGraph::new(); let a = g.get_or_create_node(mk("a")); let b = g.get_or_create_node(mk("b")); let c = g.get_or_create_node(mk("c")); g.add_dependency(b, a).unwrap(); let af = g.affected_nodes(&[a]); assert!(!af.contains(&c)); }
    #[test] fn err_handling() { let g = RelationshipGraph::new(); let af = g.affected_nodes(&[NodeId::new(999)]); assert!(af.is_empty() || af.len() <= 1); }
}
