// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Erasure coding implementation using reed-solomon-erasure (GF(2^8)).
//! Supports legacy (reed-solomon-simd) for reading/healing old-version files.
//!

use bytes::{Bytes, BytesMut};
use reed_solomon_erasure::galois_8::ReedSolomon;
use reed_solomon_simd;
use smallvec::SmallVec;
use std::{
    collections::HashMap,
    io,
    sync::{Arc, OnceLock, RwLock},
};
use tokio::io::AsyncRead;
use tracing::warn;
use uuid::Uuid;

pub(crate) struct EncodedBlock {
    data: Bytes,
    shard_size: usize,
}

impl EncodedBlock {
    fn empty() -> Self {
        Self {
            data: Bytes::new(),
            shard_size: 0,
        }
    }

    pub(crate) fn is_empty(&self) -> bool {
        self.data.is_empty()
    }

    pub(crate) fn queued_bytes(&self) -> usize {
        self.data.len()
    }

    pub(crate) fn shards(&self) -> impl ExactSizeIterator<Item = &[u8]> {
        debug_assert!(self.shard_size > 0, "only non-empty encoded blocks reach shard writers");
        debug_assert_eq!(self.data.len() % self.shard_size, 0);
        self.data.chunks_exact(self.shard_size)
    }

    fn into_shards(mut self, shard_count: usize) -> Vec<Bytes> {
        if self.shard_size == 0 {
            return vec![Bytes::new(); shard_count];
        }

        let mut shards = Vec::with_capacity(shard_count);
        for _ in 0..shard_count {
            shards.push(self.data.split_to(self.shard_size));
        }
        shards
    }
}

const MODERN_MAX_TOTAL_SHARDS: usize = <reed_solomon_erasure::galois_8::Field as reed_solomon_erasure::Field>::ORDER;
const MODERN_REED_SOLOMON_CACHE_MAX_ENTRIES: usize = 64;
const LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES: usize = 16;
// Vec growth may retain twice the requested logical length. Keeping the logical
// workspace at half the budget bounds each cached workspace's shard allocation to 1 MiB.
const LEGACY_REED_SOLOMON_CACHE_MAX_LOGICAL_SHARD_BYTES_PER_WORKSPACE: usize = 512 * 1024;

type ModernReedSolomonCache = RwLock<HashMap<(usize, usize), Arc<ReedSolomon>>>;
type LegacyReedSolomonCache = RwLock<HashMap<(usize, usize), Arc<LegacyReedSolomonEncoder>>>;

static MODERN_REED_SOLOMON_CACHE: OnceLock<ModernReedSolomonCache> = OnceLock::new();
static LEGACY_REED_SOLOMON_CACHE: OnceLock<LegacyReedSolomonCache> = OnceLock::new();

/// Errors returned when constructing an [`Erasure`] codec.
#[derive(Debug, thiserror::Error)]
pub enum ErasureConstructionError {
    /// Erasure coding requires at least one data shard.
    #[error("data_shards must be greater than zero")]
    ZeroDataShards,

    /// Erasure coding requires a non-zero logical block size.
    #[error("block_size must be greater than zero")]
    ZeroBlockSize,

    /// The total shard count cannot be represented by `usize`.
    #[error("data_shards ({data_shards}) + parity_shards ({parity_shards}) overflows usize")]
    ShardCountOverflow { data_shards: usize, parity_shards: usize },

    /// The modern GF(2^8) codec cannot represent this shard configuration.
    #[error("modern codec does not support {data_shards} data shards and {parity_shards} parity shards")]
    UnsupportedModernShardCount { data_shards: usize, parity_shards: usize },

    /// The legacy SIMD codec cannot represent this shard configuration.
    #[error("legacy codec does not support {data_shards} data shards and {parity_shards} parity shards")]
    UnsupportedLegacyShardCount { data_shards: usize, parity_shards: usize },

    /// The modern encoder rejected dimensions that passed the preflight checks.
    #[error("failed to construct modern Reed-Solomon encoder")]
    ModernEncoder {
        #[source]
        source: reed_solomon_erasure::Error,
    },

    /// The legacy encoder wrapper failed to initialize.
    #[error("failed to construct legacy Reed-Solomon encoder")]
    LegacyEncoder {
        #[source]
        source: io::Error,
    },
}

#[derive(Debug, thiserror::Error)]
#[error("{source}")]
struct ErasureConstructionIoSource {
    #[source]
    source: ErasureConstructionError,
}

impl ErasureConstructionError {
    pub(crate) fn into_io_error(self) -> io::Error {
        // `io::Error::source` forwards to the wrapped error's source rather
        // than exposing the wrapped error itself. This adapter keeps the
        // construction error as an observable link in the source chain.
        io::Error::other(ErasureConstructionIoSource { source: self })
    }
}

/// Legacy calc_shard_size formula: (block_size.div_ceil(data_shards) + 1) & !1
/// Matches main branch and filemeta::ErasureInfo for old-version files.
pub fn calc_shard_size_legacy(block_size: usize, data_shards: usize) -> usize {
    (block_size.div_ceil(data_shards) + 1) & !1
}

/// Reed-Solomon encoder for legacy (main branch) format using reed-solomon-simd.
/// Used when decoding/encoding files with uses_legacy_checksum == true.
struct LegacyReedSolomonEncoder {
    data_shards: usize,
    parity_shards: usize,
    cache_workspaces: bool,
    encoder_cache: RwLock<Option<reed_solomon_simd::ReedSolomonEncoder>>,
    decoder_cache: RwLock<Option<reed_solomon_simd::ReedSolomonDecoder>>,
}

impl LegacyReedSolomonEncoder {
    fn new(data_shards: usize, parity_shards: usize) -> io::Result<Self> {
        Self::with_workspace_cache(data_shards, parity_shards, false)
    }

    fn with_workspace_cache(data_shards: usize, parity_shards: usize, cache_workspaces: bool) -> io::Result<Self> {
        Ok(Self {
            data_shards,
            parity_shards,
            cache_workspaces,
            encoder_cache: RwLock::new(None),
            decoder_cache: RwLock::new(None),
        })
    }

    fn logical_shard_bytes_upper_bound(&self, shard_len: usize) -> Option<usize> {
        let aligned_shard_len = shard_len.checked_add(63)?.checked_div(64)?.checked_mul(64)?;
        let high_rate_decoder_work_count = self
            .parity_shards
            .checked_next_power_of_two()?
            .checked_add(self.data_shards)?
            .checked_next_power_of_two()?;
        let low_rate_decoder_work_count = self
            .data_shards
            .checked_next_power_of_two()?
            .checked_add(self.parity_shards)?
            .checked_next_power_of_two()?;
        aligned_shard_len.checked_mul(high_rate_decoder_work_count.max(low_rate_decoder_work_count))
    }

    fn should_cache_workspace(&self, shard_len: usize) -> bool {
        self.cache_workspaces
            && self
                .logical_shard_bytes_upper_bound(shard_len)
                .is_some_and(|bytes| bytes <= LEGACY_REED_SOLOMON_CACHE_MAX_LOGICAL_SHARD_BYTES_PER_WORKSPACE)
    }

    fn encode(&self, shards: SmallVec<[&mut [u8]; 16]>) -> io::Result<()> {
        let mut shards_vec: Vec<&mut [u8]> = shards.into_vec();
        if shards_vec.is_empty() {
            return Ok(());
        }
        let shard_len = shards_vec[0].len();
        let cached_encoder = self
            .encoder_cache
            .write()
            .map_err(|_| io::Error::other("Failed to acquire encoder cache lock"))?
            .take();
        let mut encoder = {
            match cached_encoder {
                Some(mut cached) => {
                    if cached.reset(self.data_shards, self.parity_shards, shard_len).is_err() {
                        reed_solomon_simd::ReedSolomonEncoder::new(self.data_shards, self.parity_shards, shard_len)
                            .map_err(|e| io::Error::other(format!("Failed to create SIMD encoder: {e:?}")))?
                    } else {
                        cached
                    }
                }
                None => reed_solomon_simd::ReedSolomonEncoder::new(self.data_shards, self.parity_shards, shard_len)
                    .map_err(|e| io::Error::other(format!("Failed to create SIMD encoder: {e:?}")))?,
            }
        };
        for (i, shard) in shards_vec.iter().enumerate().take(self.data_shards) {
            encoder
                .add_original_shard(shard)
                .map_err(|e| io::Error::other(format!("Failed to add shard {i}: {e:?}")))?;
        }
        let result = encoder
            .encode()
            .map_err(|e| io::Error::other(format!("SIMD encoding failed: {e:?}")))?;
        for (i, recovery_shard) in result.recovery_iter().enumerate() {
            if i + self.data_shards < shards_vec.len() {
                shards_vec[i + self.data_shards].copy_from_slice(recovery_shard);
            }
        }
        drop(result);
        if self.should_cache_workspace(shard_len) {
            let mut cache = self
                .encoder_cache
                .write()
                .map_err(|_| io::Error::other("Failed to return encoder to cache"))?;
            if cache.is_none() {
                *cache = Some(encoder);
            }
        }
        Ok(())
    }

    fn reconstruct_data(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        if recover_empty_payload_data_shards(shards, self.data_shards, self.parity_shards)? {
            return Ok(());
        }

        let shard_len = shards
            .iter()
            .find_map(|s| s.as_ref().map(|v| v.len()))
            .ok_or_else(|| io::Error::other("No valid shards found for reconstruction"))?;

        let cached_decoder = self
            .decoder_cache
            .write()
            .map_err(|_| io::Error::other("Failed to acquire decoder cache lock"))?
            .take();
        let mut decoder = {
            match cached_decoder {
                Some(mut cached_decoder) => {
                    if let Err(e) = cached_decoder.reset(self.data_shards, self.parity_shards, shard_len) {
                        warn!("Failed to reset SIMD decoder: {:?}, creating new one", e);
                        reed_solomon_simd::ReedSolomonDecoder::new(self.data_shards, self.parity_shards, shard_len)
                            .map_err(|e| io::Error::other(format!("Failed to create SIMD decoder: {e:?}")))?
                    } else {
                        cached_decoder
                    }
                }
                None => reed_solomon_simd::ReedSolomonDecoder::new(self.data_shards, self.parity_shards, shard_len)
                    .map_err(|e| io::Error::other(format!("Failed to create SIMD decoder: {e:?}")))?,
            }
        };

        for (i, shard_opt) in shards.iter().enumerate() {
            if let Some(shard) = shard_opt {
                if i < self.data_shards {
                    decoder
                        .add_original_shard(i, shard)
                        .map_err(|e| io::Error::other(format!("Failed to add original shard for reconstruction: {e:?}")))?;
                } else {
                    let recovery_idx = i - self.data_shards;
                    decoder
                        .add_recovery_shard(recovery_idx, shard)
                        .map_err(|e| io::Error::other(format!("Failed to add recovery shard for reconstruction: {e:?}")))?;
                }
            }
        }

        let result = decoder
            .decode()
            .map_err(|e| io::Error::other(format!("SIMD decode error: {e:?}")))?;

        for (i, shard_opt) in shards.iter_mut().enumerate() {
            if shard_opt.is_none() && i < self.data_shards {
                for (restored_index, restored_data) in result.restored_original_iter() {
                    if restored_index == i {
                        *shard_opt = Some(restored_data.to_vec());
                        break;
                    }
                }
            }
        }

        drop(result);

        if self.should_cache_workspace(shard_len) {
            let mut cache = self
                .decoder_cache
                .write()
                .map_err(|_| io::Error::other("Failed to return decoder to cache"))?;
            if cache.is_none() {
                *cache = Some(decoder);
            }
        }

        Ok(())
    }

    fn reconstruct(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        self.reconstruct_data(shards)?;
        self.encode_parity(shards)
    }

    fn verify(&self, shards: &[&[u8]]) -> io::Result<bool> {
        let expected_shards = self.data_shards + self.parity_shards;
        if shards.len() != expected_shards {
            return Err(io::Error::other(format!(
                "invalid shard count: got {}, expected {}",
                shards.len(),
                expected_shards
            )));
        }
        if shards.iter().all(|shard| shard.is_empty()) {
            return Ok(true);
        }

        let mut expected = shards.iter().map(|shard| Some(shard.to_vec())).collect::<Vec<_>>();
        self.encode_parity(&mut expected)?;

        for index in self.data_shards..expected_shards {
            let Some(expected_parity) = expected[index].as_ref() else {
                return Err(io::Error::other(format!("missing parity shard {index} after verification encode")));
            };
            if expected_parity.as_slice() != shards[index] {
                return Ok(false);
            }
        }

        Ok(true)
    }

