//! Mutation event types — proposed and committed shapes.
//!
//! Handlers emit `GraphMutationBatch { ops: Vec<GraphMutation> }` via
//! the registrar's propose path. The host validates, allocates fresh
//! ids, resolves any `NodeRef::LocalIndex` to a real `BoardNodeId`, then
//! emits a single committed `GameEvent::GraphMutated { ops: Vec<GraphOp> }`.

use crate::board::{EdgeKind};
use crate::board::ids::{EdgeId, FaceId, BoardNodeId};

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeRef {
    Existing(BoardNodeId),
    /// The Nth `AddNode` op in this same batch (0-indexed).
    LocalIndex(u32),
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProposedEdge {
    Bidirectional { a: NodeRef, b: NodeRef },
    Directed      { from: NodeRef, to: NodeRef },
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphMutation {
    AddNode,
    AddEdge { kind: ProposedEdge },
    AddFace { bounding_nodes: Vec<NodeRef> },
    RemoveNode(BoardNodeId),
    RemoveEdge(EdgeId),
    RemoveFace(FaceId),
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphMutationBatch {
    pub ops: Vec<GraphMutation>,
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphOp {
    NodeAdded   { id: BoardNodeId },
    EdgeAdded   { id: EdgeId, kind: EdgeKind },
    FaceAdded   { id: FaceId, bounding_nodes: Vec<BoardNodeId> },
    NodeRemoved { id: BoardNodeId },
    EdgeRemoved { id: EdgeId },
    FaceRemoved { id: FaceId },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn proposed_batch_is_constructible() {
        let batch = GraphMutationBatch { ops: vec![
            GraphMutation::AddNode,
            GraphMutation::AddNode,
            GraphMutation::AddEdge {
                kind: ProposedEdge::Bidirectional {
                    a: NodeRef::LocalIndex(0),
                    b: NodeRef::LocalIndex(1),
                },
            },
        ]};
        assert_eq!(batch.ops.len(), 3);
    }

    #[test]
    #[cfg(feature = "wire")]
    fn mutation_postcard_round_trip() {
        let batch = GraphMutationBatch { ops: vec![
            GraphMutation::AddNode,
            GraphMutation::AddEdge {
                kind: ProposedEdge::Directed {
                    from: NodeRef::Existing(BoardNodeId(7)),
                    to:   NodeRef::LocalIndex(0),
                },
            },
        ]};
        let bytes = postcard::to_allocvec(&batch).unwrap();
        let decoded: GraphMutationBatch = postcard::from_bytes(&bytes).unwrap();
        assert_eq!(decoded, batch);
    }

    #[test]
    fn op_distinguishes_add_from_remove() {
        let added = GraphOp::NodeAdded { id: BoardNodeId(5) };
        let removed = GraphOp::NodeRemoved { id: BoardNodeId(5) };
        assert_ne!(added, removed);
    }
}
