//! Centralized identity mutation engine.
//!
//! ALL canonical_entity writes MUST flow through one of the MutationEngine entry points:
//!
//! - `execute()`        - Public API: idempotency + version gate + invariants
//! - `apply_internal()` - System path: invariants only (no idempotency, no version gate)
//! - `apply_batch()`    - System path: loop of apply_internal for N mutations, same tx
//! - `simulate()`       - Dry-run: full logic inside SAVEPOINT, then rollback
//!
//! This ensures consistent version tracking, state management, optimistic locking,
//! idempotency, and event emission across every mutation path.
//!
//! The engine does NOT own the transaction boundary - callers begin/commit their own
//! transactions and pass `&mut PgConnection`. This allows mutations to compose within
//! larger transactional workflows (e.g., resolve = ingest + score + mutate).
//!
//! All queries use compile-time `sqlx::query!()` macros validated against the schema.

use serde::{Deserialize, Serialize};
use sqlx::PgConnection;
use uuid::Uuid;

use crate::error::CannonError;
use crate::operations::merge::{self, MergeOptions, MergeRequest, MergeType};
use crate::operations::split::{self, SplitOptions, SplitRequest, SplitType};

// ---------------------------------------------------------------------------
// Mutation types
// ---------------------------------------------------------------------------

/// Every mutation that can be applied to the identity graph.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MutationType {
    /// Create a new canonical entity (singleton from resolve or split).
    CreateEntity {
        entity_id: Uuid,
        entity_type: String,
        canonical_data: serde_json::Value,
        #[serde(default)]
        field_provenance: Option<serde_json::Value>,
        #[serde(default)]
        blocking_keys: Vec<String>,
    },
    /// Merge two canonical entities (winner absorbs loser).
    MergeEntities {
        winner_id: Uuid,
        loser_id: Uuid,
        merge_type: String,
        #[serde(default)]
        confidence: Option<f64>,
        #[serde(default)]
        reason: Option<String>,
        #[serde(default)]
        delete_loser: bool,
    },
    /// Split members out of a canonical entity.
    SplitEntity {
        entity_id: Uuid,
        member_ids: Vec<Uuid>,
        split_type: String,
        #[serde(default)]
        reason: Option<String>,
    },
    /// Update fields on a canonical entity (patch).
    UpdateFields {
        entity_id: Uuid,
        patch: serde_json::Value,
    },
    /// Set entity subtype and optional parent.
    SetSubtype {
        entity_id: Uuid,
        subtype: String,
        #[serde(default)]
        parent_id: Option<Uuid>,
    },
    /// Lock or unlock an entity.
    LockEntity {
        entity_id: Uuid,
        locked: bool,
    },
    /// Revert entity to a prior state snapshot.
    RevertEntity {
        entity_id: Uuid,
        snapshot: serde_json::Value,
    },
    /// Merge blocking keys into an entity (set union). Used by resolve hot path.
    MergeBlockingKeys {
        entity_id: Uuid,
        new_keys: Vec<String>,
    },
    /// Set parent_entity_id only if currently NULL (first-write-wins).
    /// Used by projection consumer for deferred parent updates.
    SetParentIfAbsent {
        child_entity_id: Uuid,
        parent_entity_id: Uuid,
    },
}

// ---------------------------------------------------------------------------
// Mutation result
// ---------------------------------------------------------------------------

/// The result of a successful mutation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationResult {
    /// The primary entity affected.
    pub entity_id: Uuid,
    /// The entity's version after this mutation.
    pub new_version: i32,
    /// What type of mutation was applied.
    pub mutation_type: String,
    /// Additional result data (merge: loser_id, split: new_entity_ids, etc.)
    #[serde(default)]
    pub details: serde_json::Value,
}

// ---------------------------------------------------------------------------
// Mutation errors
// ---------------------------------------------------------------------------

/// Errors specific to the mutation engine.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "error", rename_all = "snake_case")]
pub enum MutationError {
    /// Caller expected a specific version but entity has diverged.
    VersionConflict {
        entity_id: Uuid,
        expected: i32,
        actual: i32,
    },
    /// Entity does not exist.
    EntityNotFound {
        entity_id: Uuid,
    },
    /// Entity is locked and cannot be mutated.
    EntityLocked {
        entity_id: Uuid,
    },
    /// Entity is in a terminal state (merged or tombstoned).
    EntityNotActive {
        entity_id: Uuid,
        state: String,
    },
    /// The mutation is invalid (bad input).
    InvalidMutation {
        reason: String,
    },
    /// Internal database or system error.
    Internal {
        reason: String,
    },
}

impl std::fmt::Display for MutationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::VersionConflict { entity_id, expected, actual } => {
                write!(f, "Version conflict on entity {}: expected {}, actual {}", entity_id, expected, actual)
            }
            Self::EntityNotFound { entity_id } => write!(f, "Entity {} not found", entity_id),
            Self::EntityLocked { entity_id } => write!(f, "Entity {} is locked", entity_id),
            Self::EntityNotActive { entity_id, state } => {
                write!(f, "Entity {} is {} and cannot be mutated", entity_id, state)
            }
            Self::InvalidMutation { reason } => write!(f, "Invalid mutation: {}", reason),
            Self::Internal { reason } => write!(f, "Internal error: {}", reason),
        }
    }
}

impl std::error::Error for MutationError {}

impl From<MutationError> for CannonError {
    fn from(e: MutationError) -> Self {
        match e {
            MutationError::VersionConflict { .. } => CannonError::Conflict(e.to_string()),
            MutationError::EntityNotFound { .. } => CannonError::NotFound(e.to_string()),
            MutationError::EntityLocked { .. } => CannonError::Conflict(e.to_string()),
            MutationError::EntityNotActive { .. } => CannonError::Conflict(e.to_string()),
            MutationError::InvalidMutation { .. } => CannonError::BadRequest(e.to_string()),
            MutationError::Internal { .. } => CannonError::Internal(e.to_string()),
        }
    }
}

// ---------------------------------------------------------------------------
// Entity snapshot (for pre-mutation checks)
// ---------------------------------------------------------------------------

/// Lightweight entity state loaded for pre-mutation validation.
///
/// Public so that callers on the resolve hot path can construct snapshots from
/// already-loaded data and pass them to `apply_internal_preloaded()`, avoiding
/// a redundant `SELECT ... FOR UPDATE` round-trip.
pub struct EntitySnapshot {
    pub version: i32,
    pub state: String,
    pub is_locked: bool,
}

// ---------------------------------------------------------------------------
// MutationEngine
// ---------------------------------------------------------------------------

/// Centralized engine for all canonical_entity mutations.
///
/// Callers provide a `&mut PgConnection` (within their own transaction) and
/// the engine performs the mutation atomically within that context.
pub struct MutationEngine;