    fn encode_parity(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        encode_parity_shards(shards, self.data_shards, self.parity_shards, |shards| self.encode(shards))
    }
}

/// Reed-Solomon encoder using reed-solomon-erasure
pub struct ReedSolomonEncoder {
    data_shards: usize,
    parity_shards: usize,
    encoder: Option<Arc<ReedSolomon>>,
}

impl Clone for ReedSolomonEncoder {
    fn clone(&self) -> Self {
        Self {
            data_shards: self.data_shards,
            parity_shards: self.parity_shards,
            encoder: self.encoder.clone(),
        }
    }
}

impl ReedSolomonEncoder {
    fn try_new_typed(data_shards: usize, parity_shards: usize) -> Result<Self, reed_solomon_erasure::Error> {
        let encoder = if parity_shards > 0 {
            Some(cached_modern_reed_solomon(data_shards, parity_shards)?)
        } else {
            None
        };

        Ok(ReedSolomonEncoder {
            data_shards,
            parity_shards,
            encoder,
        })
    }

    /// Create a new Reed-Solomon encoder with specified data and parity shards.
    pub fn new(data_shards: usize, parity_shards: usize) -> io::Result<Self> {
        Self::try_new_typed(data_shards, parity_shards)
            .map_err(|error| io::Error::other(format!("Failed to create Reed-Solomon encoder: {error:?}")))
    }

    /// Encode data shards with parity.
    pub fn encode(&self, shards: SmallVec<[&mut [u8]; 16]>) -> io::Result<()> {
        let mut shards_vec: Vec<&mut [u8]> = shards.into_vec();
        if shards_vec.is_empty() {
            return Ok(());
        }

        if let Some(ref rs) = self.encoder {
            rs.encode(&mut shards_vec)
                .map_err(|e| io::Error::other(format!("Reed-Solomon encode failed: {e:?}")))
        } else {
            Ok(())
        }
    }

    /// Reconstruct missing data shards.
    pub fn reconstruct_data(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        if recover_empty_payload_data_shards(shards, self.data_shards, self.parity_shards)? {
            return Ok(());
        }

        if let Some(ref rs) = self.encoder {
            rs.reconstruct_data(shards)
                .map_err(|e| io::Error::other(format!("Reed-Solomon reconstruct failed: {e:?}")))
        } else {
            Ok(())
        }
    }

    /// Reconstruct missing data shards and regenerate parity shards.
    pub fn reconstruct(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        self.reconstruct_data(shards)?;
        self.encode_parity(shards)
    }

    pub fn verify(&self, shards: &[&[u8]]) -> io::Result<bool> {
        if shards.iter().all(|shard| shard.is_empty()) {
            return Ok(true);
        }

        if let Some(ref rs) = self.encoder {
            rs.verify(shards)
                .map_err(|e| io::Error::other(format!("Reed-Solomon verify failed: {e:?}")))
        } else {
            Ok(true)
        }
    }

    fn encode_parity(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        encode_parity_shards(shards, self.data_shards, self.parity_shards, |shards| self.encode(shards))
    }
}

fn cached_modern_reed_solomon(data_shards: usize, parity_shards: usize) -> Result<Arc<ReedSolomon>, reed_solomon_erasure::Error> {
    let key = (data_shards, parity_shards);
    let cache = MODERN_REED_SOLOMON_CACHE.get_or_init(|| RwLock::new(HashMap::new()));

    if let Some(encoder) = cache
        .read()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .get(&key)
        .cloned()
    {
        return Ok(encoder);
    }

    let encoder = Arc::new(ReedSolomon::new(data_shards, parity_shards)?);
    let mut cache = cache.write().unwrap_or_else(|poisoned| poisoned.into_inner());
    if let Some(existing) = cache.get(&key) {
        return Ok(Arc::clone(existing));
    }
    if cache.len() < MODERN_REED_SOLOMON_CACHE_MAX_ENTRIES {
        cache.insert(key, Arc::clone(&encoder));
    }
    Ok(encoder)
}

fn cached_legacy_reed_solomon(data_shards: usize, parity_shards: usize) -> io::Result<Arc<LegacyReedSolomonEncoder>> {
    let cache = LEGACY_REED_SOLOMON_CACHE.get_or_init(|| RwLock::new(HashMap::new()));
    cached_legacy_reed_solomon_in(cache, data_shards, parity_shards)
}

fn cached_legacy_reed_solomon_in(
    cache: &LegacyReedSolomonCache,
    data_shards: usize,
    parity_shards: usize,
) -> io::Result<Arc<LegacyReedSolomonEncoder>> {
    let key = (data_shards, parity_shards);
    if let Some(encoder) = cache
        .read()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .get(&key)
        .cloned()
    {
        return Ok(encoder);
    }

    let mut cache = cache.write().unwrap_or_else(|poisoned| poisoned.into_inner());
    if let Some(existing) = cache.get(&key) {
        return Ok(Arc::clone(existing));
    }
    if cache.len() < LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES {
        let encoder = Arc::new(LegacyReedSolomonEncoder::with_workspace_cache(data_shards, parity_shards, true)?);
        cache.insert(key, Arc::clone(&encoder));
        return Ok(encoder);
    }
    drop(cache);
    Ok(Arc::new(LegacyReedSolomonEncoder::new(data_shards, parity_shards)?))
}

fn encode_parity_shards<F>(shards: &mut [Option<Vec<u8>>], data_shards: usize, parity_shards: usize, encode: F) -> io::Result<()>
where
    F: FnOnce(SmallVec<[&mut [u8]; 16]>) -> io::Result<()>,
{
    let expected_shards = data_shards + parity_shards;
    if shards.len() != expected_shards {
        return Err(io::Error::other(format!(
            "invalid shard count: got {}, expected {}",
            shards.len(),
            expected_shards
        )));
    }

    let shard_len = shards
        .iter()
        .find_map(|s| s.as_ref().map(Vec::len))
        .ok_or_else(|| io::Error::other("No valid shards found for parity encoding"))?;

    for shard in shards.iter_mut().skip(data_shards) {
        if shard.is_none() {
            *shard = Some(vec![0; shard_len]);
        }
    }

    if shard_len == 0 {
        for (index, shard) in shards.iter().enumerate() {
            let shard = shard
                .as_ref()
                .ok_or_else(|| io::Error::other(format!("missing shard {index} after data reconstruction")))?;
            if !shard.is_empty() {
                return Err(io::Error::other(format!(
                    "inconsistent shard length at index {index}: got {}, expected {}",
                    shard.len(),
                    shard_len
                )));
            }
        }
        return Ok(());
    }

    let mut shard_refs: SmallVec<[&mut [u8]; 16]> = SmallVec::new();
    for (index, shard) in shards.iter_mut().enumerate() {
        let shard = shard
            .as_mut()
            .ok_or_else(|| io::Error::other(format!("missing shard {index} after data reconstruction")))?;
        if shard.len() != shard_len {
            return Err(io::Error::other(format!(
                "inconsistent shard length at index {index}: got {}, expected {}",
                shard.len(),
                shard_len
            )));
        }
        shard_refs.push(shard.as_mut_slice());
    }

    encode(shard_refs)
}

fn recover_empty_payload_data_shards(
    shards: &mut [Option<Vec<u8>>],
    data_shards: usize,
    parity_shards: usize,
) -> io::Result<bool> {
    let expected_shards = data_shards + parity_shards;
    if shards.len() != expected_shards {
        return Err(io::Error::other(format!(
            "invalid shard count: got {}, expected {}",
            shards.len(),
            expected_shards
        )));
    }

    let mut has_present_shard = false;
    for shard in shards.iter().filter_map(|shard| shard.as_ref()) {
        has_present_shard = true;
        if !shard.is_empty() {
            return Ok(false);
        }
    }
    if !has_present_shard {
        return Ok(false);
    }

    for shard in shards.iter_mut().take(data_shards) {
        if shard.is_none() {
            *shard = Some(Vec::new());
        }
    }
    Ok(true)
}

/// Erasure coding utility for data reliability using Reed-Solomon codes.
///
/// This struct provides encoding and decoding of data into data and parity shards.
/// It supports splitting data into multiple shards, generating parity for fault tolerance,
/// and reconstructing lost shards.
///
/// # Fields
/// - `data_shards`: Number of data shards.
/// - `parity_shards`: Number of parity shards.
/// - `encoder`: Optional ReedSolomon encoder instance.
/// - `block_size`: Block size for each shard.
/// - `_id`: Unique identifier for the erasure instance.
///
/// # Example
/// ```ignore
/// use rustfs_ecstore::api::erasure::Erasure;
/// let erasure = Erasure::new(4, 2, 8);
/// let data = b"hello world";
/// let shards = erasure.encode_data(data).expect("operation should succeed");
/// // Simulate loss and recovery...
/// ```
pub struct Erasure {
    pub data_shards: usize,
    pub parity_shards: usize,
    encoder: Option<ReedSolomonEncoder>,
    legacy_encoder: Option<Arc<LegacyReedSolomonEncoder>>,
    pub block_size: usize,
    uses_legacy: bool,
    _id: Uuid,
}

impl Default for Erasure {
    fn default() -> Self {
        Self {
            data_shards: 0,
            parity_shards: 0,
            encoder: None,
            legacy_encoder: None,
            block_size: 0,
            uses_legacy: false,
            _id: Uuid::nil(),
        }
    }
}

impl Clone for Erasure {
    fn clone(&self) -> Self {
        Self {
            data_shards: self.data_shards,
            parity_shards: self.parity_shards,
            encoder: self.encoder.clone(),
            legacy_encoder: self.legacy_encoder.clone(),
            block_size: self.block_size,
            uses_legacy: self.uses_legacy,
            _id: self._id, // Shared by clones; this field is unused in hot paths.
        }
    }
}

pub fn calc_shard_size(block_size: usize, data_shards: usize) -> usize {
    block_size.div_ceil(data_shards)
}

impl Erasure {
    /// Create a new Erasure instance
    ///
    /// # Arguments
    /// * `data_shards` - Number of data shards.
    /// * `parity_shards` - Number of parity shards.
    /// * `block_size` - Block size for each shard.
    ///
    /// # Panics
    /// Panics when the dimensions are invalid or unsupported. RustFS production
    /// paths use [`Self::try_new`] instead.
    pub fn new(data_shards: usize, parity_shards: usize, block_size: usize) -> Self {
        match Self::try_new(data_shards, parity_shards, block_size) {
            Ok(erasure) => erasure,
            Err(error) => panic!("invalid erasure codec configuration: {error}"),
        }
    }

    /// Create a new Erasure instance with legacy format support.
    ///
    /// When `uses_legacy` is true, uses main-branch shard_size formula and reed-solomon-simd
    /// for decode/reconstruct (for reading and healing old-version files).
    ///
    /// # Panics
    /// Panics when the dimensions are invalid or unsupported. RustFS production
    /// paths use [`Self::try_new_with_options`] instead.
    pub fn new_with_options(data_shards: usize, parity_shards: usize, block_size: usize, uses_legacy: bool) -> Self {
        match Self::try_new_with_options(data_shards, parity_shards, block_size, uses_legacy) {
            Ok(erasure) => erasure,
            Err(error) => panic!("invalid erasure codec configuration: {error}"),
        }
    }

    /// Tries to create a modern Erasure codec.
    ///
    /// # Errors
    /// Returns [`ErasureConstructionError`] when dimensions are zero,
    /// overflow the shard count, exceed the modern codec's supported range, or
    /// the underlying encoder rejects the configuration.
    pub fn try_new(data_shards: usize, parity_shards: usize, block_size: usize) -> Result<Self, ErasureConstructionError> {
        Self::try_new_with_options(data_shards, parity_shards, block_size, false)
    }

