//! Fault injection primitives for M8 UAT testing.
//!
//! Provides RAII handles for network partitions. The fault is active while
//! the handle is alive and automatically healed on drop.
//!
//! # Design
//!
//! `NetworkPartition` adds region IDs to the cluster's `partitioned_regions`
//! set. The cluster's convergence logic checks this set and skips isolated
//! regions. When the `NetworkPartition` handle is dropped, the regions are
//! removed from the set and the cluster can converge again.
//!
//! This is a test-only module gated behind `#[cfg(any(test, feature = "test-utils"))]`.

use std::collections::HashSet;
use std::sync::{Arc, RwLock};

use crate::replication::shard::RegionId;

/// RAII handle for a network partition.
///
/// While this handle is alive, the specified regions are isolated from
/// the cluster. Relay events are not applied to isolated followers.
/// When the handle is dropped, the partition is automatically healed.
///
/// # Examples
///
/// ```ignore
/// let partition = NetworkPartition::isolate(
///     RegionId(2),
///     cluster.partitioned_regions(),
/// );
/// // Region 2 is now isolated...
/// drop(partition);
/// // ...and now it's healed.
/// ```
pub struct NetworkPartition {
    /// Regions that were added to the partition set by this handle.
    regions: Vec<RegionId>,
    /// Shared reference to the cluster's partitioned region set.
    partitioned: Arc<RwLock<HashSet<RegionId>>>,
}

impl NetworkPartition {
    /// Isolate a single region from the cluster.
    ///
    /// The region will not receive relay events during convergence.
    /// Dropping the returned handle heals the partition.
    #[must_use]
    pub fn isolate(region: RegionId, partitioned: &Arc<RwLock<HashSet<RegionId>>>) -> Self {
        partitioned
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(region);
        Self {
            regions: vec![region],
            partitioned: Arc::clone(partitioned),
        }
    }

    /// Isolate multiple regions from the cluster simultaneously.
    ///
    /// All specified regions will be partitioned. Dropping the returned
    /// handle heals all of them.
    #[must_use]
    pub fn isolate_many(
        regions: &[RegionId],
        partitioned: &Arc<RwLock<HashSet<RegionId>>>,
    ) -> Self {
        {
            let mut set = partitioned
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            for &r in regions {
                set.insert(r);
            }
        }
        Self {
            regions: regions.to_vec(),
            partitioned: Arc::clone(partitioned),
        }
    }

    /// Create a symmetric partition between two regions.
    ///
    /// Both regions are isolated from each other and from the rest of
    /// the cluster. In practice, for our relay-based cluster, isolating
    /// a region means it stops receiving events.
    #[must_use]
    pub fn symmetric(
        region_a: RegionId,
        region_b: RegionId,
        partitioned: &Arc<RwLock<HashSet<RegionId>>>,
    ) -> Self {
        Self::isolate_many(&[region_a, region_b], partitioned)
    }

    /// Returns the regions isolated by this partition.
    #[must_use]
    pub fn regions(&self) -> &[RegionId] {
        &self.regions
    }
}

impl Drop for NetworkPartition {
    fn drop(&mut self) {
        let mut set = self
            .partitioned
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        for r in &self.regions {
            set.remove(r);
        }
    }
}

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

    fn make_set() -> Arc<RwLock<HashSet<RegionId>>> {
        Arc::new(RwLock::new(HashSet::new()))
    }

    #[test]
    fn isolate_adds_region() {
        let set = make_set();
        let _p = NetworkPartition::isolate(RegionId(1), &set);
        assert!(set.read().unwrap().contains(&RegionId(1)));
    }

    #[test]
    fn drop_heals_partition() {
        let set = make_set();
        {
            let _p = NetworkPartition::isolate(RegionId(2), &set);
            assert!(set.read().unwrap().contains(&RegionId(2)));
        }
        // After drop, region should be removed.
        assert!(!set.read().unwrap().contains(&RegionId(2)));
    }

    #[test]
    fn isolate_many_works() {
        let set = make_set();
        let _p = NetworkPartition::isolate_many(&[RegionId(1), RegionId(3)], &set);
        let guard = set.read().unwrap();
        assert!(guard.contains(&RegionId(1)));
        assert!(guard.contains(&RegionId(3)));
    }

    #[test]
    fn symmetric_isolates_both() {
        let set = make_set();
        let _p = NetworkPartition::symmetric(RegionId(0), RegionId(2), &set);
        let guard = set.read().unwrap();
        assert!(guard.contains(&RegionId(0)));
        assert!(guard.contains(&RegionId(2)));
    }

    #[test]
    fn regions_accessor() {
        let set = make_set();
        let p = NetworkPartition::isolate(RegionId(5), &set);
        assert_eq!(p.regions(), &[RegionId(5)]);
    }
}