impl MutationEngine {
    /// Execute a mutation within the caller's transaction.
    ///
    /// Steps:
    /// 1. Check idempotency_key (if provided) - returns cached result if exists
    /// 2. Load entity FOR UPDATE (if entity_id known)
    /// 3. Check expected_version (if provided) - returns VersionConflict on mismatch
    /// 4. Validate entity state (not locked, not merged/tombstoned)
    /// 5. Execute typed mutation logic
    /// 6. Store idempotency receipt (if key provided)
    /// 7. Return MutationResult (caller commits tx)
    pub async fn execute(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        mutation: MutationType,
        expected_version: Option<i32>,
        idempotency_key: Option<&str>,
    ) -> Result<MutationResult, MutationError> {
        // Check idempotency key (with advisory lock to prevent races)
        if let Some(key) = idempotency_key {
            // Advisory lock on key hash serializes concurrent requests with the same key
            let key_hash = Self::hash_idempotency_key(tenant_id, key);
            sqlx::query!("SELECT pg_advisory_xact_lock($1)", key_hash)
                .execute(&mut *conn)
                .await
                .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

            if let Some(cached) = Self::check_idempotency(conn, tenant_id, key).await? {
                return Ok(cached);
            }
        }

        // Execute the typed mutation (external API: respect entity locks)
        let result = Self::dispatch_mutation(conn, tenant_id, mutation, expected_version, false).await?;

        // Store idempotency receipt
        if let Some(key) = idempotency_key {
            Self::store_idempotency(conn, tenant_id, key, &result).await?;
        }

        Ok(result)
    }

    /// Simulate a mutation without committing.
    ///
    /// Runs the full mutation logic in a SAVEPOINT, then rolls back.
    /// Returns the result as if it had been applied.
    pub async fn simulate(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        mutation: MutationType,
    ) -> Result<MutationResult, MutationError> {
        // Use a savepoint so we can roll back within the caller's transaction
        let mut savepoint = sqlx::Acquire::begin(&mut *conn)
            .await
            .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        let result = Self::execute(&mut *savepoint, tenant_id, mutation, None, None).await?;

        // Roll back the savepoint - changes are discarded
        savepoint.rollback()
            .await
            .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        Ok(result)
    }

    /// System-owned mutation path. Enforces core invariants (state validation,
    /// version bump, event semantics) but skips optional external-API checks
    /// (idempotency keys, expected_version gating, lock enforcement).
    ///
    /// Locks are a user-level contract ("don't let agents/API users modify this
    /// entity"). The system itself must be able to mutate locked entities for
    /// resolve (blocking key merge, subtype annotation) and pipeline operations.
    ///
    /// Use this for internal system writes: resolve hot path, projection consumer,
    /// background pipeline. Callers own the transaction.
    pub async fn apply_internal(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        mutation: MutationType,
    ) -> Result<MutationResult, MutationError> {
        Self::dispatch_mutation(conn, tenant_id, mutation, None, true).await
    }

    /// Batch mutation path. Loops `apply_internal()` for N mutations within the
    /// caller's existing transaction. Stops on first error.
    ///
    /// Use this for batch system writes: pipeline subtype updates, bulk parent
    /// assignments. Caller owns the transaction boundary.
    pub async fn apply_batch(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        mutations: Vec<MutationType>,
    ) -> Result<Vec<MutationResult>, MutationError> {
        let mut results = Vec::with_capacity(mutations.len());
        for mutation in mutations {
            let result = Self::apply_internal(conn, tenant_id, mutation).await?;
            results.push(result);
        }
        Ok(results)
    }

    /// System-owned mutation path with a pre-loaded entity snapshot.
    ///
    /// Like `apply_internal()` but skips the `SELECT ... FOR UPDATE` round-trip
    /// because the caller already loaded the entity within the same transaction.
    /// The UPDATE queries still include `WHERE version = $expected` for safety -
    /// if someone else mutated between the read and write, zero rows are affected
    /// and VersionConflict is returned.
    ///
    /// Use this on the resolve hot path where:
    /// - SetSubtype after CreateEntity: entity was just INSERT'd, use synthetic snapshot
    /// - SetSubtype/MergeBlockingKeys on existing match: entity already loaded for scoring
    ///
    /// Only supports SetSubtype and MergeBlockingKeys (the resolve hot-path mutations).
    /// Other mutation types should use `apply_internal()`.
    pub async fn apply_internal_preloaded(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        mutation: MutationType,
        snapshot: EntitySnapshot,
    ) -> Result<MutationResult, MutationError> {
        // Validate snapshot state (same checks as load_and_validate minus the SELECT)
        if snapshot.state != "active" {
            let entity_id = match &mutation {
                MutationType::SetSubtype { entity_id, .. } => *entity_id,
                MutationType::MergeBlockingKeys { entity_id, .. } => *entity_id,
                _ => Uuid::nil(),
            };
            return Err(MutationError::EntityNotActive {
                entity_id,
                state: snapshot.state,
            });
        }
        // System path: skip lock checks (same as apply_internal)

        Self::dispatch_mutation_preloaded(conn, tenant_id, mutation, snapshot).await
    }

    // -----------------------------------------------------------------------
    // Pre-mutation validation
    // -----------------------------------------------------------------------