    /// Tries to create an Erasure codec using the selected format backend.
    ///
    /// # Errors
    /// Returns [`ErasureConstructionError`] when dimensions are zero,
    /// overflow the shard count, are unsupported by the selected codec, or the
    /// selected encoder fails to initialize.
    pub fn try_new_with_options(
        data_shards: usize,
        parity_shards: usize,
        block_size: usize,
        uses_legacy: bool,
    ) -> Result<Self, ErasureConstructionError> {
        if data_shards == 0 {
            return Err(ErasureConstructionError::ZeroDataShards);
        }
        if block_size == 0 {
            return Err(ErasureConstructionError::ZeroBlockSize);
        }

        let total_shards = data_shards
            .checked_add(parity_shards)
            .ok_or(ErasureConstructionError::ShardCountOverflow {
                data_shards,
                parity_shards,
            })?;

        if parity_shards > 0 {
            if uses_legacy
                && (total_shards > reed_solomon_simd::engine::GF_ORDER
                    || !reed_solomon_simd::ReedSolomonEncoder::supports(data_shards, parity_shards))
            {
                return Err(ErasureConstructionError::UnsupportedLegacyShardCount {
                    data_shards,
                    parity_shards,
                });
            }
            if !uses_legacy && total_shards > MODERN_MAX_TOTAL_SHARDS {
                return Err(ErasureConstructionError::UnsupportedModernShardCount {
                    data_shards,
                    parity_shards,
                });
            }
        }

        let encoder = if !uses_legacy && parity_shards > 0 {
            Some(
                ReedSolomonEncoder::try_new_typed(data_shards, parity_shards)
                    .map_err(|source| ErasureConstructionError::ModernEncoder { source })?,
            )
        } else {
            None
        };

        let legacy_encoder = if uses_legacy && parity_shards > 0 {
            Some(
                cached_legacy_reed_solomon(data_shards, parity_shards)
                    .map_err(|source| ErasureConstructionError::LegacyEncoder { source })?,
            )
        } else {
            None
        };

        Ok(Erasure {
            data_shards,
            parity_shards,
            block_size,
            encoder,
            legacy_encoder,
            uses_legacy,
            _id: Uuid::nil(), // Unused in hot paths; avoid CSPRNG syscall
        })
    }

    /// Encode data into data and parity shards.
    ///
    /// # Arguments
    /// * `data` - The input data to encode.
    ///
    /// # Returns
    /// A vector of encoded shards as `Bytes`.
    #[tracing::instrument(level = "debug", skip_all, fields(data_len=data.len()))]
    #[hotpath::measure(impl_type = "Erasure")]
    pub fn encode_data(&self, data: &[u8]) -> io::Result<Vec<Bytes>> {
        self.encode_data_block_inner(data)
            .map(|block| block.into_shards(self.total_shard_count()))
    }

    #[tracing::instrument(level = "debug", skip_all, fields(data_len=data.len()))]
    #[hotpath::measure(label = "Erasure::encode_data", impl_type = "Erasure")]
    pub(crate) fn encode_data_block(&self, data: &[u8]) -> io::Result<EncodedBlock> {
        self.encode_data_block_inner(data)
    }

    fn encode_data_block_inner(&self, data: &[u8]) -> io::Result<EncodedBlock> {
        let mut data_buffer = BytesMut::with_capacity(self.encoded_capacity_for_data_len(data.len()));
        data_buffer.extend_from_slice(data);
        self.encode_buffer(data_buffer, data.len())
    }

    /// Encode owned data, avoiding a copy when the caller already has a heap buffer.
    /// Falls back to copying into a new buffer if zero-copy conversion fails.
    #[hotpath::measure(impl_type = "Erasure")]
    pub fn encode_data_owned(&self, data: Vec<u8>) -> io::Result<Vec<Bytes>> {
        self.encode_data_owned_block_inner(data)
            .map(|block| block.into_shards(self.total_shard_count()))
    }

    #[hotpath::measure(label = "Erasure::encode_data_owned", impl_type = "Erasure")]
    pub(crate) fn encode_data_owned_block(&self, data: Vec<u8>) -> io::Result<EncodedBlock> {
        self.encode_data_owned_block_inner(data)
    }

    fn encode_data_owned_block_inner(&self, data: Vec<u8>) -> io::Result<EncodedBlock> {
        let data_len = data.len();
        // Try zero-copy: Vec<u8> -> Bytes -> BytesMut (succeeds when refcount == 1)
        let data_buffer = match Bytes::from(data).try_into_mut() {
            Ok(data_buffer) => data_buffer,
            Err(b) => {
                // Rare path: refcount != 1, fall back to copy
                let mut data_buffer = BytesMut::with_capacity(self.encoded_capacity_for_data_len(data_len));
                data_buffer.extend_from_slice(&b);
                data_buffer
            }
        };
        self.encode_buffer(data_buffer, data_len)
    }

    /// Encode data from an owned `BytesMut` buffer, avoiding the initial copy
    /// from a borrowed slice into a fresh `BytesMut`.
    ///
    /// Capacity contract: when the caller pre-reserves
    /// `shard_size() * total_shard_count()` bytes (the EC-expanded size of a full
    /// block), the `resize(need_total_size)` below stays within capacity for every
    /// `data_len <= block_size` — both shard-size formulas are monotone in
    /// `data_len` — so this function never reallocates the buffer.
    #[hotpath::measure(impl_type = "Erasure")]
    pub fn encode_data_bytes_mut(&self, data_buffer: BytesMut, data_len: usize) -> io::Result<Vec<Bytes>> {
        self.encode_buffer(data_buffer, data_len)
            .map(|block| block.into_shards(self.total_shard_count()))
    }

    #[hotpath::measure(label = "Erasure::encode_data_bytes_mut", impl_type = "Erasure")]
    pub(crate) fn encode_data_bytes_mut_block(&self, data_buffer: BytesMut, data_len: usize) -> io::Result<EncodedBlock> {
        self.encode_buffer(data_buffer, data_len)
    }

    fn encode_buffer(&self, mut data_buffer: BytesMut, data_len: usize) -> io::Result<EncodedBlock> {
        let shard_size_fn = if self.uses_legacy {
            calc_shard_size_legacy
        } else {
            calc_shard_size
        };
        let per_shard_size = shard_size_fn(data_len, self.data_shards);
        if per_shard_size == 0 {
            return Ok(EncodedBlock::empty());
        }
        let need_total_size = per_shard_size * self.total_shard_count();

        if data_buffer.len() > data_len {
            data_buffer.truncate(data_len);
        }
        data_buffer.resize(need_total_size, 0u8);

        {
            let data_slices: SmallVec<[&mut [u8]; 16]> = data_buffer.chunks_exact_mut(per_shard_size).collect();

            if self.parity_shards > 0 {
                if self.uses_legacy {
                    if let Some(encoder) = self.legacy_encoder.as_ref() {
                        encoder.encode(data_slices)?;
                    } else {
                        warn!("parity_shards > 0, uses_legacy but legacy_encoder is None");
                    }
                } else if let Some(encoder) = self.encoder.as_ref() {
                    encoder.encode(data_slices)?;
                } else {
                    warn!("parity_shards > 0, but encoder is None");
                }
            }
        }

        Ok(EncodedBlock {
            data: data_buffer.freeze(),
            shard_size: per_shard_size,
        })
    }

    /// Decode and reconstruct missing data shards in-place.
    ///
    /// # Arguments
    /// * `shards` - Mutable slice of optional shard data. Missing shards should be `None`.
    ///
    /// # Returns
    /// Ok if reconstruction succeeds, error otherwise.
    #[hotpath::measure(impl_type = "Erasure")]
    pub fn decode_data(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        if self.parity_shards > 0 {
            if self.uses_legacy {
                if let Some(encoder) = self.legacy_encoder.as_ref() {
                    encoder.reconstruct_data(shards)?;
                } else {
                    warn!("parity_shards > 0, uses_legacy but legacy_encoder is None");
                }
            } else if let Some(encoder) = self.encoder.as_ref() {
                encoder.reconstruct_data(shards)?;
            } else {
                warn!("parity_shards > 0, but encoder is None");
            }
        }

        Ok(())
    }

    /// Decode and reconstruct missing data shards, then regenerate parity shards.
    #[hotpath::measure(impl_type = "Erasure")]
    pub fn decode_data_and_parity(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        if self.parity_shards > 0 {
            if self.uses_legacy {
                if let Some(encoder) = self.legacy_encoder.as_ref() {
                    encoder.reconstruct(shards)?;
                } else {
                    warn!("parity_shards > 0, uses_legacy but legacy_encoder is None");
                }
            } else if let Some(encoder) = self.encoder.as_ref() {
                encoder.reconstruct(shards)?;
            } else {
                warn!("parity_shards > 0, but encoder is None");
            }
        }

        Ok(())
    }

    pub(crate) fn decode_data_with_reconstruction_verification(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
        let missing_data_source = shards.iter().take(self.data_shards).any(|shard| shard.is_none());
        let available_shards = shards.iter().filter(|shard| shard.is_some()).count();
        let source_parity = if missing_data_source && available_shards > self.data_shards {
            shards
                .iter()
                .enumerate()
                .skip(self.data_shards)
                .filter_map(|(index, shard)| shard.as_ref().map(|shard| (index, shard.clone())))
                .collect::<Vec<_>>()
        } else {
            Vec::new()
        };

        if source_parity.is_empty() {
            return self.decode_data(shards);
        }

        self.decode_data_and_parity(shards)?;
        for (index, source) in source_parity {
            let Some(rebuilt) = shards[index].as_ref() else {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    "missing rebuilt parity shard after read verification",
                ));
            };
            if rebuilt != &source {
                warn!(
                    shard_index = index,
                    data_shards = self.data_shards,
                    parity_shards = self.parity_shards,
                    "erasure decode rejected inconsistent read source shards"
                );
                return Err(io::Error::new(io::ErrorKind::InvalidData, "inconsistent read source shards"));
            }
        }

        Ok(())
    }

    pub(crate) fn verify_data_and_parity(&self, shards: &[Option<Vec<u8>>]) -> io::Result<bool> {
        let expected_shards = self.total_shard_count();
        if shards.len() != expected_shards {
            return Err(io::Error::other(format!(
                "invalid shard count: got {}, expected {}",
                shards.len(),
                expected_shards
            )));
        }
        if self.parity_shards == 0 {
            return Ok(true);
        }

        let mut shard_refs = Vec::with_capacity(expected_shards);
        let mut shard_len = None;
        for (index, shard) in shards.iter().enumerate() {
            let shard = shard
                .as_deref()
                .ok_or_else(|| io::Error::other(format!("missing shard {index} for data/parity verification")))?;
            if let Some(expected_len) = shard_len {
                if shard.len() != expected_len {
                    return Err(io::Error::other(format!(
                        "inconsistent shard length at index {index}: got {}, expected {}",
                        shard.len(),
                        expected_len
                    )));
                }
            } else {
                shard_len = Some(shard.len());
            }
            shard_refs.push(shard);
        }

        if self.uses_legacy {
            if let Some(encoder) = self.legacy_encoder.as_ref() {
                encoder.verify(&shard_refs)
            } else {
                Err(io::Error::other("parity_shards > 0, uses_legacy but legacy_encoder is None"))
            }
        } else if let Some(encoder) = self.encoder.as_ref() {
            encoder.verify(&shard_refs)
        } else {
            Err(io::Error::other("parity_shards > 0, but encoder is None"))
        }
    }

    /// Get the total number of shards (data + parity).
    pub fn total_shard_count(&self) -> usize {
        self.data_shards + self.parity_shards
    }

    pub(crate) fn encoded_capacity_for_data_len(&self, data_len: usize) -> usize {
        let shard_size_fn = if self.uses_legacy {
            calc_shard_size_legacy
        } else {
            calc_shard_size
        };
        shard_size_fn(data_len, self.data_shards).saturating_mul(self.total_shard_count())
    }

    /// Whether the erasure dimensions are safe for the shard/offset arithmetic.
    ///
    /// `block_size` and `data_shards` come straight from on-disk metadata; a
    /// corrupted or crafted `xl.meta` can carry zero values that would panic the
    /// `block_size`/`data_shards` divisions in [`Self::shard_size`],
    /// [`Self::shard_file_size`] and [`Self::shard_file_offset`]. Read paths must
    /// reject such metadata before performing those divisions.
    pub fn has_valid_dimensions(&self) -> bool {
        self.block_size > 0 && self.data_shards > 0
    }
    // /// Calculate the shard size and total size for a given data size.
    // // Returns (shard_size, total_size) for the given data size
    // fn need_size(&self, data_size: usize) -> (usize, usize) {
    //     let shard_size = self.shard_size(data_size);
    //     (shard_size, shard_size * (self.total_shard_count()))
    // }

    /// Calculate the size of each shard.
    pub fn shard_size(&self) -> usize {
        if self.uses_legacy {
            calc_shard_size_legacy(self.block_size, self.data_shards)
        } else {
            calc_shard_size(self.block_size, self.data_shards)
        }
    }
    /// Calculate the total erasure file size for a given original size.
    // Returns the final erasure size from the original size
    pub fn shard_file_size(&self, total_length: i64) -> i64 {
        if total_length == 0 {
            return 0;
        }
        if total_length < 0 {
            return total_length;
        }

        let total_length = total_length as usize;
        let shard_size_fn = if self.uses_legacy {
            calc_shard_size_legacy
        } else {
            calc_shard_size
        };

        let num_shards = total_length / self.block_size;
        let last_block_size = total_length % self.block_size;
        let last_shard_size = shard_size_fn(last_block_size, self.data_shards);
        (num_shards * self.shard_size() + last_shard_size) as i64
    }

    /// Calculate the offset in the erasure file where reading begins.
    // Returns the offset in the erasure file where reading begins
    pub fn shard_file_offset(&self, start_offset: usize, length: usize, total_length: usize) -> usize {
        let shard_size = self.shard_size();
        let shard_file_size = self.shard_file_size(total_length as i64) as usize;
        let end_shard = (start_offset + length) / self.block_size;
        let mut till_offset = end_shard * shard_size + shard_size;
        if till_offset > shard_file_size {
            till_offset = shard_file_size;
        }

        till_offset
    }

    /// Encode all data from a reader in blocks, calling an async callback for each encoded block.
    /// This method is async and returns the total bytes read after all blocks are processed.
    ///
    /// # Arguments
    /// * `reader` - An async reader implementing AsyncRead + Send + Sync + Unpin
    /// * `mut on_block` - Async callback that receives encoded blocks and returns a Result
    /// * `F` - Callback type: FnMut(Result<Vec<Bytes>, std::io::Error>) -> Future<Output=Result<(), E>> + Send
    /// * `Fut` - Future type returned by the callback
    /// * `E` - Error type returned by the callback
    /// * `R` - Reader type implementing AsyncRead + Send + Sync + Unpin
    ///
    /// # Returns
    /// Result<usize, E> containing total bytes read, or error from callback
    ///
    /// # Errors
    /// Returns error if reading from reader fails or if callback returns error
    #[allow(
        dead_code,
        reason = "callback encode path exercised only by this file's tests (backlog#1823)"
    )]
    pub(crate) async fn encode_stream_callback_async<F, Fut, E, R>(
        self: std::sync::Arc<Self>,
        reader: &mut R,
        mut on_block: F,
    ) -> Result<usize, E>
    where
        R: AsyncRead + Send + Sync + Unpin,
        F: FnMut(io::Result<Vec<Bytes>>) -> Fut + Send,
        Fut: Future<Output = Result<(), E>> + Send,
    {
        if self.block_size == 0 {
            on_block(Err(io::Error::new(io::ErrorKind::InvalidInput, "erasure block_size must be non-zero"))).await?;
            return Ok(0);
        }

        let block_size = self.block_size;
        let mut total = 0;
        let mut buf = vec![0u8; block_size];
        loop {
            match rustfs_utils::read_full_or_eof(&mut *reader, &mut buf).await {
                Ok(Some(n)) => {
                    debug_assert!(n > 0, "non-zero block_size prevents zero-length reads");
                    warn!("encode_stream_callback_async read n={}", n);
                    total += n;
                    let erasure = self.clone();
                    let encode_buf = std::mem::take(&mut buf);
                    let (res, returned_buf) = match tokio::task::spawn_blocking(move || {
                        let res = erasure.encode_data(&encode_buf[..n]);
                        (res, encode_buf)
                    })
                    .await
                    {
                        Ok(result) => result,
                        Err(err) => {
                            on_block(Err(io::Error::other(format!("EC encode task failed: {err}")))).await?;
                            break;
                        }
                    };
                    buf = returned_buf;
                    on_block(res).await?
                }
                Ok(None) => {
                    warn!("encode_stream_callback_async read unexpected ok");
                    break;
                }
                Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
                    warn!("encode_stream_callback_async read unexpected eof");
                    break;
                }
                Err(e) => {
                    warn!("encode_stream_callback_async read error={:?}", e);
                    on_block(Err(e)).await?;
                    break;
                }
            }
        }
        Ok(total)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::collection::{btree_set, vec};
    use proptest::prelude::*;
    use std::pin::Pin;
    use std::task::{Context, Poll};
    use tokio::io::ReadBuf;

    fn erasure_with_invalid_dimensions(data_shards: usize, parity_shards: usize, block_size: usize) -> Erasure {
        Erasure {
            data_shards,
            parity_shards,
            block_size,
            ..Default::default()
        }
    }

    fn optional_shards(shards: &[Bytes]) -> Vec<Option<Vec<u8>>> {
        shards.iter().map(|shard| Some(shard.to_vec())).collect()
    }

    fn recover_data(shards: &[Option<Vec<u8>>], data_shards: usize, original_len: usize) -> Vec<u8> {
        let mut recovered = Vec::new();
        for shard in shards.iter().take(data_shards) {
            recovered.extend_from_slice(
                shard
                    .as_ref()
                    .expect("reconstructed data shard should be present after decode_data_and_parity"),
            );
        }
        recovered.truncate(original_len);
        recovered
    }

    fn erasure_recoverability_case_strategy()
    -> impl Strategy<Value = (usize, usize, usize, bool, Vec<u8>, std::collections::BTreeSet<usize>)> {
        (2usize..=8, 1usize..=4, prop::sample::select(vec![64usize, 256, 1024]), any::<bool>()).prop_flat_map(
            |(data_shards, parity_shards, block_size, uses_legacy)| {
                let total_shards = data_shards + parity_shards;
                (
                    Just(data_shards),
                    Just(parity_shards),
                    Just(block_size),
                    Just(uses_legacy),
                    vec(any::<u8>(), 0..=4096),
                    btree_set(0usize..total_shards, 0..=parity_shards),
                )
            },
        )
    }

    fn assert_owned_encode_matches_borrowed(erasure: &Erasure, data: Vec<u8>) {
        let borrowed = erasure.encode_data(&data).expect("borrowed encode should succeed");
        let owned = erasure.encode_data_owned(data).expect("owned encode should succeed");

        assert_eq!(owned, borrowed);
    }

    struct ErrorAfterPartialReader {
        emitted: bool,
    }

    impl AsyncRead for ErrorAfterPartialReader {
        fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
            if !self.emitted {
                self.emitted = true;
                buf.put_slice(&[1]);
                return Poll::Ready(Ok(()));
            }
            Poll::Ready(Err(io::Error::new(io::ErrorKind::BrokenPipe, "partial read failure")))
        }
    }

    struct ImmediateErrorReader;

    impl AsyncRead for ImmediateErrorReader {
        fn poll_read(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
            Poll::Ready(Err(io::Error::other("immediate read failure")))
        }
    }

    #[test]
    fn try_new_rejects_zero_and_overflowing_dimensions_with_typed_errors() {
        let Err(zero_data) = Erasure::try_new(0, 2, 64) else {
            panic!("zero data shards must be rejected");
        };
        assert!(matches!(zero_data, ErasureConstructionError::ZeroDataShards));

        let Err(zero_block) = Erasure::try_new(2, 2, 0) else {
            panic!("zero block size must be rejected");
        };
        assert!(matches!(zero_block, ErasureConstructionError::ZeroBlockSize));

        let Err(overflow) = Erasure::try_new(usize::MAX, 1, 64) else {
            panic!("overflowing shard counts must be rejected");
        };
        assert!(matches!(
            overflow,
            ErasureConstructionError::ShardCountOverflow {
                data_shards: usize::MAX,
                parity_shards: 1,
            }
        ));
    }

    #[test]
    fn try_new_rejects_backend_specific_unsupported_dimensions() {
        let Err(modern) = Erasure::try_new(256, 1, 64) else {
            panic!("modern dimensions beyond GF(2^8) must be rejected");
        };
        assert!(matches!(
            modern,
            ErasureConstructionError::UnsupportedModernShardCount {
                data_shards: 256,
                parity_shards: 1,
            }
        ));

        let Err(legacy) = Erasure::try_new_with_options(60_000, 5_000, 64, true) else {
            panic!("unsupported legacy SIMD dimensions must be rejected");
        };
        assert!(matches!(
            legacy,
            ErasureConstructionError::UnsupportedLegacyShardCount {
                data_shards: 60_000,
                parity_shards: 5_000,
            }
        ));
    }

    #[test]
    fn try_new_accepts_exact_backend_shard_limits() {
        let modern_data_shards = MODERN_MAX_TOTAL_SHARDS - 1;
        let modern =
            Erasure::try_new(modern_data_shards, 1, 64).expect("the modern codec's exact field-size boundary must remain valid");
        assert_eq!(modern.total_shard_count(), MODERN_MAX_TOTAL_SHARDS);
        assert!(modern.encoder.is_some());
        assert!(modern.legacy_encoder.is_none());

        let legacy_data_shards = reed_solomon_simd::engine::GF_ORDER / 2;
        let legacy_parity_shards = reed_solomon_simd::engine::GF_ORDER - legacy_data_shards;
        let legacy = Erasure::try_new_with_options(legacy_data_shards, legacy_parity_shards, 64, true)
            .expect("the legacy codec's exact field-size boundary must remain valid");
        assert_eq!(legacy.total_shard_count(), reed_solomon_simd::engine::GF_ORDER);
        assert!(legacy.encoder.is_none());
        assert!(legacy.legacy_encoder.is_some());
    }

    #[test]
    fn try_new_accepts_zero_parity_without_backend_limits() {
        let no_parity = Erasure::try_new(2, 0, 64).expect("zero parity is a valid generic codec configuration");
        assert_eq!(no_parity.total_shard_count(), 2);
        assert!(no_parity.encoder.is_none());
        assert!(no_parity.legacy_encoder.is_none());

        let large_modern = Erasure::try_new(257, 0, 64).expect("zero parity must not inherit the modern backend shard limit");
        assert_eq!(large_modern.total_shard_count(), 257);
        assert!(large_modern.encoder.is_none());
        assert!(large_modern.legacy_encoder.is_none());

        let legacy_data_shards = reed_solomon_simd::engine::GF_ORDER + 1;
        let large_legacy = Erasure::try_new_with_options(legacy_data_shards, 0, 64, true)
            .expect("zero parity must not inherit the legacy backend shard limit");
        assert_eq!(large_legacy.total_shard_count(), legacy_data_shards);
        assert!(large_legacy.encoder.is_none());
        assert!(large_legacy.legacy_encoder.is_none());
    }

    #[test]
    fn try_new_accepts_generic_layouts_above_storage_drive_limits() {
        let erasure = Erasure::try_new(2, 3, 64).expect("generic 2+3 erasure coding must remain supported");
        assert_eq!(erasure.total_shard_count(), 5);
        assert!(erasure.encoder.is_some());
        assert!(erasure.legacy_encoder.is_none());

        let modern = Erasure::try_new(9, 8, 64).expect("the codec must not impose the storage layer's drive limit");
        assert_eq!(modern.total_shard_count(), 17);
        assert!(modern.encoder.is_some());
        assert!(modern.legacy_encoder.is_none());

        let legacy = Erasure::try_new_with_options(9, 8, 64, true)
            .expect("the legacy codec must not impose the storage layer's drive limit");
        assert_eq!(legacy.total_shard_count(), 17);
        assert!(legacy.encoder.is_none());
        assert!(legacy.legacy_encoder.is_some());
    }

    #[test]
    fn try_new_with_options_initializes_only_the_selected_backend() {
        let modern = Erasure::try_new_with_options(4, 2, 64, false).expect("modern codec should construct");
        assert!(modern.encoder.is_some());
        assert!(modern.legacy_encoder.is_none());

        let legacy = Erasure::try_new_with_options(4, 2, 64, true).expect("legacy codec should construct");
        assert!(legacy.encoder.is_none());
        assert!(legacy.legacy_encoder.is_some());
    }

    #[test]
    fn modern_encoder_construction_reuses_cached_codec() {
        let first = ReedSolomonEncoder::try_new_typed(31, 7).expect("modern codec should construct");
        let second = ReedSolomonEncoder::try_new_typed(31, 7).expect("modern codec should construct");

        let first = first.encoder.as_ref().expect("modern codec should initialize an encoder");
        let second = second.encoder.as_ref().expect("modern codec should initialize an encoder");
        assert!(Arc::ptr_eq(first, second));
    }

    #[test]
    fn construction_errors_preserve_encoder_sources() {
        let modern = ErasureConstructionError::ModernEncoder {
            source: reed_solomon_erasure::Error::TooManyShards,
        };
        assert!(
            std::error::Error::source(&modern)
                .and_then(|source| source.downcast_ref::<reed_solomon_erasure::Error>())
                .is_some()
        );

        let legacy = ErasureConstructionError::LegacyEncoder {
            source: io::Error::other("legacy encoder failure"),
        };
        assert!(
            std::error::Error::source(&legacy)
                .and_then(|source| source.downcast_ref::<io::Error>())
                .is_some()
        );
    }

    #[test]
    #[should_panic(expected = "invalid erasure codec configuration")]
    fn new_panics_on_invalid_dimensions() {
        let _ = Erasure::new(0, 1, 64);
    }

    #[test]
    #[should_panic(expected = "invalid erasure codec configuration")]
    fn new_with_options_panics_on_invalid_dimensions() {
        let _ = Erasure::new_with_options(1, 1, 0, true);
    }

    #[test]
    fn has_valid_dimensions_rejects_zero_block_size_or_data_shards() {
        // Well-formed erasure metadata is accepted.
        assert!(Erasure::new(4, 2, 64).has_valid_dimensions());
        assert!(Erasure::new_with_options(6, 4, 1, true).has_valid_dimensions());

        // Corrupted on-disk metadata with a zero block_size or zero data_shards
        // must be rejected before it reaches the shard/offset divisions.
        assert!(!erasure_with_invalid_dimensions(4, 2, 0).has_valid_dimensions());
        assert!(!erasure_with_invalid_dimensions(0, 0, 64).has_valid_dimensions());
        assert!(!erasure_with_invalid_dimensions(0, 0, 0).has_valid_dimensions());
    }

    #[tokio::test]
    async fn encode_stream_callback_async_stops_on_reader_errors() {
        let erasure = std::sync::Arc::new(Erasure::new(2, 1, 4));
        let mut partial_error = ErrorAfterPartialReader { emitted: false };
        let mut partial_callbacks = Vec::new();
        let total = erasure
            .clone()
            .encode_stream_callback_async(&mut partial_error, |result| {
                partial_callbacks.push(result.map(|blocks| blocks.len()).map_err(|err| err.kind()));
                async { Ok::<(), io::Error>(()) }
            })
            .await
            .expect("partial read error should not make callback fail");
        assert_eq!(total, 0);
        assert!(
            partial_callbacks.is_empty(),
            "unexpected EOF after a partial read should stop without emitting a block"
        );

        let mut immediate_error = ImmediateErrorReader;
        let mut immediate_callbacks = Vec::new();
        let total = erasure
            .encode_stream_callback_async(&mut immediate_error, |result| {
                immediate_callbacks.push(result.map(|blocks| blocks.len()).map_err(|err| err.kind()));
                async { Ok::<(), io::Error>(()) }
            })
            .await
            .expect("immediate read error should be delivered to callback");
        assert_eq!(total, 0);
        assert_eq!(immediate_callbacks, vec![Err(io::ErrorKind::Other)]);
    }

    #[test]
    fn default_and_legacy_clone_preserve_safe_zero_state_and_restore_data() {
        let default = Erasure::default();
        assert_eq!(default.total_shard_count(), 0);
        assert!(!default.has_valid_dimensions());
        assert_eq!(default.shard_file_size(0), 0);
        assert_eq!(default.shard_file_size(-7), -7);

        let legacy = Erasure::new_with_options(2, 2, 64, true);
        let cloned = legacy.clone();
        assert_eq!(cloned.data_shards, legacy.data_shards);
        assert_eq!(cloned.parity_shards, legacy.parity_shards);
        assert_eq!(cloned.block_size, legacy.block_size);
        assert!(cloned.uses_legacy);

        let data = b"legacy clone should preserve SIMD codec behavior";
        let encoded = cloned.encode_data(data).expect("legacy clone should encode");
        let mut shards = optional_shards(&encoded);
        shards[0] = None;
        cloned.decode_data(&mut shards).expect("legacy clone should decode");
        assert_eq!(recover_data(&shards, cloned.data_shards, data.len()), data);
    }

    #[test]
    fn legacy_codecs_share_process_cache_across_erasure_instances() {
        let first = Erasure::new_with_options(6, 3, 64, true)
            .legacy_encoder
            .expect("legacy codec should be initialized");
        let second = Erasure::new_with_options(6, 3, 128, true)
            .legacy_encoder
            .expect("same legacy shard layout should be initialized");

        assert!(Arc::ptr_eq(&first, &second));
    }

    #[test]
    fn legacy_workspace_cache_rejects_oversize_buffers_and_isolates_layouts() {
        let four_plus_two = Erasure::new_with_options(4, 2, 64, true)
            .legacy_encoder
            .expect("legacy codec should be initialized");
        let four_plus_one = Erasure::new_with_options(4, 1, 64, true)
            .legacy_encoder
            .expect("distinct parity layout should be initialized");
        let three_plus_two = Erasure::new_with_options(3, 2, 64, true)
            .legacy_encoder
            .expect("distinct data layout should be initialized");

        assert!(!Arc::ptr_eq(&four_plus_two, &four_plus_one));
        assert!(!Arc::ptr_eq(&four_plus_two, &three_plus_two));
        assert_eq!(four_plus_two.logical_shard_bytes_upper_bound(64 * 1024), Some(512 * 1024));
        assert!(four_plus_two.should_cache_workspace(64 * 1024));
        assert!(!four_plus_two.should_cache_workspace(64 * 1024 + 1));

        let nine_plus_seven =
            LegacyReedSolomonEncoder::with_workspace_cache(9, 7, true).expect("9+7 legacy codec should construct");
        assert_eq!(nine_plus_seven.logical_shard_bytes_upper_bound(16 * 1024), Some(512 * 1024));
        assert!(nine_plus_seven.should_cache_workspace(16 * 1024));
        assert!(!nine_plus_seven.should_cache_workspace(16 * 1024 + 1));

        let uncached = LegacyReedSolomonEncoder::new(4, 2).expect("uncached legacy codec should construct");
        assert!(!uncached.should_cache_workspace(64));
    }

    #[test]
    fn saturated_legacy_codec_cache_does_not_retain_more_workspaces() {
        let cache = RwLock::new(HashMap::new());
        for parity_shards in 1..=LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES {
            let cached =
                cached_legacy_reed_solomon_in(&cache, 32, parity_shards).expect("cacheable legacy codec should construct");
            assert!(cached.cache_workspaces);
        }

        let uncached =
            cached_legacy_reed_solomon_in(&cache, 31, 1).expect("uncached legacy codec should construct after saturation");
        assert!(!uncached.cache_workspaces);
        assert_eq!(
            cache.read().expect("cache lock should remain healthy").len(),
            LEGACY_REED_SOLOMON_CACHE_MAX_ENTRIES
        );
    }

    #[test]
    fn concurrent_legacy_codecs_preserve_byte_exact_results() {
        let barrier = Arc::new(std::sync::Barrier::new(2));
        let payloads = [vec![0x35; 257], vec![0xca; 1025]];

        std::thread::scope(|scope| {
            let handles = payloads.each_ref().map(|payload| {
                let barrier = Arc::clone(&barrier);
                scope.spawn(move || {
                    let erasure = Erasure::new_with_options(6, 3, 2048, true);
                    barrier.wait();
                    let encoded = erasure.encode_data(payload).expect("concurrent legacy encode should succeed");
                    barrier.wait();

                    let mut shards = optional_shards(&encoded);
                    shards[0] = None;
                    erasure
                        .decode_data(&mut shards)
                        .expect("concurrent legacy decode should reconstruct the missing shard");
                    recover_data(&shards, erasure.data_shards, payload.len())
                })
            });

            for (handle, payload) in handles.into_iter().zip(payloads.iter()) {
                assert_eq!(handle.join().expect("concurrent legacy codec worker should not panic"), *payload);
            }
        });
    }

    #[test]
    fn legacy_verify_reports_invalid_empty_valid_and_corrupt_parity_sets() {
        let legacy = LegacyReedSolomonEncoder::new(2, 2).expect("legacy encoder should construct");
        let wrong_count: [&[u8]; 1] = [&[]];
        let err = legacy.verify(&wrong_count).expect_err("wrong shard count must be rejected");
        assert!(err.to_string().contains("invalid shard count"));

        let empty: [&[u8]; 4] = [&[], &[], &[], &[]];
        assert!(
            legacy
                .verify(&empty)
                .expect("all-empty legacy shards are internally consistent")
        );

        let erasure = Erasure::new_with_options(2, 2, 64, true);
        let encoded = erasure
            .encode_data(b"legacy verify should compare regenerated parity")
            .expect("legacy encode should succeed");
        let refs = encoded.iter().map(Bytes::as_ref).collect::<Vec<_>>();
        assert!(legacy.verify(&refs).expect("valid legacy shards should verify"));

        let mut corrupt = encoded.iter().map(|shard| shard.to_vec()).collect::<Vec<_>>();
        corrupt[erasure.data_shards][0] ^= 0x80;
        let corrupt_refs = corrupt.iter().map(Vec::as_slice).collect::<Vec<_>>();
        assert!(!legacy.verify(&corrupt_refs).expect("corrupt parity should be detected"));
    }

    #[test]
    fn parity_helper_and_empty_payload_recovery_fail_closed_on_malformed_shards() {
        let mut wrong_count = vec![Some(vec![1])];
        let err = encode_parity_shards(&mut wrong_count, 2, 1, |_| panic!("wrong count must fail before encode"))
            .expect_err("wrong shard count must be rejected");
        assert!(err.to_string().contains("invalid shard count"));

        let mut inconsistent_empty = vec![Some(Vec::new()), Some(vec![1])];
        let err = encode_parity_shards(&mut inconsistent_empty, 1, 1, |_| panic!("zero-length mismatch must fail before encode"))
            .expect_err("mixed empty and non-empty shards must be rejected");
        assert!(err.to_string().contains("inconsistent shard length"));

        let mut inconsistent_non_empty = vec![Some(vec![1]), Some(vec![2, 3])];
        let err = encode_parity_shards(&mut inconsistent_non_empty, 1, 1, |_| {
            panic!("non-empty length mismatch must fail before encode")
        })
        .expect_err("mismatched non-empty shards must be rejected");
        assert!(err.to_string().contains("inconsistent shard length"));

        let mut no_present_empty_payload = vec![None, None, None];
        assert!(
            !recover_empty_payload_data_shards(&mut no_present_empty_payload, 2, 1)
                .expect("all-missing empty payload marker should not be synthesized")
        );
    }

    #[test]
    fn verify_data_and_parity_handles_zero_parity_and_rejects_wrong_count() {
        let no_parity = Erasure::new(2, 0, 64);
        assert!(
            no_parity
                .verify_data_and_parity(&[Some(vec![1]), Some(vec![2, 3])])
                .expect("zero parity requires no parity verification")
        );

        let erasure = Erasure::new(2, 2, 64);
        let err = erasure
            .verify_data_and_parity(&[Some(Vec::new())])
            .expect_err("wrong shard count must fail verification");
        assert!(err.to_string().contains("invalid shard count"));
    }

    #[test]
    fn encode_data_owned_matches_borrowed_path() {
        for uses_legacy in [false, true] {
            let erasure = Erasure::new_with_options(4, 2, 64, uses_legacy);
            for data in [
                Vec::new(),
                vec![0xA5; 1],
                b"small payload".to_vec(),
                (0_u8..37).collect(),
                vec![0xA5; erasure.block_size - 1],
                vec![0x5A; erasure.block_size],
            ] {
                assert_owned_encode_matches_borrowed(&erasure, data);
            }
        }
    }

    #[test]
    fn encode_data_bytes_mut_matches_borrowed_path() {
        for uses_legacy in [false, true] {
            let erasure = Erasure::new_with_options(4, 2, 64, uses_legacy);
            let block_size = erasure.block_size;
            let expanded_block_bytes = erasure.shard_size() * erasure.total_shard_count();
            let cases: Vec<Vec<u8>> = vec![
                Vec::new(),
                b"small payload".to_vec(),
                (0_u8..37).collect(),
                vec![0xA5; block_size - 1], // last block one byte short of full
                vec![0x5A; block_size],     // exactly one full block
            ];
            for data in cases {
                let borrowed = erasure.encode_data(&data).expect("borrowed encode should succeed");

                let owned = erasure
                    .encode_data_bytes_mut(BytesMut::from(&data[..]), data.len())
                    .expect("bytesmut encode should succeed");
                assert_eq!(owned, borrowed);

                // Ingest-shaped buffer: spare capacity pre-reserved for the EC-expanded
                // block, exactly what the HP-10 streaming ingest path hands over.
                let mut ingest = BytesMut::with_capacity(expanded_block_bytes.max(block_size));
                ingest.extend_from_slice(&data);
                let preallocated = erasure
                    .encode_data_bytes_mut(ingest, data.len())
                    .expect("preallocated bytesmut encode should succeed");
                assert_eq!(preallocated, borrowed);

                // Buffer longer than data_len (stale bytes past the logical block)
                // must be truncated before padding.
                let mut oversized = BytesMut::from(&data[..]);
                oversized.extend_from_slice(&[0xFF; 8]);
                let truncated = erasure
                    .encode_data_bytes_mut(oversized, data.len())
                    .expect("oversized bytesmut encode should succeed");
                assert_eq!(truncated, borrowed);
            }
        }
    }

    #[test]
    fn streaming_encoded_block_uses_one_contiguous_backing_buffer() {
        for uses_legacy in [false, true] {
            let erasure = Erasure::new_with_options(8, 8, 64, uses_legacy);

            for data_len in [0, 1, 63, 64] {
                let data = (0..data_len).map(|i| i as u8).collect::<Vec<_>>();
                let expected = erasure.encode_data(&data).expect("public encode should succeed");
                let borrowed = erasure
                    .encode_data_block(&data)
                    .expect("borrowed streaming encode should succeed");
                let owned = erasure
                    .encode_data_owned_block(data.clone())
                    .expect("owned streaming encode should succeed");
                let bytes_mut = erasure
                    .encode_data_bytes_mut_block(BytesMut::from(&data[..]), data.len())
                    .expect("BytesMut streaming encode should succeed");

                assert_eq!(borrowed.queued_bytes(), owned.queued_bytes());
                assert_eq!(borrowed.queued_bytes(), bytes_mut.queued_bytes());

                if data_len == 0 {
                    assert!(expected.iter().all(Bytes::is_empty));
                    assert!(borrowed.is_empty());
                    assert!(owned.is_empty());
                    assert!(bytes_mut.is_empty());
                    continue;
                }

                assert!(borrowed.shards().eq(expected.iter().map(Bytes::as_ref)));
                assert!(owned.shards().eq(expected.iter().map(Bytes::as_ref)));
                assert!(bytes_mut.shards().eq(expected.iter().map(Bytes::as_ref)));
                assert_eq!(borrowed.shards().len(), 16);
                let first = borrowed.shards().next().expect("encoded block should have shards").as_ptr();
                for (index, shard) in borrowed.shards().enumerate() {
                    assert_eq!(shard.as_ptr(), first.wrapping_add(index * shard.len()));
                }
            }
        }
        assert_eq!(
            std::mem::size_of::<EncodedBlock>(),
            std::mem::size_of::<Bytes>() + std::mem::size_of::<usize>(),
            "queue entries must contain one backing buffer handle, not per-shard handles"
        );
    }

    /// HP-10 capacity invariant: both shard-size formulas are monotone in `data_len`,
    /// so pre-reserving `shard_size(block_size) * total_shard_count` covers the
    /// `need_total_size` of every block-or-smaller payload and the ingest buffer
    /// never reallocates inside `encode_data_bytes_mut`.
    #[test]
    fn shard_size_monotonicity_bounds_expanded_block_capacity() {
        for shard_fn in [calc_shard_size, calc_shard_size_legacy] {
            for data_shards in 1usize..=16 {
                for block_size in [1usize, 2, 63, 64, 65, 1024, 4096] {
                    let full = shard_fn(block_size, data_shards);
                    let mut prev = shard_fn(0, data_shards);
                    for data_len in 1..=block_size {
                        let cur = shard_fn(data_len, data_shards);
                        assert!(cur >= prev, "shard size must be monotone (len {data_len}, shards {data_shards})");
                        assert!(cur <= full, "per-block shard size must not exceed the full-block bound");
                        prev = cur;
                    }
                }
                // Spot-check the production-scale block size at its boundaries.
                let block_size = 1usize << 20;
                let full = shard_fn(block_size, data_shards);
                for data_len in [0usize, 1, block_size / 2, block_size - 1, block_size] {
                    assert!(shard_fn(data_len, data_shards) <= full);
                }
            }
        }
    }

    #[test]
    fn decode_data_keeps_missing_parity_shard_unreconstructed() {
        let erasure = Erasure::new(2, 2, 64);
        let data = b"read decode should not rebuild parity";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let mut shards = optional_shards(&encoded);
        let missing_parity = erasure.data_shards;
        shards[missing_parity] = None;

        erasure.decode_data(&mut shards).expect("decode should succeed");

        assert!(shards[missing_parity].is_none(), "read decode should leave parity missing");
        for index in 0..erasure.data_shards {
            assert_eq!(
                shards[index].as_deref(),
                Some(encoded[index].as_ref()),
                "data shard {index} should remain unchanged"
            );
        }
    }

    #[test]
    fn legacy_decode_data_keeps_missing_parity_shard_unreconstructed() {
        let erasure = Erasure::new_with_options(2, 2, 64, true);
        let data = b"legacy read decode should not rebuild parity";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let mut shards = optional_shards(&encoded);
        let missing_parity = erasure.data_shards + 1;
        shards[missing_parity] = None;

        erasure.decode_data(&mut shards).expect("decode should succeed");

        assert!(shards[missing_parity].is_none(), "legacy read decode should leave parity missing");
        for index in 0..erasure.data_shards {
            assert_eq!(
                shards[index].as_deref(),
                Some(encoded[index].as_ref()),
                "legacy data shard {index} should remain unchanged"
            );
        }
    }

    #[test]
    fn decode_data_rejects_missing_data_above_parity_budget() {
        for uses_legacy in [false, true] {
            let erasure = Erasure::new_with_options(3, 2, 64, uses_legacy);
            let data = b"read restore must fail when too many data shards are gone";
            let encoded = erasure.encode_data(data).expect("encode should succeed");
            let mut shards = optional_shards(&encoded);
            shards[0] = None;
            shards[1] = None;
            shards[2] = None;

            let err = erasure
                .decode_data(&mut shards)
                .expect_err("decode_data must fail when missing data shards exceed parity");

            assert!(!err.to_string().is_empty());
            assert!(shards.iter().take(erasure.data_shards).any(Option::is_none));
        }
    }

    #[test]
    fn decode_data_and_parity_rejects_wrong_shard_count() {
        for uses_legacy in [false, true] {
            let erasure = Erasure::new_with_options(4, 2, 128, uses_legacy);
            let data = b"wrong shard count should not be accepted as a restored object";
            let encoded = erasure.encode_data(data).expect("encode should succeed");
            let mut shards = optional_shards(&encoded);
            let _ = shards.pop();

            let err = erasure
                .decode_data_and_parity(&mut shards)
                .expect_err("decode_data_and_parity must reject wrong shard count");

            assert!(!err.to_string().is_empty());
        }
    }

    #[test]
    fn decode_data_with_verification_rejects_corrupt_surplus_parity() {
        let erasure = Erasure::new(3, 2, 128);
        let data = b"verified reads must fail closed on stale surplus parity";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let mut shards = optional_shards(&encoded);
        shards[1] = None;
        shards[erasure.data_shards].as_mut().expect("parity shard should be present")[0] ^= 0x7d;

        let err = erasure
            .decode_data_with_reconstruction_verification(&mut shards)
            .expect_err("verified decode must reject inconsistent parity");

        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn verify_data_and_parity_rejects_missing_and_mismatched_shards() {
        let erasure = Erasure::new(4, 2, 128);
        let data = b"verification must reject incomplete or malformed shard sets";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let mut shards = optional_shards(&encoded);

        shards[0] = None;
        let missing = erasure
            .verify_data_and_parity(&shards)
            .expect_err("verification must reject missing shards");
        assert!(missing.to_string().contains("missing shard"));

        let mut mismatched = optional_shards(&encoded);
        let _ = mismatched[0].as_mut().expect("data shard should be present").pop();
        let mismatch = erasure
            .verify_data_and_parity(&mismatched)
            .expect_err("verification must reject mismatched shard lengths");
        assert!(mismatch.to_string().contains("inconsistent shard length"));
    }

    #[test]
    fn decode_data_and_parity_leaves_complete_shards_unchanged() {
        let erasure = Erasure::new(4, 2, 128);
        let data = b"complete shards should not be changed";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let original = optional_shards(&encoded);
        let mut shards = original.clone();

        erasure
            .decode_data_and_parity(&mut shards)
            .expect("decode should succeed without missing shards");

        assert_eq!(shards, original);
    }

    #[test]
    fn decode_data_and_parity_reconstructs_missing_parity_shard() {
        let erasure = Erasure::new(2, 2, 64);
        let data = b"parity shard must be rebuilt";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let mut shards = optional_shards(&encoded);
        shards[2] = None;

        erasure
            .decode_data_and_parity(&mut shards)
            .expect("decode should rebuild parity");

        for (index, shard) in shards.iter().enumerate() {
            assert_eq!(
                shard.as_deref(),
                Some(encoded[index].as_ref()),
                "shard {index} should match encoded source"
            );
        }
    }

    #[test]
    fn decode_data_and_parity_reconstructs_missing_data_and_parity_shards() {
        let erasure = Erasure::new(4, 2, 128);
        let data = b"data and parity shards should both be reconstructed";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let mut shards = optional_shards(&encoded);
        shards[1] = None;
        shards[4] = None;

        erasure
            .decode_data_and_parity(&mut shards)
            .expect("decode should rebuild all missing shards");

        for (index, shard) in shards.iter().enumerate() {
            assert_eq!(
                shard.as_deref(),
                Some(encoded[index].as_ref()),
                "shard {index} should match encoded source"
            );
        }
    }

    #[test]
    fn legacy_decode_data_and_parity_reconstructs_missing_parity_shard() {
        let erasure = Erasure::new_with_options(2, 2, 64, true);
        let data = b"legacy parity shard must be rebuilt";
        let encoded = erasure.encode_data(data).expect("encode should succeed");
        let mut shards = optional_shards(&encoded);
        shards[3] = None;

        erasure
            .decode_data_and_parity(&mut shards)
            .expect("decode should rebuild parity");

        for (index, shard) in shards.iter().enumerate() {
            assert_eq!(
                shard.as_deref(),
                Some(encoded[index].as_ref()),
                "shard {index} should match encoded source"
            );
        }
    }

    #[test]
    fn legacy_decode_data_and_parity_reconstructs_empty_object_shards() {
        let erasure = Erasure::new_with_options(3, 3, 64, true);
        let encoded = erasure.encode_data(&[]).expect("empty encode should succeed");
        let mut shards = optional_shards(&encoded);
        shards[1] = None;
        shards[4] = None;

        erasure
            .decode_data_and_parity(&mut shards)
            .expect("empty decode should rebuild missing shards without SIMD");

        for (index, shard) in shards.iter().enumerate() {
            assert_eq!(
                shard.as_deref(),
                Some(encoded[index].as_ref()),
                "empty shard {index} should match encoded source"
            );
        }
    }

    #[test]
    fn test_shard_file_size_cases2() {
        let erasure = Erasure::new(12, 4, 1024 * 1024);
        assert_eq!(erasure.shard_file_size(1572864), 131073);
    }

    proptest! {
        #[test]
        fn decode_data_and_parity_round_trips_bounded_recoverability(
            (data_shards, parity_shards, block_size, uses_legacy, data, missing_indices) in
                erasure_recoverability_case_strategy(),
        ) {
            let erasure = Erasure::new_with_options(data_shards, parity_shards, block_size, uses_legacy);
            let encoded = erasure.encode_data(&data)
                .expect("encode_data should succeed for bounded recoverability property cases");
            let mut shards = optional_shards(&encoded);

            for index in &missing_indices {
                shards[*index] = None;
            }

            erasure.decode_data_and_parity(&mut shards)
                .expect("decode_data_and_parity should recover when missing shard count does not exceed parity");

            let recovered = recover_data(&shards, data_shards, data.len());
            prop_assert_eq!(recovered, data);

            for (index, shard) in shards.iter().enumerate() {
                prop_assert_eq!(
                    shard.as_deref(),
                    Some(encoded[index].as_ref()),
                    "reconstructed shard {} should match the original encoded shard",
                    index
                );
            }
        }
    }

    #[test]
    fn test_shard_file_size_cases() {
        let erasure = Erasure::new(4, 2, 8);

        // Case 1: total_length == 0
        assert_eq!(erasure.shard_file_size(0), 0);

        // Case 2: total_length < block_size
        assert_eq!(erasure.shard_file_size(5), 2); // 5 div_ceil 4 = 2

        // Case 3: total_length == block_size
        assert_eq!(erasure.shard_file_size(8), 2);

        // Case 4: total_length > block_size, not aligned
        assert_eq!(erasure.shard_file_size(13), 4); // 8/8=1, last=5, 5 div_ceil 4=2, 1*2+2=4

        // Case 5: total_length > block_size, aligned
        assert_eq!(erasure.shard_file_size(16), 4); // 16/8=2, last=0, 2*2+0=4

        // MinIO-compatible: 1248739/8=156092, last=3, ceil(3/4)=1, 156092*2+1=312185
        assert_eq!(erasure.shard_file_size(1248739), 312185);

        // MinIO-compatible: 43/8=5, last=3, ceil(3/4)=1, 5*2+1=11
        assert_eq!(erasure.shard_file_size(43), 11);

        // 1572864 with block_size=8: 196608 full blocks, last=0, 196608*2+0=393216
        assert_eq!(erasure.shard_file_size(1572864), 393216);
    }

    #[test]
    fn test_encode_decode_roundtrip() {
        let data_shards = 4;
        let parity_shards = 2;
        let block_size = 1024; // SIMD mode
        let erasure = Erasure::new(data_shards, parity_shards, block_size);

        // Use sufficient test data for SIMD optimization
        let test_data = b"SIMD mode test data for encoding and decoding roundtrip verification with sufficient length to ensure shard size requirements are met for proper SIMD optimization.".repeat(20); // ~3KB for SIMD

        let data = &test_data;
        let encoded_shards = erasure.encode_data(data).expect("operation should succeed");
        assert_eq!(encoded_shards.len(), data_shards + parity_shards);

        // Create decode input with some shards missing, convert to the format expected by decode_data
        let mut decode_input: Vec<Option<Vec<u8>>> = vec![None; data_shards + parity_shards];
        for i in 0..data_shards {
            decode_input[i] = Some(encoded_shards[i].to_vec());
        }

        erasure.decode_data(&mut decode_input).expect("operation should succeed");

        // Recover original data
        let mut recovered = Vec::new();
        for shard in decode_input.iter().take(data_shards) {
            recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
        }
        recovered.truncate(data.len());
        assert_eq!(&recovered, data);
    }

    #[test]
    fn test_encode_decode_large_1m() {
        let data_shards = 4;
        let parity_shards = 2;
        let block_size = 512 * 3; // SIMD mode
        let erasure = Erasure::new(data_shards, parity_shards, block_size);

        // Generate 1MB test data
        let data: Vec<u8> = (0..1048576).map(|i| (i % 256) as u8).collect();

        let encoded_shards = erasure.encode_data(&data).expect("operation should succeed");
        assert_eq!(encoded_shards.len(), data_shards + parity_shards);

        // Create decode input with some shards missing, convert to the format expected by decode_data
        let mut decode_input: Vec<Option<Vec<u8>>> = vec![None; data_shards + parity_shards];
        for i in 0..data_shards {
            decode_input[i] = Some(encoded_shards[i].to_vec());
        }

        erasure.decode_data(&mut decode_input).expect("operation should succeed");

        // Recover original data
        let mut recovered = Vec::new();
        for shard in decode_input.iter().take(data_shards) {
            recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
        }
        recovered.truncate(data.len());
        assert_eq!(recovered, data);
    }

    #[test]
    fn test_encode_all_zero_data() {
        let data_shards = 3;
        let parity_shards = 2;
        let block_size = 6;
        let erasure = Erasure::new(data_shards, parity_shards, block_size);
        let data = vec![0u8; block_size];
        let shards = erasure.encode_data(&data).expect("operation should succeed");
        assert_eq!(shards.len(), data_shards + parity_shards);
        let total_len: usize = shards.iter().map(|b| b.len()).sum();
        assert_eq!(total_len, erasure.shard_size() * (data_shards + parity_shards));
    }

    #[test]
    fn test_shard_size_and_file_size() {
        let erasure = Erasure::new(4, 2, 8);
        assert_eq!(erasure.shard_file_size(33), 9);
        assert_eq!(erasure.shard_file_size(0), 0);
    }

    #[test]
    fn test_legacy_shard_size_and_file_size() {
        let erasure = Erasure::new_with_options(4, 2, 8, true);
        assert_eq!(erasure.shard_size(), 2);
        assert_eq!(calc_shard_size_legacy(8, 4), 2);
        assert_eq!(calc_shard_size_legacy(1, 4), 2);
        assert_eq!(erasure.shard_file_size(33), 10);
        assert_eq!(erasure.shard_file_size(0), 0);
    }

    #[test]
    fn test_legacy_encode_decode_roundtrip() {
        let data_shards = 4;
        let parity_shards = 2;
        let block_size = 1024;
        let erasure = Erasure::new_with_options(data_shards, parity_shards, block_size, true);

        let data = b"Legacy encode/decode roundtrip test data with sufficient length.".repeat(20);
        let encoded_shards = erasure.encode_data(&data).expect("operation should succeed");
        assert_eq!(encoded_shards.len(), data_shards + parity_shards);

        let mut decode_input: Vec<Option<Vec<u8>>> = vec![None; data_shards + parity_shards];
        for i in 0..data_shards {
            decode_input[i] = Some(encoded_shards[i].to_vec());
        }

        erasure.decode_data(&mut decode_input).expect("operation should succeed");

        let mut recovered = Vec::new();
        for shard in decode_input.iter().take(data_shards) {
            recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
        }
        recovered.truncate(data.len());
        assert_eq!(&recovered, &data);
    }

    #[test]
    fn test_legacy_decode_with_missing_shards() {
        let data_shards = 4;
        let parity_shards = 2;
        let block_size = 256;
        let erasure = Erasure::new_with_options(data_shards, parity_shards, block_size, true);

        let data = b"Legacy decode with missing shards test.".repeat(10);
        let encoded_shards = erasure.encode_data(&data).expect("operation should succeed");

        let mut shards_opt: Vec<Option<Vec<u8>>> = encoded_shards.iter().map(|s| Some(s.to_vec())).collect();
        shards_opt[1] = None;
        shards_opt[5] = None;

        erasure.decode_data(&mut shards_opt).expect("operation should succeed");

        let mut recovered = Vec::new();
        for shard in shards_opt.iter().take(data_shards) {
            recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
        }
        recovered.truncate(data.len());
        assert_eq!(&recovered, &data);
    }

    #[test]
    fn test_shard_file_offset() {
        let erasure = Erasure::new(8, 8, 1024 * 1024);
        let offset = erasure.shard_file_offset(0, 86, 86);
        println!("offset={offset}");
        assert!(offset > 0);

        let total_length = erasure.shard_file_size(86);
        println!("total_length={total_length}");
        assert!(total_length > 0);
    }

    #[tokio::test]
    async fn test_encode_stream_callback_async_error_propagation() {
        use std::io::Cursor;
        use std::sync::Arc;
        use tokio::sync::mpsc;

        let data_shards = 4;
        let parity_shards = 2;
        let block_size = 1024; // SIMD mode
        let erasure = Arc::new(Erasure::new(data_shards, parity_shards, block_size));

        // Use test data suitable for SIMD mode
        let data =
            b"Async error test data with sufficient length to meet requirements for proper testing and validation.".repeat(20); // ~2KB

        let mut reader = Cursor::new(data);
        let (tx, mut rx) = mpsc::channel::<Vec<Bytes>>(8);
        let erasure_clone = erasure.clone();
        let handle = tokio::spawn(async move {
            erasure_clone
                .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| {
                    let tx = tx.clone();
                    async move {
                        let shards = res.expect("operation should succeed");
                        tx.send(shards).await.expect("operation should succeed");
                        Ok(())
                    }
                })
                .await
                .expect("operation should succeed");
        });
        let result = handle.await;
        assert!(result.is_ok());
        let collected_shards = rx.recv().await.expect("operation should succeed");
        assert_eq!(collected_shards.len(), data_shards + parity_shards);
    }

    #[tokio::test]
    async fn test_encode_stream_callback_async_channel_decode() {
        use std::io::Cursor;
        use std::sync::Arc;
        use tokio::sync::mpsc;

        let data_shards = 4;
        let parity_shards = 2;
        let block_size = 1024; // SIMD mode
        let erasure = Arc::new(Erasure::new(data_shards, parity_shards, block_size));

        // Use test data that fits in exactly one block to avoid multi-block complexity
        let data =
            b"Channel async callback test data with sufficient length to ensure proper operation and validation requirements."
                .repeat(8); // ~1KB

        let data_clone = data.clone(); // Clone for later comparison
        let mut reader = Cursor::new(data);
        let (tx, mut rx) = mpsc::channel::<Vec<Bytes>>(8);
        let erasure_clone = erasure.clone();
        let handle = tokio::spawn(async move {
            erasure_clone
                .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| {
                    let tx = tx.clone();
                    async move {
                        let shards = res.expect("operation should succeed");
                        tx.send(shards).await.expect("operation should succeed");
                        Ok(())
                    }
                })
                .await
                .expect("operation should succeed");
        });
        let result = handle.await;
        assert!(result.is_ok());
        let shards = rx.recv().await.expect("operation should succeed");
        assert_eq!(shards.len(), data_shards + parity_shards);

        // Test decode using the old API that operates in-place
        let mut decode_input: Vec<Option<Vec<u8>>> = vec![None; data_shards + parity_shards];
        for i in 0..data_shards {
            decode_input[i] = Some(shards[i].to_vec());
        }
        erasure.decode_data(&mut decode_input).expect("operation should succeed");

        // Recover original data
        let mut recovered = Vec::new();
        for shard in decode_input.iter().take(data_shards) {
            recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
        }
        recovered.truncate(data_clone.len());
        assert_eq!(&recovered, &data_clone);
    }

    #[tokio::test]
    async fn test_encode_stream_callback_async_reports_zero_block_size() {
        use std::io::Cursor;
        use std::sync::{Arc, Mutex};

        let erasure = Arc::new(erasure_with_invalid_dimensions(1, 0, 0));
        let mut reader = Cursor::new(b"payload".to_vec());
        let observed = Arc::new(Mutex::new(None));
        let observed_clone = observed.clone();

        let total = erasure
            .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| {
                let observed = observed_clone.clone();
                async move {
                    let err = res.expect_err("zero block size should report an error");
                    *observed.lock().expect("operation should succeed") = Some((err.kind(), err.to_string()));
                    Ok(())
                }
            })
            .await
            .expect("callback should handle the zero block size error");

        assert_eq!(total, 0);
        let observed = observed.lock().expect("operation should succeed");
        let (kind, message) = observed.as_ref().expect("callback should be invoked once");
        assert_eq!(*kind, io::ErrorKind::InvalidInput);
        assert!(message.contains("block_size"));
    }

    // SIMD mode specific tests
    mod simd_tests {
        use super::*;

        #[test]
        fn test_simd_encode_decode_roundtrip() {
            let data_shards = 4;
            let parity_shards = 2;
            let block_size = 1024; // Use larger block size for SIMD mode
            let erasure = Erasure::new(data_shards, parity_shards, block_size);

            // Use data that will create shards >= 512 bytes for SIMD optimization
            let test_data = b"SIMD mode test data for encoding and decoding roundtrip verification with sufficient length to ensure shard size requirements are met for proper SIMD optimization and validation.";
            let data = test_data.repeat(25); // Create much larger data: ~5KB total, ~1.25KB per shard

            let encoded_shards = erasure.encode_data(&data).expect("operation should succeed");
            assert_eq!(encoded_shards.len(), data_shards + parity_shards);

            // Create decode input with some shards missing
            let mut shards_opt: Vec<Option<Vec<u8>>> = encoded_shards.iter().map(|shard| Some(shard.to_vec())).collect();

            // Lose one data shard and one parity shard (should still be recoverable)
            shards_opt[1] = None; // Lose second data shard
            shards_opt[5] = None; // Lose second parity shard

            erasure.decode_data(&mut shards_opt).expect("operation should succeed");

            // Verify recovered data
            let mut recovered = Vec::new();
            for shard in shards_opt.iter().take(data_shards) {
                recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
            }
            recovered.truncate(data.len());
            assert_eq!(&recovered, &data);
        }

        #[test]
        fn test_simd_all_zero_data() {
            let data_shards = 4;
            let parity_shards = 2;
            let block_size = 1024; // Use larger block size for SIMD mode
            let erasure = Erasure::new(data_shards, parity_shards, block_size);

            // Create all-zero data that ensures adequate shard size for SIMD optimization
            let data = vec![0u8; 1024]; // 1KB of zeros, each shard will be 256 bytes

            let encoded_shards = erasure.encode_data(&data).expect("operation should succeed");
            assert_eq!(encoded_shards.len(), data_shards + parity_shards);

            // Verify that all data shards are zeros
            for (i, shard) in encoded_shards.iter().enumerate().take(data_shards) {
                assert!(shard.iter().all(|&x| x == 0), "Data shard {i} should be all zeros");
            }

            // Test recovery with some shards missing
            let mut shards_opt: Vec<Option<Vec<u8>>> = encoded_shards.iter().map(|shard| Some(shard.to_vec())).collect();

            // Lose maximum recoverable shards (equal to parity_shards)
            shards_opt[0] = None; // Lose first data shard
            shards_opt[4] = None; // Lose first parity shard

            erasure.decode_data(&mut shards_opt).expect("operation should succeed");

            // Verify recovered data is still all zeros
            let mut recovered = Vec::new();
            for shard in shards_opt.iter().take(data_shards) {
                recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
            }
            recovered.truncate(data.len());
            assert!(recovered.iter().all(|&x| x == 0), "Recovered data should be all zeros");
        }

        #[test]
        fn test_simd_large_data_1kb() {
            let data_shards = 8;
            let parity_shards = 4;
            let block_size = 1024; // 1KB block size optimal for SIMD
            let erasure = Erasure::new(data_shards, parity_shards, block_size);

            // Create 1KB of test data
            let mut data = Vec::with_capacity(1024);
            for i in 0..1024 {
                data.push((i % 256) as u8);
            }

            let shards = erasure.encode_data(&data).expect("operation should succeed");
            assert_eq!(shards.len(), data_shards + parity_shards);

            // Simulate the loss of multiple shards
            let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|b| Some(b.to_vec())).collect();
            shards_opt[0] = None;
            shards_opt[3] = None;
            shards_opt[9] = None; // Parity shard
            shards_opt[11] = None; // Parity shard

            // Decode
            erasure.decode_data(&mut shards_opt).expect("operation should succeed");

            // Recover original data
            let mut recovered = Vec::new();
            for shard in shards_opt.iter().take(data_shards) {
                recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
            }
            recovered.truncate(data.len());
            assert_eq!(&recovered, &data);
        }

        #[test]
        fn test_simd_minimum_shard_size() {
            let data_shards = 4;
            let parity_shards = 2;
            let block_size = 256; // Use 256 bytes to ensure sufficient shard size
            let erasure = Erasure::new(data_shards, parity_shards, block_size);

            // Create data that will result in 64+ byte shards
            let data = vec![0x42u8; 200]; // 200 bytes, should create ~50 byte shards per data shard

            let shards = erasure.encode_data(&data).expect("minimum shard size data should encode");
            println!("SIMD encoding succeeded with shard size: {}", shards[0].len());

            // Test decoding
            let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|b| Some(b.to_vec())).collect();
            shards_opt[1] = None;

            erasure
                .decode_data(&mut shards_opt)
                .expect("minimum shard size data should decode");
            let mut recovered = Vec::new();
            for shard in shards_opt.iter().take(data_shards) {
                recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
            }
            recovered.truncate(data.len());
            assert_eq!(&recovered, &data);
        }

        #[test]
        fn test_simd_maximum_erasures() {
            let data_shards = 5;
            let parity_shards = 3;
            let block_size = 512;
            let erasure = Erasure::new(data_shards, parity_shards, block_size);

            let data =
                b"Testing maximum erasure capacity with SIMD Reed-Solomon implementation for robustness verification!".repeat(3);

            let shards = erasure.encode_data(&data).expect("operation should succeed");

            // Lose exactly the maximum number of shards (equal to parity_shards)
            let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|b| Some(b.to_vec())).collect();
            shards_opt[0] = None; // Data shard
            shards_opt[2] = None; // Data shard  
            shards_opt[6] = None; // Parity shard

            // Should succeed with maximum erasures
            erasure.decode_data(&mut shards_opt).expect("operation should succeed");

            let mut recovered = Vec::new();
            for shard in shards_opt.iter().take(data_shards) {
                recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
            }
            recovered.truncate(data.len());
            assert_eq!(&recovered, &data);
        }

        /// Generates 7557 bytes identical to MinIO generateCompatTestData.
        fn generate_compat_test_data(size: usize) -> Vec<u8> {
            (0..size).map(|i| ((i * 7 + 13) % 256) as u8).collect()
        }

        /// Verifies reed-solomon-simd produces same shards.
        /// Data shards (0-3) must match for MinIO to read RustFS part files.
        /// Parity shards (4-5) differ: reed-solomon-simd vs klauspost use different RS encoding.
        /// Run: cargo test -p rustfs-ecstore test_reed_solomon_compat
        #[test]
        fn test_reed_solomon_compat() {
            let data = generate_compat_test_data(7557);
            let erasure = Erasure::new(4, 2, 7557);
            let shards = erasure.encode_data(&data).expect("operation should succeed");
            assert_eq!(shards.len(), 6, "expected 6 shards (4 data + 2 parity)");

            // Per-shard HighwayHash
            let expected_hashes: [&str; 6] = [
                "fb3db9338e610cec541504ddae4b0bfd54445bcbd45318cf21f35f024240914d", // data 0
                "a545269a3196e18e77ef9f5ec6e735a4f4ebe82d342db666b11a5256eb305720", // data 1
                "2adbf0058f36c4cbcb5c9c16c38a6530c54198dfe504179a6f92d2349f245318", // data 2
                "898e6d060b0cb4f0e830add7e1f936bc8b78442bf582283ee244a3a058602db8", // data 3
                "4a20460bca044b3a777b26f2b0bcd371e3eab2f156f84778be3ccd8edd521ef2", // parity 4
                "eb8ba4c0db15ca910d58d031f74e4601ba2fed62ad03ec29cadde3367ab0d415", // parity 5
            ];

            let mut data_shards_match = true;
            let mut parity_shards_match = true;
            for (i, shard) in shards.iter().enumerate() {
                let hash = rustfs_utils::HashAlgorithm::HighwayHash256S.hash_encode(shard);
                let got = hex_simd::encode_to_string(hash.as_ref(), hex_simd::AsciiCase::Lower);
                let matches = got == expected_hashes[i];
                if i < 4 {
                    data_shards_match &= matches;
                } else {
                    parity_shards_match &= matches;
                }
                if !matches {
                    eprintln!(
                        "Shard {} ({}): got {} want {}",
                        i,
                        if i < 4 { "data" } else { "parity" },
                        got,
                        expected_hashes[i]
                    );
                }
            }
            assert!(data_shards_match, "Data shards (0-3) must match");
            assert!(parity_shards_match, "Parity shards (4-5): reed-solomon-simd differs");
        }

        #[test]
        fn test_simd_small_data_handling() {
            let data_shards = 4;
            let parity_shards = 2;
            let block_size = 32; // Small block size for testing edge cases
            let erasure = Erasure::new(data_shards, parity_shards, block_size);

            // Use small data to test SIMD handling of small shards
            let small_data = b"tiny!123".to_vec(); // 8 bytes data

            // Test encoding with small data
            let shards = erasure.encode_data(&small_data).expect("small data should encode");
            println!("SIMD encoding succeeded: {} bytes into {} shards", small_data.len(), shards.len());
            assert_eq!(shards.len(), data_shards + parity_shards);

            // Test decoding
            let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|shard| Some(shard.to_vec())).collect();

            // Lose some shards to test recovery
            shards_opt[1] = None; // Lose one data shard
            shards_opt[4] = None; // Lose one parity shard

            erasure.decode_data(&mut shards_opt).expect("small data should decode");
            let mut recovered = Vec::new();
            for shard in shards_opt.iter().take(data_shards) {
                recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
            }
            recovered.truncate(small_data.len());
            println!("recovered: {recovered:?}");
            println!("small_data: {small_data:?}");
            assert_eq!(&recovered, &small_data);
        }

        #[test]
        fn test_simd_large_block_1mb() {
            let data_shards = 6;
            let parity_shards = 3;
            let block_size = 1024 * 1024; // 1MB block size
            let erasure = Erasure::new(data_shards, parity_shards, block_size);

            // Build 2 MB of test data so multiple 1 MB chunks are exercised
            let mut data = Vec::with_capacity(2 * 1024 * 1024);
            for i in 0..(2 * 1024 * 1024) {
                data.push((i % 256) as u8);
            }

            println!("🚀 Testing SIMD with 1MB block size and 2MB data");
            println!(
                "📊 Data shards: {}, Parity shards: {}, Total data: {}KB",
                data_shards,
                parity_shards,
                data.len() / 1024
            );

            // Encode the data
            let start = std::time::Instant::now();
            let shards = erasure.encode_data(&data).expect("operation should succeed");
            let encode_duration = start.elapsed();

            println!("⏱️  Encoding completed in: {encode_duration:?}");
            println!("📦 Generated {} shards, each shard size: {}KB", shards.len(), shards[0].len() / 1024);

            assert_eq!(shards.len(), data_shards + parity_shards);

            // Verify that each shard is large enough for SIMD optimization
            for (i, shard) in shards.iter().enumerate() {
                println!("🔍 Shard {}: {} bytes ({}KB)", i, shard.len(), shard.len() / 1024);
                assert!(shard.len() >= 512, "Shard {} is too small for SIMD: {} bytes", i, shard.len());
            }

            // Simulate data loss - lose maximum recoverable number of shards
            let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|b| Some(b.to_vec())).collect();
            shards_opt[0] = None; // Lose 1st data shard
            shards_opt[2] = None; // Lose 3rd data shard  
            shards_opt[8] = None; // Lose 3rd parity shard (index 6+3-1=8)

            println!("💥 Simulated loss of 3 shards (max recoverable with 3 parity shards)");

            // Decode and recover data
            let start = std::time::Instant::now();
            erasure.decode_data(&mut shards_opt).expect("operation should succeed");
            let decode_duration = start.elapsed();

            println!("⏱️  Decoding completed in: {decode_duration:?}");

            // Verify recovered data integrity
            let mut recovered = Vec::new();
            for shard in shards_opt.iter().take(data_shards) {
                recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
            }
            recovered.truncate(data.len());

            assert_eq!(recovered.len(), data.len());
            assert_eq!(&recovered, &data, "Data mismatch after recovery!");

            println!("✅ Successfully verified data integrity after recovery");
            println!("📈 Performance summary:");
            println!(
                "   - Encode: {:?} ({:.2} MB/s)",
                encode_duration,
                (data.len() as f64 / (1024.0 * 1024.0)) / encode_duration.as_secs_f64()
            );
            println!(
                "   - Decode: {:?} ({:.2} MB/s)",
                decode_duration,
                (data.len() as f64 / (1024.0 * 1024.0)) / decode_duration.as_secs_f64()
            );
        }

        #[tokio::test]
        async fn test_simd_stream_callback() {
            use std::io::Cursor;
            use std::sync::Arc;
            use tokio::sync::mpsc;

            let data_shards = 4;
            let parity_shards = 2;
            let block_size = 256; // Larger block for SIMD
            let erasure = Arc::new(Erasure::new(data_shards, parity_shards, block_size));

            let test_data = b"SIMD stream processing test with sufficient data length for multiple blocks and proper SIMD optimization verification!";
            let data = test_data.repeat(5); // Create owned Vec<u8>
            let data_clone = data.clone(); // Clone for later comparison
            let mut reader = Cursor::new(data);

            let (tx, mut rx) = mpsc::channel::<Vec<Bytes>>(16);
            let erasure_clone = erasure.clone();

            let handle = tokio::spawn(async move {
                erasure_clone
                    .encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| {
                        let tx = tx.clone();
                        async move {
                            let shards = res.expect("operation should succeed");
                            tx.send(shards).await.expect("operation should succeed");
                            Ok(())
                        }
                    })
                    .await
                    .expect("operation should succeed");
            });

            let mut all_blocks = Vec::new();
            while let Some(block) = rx.recv().await {
                all_blocks.push(block);
            }
            handle.await.expect("operation should succeed");

            // Verify we got multiple blocks
            assert!(all_blocks.len() > 1, "Should have multiple blocks for stream test");

            // Test recovery for each block
            let mut recovered = Vec::new();
            for block in &all_blocks {
                let mut shards_opt: Vec<Option<Vec<u8>>> = block.iter().map(|b| Some(b.to_vec())).collect();
                // Lose one data shard and one parity shard
                shards_opt[1] = None;
                shards_opt[5] = None;

                erasure.decode_data(&mut shards_opt).expect("operation should succeed");

                for shard in shards_opt.iter().take(data_shards) {
                    recovered.extend_from_slice(shard.as_ref().expect("operation should succeed"));
                }
            }

            recovered.truncate(data_clone.len());
            assert_eq!(&recovered, &data_clone);
        }
    }
}