    /// Load entity state with FOR UPDATE lock and validate preconditions.
    async fn load_and_validate(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        expected_version: Option<i32>,
        allow_locked: bool,
    ) -> Result<EntitySnapshot, MutationError> {
        let row = sqlx::query!(
            "SELECT version, state, is_locked FROM canonical_entities WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
            entity_id,
            tenant_id,
        )
        .fetch_optional(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?
        .ok_or(MutationError::EntityNotFound { entity_id })?;

        let snap = EntitySnapshot {
            version: row.version,
            state: row.state,
            is_locked: row.is_locked,
        };

        // Check state
        if snap.state != "active" {
            return Err(MutationError::EntityNotActive {
                entity_id,
                state: snap.state.clone(),
            });
        }

        // Check lock (unless this is a lock/unlock operation)
        if !allow_locked && snap.is_locked {
            return Err(MutationError::EntityLocked { entity_id });
        }

        // Check expected version
        if let Some(expected) = expected_version {
            if snap.version != expected {
                return Err(MutationError::VersionConflict {
                    entity_id,
                    expected,
                    actual: snap.version,
                });
            }
        }

        Ok(snap)
    }

    /// Read current version of an entity.
    async fn read_version(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
    ) -> Result<i32, MutationError> {
        let row = sqlx::query!(
            "SELECT version FROM canonical_entities WHERE id = $1 AND tenant_id = $2",
            entity_id,
            tenant_id,
        )
        .fetch_one(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;
        Ok(row.version)
    }

    // -----------------------------------------------------------------------
    // Mutation dispatch (shared by execute and apply_internal)
    // -----------------------------------------------------------------------

    /// Dispatch a mutation to the correct execute_* method.
    ///
    /// - `expected_version`: passed through for external API paths (None for internal)
    /// - `system_path`: if true, skip entity lock checks (system owns the entity).
    ///   External API (`execute`) passes false; system path (`apply_internal`) passes true.
    async fn dispatch_mutation(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        mutation: MutationType,
        expected_version: Option<i32>,
        system_path: bool,
    ) -> Result<MutationResult, MutationError> {
        match mutation {
            MutationType::MergeEntities {
                winner_id,
                loser_id,
                merge_type,
                confidence,
                reason,
                delete_loser,
            } => {
                Self::execute_merge(
                    conn, tenant_id, winner_id, loser_id,
                    &merge_type, confidence, reason.as_deref(), delete_loser,
                    expected_version, system_path,
                ).await
            }
            MutationType::SplitEntity {
                entity_id,
                member_ids,
                split_type,
                reason,
            } => {
                Self::execute_split(
                    conn, tenant_id, entity_id, member_ids,
                    &split_type, reason.as_deref(), expected_version, system_path,
                ).await
            }
            MutationType::UpdateFields { entity_id, patch } => {
                Self::execute_update_fields(
                    conn, tenant_id, entity_id, patch, expected_version, system_path,
                ).await
            }
            MutationType::LockEntity { entity_id, locked } => {
                // Lock/unlock always needs allow_locked = true (you must unlock locked entities)
                Self::execute_lock(
                    conn, tenant_id, entity_id, locked, expected_version,
                ).await
            }
            MutationType::RevertEntity { entity_id, snapshot } => {
                Self::execute_revert(
                    conn, tenant_id, entity_id, snapshot, expected_version, system_path,
                ).await
            }
            MutationType::SetSubtype { entity_id, subtype, parent_id } => {
                Self::execute_set_subtype(
                    conn, tenant_id, entity_id, &subtype, parent_id, expected_version, system_path,
                ).await
            }
            MutationType::CreateEntity { entity_id, entity_type, canonical_data, field_provenance, blocking_keys } => {
                Self::execute_create(
                    conn, tenant_id, entity_id, &entity_type, canonical_data,
                    field_provenance, blocking_keys,
                ).await
            }
            MutationType::MergeBlockingKeys { entity_id, new_keys } => {
                Self::execute_merge_blocking_keys(
                    conn, tenant_id, entity_id, new_keys, expected_version, system_path,
                ).await
            }
            MutationType::SetParentIfAbsent { child_entity_id, parent_entity_id } => {
                Self::execute_set_parent_if_absent(
                    conn, tenant_id, child_entity_id, parent_entity_id,
                ).await
            }
        }
    }

    /// Dispatch a mutation using a pre-loaded snapshot (no DB fetch).
    ///
    /// Only supports SetSubtype and MergeBlockingKeys. Other mutation types
    /// fall back to `dispatch_mutation()` which does the full load_and_validate.
    async fn dispatch_mutation_preloaded(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        mutation: MutationType,
        snapshot: EntitySnapshot,
    ) -> Result<MutationResult, MutationError> {
        match mutation {
            MutationType::SetSubtype { entity_id, subtype, parent_id } => {
                Self::execute_set_subtype_preloaded(
                    conn, tenant_id, entity_id, &subtype, parent_id, snapshot,
                ).await
            }
            MutationType::MergeBlockingKeys { entity_id, new_keys } => {
                Self::execute_merge_blocking_keys_preloaded(
                    conn, tenant_id, entity_id, new_keys, snapshot,
                ).await
            }
            // Unsupported mutation types fall back to full path
            other => {
                Self::dispatch_mutation(conn, tenant_id, other, None, true).await
            }
        }
    }

    // -----------------------------------------------------------------------
    // Idempotency
    // -----------------------------------------------------------------------

    /// Hash tenant_id + idempotency key to an i64 for pg_advisory_xact_lock.
    fn hash_idempotency_key(tenant_id: Uuid, key: &str) -> i64 {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        tenant_id.hash(&mut hasher);
        key.hash(&mut hasher);
        hasher.finish() as i64
    }

    async fn check_idempotency(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        key: &str,
    ) -> Result<Option<MutationResult>, MutationError> {
        let row = sqlx::query!(
            "SELECT response_body FROM mutation_receipts WHERE tenant_id = $1 AND idempotency_key = $2",
            tenant_id,
            key,
        )
        .fetch_optional(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        match row {
            Some(r) => {
                let result: MutationResult = serde_json::from_value(r.response_body)
                    .map_err(|e| MutationError::Internal { reason: format!("Corrupt idempotency receipt: {}", e) })?;
                Ok(Some(result))
            }
            None => Ok(None),
        }
    }

    async fn store_idempotency(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        key: &str,
        result: &MutationResult,
    ) -> Result<(), MutationError> {
        let body = serde_json::to_value(result)
            .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        sqlx::query!(
            "INSERT INTO mutation_receipts (tenant_id, idempotency_key, entity_id, resulting_version, response_body)
             VALUES ($1, $2, $3, $4, $5)
             ON CONFLICT (tenant_id, idempotency_key) DO NOTHING",
            tenant_id,
            key,
            result.entity_id,
            result.new_version,
            body,
        )
        .execute(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        Ok(())
    }

    // -----------------------------------------------------------------------
    // Mutation implementations
    // -----------------------------------------------------------------------

    async fn execute_merge(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        winner_id: Uuid,
        loser_id: Uuid,
        merge_type: &str,
        confidence: Option<f64>,
        reason: Option<&str>,
        delete_loser: bool,
        expected_version: Option<i32>,
        system_path: bool,
    ) -> Result<MutationResult, MutationError> {
        // Deterministic lock ordering: always lock lower UUID first to prevent deadlocks
        let (first, second) = if winner_id < loser_id {
            (winner_id, loser_id)
        } else {
            (loser_id, winner_id)
        };

        // Load and validate both entities (with lock ordering)
        let first_snap = Self::load_and_validate(conn, tenant_id, first, None, system_path).await?;
        let second_snap = Self::load_and_validate(conn, tenant_id, second, None, system_path).await?;

        // If expected_version is provided, it applies to the winner
        if let Some(expected) = expected_version {
            let winner_version = if first == winner_id {
                first_snap.version
            } else {
                second_snap.version
            };
            if winner_version != expected {
                return Err(MutationError::VersionConflict {
                    entity_id: winner_id,
                    expected,
                    actual: winner_version,
                });
            }
        }

        let mt = match merge_type {
            "realtime" => MergeType::Realtime,
            "agent" => MergeType::Agent,
            "manual" => MergeType::Manual,
            _ => MergeType::Auto,
        };

        let mut metadata = serde_json::json!({});
        if let Some(r) = reason {
            metadata["reason"] = serde_json::json!(r);
        }

        let req = MergeRequest {
            tenant_id,
            winner_canonical_id: winner_id,
            loser_canonical_id: loser_id,
            merge_type: mt,
            confidence,
            metadata,
        };
        let opts = MergeOptions {
            delete_loser,
            recompute_canonical: true,
        };

        let result = merge::execute_merge(&mut *conn, &req, &opts)
            .await
            .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        // Read the winner's final version
        let new_version = Self::read_version(conn, tenant_id, winner_id).await?;

        Ok(MutationResult {
            entity_id: winner_id,
            new_version,
            mutation_type: "merge".to_string(),
            details: serde_json::json!({
                "winner_id": winner_id,
                "loser_id": loser_id,
                "members_moved": result.members_moved,
                "total_members": result.total_members,
            }),
        })
    }

    async fn execute_split(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        member_ids: Vec<Uuid>,
        split_type: &str,
        reason: Option<&str>,
        expected_version: Option<i32>,
        system_path: bool,
    ) -> Result<MutationResult, MutationError> {
        Self::load_and_validate(conn, tenant_id, entity_id, expected_version, system_path).await?;

        let st = match split_type {
            "agent" => SplitType::Agent,
            "manual" => SplitType::Manual,
            _ => SplitType::Auto,
        };

        let mut metadata = serde_json::json!({});
        if let Some(r) = reason {
            metadata["reason"] = serde_json::json!(r);
        }

        let req = SplitRequest {
            tenant_id,
            canonical_id: entity_id,
            member_ids,
            split_type: st,
            metadata,
        };

        let result = split::execute_split(&mut *conn, &req, &SplitOptions::default())
            .await
            .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        // Read the original entity's final version
        let new_version = Self::read_version(conn, tenant_id, entity_id).await?;

        let new_entity_ids: Vec<String> = result.new_entities.iter()
            .map(|ne| ne.canonical_id.to_string())
            .collect();

        Ok(MutationResult {
            entity_id,
            new_version,
            mutation_type: "split".to_string(),
            details: serde_json::json!({
                "remaining_members": result.remaining_members,
                "new_entity_ids": new_entity_ids,
            }),
        })
    }

    async fn execute_update_fields(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        patch: serde_json::Value,
        expected_version: Option<i32>,
        system_path: bool,
    ) -> Result<MutationResult, MutationError> {
        Self::load_and_validate(conn, tenant_id, entity_id, expected_version, system_path).await?;

        // Apply patch to canonical_data (top-level key merge via jsonb ||)
        let row = sqlx::query!(
            "UPDATE canonical_entities
             SET canonical_data = canonical_data || $3,
                 updated_at = NOW(),
                 version = version + 1
             WHERE id = $1 AND tenant_id = $2
             RETURNING version",
            entity_id,
            tenant_id,
            patch,
        )
        .fetch_one(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        let new_version = row.version;

        Ok(MutationResult {
            entity_id,
            new_version,
            mutation_type: "update_fields".to_string(),
            details: serde_json::json!({ "patch": patch }),
        })
    }

    async fn execute_lock(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        locked: bool,
        expected_version: Option<i32>,
    ) -> Result<MutationResult, MutationError> {
        // allow_locked = true: you need to be able to unlock a locked entity
        Self::load_and_validate(conn, tenant_id, entity_id, expected_version, true).await?;

        let row = sqlx::query!(
            "UPDATE canonical_entities
             SET is_locked = $3, version = version + 1
             WHERE id = $1 AND tenant_id = $2
             RETURNING version",
            entity_id,
            tenant_id,
            locked,
        )
        .fetch_one(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        let new_version = row.version;

        Ok(MutationResult {
            entity_id,
            new_version,
            mutation_type: "lock".to_string(),
            details: serde_json::json!({ "locked": locked }),
        })
    }

    async fn execute_revert(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        snapshot: serde_json::Value,
        expected_version: Option<i32>,
        system_path: bool,
    ) -> Result<MutationResult, MutationError> {
        Self::load_and_validate(conn, tenant_id, entity_id, expected_version, system_path).await?;

        let row = sqlx::query!(
            "UPDATE canonical_entities
             SET canonical_data = $3, updated_at = NOW(), version = version + 1
             WHERE id = $1 AND tenant_id = $2
             RETURNING version",
            entity_id,
            tenant_id,
            snapshot,
        )
        .fetch_one(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        let new_version = row.version;

        Ok(MutationResult {
            entity_id,
            new_version,
            mutation_type: "revert".to_string(),
            details: serde_json::json!({}),
        })
    }

    async fn execute_set_subtype(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        subtype: &str,
        parent_id: Option<Uuid>,
        expected_version: Option<i32>,
        system_path: bool,
    ) -> Result<MutationResult, MutationError> {
        Self::load_and_validate(conn, tenant_id, entity_id, expected_version, system_path).await?;

        let row = sqlx::query!(
            "UPDATE canonical_entities
             SET entity_subtype = $3,
                 parent_entity_id = COALESCE($4, parent_entity_id),
                 version = version + 1
             WHERE id = $1 AND tenant_id = $2
             RETURNING version",
            entity_id,
            tenant_id,
            subtype,
            parent_id,
        )
        .fetch_one(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        let new_version = row.version;

        Ok(MutationResult {
            entity_id,
            new_version,
            mutation_type: "set_subtype".to_string(),
            details: serde_json::json!({
                "subtype": subtype,
                "parent_id": parent_id,
            }),
        })
    }

    async fn execute_create(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        entity_type: &str,
        canonical_data: serde_json::Value,
        field_provenance: Option<serde_json::Value>,
        blocking_keys: Vec<String>,
    ) -> Result<MutationResult, MutationError> {
        let provenance = field_provenance.unwrap_or(serde_json::json!({}));

        sqlx::query!(
            "INSERT INTO canonical_entities (id, tenant_id, entity_type, canonical_data, field_provenance, confidence_score, blocking_keys, version, state)
             VALUES ($1, $2, $3, $4, $5, 1.0, $6, 1, 'active')",
            entity_id,
            tenant_id,
            entity_type,
            canonical_data,
            provenance,
            &blocking_keys,
        )
        .execute(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        Ok(MutationResult {
            entity_id,
            new_version: 1,
            mutation_type: "create".to_string(),
            details: serde_json::json!({
                "entity_type": entity_type,
            }),
        })
    }

    async fn execute_merge_blocking_keys(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        new_keys: Vec<String>,
        expected_version: Option<i32>,
        system_path: bool,
    ) -> Result<MutationResult, MutationError> {
        if new_keys.is_empty() {
            // No-op: nothing to merge. Read current version and return.
            let version = Self::read_version(conn, tenant_id, entity_id).await?;
            return Ok(MutationResult {
                entity_id,
                new_version: version,
                mutation_type: "merge_blocking_keys".to_string(),
                details: serde_json::json!({ "keys_added": 0 }),
            });
        }

        Self::load_and_validate(conn, tenant_id, entity_id, expected_version, system_path).await?;

        let row = sqlx::query!(
            "UPDATE canonical_entities
             SET blocking_keys = (
                 SELECT ARRAY(SELECT DISTINCT unnest(blocking_keys || $3))
             ),
             version = version + 1
             WHERE id = $1 AND tenant_id = $2
             RETURNING version",
            entity_id,
            tenant_id,
            &new_keys as &[String],
        )
        .fetch_one(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        let new_version = row.version;

        Ok(MutationResult {
            entity_id,
            new_version,
            mutation_type: "merge_blocking_keys".to_string(),
            details: serde_json::json!({ "keys_merged": new_keys.len() }),
        })
    }

    /// SetSubtype with pre-loaded snapshot. Skips load_and_validate SELECT.
    /// Uses version-conditional UPDATE for safety.
    async fn execute_set_subtype_preloaded(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        subtype: &str,
        parent_id: Option<Uuid>,
        snapshot: EntitySnapshot,
    ) -> Result<MutationResult, MutationError> {
        let result = sqlx::query!(
            "UPDATE canonical_entities
             SET entity_subtype = $3,
                 parent_entity_id = COALESCE($4, parent_entity_id),
                 version = version + 1
             WHERE id = $1 AND tenant_id = $2 AND version = $5
             RETURNING version",
            entity_id,
            tenant_id,
            subtype,
            parent_id,
            snapshot.version,
        )
        .fetch_optional(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        match result {
            Some(row) => {
                let new_version = row.version;
                Ok(MutationResult {
                    entity_id,
                    new_version,
                    mutation_type: "set_subtype".to_string(),
                    details: serde_json::json!({
                        "subtype": subtype,
                        "parent_id": parent_id,
                    }),
                })
            }
            None => {
                // Version mismatch - entity was mutated between our read and write
                let actual = Self::read_version(conn, tenant_id, entity_id).await
                    .unwrap_or(0);
                Err(MutationError::VersionConflict {
                    entity_id,
                    expected: snapshot.version,
                    actual,
                })
            }
        }
    }

    /// MergeBlockingKeys with pre-loaded snapshot. Skips load_and_validate SELECT.
    /// Uses version-conditional UPDATE for safety.
    async fn execute_merge_blocking_keys_preloaded(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        entity_id: Uuid,
        new_keys: Vec<String>,
        snapshot: EntitySnapshot,
    ) -> Result<MutationResult, MutationError> {
        if new_keys.is_empty() {
            return Ok(MutationResult {
                entity_id,
                new_version: snapshot.version,
                mutation_type: "merge_blocking_keys".to_string(),
                details: serde_json::json!({ "keys_added": 0 }),
            });
        }

        let result = sqlx::query!(
            "UPDATE canonical_entities
             SET blocking_keys = (
                 SELECT ARRAY(SELECT DISTINCT unnest(blocking_keys || $3))
             ),
             version = version + 1
             WHERE id = $1 AND tenant_id = $2 AND version = $4
             RETURNING version",
            entity_id,
            tenant_id,
            &new_keys as &[String],
            snapshot.version,
        )
        .fetch_optional(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        match result {
            Some(row) => {
                let new_version = row.version;
                Ok(MutationResult {
                    entity_id,
                    new_version,
                    mutation_type: "merge_blocking_keys".to_string(),
                    details: serde_json::json!({ "keys_merged": new_keys.len() }),
                })
            }
            None => {
                let actual = Self::read_version(conn, tenant_id, entity_id).await
                    .unwrap_or(0);
                Err(MutationError::VersionConflict {
                    entity_id,
                    expected: snapshot.version,
                    actual,
                })
            }
        }
    }

    async fn execute_set_parent_if_absent(
        conn: &mut PgConnection,
        tenant_id: Uuid,
        child_entity_id: Uuid,
        parent_entity_id: Uuid,
    ) -> Result<MutationResult, MutationError> {
        // First-write-wins: only set parent if currently NULL.
        // We skip load_and_validate here because this is a conditional update
        // that should silently succeed even if parent is already set (no-op).
        // We do NOT use expected_version - this is always an internal system write.
        let result = sqlx::query!(
            "UPDATE canonical_entities
             SET parent_entity_id = $1, version = version + 1
             WHERE id = $2 AND tenant_id = $3 AND parent_entity_id IS NULL
             RETURNING version",
            parent_entity_id,
            child_entity_id,
            tenant_id,
        )
        .fetch_optional(&mut *conn)
        .await
        .map_err(|e| MutationError::Internal { reason: e.to_string() })?;

        match result {
            Some(row) => {
                let new_version = row.version;
                Ok(MutationResult {
                    entity_id: child_entity_id,
                    new_version,
                    mutation_type: "set_parent".to_string(),
                    details: serde_json::json!({
                        "parent_entity_id": parent_entity_id,
                        "was_set": true,
                    }),
                })
            }
            None => {
                // Parent already set or entity doesn't exist - read current version
                let version = Self::read_version(conn, tenant_id, child_entity_id)
                    .await
                    .unwrap_or(0);
                Ok(MutationResult {
                    entity_id: child_entity_id,
                    new_version: version,
                    mutation_type: "set_parent".to_string(),
                    details: serde_json::json!({
                        "parent_entity_id": parent_entity_id,
                        "was_set": false,
                    }),
                })
            }
        }
    }
}

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

    #[test]
    fn test_mutation_error_display() {
        let err = MutationError::VersionConflict {
            entity_id: Uuid::nil(),
            expected: 5,
            actual: 7,
        };
        assert!(err.to_string().contains("expected 5"));
        assert!(err.to_string().contains("actual 7"));
    }

    #[test]
    fn test_mutation_error_into_cannon_error() {
        let err: CannonError = MutationError::VersionConflict {
            entity_id: Uuid::nil(),
            expected: 1,
            actual: 2,
        }.into();
        match err {
            CannonError::Conflict(_) => {}
            _ => panic!("Expected Conflict"),
        }

        let err: CannonError = MutationError::EntityNotFound {
            entity_id: Uuid::nil(),
        }.into();
        match err {
            CannonError::NotFound(_) => {}
            _ => panic!("Expected NotFound"),
        }

        let err: CannonError = MutationError::EntityLocked {
            entity_id: Uuid::nil(),
        }.into();
        match err {
            CannonError::Conflict(_) => {}
            _ => panic!("Expected Conflict"),
        }

        let err: CannonError = MutationError::InvalidMutation {
            reason: "test".into(),
        }.into();
        match err {
            CannonError::BadRequest(_) => {}
            _ => panic!("Expected BadRequest"),
        }
    }

    #[test]
    fn test_mutation_type_serialization() {
        let mt = MutationType::MergeEntities {
            winner_id: Uuid::nil(),
            loser_id: Uuid::nil(),
            merge_type: "manual".to_string(),
            confidence: Some(0.95),
            reason: Some("duplicate".to_string()),
            delete_loser: false,
        };
        let json = serde_json::to_value(&mt).unwrap();
        assert_eq!(json["type"], "merge_entities");
        assert_eq!(json["merge_type"], "manual");

        let mt = MutationType::UpdateFields {
            entity_id: Uuid::nil(),
            patch: serde_json::json!({"email": "test@example.com"}),
        };
        let json = serde_json::to_value(&mt).unwrap();
        assert_eq!(json["type"], "update_fields");
    }

    #[test]
    fn test_mutation_result_serialization() {
        let result = MutationResult {
            entity_id: Uuid::nil(),
            new_version: 3,
            mutation_type: "merge".to_string(),
            details: serde_json::json!({"winner_id": "abc"}),
        };
        let json = serde_json::to_value(&result).unwrap();
        assert_eq!(json["new_version"], 3);
        assert_eq!(json["mutation_type"], "merge");
    }

    #[test]
    fn test_mutation_result_roundtrip() {
        let result = MutationResult {
            entity_id: Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap(),
            new_version: 7,
            mutation_type: "split".to_string(),
            details: serde_json::json!({
                "remaining_members": 3,
                "new_entity_ids": ["id-1", "id-2"],
            }),
        };
        let json = serde_json::to_value(&result).unwrap();
        let deserialized: MutationResult = serde_json::from_value(json).unwrap();
        assert_eq!(deserialized.entity_id, result.entity_id);
        assert_eq!(deserialized.new_version, 7);
        assert_eq!(deserialized.mutation_type, "split");
        assert_eq!(deserialized.details["remaining_members"], 3);
        assert_eq!(deserialized.details["new_entity_ids"].as_array().unwrap().len(), 2);
    }

    #[test]
    fn test_mutation_result_default_details() {
        let json = serde_json::json!({
            "entity_id": "00000000-0000-0000-0000-000000000000",
            "new_version": 1,
            "mutation_type": "create",
        });
        let result: MutationResult = serde_json::from_value(json).unwrap();
        // details should default to null/empty since it has #[serde(default)]
        assert!(result.details.is_null());
    }

    // ---- MutationError Display for all variants ----

    #[test]
    fn test_mutation_error_display_entity_not_found() {
        let id = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let err = MutationError::EntityNotFound { entity_id: id };
        let msg = err.to_string();
        assert!(msg.contains("not found"), "Message: {}", msg);
        assert!(msg.contains("aaaaaaaa"), "Should contain entity_id: {}", msg);
    }

    #[test]
    fn test_mutation_error_display_entity_locked() {
        let id = Uuid::parse_str("bbbbbbbb-5555-6666-7777-888888888888").unwrap();
        let err = MutationError::EntityLocked { entity_id: id };
        let msg = err.to_string();
        assert!(msg.contains("locked"), "Message: {}", msg);
        assert!(msg.contains("bbbbbbbb"), "Should contain entity_id: {}", msg);
    }

    #[test]
    fn test_mutation_error_display_entity_not_active() {
        let id = Uuid::nil();
        let err = MutationError::EntityNotActive {
            entity_id: id,
            state: "merged".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("merged"), "Message: {}", msg);
        assert!(msg.contains("cannot be mutated"), "Message: {}", msg);
    }

    #[test]
    fn test_mutation_error_display_invalid_mutation() {
        let err = MutationError::InvalidMutation {
            reason: "empty member list".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("empty member list"), "Message: {}", msg);
        assert!(msg.contains("Invalid mutation"), "Message: {}", msg);
    }

    #[test]
    fn test_mutation_error_display_internal() {
        let err = MutationError::Internal {
            reason: "connection timeout".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("connection timeout"), "Message: {}", msg);
        assert!(msg.contains("Internal error"), "Message: {}", msg);
    }

    // ---- MutationError -> CannonError for remaining variants ----

    #[test]
    fn test_mutation_error_entity_not_active_into_cannon_error() {
        let err: CannonError = MutationError::EntityNotActive {
            entity_id: Uuid::nil(),
            state: "tombstoned".to_string(),
        }.into();
        match err {
            CannonError::Conflict(_) => {}
            _ => panic!("Expected Conflict, got {:?}", err),
        }
    }

    #[test]
    fn test_mutation_error_internal_into_cannon_error() {
        let err: CannonError = MutationError::Internal {
            reason: "db error".to_string(),
        }.into();
        match err {
            CannonError::Internal(_) => {}
            _ => panic!("Expected Internal, got {:?}", err),
        }
    }

    // ---- MutationType serde tags for all variants ----

    #[test]
    fn test_mutation_type_serde_tags_all_variants() {
        let id1 = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let id2 = Uuid::parse_str("bbbbbbbb-5555-6666-7777-888888888888").unwrap();

        let cases: Vec<(MutationType, &str)> = vec![
            (MutationType::CreateEntity {
                entity_id: id1,
                entity_type: "person".to_string(),
                canonical_data: serde_json::json!({"name": "Alice"}),
                field_provenance: None,
                blocking_keys: vec!["alice@test.com".to_string()],
            }, "create_entity"),
            (MutationType::MergeEntities {
                winner_id: id1,
                loser_id: id2,
                merge_type: "auto".to_string(),
                confidence: Some(0.92),
                reason: None,
                delete_loser: true,
            }, "merge_entities"),
            (MutationType::SplitEntity {
                entity_id: id1,
                member_ids: vec![id2],
                split_type: "manual".to_string(),
                reason: Some("wrong cluster".to_string()),
            }, "split_entity"),
            (MutationType::UpdateFields {
                entity_id: id1,
                patch: serde_json::json!({"email": "new@test.com"}),
            }, "update_fields"),
            (MutationType::SetSubtype {
                entity_id: id1,
                subtype: "product_variant".to_string(),
                parent_id: Some(id2),
            }, "set_subtype"),
            (MutationType::LockEntity {
                entity_id: id1,
                locked: true,
            }, "lock_entity"),
            (MutationType::RevertEntity {
                entity_id: id1,
                snapshot: serde_json::json!({"old_data": true}),
            }, "revert_entity"),
            (MutationType::MergeBlockingKeys {
                entity_id: id1,
                new_keys: vec!["key1".to_string(), "key2".to_string()],
            }, "merge_blocking_keys"),
            (MutationType::SetParentIfAbsent {
                child_entity_id: id1,
                parent_entity_id: id2,
            }, "set_parent_if_absent"),
        ];

        let mut seen_tags = std::collections::HashSet::new();
        for (mt, expected_tag) in &cases {
            let json = serde_json::to_value(mt).unwrap();
            let actual_tag = json["type"].as_str().unwrap();
            assert_eq!(actual_tag, *expected_tag, "Wrong serde tag for {:?}", mt);
            assert!(seen_tags.insert(actual_tag.to_string()), "Duplicate tag: {}", actual_tag);
        }
        assert_eq!(seen_tags.len(), 9, "Should cover all 9 MutationType variants");
    }

    // ---- MutationType deserialization (round-trip) ----

    #[test]
    fn test_mutation_type_deserialize_create_entity() {
        let json = serde_json::json!({
            "type": "create_entity",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "entity_type": "company",
            "canonical_data": {"name": "Acme Corp"},
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::CreateEntity { entity_type, blocking_keys, field_provenance, .. } => {
                assert_eq!(entity_type, "company");
                assert!(blocking_keys.is_empty(), "blocking_keys should default to empty");
                assert!(field_provenance.is_none(), "field_provenance should default to None");
            }
            _ => panic!("Expected CreateEntity"),
        }
    }

    #[test]
    fn test_mutation_type_deserialize_merge_with_defaults() {
        let json = serde_json::json!({
            "type": "merge_entities",
            "winner_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "loser_id": "bbbbbbbb-5555-6666-7777-888888888888",
            "merge_type": "agent",
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::MergeEntities { confidence, reason, delete_loser, .. } => {
                assert!(confidence.is_none(), "confidence should default to None");
                assert!(reason.is_none(), "reason should default to None");
                assert!(!delete_loser, "delete_loser should default to false");
            }
            _ => panic!("Expected MergeEntities"),
        }
    }

    #[test]
    fn test_mutation_type_deserialize_split_with_defaults() {
        let json = serde_json::json!({
            "type": "split_entity",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "member_ids": ["bbbbbbbb-5555-6666-7777-888888888888"],
            "split_type": "auto",
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::SplitEntity { reason, member_ids, .. } => {
                assert!(reason.is_none());
                assert_eq!(member_ids.len(), 1);
            }
            _ => panic!("Expected SplitEntity"),
        }
    }

    #[test]
    fn test_mutation_type_deserialize_lock_entity() {
        let json = serde_json::json!({
            "type": "lock_entity",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "locked": false,
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::LockEntity { locked, .. } => assert!(!locked),
            _ => panic!("Expected LockEntity"),
        }
    }

    #[test]
    fn test_mutation_type_deserialize_set_subtype_no_parent() {
        let json = serde_json::json!({
            "type": "set_subtype",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "subtype": "product_model",
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::SetSubtype { subtype, parent_id, .. } => {
                assert_eq!(subtype, "product_model");
                assert!(parent_id.is_none());
            }
            _ => panic!("Expected SetSubtype"),
        }
    }

    #[test]
    fn test_mutation_type_rejects_unknown_variant() {
        let json = serde_json::json!({
            "type": "delete_entity",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
        });
        let result = serde_json::from_value::<MutationType>(json);
        assert!(result.is_err(), "Unknown type should fail deserialization");
    }

    #[test]
    fn test_mutation_type_deserialize_merge_blocking_keys() {
        let json = serde_json::json!({
            "type": "merge_blocking_keys",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "new_keys": ["key-a", "key-b"],
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::MergeBlockingKeys { new_keys, .. } => {
                assert_eq!(new_keys.len(), 2);
                assert_eq!(new_keys[0], "key-a");
            }
            _ => panic!("Expected MergeBlockingKeys"),
        }
    }

    #[test]
    fn test_mutation_type_deserialize_set_parent_if_absent() {
        let json = serde_json::json!({
            "type": "set_parent_if_absent",
            "child_entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "parent_entity_id": "bbbbbbbb-5555-6666-7777-888888888888",
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::SetParentIfAbsent { child_entity_id, parent_entity_id } => {
                assert_eq!(child_entity_id.to_string(), "aaaaaaaa-1111-2222-3333-444444444444");
                assert_eq!(parent_entity_id.to_string(), "bbbbbbbb-5555-6666-7777-888888888888");
            }
            _ => panic!("Expected SetParentIfAbsent"),
        }
    }

    // ---- Idempotency key hashing ----

    #[test]
    fn test_hash_idempotency_key_deterministic() {
        let tenant = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let key = "merge-abc-123";
        let hash1 = MutationEngine::hash_idempotency_key(tenant, key);
        let hash2 = MutationEngine::hash_idempotency_key(tenant, key);
        assert_eq!(hash1, hash2, "Same inputs must produce same hash");
    }

    #[test]
    fn test_hash_idempotency_key_different_keys_differ() {
        let tenant = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let hash_a = MutationEngine::hash_idempotency_key(tenant, "key-alpha");
        let hash_b = MutationEngine::hash_idempotency_key(tenant, "key-beta");
        assert_ne!(hash_a, hash_b, "Different keys must produce different hashes");
    }

    #[test]
    fn test_hash_idempotency_key_different_tenants_differ() {
        let tenant_a = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let tenant_b = Uuid::parse_str("bbbbbbbb-5555-6666-7777-888888888888").unwrap();
        let hash_a = MutationEngine::hash_idempotency_key(tenant_a, "same-key");
        let hash_b = MutationEngine::hash_idempotency_key(tenant_b, "same-key");
        assert_ne!(hash_a, hash_b, "Different tenants must produce different hashes");
    }

    #[test]
    fn test_hash_idempotency_key_empty_key() {
        let tenant = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let hash = MutationEngine::hash_idempotency_key(tenant, "");
        // Should not panic and should produce a valid i64
        assert!(hash != 0 || hash == 0); // any i64 is valid
    }

    // ---- MutationError serialization (used in idempotency receipts) ----

    #[test]
    fn test_mutation_error_serialization_roundtrip() {
        let errors = vec![
            MutationError::VersionConflict { entity_id: Uuid::nil(), expected: 3, actual: 5 },
            MutationError::EntityNotFound { entity_id: Uuid::nil() },
            MutationError::EntityLocked { entity_id: Uuid::nil() },
            MutationError::EntityNotActive { entity_id: Uuid::nil(), state: "merged".into() },
            MutationError::InvalidMutation { reason: "bad input".into() },
            MutationError::Internal { reason: "timeout".into() },
        ];
        for err in &errors {
            let json = serde_json::to_value(err).unwrap();
            let tag = json["error"].as_str().unwrap();
            assert!(!tag.is_empty(), "Each error variant needs a serde tag");
            // Round-trip
            let deserialized: MutationError = serde_json::from_value(json).unwrap();
            assert_eq!(err.to_string(), deserialized.to_string());
        }
    }

    #[test]
    fn test_mutation_error_serde_tags() {
        let cases: Vec<(MutationError, &str)> = vec![
            (MutationError::VersionConflict { entity_id: Uuid::nil(), expected: 1, actual: 2 }, "version_conflict"),
            (MutationError::EntityNotFound { entity_id: Uuid::nil() }, "entity_not_found"),
            (MutationError::EntityLocked { entity_id: Uuid::nil() }, "entity_locked"),
            (MutationError::EntityNotActive { entity_id: Uuid::nil(), state: "merged".into() }, "entity_not_active"),
            (MutationError::InvalidMutation { reason: "x".into() }, "invalid_mutation"),
            (MutationError::Internal { reason: "x".into() }, "internal"),
        ];
        for (err, expected_tag) in &cases {
            let json = serde_json::to_value(err).unwrap();
            assert_eq!(json["error"].as_str().unwrap(), *expected_tag);
        }
    }

    // ---- MutationType with full field coverage ----

    #[test]
    fn test_merge_entities_all_fields_set() {
        let mt = MutationType::MergeEntities {
            winner_id: Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap(),
            loser_id: Uuid::parse_str("bbbbbbbb-5555-6666-7777-888888888888").unwrap(),
            merge_type: "agent".to_string(),
            confidence: Some(0.87),
            reason: Some("Agent-detected duplicate via email match".to_string()),
            delete_loser: true,
        };
        let json = serde_json::to_value(&mt).unwrap();
        assert_eq!(json["confidence"], 0.87);
        assert_eq!(json["delete_loser"], true);
        assert!(json["reason"].as_str().unwrap().contains("email match"));

        // Round-trip
        let deserialized: MutationType = serde_json::from_value(json).unwrap();
        match deserialized {
            MutationType::MergeEntities { delete_loser, confidence, .. } => {
                assert!(delete_loser);
                assert_eq!(confidence, Some(0.87));
            }
            _ => panic!("Expected MergeEntities"),
        }
    }

    #[test]
    fn test_create_entity_with_provenance_and_keys() {
        let mt = MutationType::CreateEntity {
            entity_id: Uuid::nil(),
            entity_type: "person".to_string(),
            canonical_data: serde_json::json!({"first_name": "Alice", "email": "alice@test.com"}),
            field_provenance: Some(serde_json::json!({"first_name": "source_crm", "email": "source_billing"})),
            blocking_keys: vec!["alice@test.com".to_string(), "alice".to_string()],
        };
        let json = serde_json::to_value(&mt).unwrap();
        assert_eq!(json["blocking_keys"].as_array().unwrap().len(), 2);
        assert!(json["field_provenance"].is_object());

        let deserialized: MutationType = serde_json::from_value(json).unwrap();
        match deserialized {
            MutationType::CreateEntity { blocking_keys, field_provenance, .. } => {
                assert_eq!(blocking_keys.len(), 2);
                assert!(field_provenance.is_some());
            }
            _ => panic!("Expected CreateEntity"),
        }
    }

    // ---- MergeBlockingKeys full field coverage ----

    #[test]
    fn test_merge_blocking_keys_roundtrip() {
        let id = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let mt = MutationType::MergeBlockingKeys {
            entity_id: id,
            new_keys: vec!["alice@test.com".to_string(), "bob@test.com".to_string(), "charlie@test.com".to_string()],
        };
        let json = serde_json::to_value(&mt).unwrap();
        assert_eq!(json["type"], "merge_blocking_keys");
        assert_eq!(json["entity_id"], "aaaaaaaa-1111-2222-3333-444444444444");
        assert_eq!(json["new_keys"].as_array().unwrap().len(), 3);
        assert_eq!(json["new_keys"][0], "alice@test.com");

        let deserialized: MutationType = serde_json::from_value(json).unwrap();
        match deserialized {
            MutationType::MergeBlockingKeys { entity_id: eid, new_keys } => {
                assert_eq!(eid, id);
                assert_eq!(new_keys.len(), 3);
                assert_eq!(new_keys[2], "charlie@test.com");
            }
            _ => panic!("Expected MergeBlockingKeys"),
        }
    }

    #[test]
    fn test_merge_blocking_keys_empty_keys() {
        let json = serde_json::json!({
            "type": "merge_blocking_keys",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
            "new_keys": [],
        });
        let mt: MutationType = serde_json::from_value(json).unwrap();
        match mt {
            MutationType::MergeBlockingKeys { new_keys, .. } => {
                assert!(new_keys.is_empty(), "Empty keys should deserialize to empty vec");
            }
            _ => panic!("Expected MergeBlockingKeys"),
        }
    }

    #[test]
    fn test_merge_blocking_keys_default_keys() {
        // new_keys has no #[serde(default)], so omitting it should fail
        let json = serde_json::json!({
            "type": "merge_blocking_keys",
            "entity_id": "aaaaaaaa-1111-2222-3333-444444444444",
        });
        let result = serde_json::from_value::<MutationType>(json);
        assert!(result.is_err(), "Missing new_keys should fail deserialization");
    }

    // ---- SetParentIfAbsent full field coverage ----

    #[test]
    fn test_set_parent_if_absent_roundtrip() {
        let child = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let parent = Uuid::parse_str("bbbbbbbb-5555-6666-7777-888888888888").unwrap();
        let mt = MutationType::SetParentIfAbsent {
            child_entity_id: child,
            parent_entity_id: parent,
        };
        let json = serde_json::to_value(&mt).unwrap();
        assert_eq!(json["type"], "set_parent_if_absent");
        assert_eq!(json["child_entity_id"], "aaaaaaaa-1111-2222-3333-444444444444");
        assert_eq!(json["parent_entity_id"], "bbbbbbbb-5555-6666-7777-888888888888");
        // Verify no extra fields leaked
        let obj = json.as_object().unwrap();
        assert_eq!(obj.len(), 3, "Should have exactly type + child + parent fields");

        let deserialized: MutationType = serde_json::from_value(json).unwrap();
        match deserialized {
            MutationType::SetParentIfAbsent { child_entity_id, parent_entity_id } => {
                assert_eq!(child_entity_id, child);
                assert_eq!(parent_entity_id, parent);
            }
            _ => panic!("Expected SetParentIfAbsent"),
        }
    }

    #[test]
    fn test_set_parent_if_absent_same_ids() {
        // Edge case: child == parent should still deserialize (validation happens at execute time)
        let id = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let mt = MutationType::SetParentIfAbsent {
            child_entity_id: id,
            parent_entity_id: id,
        };
        let json = serde_json::to_value(&mt).unwrap();
        let deserialized: MutationType = serde_json::from_value(json).unwrap();
        match deserialized {
            MutationType::SetParentIfAbsent { child_entity_id, parent_entity_id } => {
                assert_eq!(child_entity_id, parent_entity_id);
            }
            _ => panic!("Expected SetParentIfAbsent"),
        }
    }

    // ---- system_path contract tests ----
    // These verify the architectural contract: execute() uses system_path=false,
    // apply_internal() uses system_path=true. Since dispatch_mutation is private,
    // we verify this through the public API signatures and MutationType completeness.

    #[test]
    fn test_all_mutation_types_have_entity_id() {
        // Every MutationType variant must carry an entity_id (or child_entity_id)
        // so that dispatch_mutation can route it properly.
        // This test ensures new variants don't accidentally omit the identifier.
        let id1 = Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap();
        let id2 = Uuid::parse_str("bbbbbbbb-5555-6666-7777-888888888888").unwrap();

        let variants: Vec<(MutationType, Uuid)> = vec![
            (MutationType::CreateEntity {
                entity_id: id1, entity_type: "t".into(),
                canonical_data: serde_json::json!({}),
                field_provenance: None, blocking_keys: vec![],
            }, id1),
            (MutationType::MergeEntities {
                winner_id: id1, loser_id: id2,
                merge_type: "auto".into(), confidence: None,
                reason: None, delete_loser: false,
            }, id1),
            (MutationType::SplitEntity {
                entity_id: id1, member_ids: vec![],
                split_type: "auto".into(), reason: None,
            }, id1),
            (MutationType::UpdateFields {
                entity_id: id1,
                patch: serde_json::json!({}),
            }, id1),
            (MutationType::SetSubtype {
                entity_id: id1, subtype: "s".into(), parent_id: None,
            }, id1),
            (MutationType::LockEntity { entity_id: id1, locked: true }, id1),
            (MutationType::RevertEntity {
                entity_id: id1, snapshot: serde_json::json!({}),
            }, id1),
            (MutationType::MergeBlockingKeys {
                entity_id: id1, new_keys: vec![],
            }, id1),
            (MutationType::SetParentIfAbsent {
                child_entity_id: id1, parent_entity_id: id2,
            }, id1),
        ];

        assert_eq!(variants.len(), 9, "Must cover all 9 MutationType variants");

        for (mt, expected_id) in &variants {
            let json = serde_json::to_value(mt).unwrap();
            // Every variant should serialize to JSON with an identifiable entity
            let has_entity_field = json.get("entity_id")
                .or_else(|| json.get("winner_id"))
                .or_else(|| json.get("child_entity_id"));
            assert!(has_entity_field.is_some(),
                "Variant {:?} must have entity_id, winner_id, or child_entity_id field", json["type"]);

            // Verify the ID matches what we expect
            let actual_id: Uuid = serde_json::from_value(has_entity_field.unwrap().clone()).unwrap();
            assert_eq!(actual_id, *expected_id,
                "Entity ID mismatch for variant {}", json["type"]);
        }
    }

    #[test]
    fn test_mutation_result_set_parent_was_set_true() {
        let result = MutationResult {
            entity_id: Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap(),
            new_version: 4,
            mutation_type: "set_parent".to_string(),
            details: serde_json::json!({
                "parent_entity_id": "bbbbbbbb-5555-6666-7777-888888888888",
                "was_set": true,
            }),
        };
        let json = serde_json::to_value(&result).unwrap();
        assert_eq!(json["mutation_type"], "set_parent");
        assert_eq!(json["details"]["was_set"], true);
        assert_eq!(json["new_version"], 4);
    }

    #[test]
    fn test_mutation_result_set_parent_was_set_false() {
        // When parent is already set, was_set should be false and version unchanged
        let result = MutationResult {
            entity_id: Uuid::parse_str("aaaaaaaa-1111-2222-3333-444444444444").unwrap(),
            new_version: 3,
            mutation_type: "set_parent".to_string(),
            details: serde_json::json!({
                "parent_entity_id": "bbbbbbbb-5555-6666-7777-888888888888",
                "was_set": false,
            }),
        };
        let json = serde_json::to_value(&result).unwrap();
        assert_eq!(json["details"]["was_set"], false);
        assert_eq!(json["new_version"], 3);
    }

    #[test]
    fn test_mutation_result_merge_blocking_keys_details() {
        let result = MutationResult {
            entity_id: Uuid::nil(),
            new_version: 5,
            mutation_type: "merge_blocking_keys".to_string(),
            details: serde_json::json!({ "keys_merged": 3 }),
        };
        let json = serde_json::to_value(&result).unwrap();
        assert_eq!(json["mutation_type"], "merge_blocking_keys");
        assert_eq!(json["details"]["keys_merged"], 3);
    }

    #[test]
    fn test_mutation_result_merge_blocking_keys_empty_noop() {
        // When no keys to merge, should report keys_added: 0
        let result = MutationResult {
            entity_id: Uuid::nil(),
            new_version: 2,
            mutation_type: "merge_blocking_keys".to_string(),
            details: serde_json::json!({ "keys_added": 0 }),
        };
        let json = serde_json::to_value(&result).unwrap();
        assert_eq!(json["details"]["keys_added"], 0);
        assert_eq!(json["new_version"], 2);
    }

    // ---- EntitySnapshot public construction ----

    #[test]
    fn test_entity_snapshot_construction() {
        // Verify EntitySnapshot can be constructed publicly (used by resolve hot path)
        let snap = EntitySnapshot {
            version: 1,
            state: "active".to_string(),
            is_locked: false,
        };
        assert_eq!(snap.version, 1);
        assert_eq!(snap.state, "active");
        assert!(!snap.is_locked);
    }

    #[test]
    fn test_entity_snapshot_synthetic_after_create() {
        // Synthetic snapshot for SetSubtype after CreateEntity
        let snap = EntitySnapshot {
            version: 1,
            state: "active".to_string(),
            is_locked: false,
        };
        // This is the pattern used in the resolve hot path
        assert_eq!(snap.version, 1);
        assert_eq!(snap.state, "active");
    }

    #[test]
    fn test_entity_snapshot_from_match() {
        // Snapshot built from an existing match candidate
        let snap = EntitySnapshot {
            version: 5,
            state: "active".to_string(),
            is_locked: true, // system path ignores locks
        };
        assert_eq!(snap.version, 5);
        assert!(snap.is_locked);
    }
}
