// 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.

// #730: error taxonomy still exposes compatibility variants while callers move to contracts.

use crate::bucket::error::BucketMetadataError;
use crate::disk::error::DiskError;
use crate::storage_api_contracts::{error::StorageErrorCode, range::HTTPRangeError};
use rustfs_utils::path::decode_dir_object;
use s3s::S3ErrorCode;

pub type Error = StorageError;
pub type Result<T> = core::result::Result<T, Error>;

/// Storage layer error type covering disk, volume, bucket, object, multipart,
/// erasure-coding, and operational error conditions.
///
/// Variants are organized by domain:
///   - **Disk / Volume** – low-level storage I/O
///   - **Bucket** – bucket-level operations
///   - **Object** – object-level operations (including versioning)
///   - **Multipart** – multipart upload lifecycle
///   - **Erasure / Quorum** – erasure coding and quorum failures
///   - **Operational** – decommission, healing, rate-limiting, etc.
///   - **Generic** – I/O, locks, and catch-all errors
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
    // ── Disk / Volume ────────────────────────────────────────────────
    #[error("Faulty disk")]
    FaultyDisk,
    #[error("Faulty remote disk")]
    FaultyRemoteDisk,
    #[error("Disk full")]
    DiskFull,
    #[error("Disk not found")]
    DiskNotFound,
    #[error("Disk access denied")]
    DiskAccessDenied,
    #[error("Drive is root")]
    DriveIsRoot,
    #[error("Unformatted disk")]
    UnformattedDisk,
    #[error("Corrupted format")]
    CorruptedFormat,
    #[error("Corrupted backend")]
    CorruptedBackend,
    #[error("Too many open files")]
    TooManyOpenFiles,
    #[error("maximum versions exceeded, please delete few versions to proceed")]
    MaxVersionsExceeded,
    #[error("inconsistent drive found")]
    InconsistentDisk,
    #[error("drive does not support O_DIRECT")]
    UnsupportedDisk,
    #[error("disk not a dir")]
    DiskNotDir,
    #[error("drive still did not complete the request")]
    DiskOngoingReq,
    #[error("path not found")]
    PathNotFound,
    #[error("bit-rot hash algorithm is invalid")]
    BitrotHashAlgoInvalid,
    #[error("Rename across devices not allowed, please fix your backend configuration")]
    CrossDeviceLink,
    #[error("less data available than what was requested")]
    LessData,
    #[error("more data was sent than what was advertised")]
    MoreData,
    #[error("outdated XL meta")]
    OutdatedXLMeta,
    #[error("part missing or corrupt")]
    PartMissingOrCorrupt,
    #[error("short write")]
    ShortWrite,
    #[error("source stalled")]
    SourceStalled,
    #[error("timeout")]
    Timeout,
    #[error("invalid path")]
    InvalidPath,

    #[error("Volume not found")]
    VolumeNotFound,
    #[error("Volume exists")]
    VolumeExists,
    #[error("Volume not empty")]
    VolumeNotEmpty,
    #[error("Volume access denied")]
    VolumeAccessDenied,

    #[error("File not found")]
    FileNotFound,
    #[error("File version not found")]
    FileVersionNotFound,
    #[error("File name too long")]
    FileNameTooLong,
    #[error("File access denied")]
    FileAccessDenied,
    #[error("File is corrupted")]
    FileCorrupt,
    #[error("Not a regular file")]
    IsNotRegular,

    // ── Bucket ───────────────────────────────────────────────────────
    #[error("Bucket not found: {0}")]
    BucketNotFound(String),
    #[error("Bucket exists: {0}")]
    BucketExists(String),
    #[error("Bucket not empty: {0}")]
    BucketNotEmpty(String),
    #[error("Bucket not empty: {bucket} ({details})")]
    BucketNotEmptyWithDetails { bucket: String, details: String },
    #[error("Bucket name invalid: {0}")]
    BucketNameInvalid(String),

    // ── Object ───────────────────────────────────────────────────────
    #[error("Object not found: {0}/{1}")]
    ObjectNotFound(String, String),
    #[error("Object name invalid: {0}/{1}")]
    ObjectNameInvalid(String, String),
    #[error("Object name too long: {0}/{1}")]
    ObjectNameTooLong(String, String),
    #[error("Object name contains forward slash as prefix: {0}/{1}")]
    ObjectNamePrefixAsSlash(String, String),
    #[error("Object exists on :{0} as directory {1}")]
    ObjectExistsAsDirectory(String, String),
    #[error("Version not found: {0}/{1}-{2}")]
    VersionNotFound(String, String, String),
    #[error("Invalid version id: {0}/{1}-{2}")]
    InvalidVersionID(String, String, String),
    #[error("invalid data movement operation, source and destination pool are the same for : {0}/{1}-{2}")]
    DataMovementOverwriteErr(String, String, String),
    #[error("Prefix access is denied:{0}/{1}")]
    PrefixAccessDenied(String, String),

    // ── Multipart ────────────────────────────────────────────────────
    #[error("Invalid upload id: {0}/{1}-{2}")]
    InvalidUploadID(String, String, String),
    #[error("Invalid UploadID KeyCombination: {0}/{1}")]
    InvalidUploadIDKeyCombination(String, String),
    #[error("Malformed UploadID: {0}")]
    MalformedUploadID(String),
    #[error("Specified part could not be found. PartNumber {0}, Expected {1}, got {2}")]
    InvalidPart(usize, String, String),
    #[error("Invalid part number: {0}")]
    InvalidPartNumber(usize),
    #[error("Your proposed upload is smaller than the minimum allowed size. Part {0} size {1} is less than minimum {2}")]
    EntityTooSmall(usize, i64, i64),
    #[error("multipart upload size {0} exceeds the configured limit {1}")]
    EntityTooLarge(u64, u64),

    // ── Erasure / Quorum ─────────────────────────────────────────────
    #[error("erasure read quorum")]
    ErasureReadQuorum,
    #[error("erasure write quorum")]
    ErasureWriteQuorum,
    #[error("Storage resources are insufficient for the read operation: {0}/{1}")]
    InsufficientReadQuorum(String, String),
    #[error("Storage resources are insufficient for the write operation: {0}/{1}")]
    InsufficientWriteQuorum(String, String),
    #[error("not first disk")]
    NotFirstDisk,
    #[error("first disk wait")]
    FirstDiskWait,

    // ── Operational ──────────────────────────────────────────────────
    #[error("Storage reached its minimum free drive threshold.")]
    StorageFull,
    #[error("Please reduce your request rate")]
    SlowDown,
    #[error("Decommission not started")]
    DecommissionNotStarted,
    #[error("Decommission already running")]
    DecommissionAlreadyRunning,
    #[error("Rebalance already running")]
    RebalanceAlreadyRunning,
    #[error("Operation canceled")]
    OperationCanceled,
    #[error("No heal required")]
    NoHealRequired,
    #[error("DoneForNow")]
    DoneForNow,
    #[error("Config not found")]
    ConfigNotFound,
    #[error("Precondition failed")]
    PreconditionFailed,
    #[error("Not modified")]
    NotModified,
    #[error("Invalid range specified: {0}")]
    InvalidRangeSpec(String),
    #[error("Namespace lock quorum unavailable for {mode} lock on {bucket}/{object}: required {required}, achieved {achieved}")]
    NamespaceLockQuorumUnavailable {
        mode: &'static str,
        bucket: String,
        object: String,
        required: usize,
        achieved: usize,
    },
    #[error("Bucket quota exceeded. Current usage: {current} bytes, limit: {limit} bytes")]
    QuotaExceeded { current: u64, limit: u64 },

    // ── Generic ──────────────────────────────────────────────────────
    #[error("Unexpected error")]
    Unexpected,
    #[error("not implemented")]
    NotImplemented,
    #[error("Invalid arguments provided for {0}/{1}-{2}")]
    InvalidArgument(String, String, String),
    #[error("method not allowed")]
    MethodNotAllowed,
    #[error("Io error: {0}")]
    Io(#[source] std::io::Error),
    #[error("Lock error: {0}")]
    Lock(#[from] rustfs_lock::LockError),

    /// Internode RPC client acquisition failed. Mirrors
    /// `DiskError::RemoteClientUnavailable`: the detail is diagnostic only and
    /// excluded from equality/hashing so same-cause failures bucket together
    /// during quorum aggregation (backlog#1845).
    #[error("remote rpc client unavailable: {0}")]
    RemoteClientUnavailable(String),

    /// A peer answered a control-plane RPC but its storage/IAM layer is not
    /// initialized yet. Typed form of the legacy "errServerNotInitialized"
    /// error_info string (backlog#1845); the wire carries it as
    /// `ControlPlaneErrorCode::ControlPlaneErrorNotInitialized` alongside the
    /// legacy string for rolling-upgrade compatibility.
    #[error("remote peer not initialized")]
    RemoteNotInitialized,
}

impl From<crate::erasure::coding::ErasureConstructionError> for StorageError {
    fn from(error: crate::erasure::coding::ErasureConstructionError) -> Self {
        Self::Io(error.into_io_error())
    }
}

impl StorageError {
    pub fn other<E>(error: E) -> Self
    where
        E: Into<Box<dyn std::error::Error + Send + Sync>>,
    {
        StorageError::Io(std::io::Error::other(error))
    }

    pub fn is_not_found(&self) -> bool {
        matches!(
            self,
            StorageError::FileNotFound
                | StorageError::ObjectNotFound(_, _)
                | StorageError::FileVersionNotFound
                | StorageError::VersionNotFound(_, _, _)
                | StorageError::VolumeNotFound
                | StorageError::DiskNotFound
                | StorageError::BucketNotFound(_)
        )
    }

    pub fn is_quorum_error(&self) -> bool {
        matches!(
            self,
            StorageError::ErasureReadQuorum
                | StorageError::ErasureWriteQuorum
                | StorageError::InsufficientReadQuorum(_, _)
                | StorageError::InsufficientWriteQuorum(_, _)
                | StorageError::NamespaceLockQuorumUnavailable { .. }
        )
    }

    pub fn is_dangling_delete_grace(&self) -> bool {
        matches!(self, StorageError::Io(io_error) if DiskError::io_error_is_dangling_delete_grace(io_error))
    }
}

impl From<HTTPRangeError> for StorageError {
    fn from(err: HTTPRangeError) -> Self {
        match err {
            HTTPRangeError::InvalidRangeSpec(message) => Self::InvalidRangeSpec(message),
        }
    }
}

impl From<DiskError> for StorageError {
    fn from(e: DiskError) -> Self {
        match e {
            DiskError::Io(io_error) => StorageError::Io(io_error),
            DiskError::MaxVersionsExceeded => StorageError::MaxVersionsExceeded,
            DiskError::Unexpected => StorageError::Unexpected,
            DiskError::CorruptedFormat => StorageError::CorruptedFormat,
            DiskError::CorruptedBackend => StorageError::CorruptedBackend,
            DiskError::UnformattedDisk => StorageError::UnformattedDisk,
            DiskError::InconsistentDisk => StorageError::InconsistentDisk,
            DiskError::UnsupportedDisk => StorageError::UnsupportedDisk,
            DiskError::DiskFull => StorageError::DiskFull,
            DiskError::DiskNotDir => StorageError::DiskNotDir,
            DiskError::DiskNotFound => StorageError::DiskNotFound,
            DiskError::DiskOngoingReq => StorageError::DiskOngoingReq,
            DiskError::DriveIsRoot => StorageError::DriveIsRoot,
            DiskError::FaultyRemoteDisk => StorageError::FaultyRemoteDisk,
            DiskError::FaultyDisk => StorageError::FaultyDisk,
            DiskError::DiskAccessDenied => StorageError::DiskAccessDenied,
            DiskError::FileNotFound => StorageError::FileNotFound,
            DiskError::FileVersionNotFound => StorageError::FileVersionNotFound,
            DiskError::TooManyOpenFiles => StorageError::TooManyOpenFiles,
            DiskError::FileNameTooLong => StorageError::FileNameTooLong,
            DiskError::VolumeExists => StorageError::VolumeExists,
            DiskError::IsNotRegular => StorageError::IsNotRegular,
            DiskError::PathNotFound => StorageError::PathNotFound,
            DiskError::VolumeNotFound => StorageError::VolumeNotFound,
            DiskError::VolumeNotEmpty => StorageError::VolumeNotEmpty,
            DiskError::VolumeAccessDenied => StorageError::VolumeAccessDenied,
            DiskError::FileAccessDenied => StorageError::FileAccessDenied,
            DiskError::FileCorrupt => StorageError::FileCorrupt,
            DiskError::BitrotHashAlgoInvalid => StorageError::BitrotHashAlgoInvalid,
            DiskError::CrossDeviceLink => StorageError::CrossDeviceLink,
            DiskError::LessData => StorageError::LessData,
            DiskError::MoreData => StorageError::MoreData,
            DiskError::OutdatedXLMeta => StorageError::OutdatedXLMeta,
            DiskError::PartMissingOrCorrupt => StorageError::PartMissingOrCorrupt,
            DiskError::NoHealRequired => StorageError::NoHealRequired,
            DiskError::MethodNotAllowed => StorageError::MethodNotAllowed,
            DiskError::ErasureReadQuorum => StorageError::ErasureReadQuorum,
            DiskError::ErasureWriteQuorum => StorageError::ErasureWriteQuorum,
            DiskError::ShortWrite => StorageError::ShortWrite,
            DiskError::SourceStalled => StorageError::SourceStalled,
            DiskError::Timeout => StorageError::Timeout,
            DiskError::InvalidPath => StorageError::InvalidPath,
            DiskError::RemoteClientUnavailable(detail) => StorageError::RemoteClientUnavailable(detail),
        }
    }
}

impl StorageError {
    /// Narrow a store-layer error to the disk-layer vocabulary.
    ///
    /// This used to be a blanket `impl From<StorageError> for DiskError`, which
    /// let `?` silently push store-only errors across the disk boundary into
    /// `DiskError::other`, fragmenting `reduce_errs` quorum buckets
    /// (backlog#1845). Narrowing is now a named, fallible operation: variants
    /// with a disk-layer identity map across (including two documented lossy
    /// collapses kept for compatibility — `SlowDown` → `TooManyOpenFiles` and
    /// `StorageFull` → `DiskFull`, pinned by conversion_roundtrip_tests), and
    /// everything else comes back as `Err(self)` so the caller decides what
    /// crossing the boundary means for it.
    pub fn narrow_to_disk(self) -> core::result::Result<DiskError, StorageError> {
        Ok(match self {
            StorageError::Io(io_error) => io_error.into(),
            StorageError::Unexpected => DiskError::Unexpected,
            StorageError::FileNotFound => DiskError::FileNotFound,
            StorageError::FileVersionNotFound => DiskError::FileVersionNotFound,
            StorageError::FileCorrupt => DiskError::FileCorrupt,
            StorageError::MethodNotAllowed => DiskError::MethodNotAllowed,
            StorageError::StorageFull => DiskError::DiskFull,
            StorageError::SlowDown => DiskError::TooManyOpenFiles,
            StorageError::MaxVersionsExceeded => DiskError::MaxVersionsExceeded,
            StorageError::InconsistentDisk => DiskError::InconsistentDisk,
            StorageError::UnsupportedDisk => DiskError::UnsupportedDisk,
            StorageError::DiskNotDir => DiskError::DiskNotDir,
            StorageError::DiskOngoingReq => DiskError::DiskOngoingReq,
            StorageError::PathNotFound => DiskError::PathNotFound,
            StorageError::BitrotHashAlgoInvalid => DiskError::BitrotHashAlgoInvalid,
            StorageError::CrossDeviceLink => DiskError::CrossDeviceLink,
            StorageError::LessData => DiskError::LessData,
            StorageError::MoreData => DiskError::MoreData,
            StorageError::OutdatedXLMeta => DiskError::OutdatedXLMeta,
            StorageError::PartMissingOrCorrupt => DiskError::PartMissingOrCorrupt,
            StorageError::ShortWrite => DiskError::ShortWrite,
            StorageError::SourceStalled => DiskError::SourceStalled,
            StorageError::Timeout => DiskError::Timeout,
            StorageError::InvalidPath => DiskError::InvalidPath,
            StorageError::ErasureReadQuorum => DiskError::ErasureReadQuorum,
            StorageError::ErasureWriteQuorum => DiskError::ErasureWriteQuorum,
            StorageError::TooManyOpenFiles => DiskError::TooManyOpenFiles,
            StorageError::NoHealRequired => DiskError::NoHealRequired,
            StorageError::CorruptedFormat => DiskError::CorruptedFormat,
            StorageError::CorruptedBackend => DiskError::CorruptedBackend,
            StorageError::UnformattedDisk => DiskError::UnformattedDisk,
            StorageError::DiskNotFound => DiskError::DiskNotFound,
            StorageError::FaultyDisk => DiskError::FaultyDisk,
            StorageError::DiskFull => DiskError::DiskFull,
            StorageError::VolumeNotFound => DiskError::VolumeNotFound,
            StorageError::VolumeExists => DiskError::VolumeExists,
            StorageError::FileNameTooLong => DiskError::FileNameTooLong,
            StorageError::FaultyRemoteDisk => DiskError::FaultyRemoteDisk,
            StorageError::DiskAccessDenied => DiskError::DiskAccessDenied,
            StorageError::DriveIsRoot => DiskError::DriveIsRoot,
            StorageError::IsNotRegular => DiskError::IsNotRegular,
            StorageError::VolumeNotEmpty => DiskError::VolumeNotEmpty,
            StorageError::VolumeAccessDenied => DiskError::VolumeAccessDenied,
            StorageError::FileAccessDenied => DiskError::FileAccessDenied,
            StorageError::RemoteClientUnavailable(detail) => DiskError::RemoteClientUnavailable(detail),
            val => return Err(val),
        })
    }

    /// Same contract as [`StorageError::narrow_to_disk`], for the
    /// `rustfs_filemeta::Error` vocabulary (formerly a blanket `From` impl
    /// with an `other()` catch-all). No production path currently narrows in
    /// this direction; the named form keeps future callers deliberate.
    pub fn narrow_to_filemeta(self) -> core::result::Result<rustfs_filemeta::Error, StorageError> {
        Ok(match self {
            StorageError::Unexpected => rustfs_filemeta::Error::Unexpected,
            StorageError::FileNotFound => rustfs_filemeta::Error::FileNotFound,
            StorageError::FileVersionNotFound => rustfs_filemeta::Error::FileVersionNotFound,
            StorageError::FileCorrupt => rustfs_filemeta::Error::FileCorrupt,
            StorageError::DoneForNow => rustfs_filemeta::Error::DoneForNow,
            StorageError::MethodNotAllowed => rustfs_filemeta::Error::MethodNotAllowed,
            StorageError::VolumeNotFound => rustfs_filemeta::Error::VolumeNotFound,
            StorageError::Io(io_error) => io_error.into(),
            val => return Err(val),
        })
    }
}

impl From<BucketMetadataError> for Error {
    fn from(e: BucketMetadataError) -> Self {
        match e {
            BucketMetadataError::TaggingNotFound => Error::ConfigNotFound,
            BucketMetadataError::BucketPolicyNotFound => Error::ConfigNotFound,
            BucketMetadataError::BucketObjectLockConfigNotFound => Error::ConfigNotFound,
            BucketMetadataError::BucketLifecycleNotFound => Error::ConfigNotFound,
            BucketMetadataError::BucketSSEConfigNotFound => Error::ConfigNotFound,
            BucketMetadataError::BucketQuotaConfigNotFound => Error::ConfigNotFound,
            BucketMetadataError::BucketReplicationConfigNotFound => Error::ConfigNotFound,
            BucketMetadataError::BucketRemoteTargetNotFound => Error::ConfigNotFound,
            _ => Error::other(e),
        }
    }
}

impl From<std::io::Error> for StorageError {
    fn from(e: std::io::Error) -> Self {
        match e.downcast::<StorageError>() {
            Ok(storage_error) => storage_error,
            Err(io_error) => match io_error.downcast::<DiskError>() {
                Ok(disk_error) => disk_error.into(),
                Err(io_error) => StorageError::Io(io_error),
            },
        }
    }
}

impl From<StorageError> for std::io::Error {
    fn from(e: StorageError) -> Self {
        match e {
            StorageError::Io(io_error) => io_error,
            e => std::io::Error::other(e),
        }
    }
}

impl From<rustfs_filemeta::Error> for StorageError {
    fn from(e: rustfs_filemeta::Error) -> Self {
        match e {
            rustfs_filemeta::Error::DoneForNow => StorageError::DoneForNow,
            rustfs_filemeta::Error::MethodNotAllowed => StorageError::MethodNotAllowed,
            rustfs_filemeta::Error::VolumeNotFound => StorageError::VolumeNotFound,
            rustfs_filemeta::Error::FileNotFound => StorageError::FileNotFound,
            rustfs_filemeta::Error::FileVersionNotFound => StorageError::FileVersionNotFound,
            rustfs_filemeta::Error::FileCorrupt => StorageError::FileCorrupt,
            rustfs_filemeta::Error::Unexpected => StorageError::Unexpected,
            rustfs_filemeta::Error::Io(io_error) => io_error.into(),
            _ => StorageError::Io(std::io::Error::other(e)),
        }
    }
}

impl PartialEq for StorageError {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (StorageError::Io(e1), StorageError::Io(e2)) => e1.kind() == e2.kind() && e1.to_string() == e2.to_string(),
            (e1, e2) => e1.to_u32() == e2.to_u32(),
        }
    }
}

impl Clone for StorageError {
    fn clone(&self) -> Self {
        match self {
            StorageError::Io(e) => StorageError::Io(std::io::Error::new(e.kind(), e.to_string())),
            StorageError::FaultyDisk => StorageError::FaultyDisk,
            StorageError::DiskFull => StorageError::DiskFull,
            StorageError::VolumeNotFound => StorageError::VolumeNotFound,
            StorageError::VolumeExists => StorageError::VolumeExists,
            StorageError::FileNotFound => StorageError::FileNotFound,
            StorageError::FileVersionNotFound => StorageError::FileVersionNotFound,
            StorageError::FileNameTooLong => StorageError::FileNameTooLong,
            StorageError::FileAccessDenied => StorageError::FileAccessDenied,
            StorageError::FileCorrupt => StorageError::FileCorrupt,
            StorageError::IsNotRegular => StorageError::IsNotRegular,
            StorageError::VolumeNotEmpty => StorageError::VolumeNotEmpty,
            StorageError::VolumeAccessDenied => StorageError::VolumeAccessDenied,
            StorageError::CorruptedFormat => StorageError::CorruptedFormat,
            StorageError::CorruptedBackend => StorageError::CorruptedBackend,
            StorageError::UnformattedDisk => StorageError::UnformattedDisk,
            StorageError::DiskNotFound => StorageError::DiskNotFound,
            StorageError::MaxVersionsExceeded => StorageError::MaxVersionsExceeded,
            StorageError::InconsistentDisk => StorageError::InconsistentDisk,
            StorageError::UnsupportedDisk => StorageError::UnsupportedDisk,
            StorageError::DiskNotDir => StorageError::DiskNotDir,
            StorageError::DiskOngoingReq => StorageError::DiskOngoingReq,
            StorageError::PathNotFound => StorageError::PathNotFound,
            StorageError::BitrotHashAlgoInvalid => StorageError::BitrotHashAlgoInvalid,
            StorageError::CrossDeviceLink => StorageError::CrossDeviceLink,
            StorageError::LessData => StorageError::LessData,
            StorageError::MoreData => StorageError::MoreData,
            StorageError::OutdatedXLMeta => StorageError::OutdatedXLMeta,
            StorageError::PartMissingOrCorrupt => StorageError::PartMissingOrCorrupt,
            StorageError::ShortWrite => StorageError::ShortWrite,
            StorageError::SourceStalled => StorageError::SourceStalled,
            StorageError::Timeout => StorageError::Timeout,
            StorageError::InvalidPath => StorageError::InvalidPath,
            StorageError::DriveIsRoot => StorageError::DriveIsRoot,
            StorageError::FaultyRemoteDisk => StorageError::FaultyRemoteDisk,
            StorageError::DiskAccessDenied => StorageError::DiskAccessDenied,
            StorageError::Unexpected => StorageError::Unexpected,
            StorageError::ConfigNotFound => StorageError::ConfigNotFound,
            StorageError::NotImplemented => StorageError::NotImplemented,
            StorageError::InvalidArgument(a, b, c) => StorageError::InvalidArgument(a.clone(), b.clone(), c.clone()),
            StorageError::MethodNotAllowed => StorageError::MethodNotAllowed,
            StorageError::BucketNotFound(a) => StorageError::BucketNotFound(a.clone()),
            StorageError::BucketNotEmpty(a) => StorageError::BucketNotEmpty(a.clone()),
            StorageError::BucketNotEmptyWithDetails { bucket, details } => StorageError::BucketNotEmptyWithDetails {
                bucket: bucket.clone(),
                details: details.clone(),
            },
            StorageError::BucketNameInvalid(a) => StorageError::BucketNameInvalid(a.clone()),
            StorageError::ObjectNameInvalid(a, b) => StorageError::ObjectNameInvalid(a.clone(), b.clone()),
            StorageError::BucketExists(a) => StorageError::BucketExists(a.clone()),
            StorageError::StorageFull => StorageError::StorageFull,
            StorageError::SlowDown => StorageError::SlowDown,
            StorageError::PrefixAccessDenied(a, b) => StorageError::PrefixAccessDenied(a.clone(), b.clone()),
            StorageError::InvalidUploadIDKeyCombination(a, b) => {
                StorageError::InvalidUploadIDKeyCombination(a.clone(), b.clone())
            }
            StorageError::MalformedUploadID(a) => StorageError::MalformedUploadID(a.clone()),
            StorageError::ObjectNameTooLong(a, b) => StorageError::ObjectNameTooLong(a.clone(), b.clone()),
            StorageError::ObjectNamePrefixAsSlash(a, b) => StorageError::ObjectNamePrefixAsSlash(a.clone(), b.clone()),
            StorageError::ObjectNotFound(a, b) => StorageError::ObjectNotFound(a.clone(), b.clone()),
            StorageError::VersionNotFound(a, b, c) => StorageError::VersionNotFound(a.clone(), b.clone(), c.clone()),
            StorageError::InvalidUploadID(a, b, c) => StorageError::InvalidUploadID(a.clone(), b.clone(), c.clone()),
            StorageError::InvalidVersionID(a, b, c) => StorageError::InvalidVersionID(a.clone(), b.clone(), c.clone()),
            StorageError::DataMovementOverwriteErr(a, b, c) => {
                StorageError::DataMovementOverwriteErr(a.clone(), b.clone(), c.clone())
            }
            StorageError::ObjectExistsAsDirectory(a, b) => StorageError::ObjectExistsAsDirectory(a.clone(), b.clone()),
            // StorageError::InsufficientReadQuorum => StorageError::InsufficientReadQuorum,
            // StorageError::InsufficientWriteQuorum => StorageError::InsufficientWriteQuorum,
            StorageError::DecommissionNotStarted => StorageError::DecommissionNotStarted,
            StorageError::InvalidPart(a, b, c) => StorageError::InvalidPart(*a, b.clone(), c.clone()),
            StorageError::EntityTooSmall(a, b, c) => StorageError::EntityTooSmall(*a, *b, *c),
            StorageError::EntityTooLarge(a, b) => StorageError::EntityTooLarge(*a, *b),
            StorageError::DoneForNow => StorageError::DoneForNow,
            StorageError::DecommissionAlreadyRunning => StorageError::DecommissionAlreadyRunning,
            StorageError::RebalanceAlreadyRunning => StorageError::RebalanceAlreadyRunning,
            StorageError::OperationCanceled => StorageError::OperationCanceled,
            StorageError::ErasureReadQuorum => StorageError::ErasureReadQuorum,
            StorageError::ErasureWriteQuorum => StorageError::ErasureWriteQuorum,
            StorageError::NotFirstDisk => StorageError::NotFirstDisk,
            StorageError::FirstDiskWait => StorageError::FirstDiskWait,
            StorageError::TooManyOpenFiles => StorageError::TooManyOpenFiles,
            StorageError::NoHealRequired => StorageError::NoHealRequired,
            StorageError::Lock(e) => StorageError::Lock(e.clone()),
            StorageError::InsufficientReadQuorum(a, b) => StorageError::InsufficientReadQuorum(a.clone(), b.clone()),
            StorageError::InsufficientWriteQuorum(a, b) => StorageError::InsufficientWriteQuorum(a.clone(), b.clone()),
            StorageError::PreconditionFailed => StorageError::PreconditionFailed,
            StorageError::NotModified => StorageError::NotModified,
            StorageError::InvalidPartNumber(a) => StorageError::InvalidPartNumber(*a),
            StorageError::InvalidRangeSpec(a) => StorageError::InvalidRangeSpec(a.clone()),
            StorageError::NamespaceLockQuorumUnavailable {
                mode,
                bucket,
                object,
                required,
                achieved,
            } => StorageError::NamespaceLockQuorumUnavailable {
                mode,
                bucket: bucket.clone(),
                object: object.clone(),
                required: *required,
                achieved: *achieved,
            },
            StorageError::QuotaExceeded { current, limit } => StorageError::QuotaExceeded {
                current: *current,
                limit: *limit,
            },
            StorageError::RemoteClientUnavailable(detail) => StorageError::RemoteClientUnavailable(detail.clone()),
            StorageError::RemoteNotInitialized => StorageError::RemoteNotInitialized,
        }
    }
}

impl StorageError {
    fn code(&self) -> StorageErrorCode {
        match self {
            StorageError::Io(_) => StorageErrorCode::Io,
            StorageError::FaultyDisk => StorageErrorCode::FaultyDisk,
            StorageError::DiskFull => StorageErrorCode::DiskFull,
            StorageError::VolumeNotFound => StorageErrorCode::VolumeNotFound,
            StorageError::VolumeExists => StorageErrorCode::VolumeExists,
            StorageError::FileNotFound => StorageErrorCode::FileNotFound,
            StorageError::FileVersionNotFound => StorageErrorCode::FileVersionNotFound,
            StorageError::FileNameTooLong => StorageErrorCode::FileNameTooLong,
            StorageError::FileAccessDenied => StorageErrorCode::FileAccessDenied,
            StorageError::FileCorrupt => StorageErrorCode::FileCorrupt,
            StorageError::IsNotRegular => StorageErrorCode::IsNotRegular,
            StorageError::VolumeNotEmpty => StorageErrorCode::VolumeNotEmpty,
            StorageError::VolumeAccessDenied => StorageErrorCode::VolumeAccessDenied,
            StorageError::CorruptedFormat => StorageErrorCode::CorruptedFormat,
            StorageError::CorruptedBackend => StorageErrorCode::CorruptedBackend,
            StorageError::UnformattedDisk => StorageErrorCode::UnformattedDisk,
            StorageError::DiskNotFound => StorageErrorCode::DiskNotFound,
            StorageError::MaxVersionsExceeded => StorageErrorCode::MaxVersionsExceeded,
            StorageError::InconsistentDisk => StorageErrorCode::InconsistentDisk,
            StorageError::UnsupportedDisk => StorageErrorCode::UnsupportedDisk,
            StorageError::DiskNotDir => StorageErrorCode::DiskNotDir,
            StorageError::DiskOngoingReq => StorageErrorCode::DiskOngoingReq,
            StorageError::PathNotFound => StorageErrorCode::PathNotFound,
            StorageError::BitrotHashAlgoInvalid => StorageErrorCode::BitrotHashAlgoInvalid,
            StorageError::CrossDeviceLink => StorageErrorCode::CrossDeviceLink,
            StorageError::LessData => StorageErrorCode::LessData,
            StorageError::MoreData => StorageErrorCode::MoreData,
            StorageError::OutdatedXLMeta => StorageErrorCode::OutdatedXLMeta,
            StorageError::PartMissingOrCorrupt => StorageErrorCode::PartMissingOrCorrupt,
            StorageError::ShortWrite => StorageErrorCode::ShortWrite,
            StorageError::SourceStalled => StorageErrorCode::SourceStalled,
            StorageError::Timeout => StorageErrorCode::Timeout,
            StorageError::InvalidPath => StorageErrorCode::InvalidPath,
            StorageError::DriveIsRoot => StorageErrorCode::DriveIsRoot,
            StorageError::FaultyRemoteDisk => StorageErrorCode::FaultyRemoteDisk,
            StorageError::DiskAccessDenied => StorageErrorCode::DiskAccessDenied,
            StorageError::Unexpected => StorageErrorCode::Unexpected,
            StorageError::NotImplemented => StorageErrorCode::NotImplemented,
            StorageError::InvalidArgument(_, _, _) => StorageErrorCode::InvalidArgument,
            StorageError::MethodNotAllowed => StorageErrorCode::MethodNotAllowed,
            StorageError::BucketNotFound(_) => StorageErrorCode::BucketNotFound,
            StorageError::BucketNotEmpty(_) | StorageError::BucketNotEmptyWithDetails { .. } => StorageErrorCode::BucketNotEmpty,
            StorageError::BucketNameInvalid(_) => StorageErrorCode::BucketNameInvalid,
            StorageError::ObjectNameInvalid(_, _) => StorageErrorCode::ObjectNameInvalid,
            StorageError::BucketExists(_) => StorageErrorCode::BucketExists,
            StorageError::StorageFull => StorageErrorCode::StorageFull,
            StorageError::SlowDown => StorageErrorCode::SlowDown,
            StorageError::PrefixAccessDenied(_, _) => StorageErrorCode::PrefixAccessDenied,
            StorageError::InvalidUploadIDKeyCombination(_, _) => StorageErrorCode::InvalidUploadIDKeyCombination,
            StorageError::MalformedUploadID(_) => StorageErrorCode::MalformedUploadID,
            StorageError::ObjectNameTooLong(_, _) => StorageErrorCode::ObjectNameTooLong,
            StorageError::ObjectNamePrefixAsSlash(_, _) => StorageErrorCode::ObjectNamePrefixAsSlash,
            StorageError::ObjectNotFound(_, _) => StorageErrorCode::ObjectNotFound,
            StorageError::VersionNotFound(_, _, _) => StorageErrorCode::VersionNotFound,
            StorageError::InvalidUploadID(_, _, _) => StorageErrorCode::InvalidUploadID,
            StorageError::InvalidVersionID(_, _, _) => StorageErrorCode::InvalidVersionID,
            StorageError::DataMovementOverwriteErr(_, _, _) => StorageErrorCode::DataMovementOverwriteErr,
            StorageError::ObjectExistsAsDirectory(_, _) => StorageErrorCode::ObjectExistsAsDirectory,
            StorageError::DecommissionNotStarted => StorageErrorCode::DecommissionNotStarted,
            StorageError::InvalidPart(_, _, _) => StorageErrorCode::InvalidPart,
            StorageError::DoneForNow => StorageErrorCode::DoneForNow,
            StorageError::DecommissionAlreadyRunning => StorageErrorCode::DecommissionAlreadyRunning,
            StorageError::RebalanceAlreadyRunning => StorageErrorCode::RebalanceAlreadyRunning,
            StorageError::OperationCanceled => StorageErrorCode::OperationCanceled,
            StorageError::ErasureReadQuorum => StorageErrorCode::ErasureReadQuorum,
            StorageError::ErasureWriteQuorum => StorageErrorCode::ErasureWriteQuorum,
            StorageError::NotFirstDisk => StorageErrorCode::NotFirstDisk,
            StorageError::FirstDiskWait => StorageErrorCode::FirstDiskWait,
            StorageError::ConfigNotFound => StorageErrorCode::ConfigNotFound,
            StorageError::TooManyOpenFiles => StorageErrorCode::TooManyOpenFiles,
            StorageError::NoHealRequired => StorageErrorCode::NoHealRequired,
            StorageError::Lock(_) => StorageErrorCode::Lock,
            StorageError::InsufficientReadQuorum(_, _) => StorageErrorCode::InsufficientReadQuorum,
            StorageError::InsufficientWriteQuorum(_, _) => StorageErrorCode::InsufficientWriteQuorum,
            StorageError::PreconditionFailed => StorageErrorCode::PreconditionFailed,
            StorageError::EntityTooSmall(_, _, _) => StorageErrorCode::EntityTooSmall,
            StorageError::EntityTooLarge(_, _) => StorageErrorCode::EntityTooLarge,
            StorageError::InvalidRangeSpec(_) => StorageErrorCode::InvalidRangeSpec,
            StorageError::NotModified => StorageErrorCode::NotModified,
            StorageError::InvalidPartNumber(_) => StorageErrorCode::InvalidPartNumber,
            StorageError::NamespaceLockQuorumUnavailable { .. } => StorageErrorCode::NamespaceLockQuorumUnavailable,
            StorageError::QuotaExceeded { .. } => StorageErrorCode::QuotaExceeded,
            StorageError::RemoteClientUnavailable(_) => StorageErrorCode::RemoteClientUnavailable,
            StorageError::RemoteNotInitialized => StorageErrorCode::RemoteNotInitialized,
        }
    }

    pub fn to_u32(&self) -> u32 {
        self.code().as_u32()
    }

    pub fn from_u32(error: u32) -> Option<Self> {
        match StorageErrorCode::from_u32(error)? {
            StorageErrorCode::Io => Some(StorageError::Io(std::io::Error::other("Io error"))),
            StorageErrorCode::FaultyDisk => Some(StorageError::FaultyDisk),
            StorageErrorCode::DiskFull => Some(StorageError::DiskFull),
            StorageErrorCode::VolumeNotFound => Some(StorageError::VolumeNotFound),
            StorageErrorCode::VolumeExists => Some(StorageError::VolumeExists),
            StorageErrorCode::FileNotFound => Some(StorageError::FileNotFound),
            StorageErrorCode::FileVersionNotFound => Some(StorageError::FileVersionNotFound),
            StorageErrorCode::FileNameTooLong => Some(StorageError::FileNameTooLong),
            StorageErrorCode::FileAccessDenied => Some(StorageError::FileAccessDenied),
            StorageErrorCode::FileCorrupt => Some(StorageError::FileCorrupt),
            StorageErrorCode::IsNotRegular => Some(StorageError::IsNotRegular),
            StorageErrorCode::VolumeNotEmpty => Some(StorageError::VolumeNotEmpty),
            StorageErrorCode::VolumeAccessDenied => Some(StorageError::VolumeAccessDenied),
            StorageErrorCode::CorruptedFormat => Some(StorageError::CorruptedFormat),
            StorageErrorCode::CorruptedBackend => Some(StorageError::CorruptedBackend),
            StorageErrorCode::UnformattedDisk => Some(StorageError::UnformattedDisk),
            StorageErrorCode::DiskNotFound => Some(StorageError::DiskNotFound),
            StorageErrorCode::MaxVersionsExceeded => Some(StorageError::MaxVersionsExceeded),
            StorageErrorCode::InconsistentDisk => Some(StorageError::InconsistentDisk),
            StorageErrorCode::UnsupportedDisk => Some(StorageError::UnsupportedDisk),
            StorageErrorCode::DiskNotDir => Some(StorageError::DiskNotDir),
            StorageErrorCode::DiskOngoingReq => Some(StorageError::DiskOngoingReq),
            StorageErrorCode::PathNotFound => Some(StorageError::PathNotFound),
            StorageErrorCode::BitrotHashAlgoInvalid => Some(StorageError::BitrotHashAlgoInvalid),
            StorageErrorCode::CrossDeviceLink => Some(StorageError::CrossDeviceLink),
            StorageErrorCode::LessData => Some(StorageError::LessData),
            StorageErrorCode::MoreData => Some(StorageError::MoreData),
            StorageErrorCode::OutdatedXLMeta => Some(StorageError::OutdatedXLMeta),
            StorageErrorCode::PartMissingOrCorrupt => Some(StorageError::PartMissingOrCorrupt),
            StorageErrorCode::ShortWrite => Some(StorageError::ShortWrite),
            StorageErrorCode::SourceStalled => Some(StorageError::SourceStalled),
            StorageErrorCode::Timeout => Some(StorageError::Timeout),
            StorageErrorCode::InvalidPath => Some(StorageError::InvalidPath),
            StorageErrorCode::DriveIsRoot => Some(StorageError::DriveIsRoot),
            StorageErrorCode::FaultyRemoteDisk => Some(StorageError::FaultyRemoteDisk),
            StorageErrorCode::DiskAccessDenied => Some(StorageError::DiskAccessDenied),
            StorageErrorCode::Unexpected => Some(StorageError::Unexpected),
            StorageErrorCode::NotImplemented => Some(StorageError::NotImplemented),
            StorageErrorCode::InvalidArgument => {
                Some(StorageError::InvalidArgument(Default::default(), Default::default(), Default::default()))
            }
            StorageErrorCode::MethodNotAllowed => Some(StorageError::MethodNotAllowed),
            StorageErrorCode::BucketNotFound => Some(StorageError::BucketNotFound(Default::default())),
            StorageErrorCode::BucketNotEmpty => Some(StorageError::BucketNotEmpty(Default::default())),
            StorageErrorCode::BucketNameInvalid => Some(StorageError::BucketNameInvalid(Default::default())),
            StorageErrorCode::ObjectNameInvalid => Some(StorageError::ObjectNameInvalid(Default::default(), Default::default())),
            StorageErrorCode::BucketExists => Some(StorageError::BucketExists(Default::default())),
            StorageErrorCode::StorageFull => Some(StorageError::StorageFull),
            StorageErrorCode::SlowDown => Some(StorageError::SlowDown),
            StorageErrorCode::PrefixAccessDenied => {
                Some(StorageError::PrefixAccessDenied(Default::default(), Default::default()))
            }
            StorageErrorCode::InvalidUploadIDKeyCombination => {
                Some(StorageError::InvalidUploadIDKeyCombination(Default::default(), Default::default()))
            }
            StorageErrorCode::MalformedUploadID => Some(StorageError::MalformedUploadID(Default::default())),
            StorageErrorCode::ObjectNameTooLong => Some(StorageError::ObjectNameTooLong(Default::default(), Default::default())),
            StorageErrorCode::ObjectNamePrefixAsSlash => {
                Some(StorageError::ObjectNamePrefixAsSlash(Default::default(), Default::default()))
            }
            StorageErrorCode::ObjectNotFound => Some(StorageError::ObjectNotFound(Default::default(), Default::default())),
            StorageErrorCode::VersionNotFound => {
                Some(StorageError::VersionNotFound(Default::default(), Default::default(), Default::default()))
            }
            StorageErrorCode::InvalidUploadID => {
                Some(StorageError::InvalidUploadID(Default::default(), Default::default(), Default::default()))
            }
            StorageErrorCode::InvalidVersionID => {
                Some(StorageError::InvalidVersionID(Default::default(), Default::default(), Default::default()))
            }
            StorageErrorCode::DataMovementOverwriteErr => Some(StorageError::DataMovementOverwriteErr(
                Default::default(),
                Default::default(),
                Default::default(),
            )),
            StorageErrorCode::ObjectExistsAsDirectory => {
                Some(StorageError::ObjectExistsAsDirectory(Default::default(), Default::default()))
            }
            StorageErrorCode::DecommissionNotStarted => Some(StorageError::DecommissionNotStarted),
            StorageErrorCode::InvalidPart => {
                Some(StorageError::InvalidPart(Default::default(), Default::default(), Default::default()))
            }
            StorageErrorCode::DoneForNow => Some(StorageError::DoneForNow),
            StorageErrorCode::DecommissionAlreadyRunning => Some(StorageError::DecommissionAlreadyRunning),
            StorageErrorCode::RebalanceAlreadyRunning => Some(StorageError::RebalanceAlreadyRunning),
            StorageErrorCode::OperationCanceled => Some(StorageError::OperationCanceled),
            StorageErrorCode::ErasureReadQuorum => Some(StorageError::ErasureReadQuorum),
            StorageErrorCode::ErasureWriteQuorum => Some(StorageError::ErasureWriteQuorum),
            StorageErrorCode::NotFirstDisk => Some(StorageError::NotFirstDisk),
            StorageErrorCode::FirstDiskWait => Some(StorageError::FirstDiskWait),
            StorageErrorCode::ConfigNotFound => Some(StorageError::ConfigNotFound),
            StorageErrorCode::TooManyOpenFiles => Some(StorageError::TooManyOpenFiles),
            StorageErrorCode::NoHealRequired => Some(StorageError::NoHealRequired),
            StorageErrorCode::Lock => {
                Some(StorageError::Lock(rustfs_lock::LockError::internal("Generic lock error".to_string())))
            }
            StorageErrorCode::InsufficientReadQuorum => {
                Some(StorageError::InsufficientReadQuorum(Default::default(), Default::default()))
            }
            StorageErrorCode::InsufficientWriteQuorum => {
                Some(StorageError::InsufficientWriteQuorum(Default::default(), Default::default()))
            }
            StorageErrorCode::PreconditionFailed => Some(StorageError::PreconditionFailed),
            StorageErrorCode::EntityTooSmall => {
                Some(StorageError::EntityTooSmall(Default::default(), Default::default(), Default::default()))
            }
            StorageErrorCode::EntityTooLarge => Some(StorageError::EntityTooLarge(Default::default(), Default::default())),
            StorageErrorCode::InvalidRangeSpec => Some(StorageError::InvalidRangeSpec(Default::default())),
            StorageErrorCode::NotModified => Some(StorageError::NotModified),
            StorageErrorCode::InvalidPartNumber => Some(StorageError::InvalidPartNumber(Default::default())),
            StorageErrorCode::NamespaceLockQuorumUnavailable => Some(StorageError::NamespaceLockQuorumUnavailable {
                mode: "write",
                bucket: Default::default(),
                object: Default::default(),
                required: Default::default(),
                achieved: Default::default(),
            }),
            StorageErrorCode::QuotaExceeded => Some(StorageError::QuotaExceeded {
                current: Default::default(),
                limit: Default::default(),
            }),
            StorageErrorCode::RemoteClientUnavailable => Some(StorageError::RemoteClientUnavailable(Default::default())),
            StorageErrorCode::RemoteNotInitialized => Some(StorageError::RemoteNotInitialized),
        }
    }
}

impl From<tokio::task::JoinError> for StorageError {
    fn from(e: tokio::task::JoinError) -> Self {
        StorageError::other(e)
    }
}

impl From<serde_json::Error> for StorageError {
    fn from(e: serde_json::Error) -> Self {
        StorageError::other(e)
    }
}

impl From<rmp_serde::encode::Error> for Error {
    fn from(e: rmp_serde::encode::Error) -> Self {
        Error::other(e)
    }
}

impl From<rmp::encode::ValueWriteError> for Error {
    fn from(e: rmp::encode::ValueWriteError) -> Self {
        Error::other(e)
    }
}

impl From<rmp::decode::ValueReadError> for Error {
    fn from(e: rmp::decode::ValueReadError) -> Self {
        Error::other(e)
    }
}

impl From<std::string::FromUtf8Error> for Error {
    fn from(e: std::string::FromUtf8Error) -> Self {
        Error::other(e)
    }
}

impl From<rmp::decode::NumValueReadError> for Error {
    fn from(e: rmp::decode::NumValueReadError) -> Self {
        Error::other(e)
    }
}

impl From<rmp_serde::decode::Error> for Error {
    fn from(e: rmp_serde::decode::Error) -> Self {
        Error::other(e)
    }
}

impl From<s3s::xml::SerError> for Error {
    fn from(e: s3s::xml::SerError) -> Self {
        Error::other(e)
    }
}

impl From<s3s::xml::DeError> for Error {
    fn from(e: s3s::xml::DeError) -> Self {
        Error::other(e)
    }
}

impl From<tonic::Status> for Error {
    fn from(e: tonic::Status) -> Self {
        // Keep the typed status as the io::Error payload instead of a
        // flattened string so RPC failure classifiers can read the gRPC code
        // via downcast (see `PeerRestClient::is_network_like_error`).
        // `RpcStatusError` renders exactly as `e.to_string()` did.
        Error::other(crate::disk::error::RpcStatusError::from(e))
    }
}

impl From<uuid::Error> for Error {
    fn from(e: uuid::Error) -> Self {
        Error::other(e)
    }
}

impl From<time::error::ComponentRange> for Error {
    fn from(e: time::error::ComponentRange) -> Self {
        Error::other(e)
    }
}

pub fn is_err_object_not_found(err: &Error) -> bool {
    matches!(err, &Error::FileNotFound) || matches!(err, &Error::ObjectNotFound(_, _))
}

pub fn is_err_version_not_found(err: &Error) -> bool {
    matches!(err, &Error::FileVersionNotFound) || matches!(err, &Error::VersionNotFound(_, _, _))
}

pub fn is_err_bucket_exists(err: &Error) -> bool {
    matches!(err, &StorageError::BucketExists(_))
}

pub fn is_err_read_quorum(err: &Error) -> bool {
    matches!(err, &StorageError::ErasureReadQuorum)
}

pub fn classify_system_path_failure_reason(err: &Error) -> &'static str {
    match err {
        StorageError::ConfigNotFound => "config_not_found",
        StorageError::ErasureReadQuorum | StorageError::InsufficientReadQuorum(_, _) => "read_quorum",
        StorageError::Io(io_err) => match io_err.kind() {
            std::io::ErrorKind::TimedOut => "timeout",
            _ => "io",
        },
        _ => "other",
    }
}

pub fn is_err_invalid_upload_id(err: &Error) -> bool {
    matches!(err, &StorageError::InvalidUploadID(_, _, _))
}

pub fn is_err_bucket_not_found(err: &Error) -> bool {
    matches!(err, &StorageError::VolumeNotFound)
        | matches!(err, &StorageError::DiskNotFound)
        | matches!(err, &StorageError::BucketNotFound(_))
}

pub fn is_err_data_movement_overwrite(err: &Error) -> bool {
    matches!(err, &StorageError::DataMovementOverwriteErr(_, _, _))
}

pub fn is_err_decommission_running(err: &Error) -> bool {
    matches!(err, &StorageError::DecommissionAlreadyRunning)
}

#[allow(dead_code, reason = "predicate asserted by this file's tests (backlog#1823)")]
pub fn is_err_rebalance_running(err: &Error) -> bool {
    matches!(err, &StorageError::RebalanceAlreadyRunning)
}

pub fn is_err_operation_canceled(err: &Error) -> bool {
    matches!(err, &StorageError::OperationCanceled)
}

#[allow(dead_code, reason = "predicate asserted by this file's tests (backlog#1823)")]
pub fn is_err_not_initialized(err: &Error) -> bool {
    // Typed-first: peers at or above the ControlPlaneErrorCode change decode to
    // the typed variant. The substring form only matches legacy peers' string
    // responses.
    // RUSTFS_COMPAT_TODO(not-initialized-error-code-v1): substring fallback for peers that predate the typed wire code. Remove after the minimum supported RustFS peer version always sends error_code.
    matches!(err, StorageError::RemoteNotInitialized)
        || err.to_string().contains("errServerNotInitialized")
        || err.to_string().contains("ServerNotInitialized")
}

/// Strict "not found" predicate that only matches genuine object/version/volume
/// absence errors: `FileNotFound`/`VolumeNotFound`/`FileVersionNotFound`/
/// `ObjectNotFound`/`VersionNotFound`.
///
/// Unlike [`is_err_bucket_not_found`], this deliberately excludes
/// `DiskNotFound` so that an all-drives-unreachable error slice is treated as an
/// availability/read-quorum failure rather than "the object does not exist".
/// This mirrors MinIO's `isAllNotFound` and the sibling
/// [`crate::disk::error::DiskError::is_all_not_found`].
pub fn is_err_strict_not_found(err: &Error) -> bool {
    is_err_object_not_found(err) || is_err_version_not_found(err) || matches!(err, &StorageError::VolumeNotFound)
}

pub fn is_all_not_found(errs: &[Option<Error>]) -> bool {
    for err in errs.iter() {
        if let Some(err) = err {
            if is_err_strict_not_found(err) {
                continue;
            }

            return false;
        }
        return false;
    }

    !errs.is_empty()
}

/// Strict "volume not found" predicate matching only `VolumeNotFound`/
/// `BucketNotFound`.
///
/// This is [`is_err_bucket_not_found`] minus `DiskNotFound`: an unreachable
/// drive must not be mistaken for a missing volume/bucket. Unlike
/// [`is_err_strict_not_found`] it deliberately excludes file/version level
/// not-found so that "the object is missing under an existing volume" is not
/// escalated to "the volume itself is missing".
pub fn is_err_strict_volume_not_found(err: &Error) -> bool {
    matches!(err, &StorageError::VolumeNotFound) || matches!(err, &StorageError::BucketNotFound(_))
}

pub fn is_all_volume_not_found(errs: &[Option<Error>]) -> bool {
    for err in errs.iter() {
        if let Some(err) = err {
            if is_err_strict_volume_not_found(err) {
                continue;
            }

            return false;
        }

        return false;
    }

    !errs.is_empty()
}

/// Returns true when every entry is `DiskNotFound` (all drives offline /
/// drive-id mismatch across all sets) and the slice is non-empty.
///
/// Used to distinguish a full-availability failure from a genuine empty listing
/// so it can be surfaced as an error instead of being swallowed as "no entries".
pub fn is_all_disk_not_found(errs: &[Option<Error>]) -> bool {
    for err in errs.iter() {
        match err {
            Some(err) if matches!(err, &StorageError::DiskNotFound) => continue,
            _ => return false,
        }
    }

    !errs.is_empty()
}

pub fn to_object_err(err: Error, params: Vec<&str>) -> Error {
    match err {
        StorageError::DiskFull => StorageError::StorageFull,

        StorageError::FileNotFound => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
            StorageError::ObjectNotFound(bucket, object)
        }
        StorageError::FileVersionNotFound => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
            let version = params.get(2).cloned().unwrap_or_default().to_owned();

            StorageError::VersionNotFound(bucket, object, version)
        }
        StorageError::TooManyOpenFiles => StorageError::SlowDown,
        StorageError::FileNameTooLong => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();

            StorageError::ObjectNameInvalid(bucket, object)
        }
        StorageError::VolumeExists => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            StorageError::BucketExists(bucket)
        }
        StorageError::IsNotRegular => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();

            StorageError::ObjectExistsAsDirectory(bucket, object)
        }

        StorageError::VolumeNotFound => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            StorageError::BucketNotFound(bucket)
        }
        StorageError::VolumeNotEmpty => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            StorageError::BucketNotEmpty(bucket)
        }

        StorageError::FileAccessDenied => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();

            StorageError::PrefixAccessDenied(bucket, object)
        }

        StorageError::ErasureReadQuorum => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
            StorageError::InsufficientReadQuorum(bucket, object)
        }
        StorageError::ErasureWriteQuorum => {
            let bucket = params.first().cloned().unwrap_or_default().to_owned();
            let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
            StorageError::InsufficientWriteQuorum(bucket, object)
        }

        _ => err,
    }
}

pub fn is_network_or_host_down(err: &str, _expect_timeouts: bool) -> bool {
    err.contains("Connection closed by foreign host")
        || err.contains("TLS handshake timeout")
        || err.contains("i/o timeout")
        || err.contains("connection timed out")
        || err.contains("connection reset by peer")
        || err.contains("broken pipe")
        || err.to_lowercase().contains("503 service unavailable")
        || err.contains("use of closed network connection")
        || err.contains("An existing connection was forcibly closed by the remote host")
        || err.contains("client error (Connect)")
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct GenericError {
    pub bucket: String,
    pub object: String,
    pub version_id: String,
    //pub err:       Error,
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ObjectApiError {
    #[error("The operation is not valid for the current state of the object {}/{}({})", .0.bucket, .0.object, .0.version_id)]
    InvalidObjectState(GenericError),
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[error("{}", .message)]
pub struct ErrorResponse {
    pub code: S3ErrorCode,
    pub message: String,
    pub key: Option<String>,
    pub bucket_name: Option<String>,
    pub region: Option<String>,
    pub request_id: Option<String>,
    pub host_id: String,
}

#[cfg(test)]
mod conversion_roundtrip_tests;

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{Error as IoError, ErrorKind};

    #[test]
    fn other_preserves_erasure_construction_source_chain() {
        use crate::erasure::coding::ErasureConstructionError;
        use std::error::Error as _;

        let error = StorageError::from(ErasureConstructionError::ModernEncoder {
            source: reed_solomon_erasure::Error::TooManyShards,
        });
        let io_source = error.source().expect("StorageError::Io must expose its io::Error source");
        assert!(io_source.is::<std::io::Error>());
        let construction_source = io_source
            .source()
            .expect("io::Error must expose the erasure construction error");
        assert!(construction_source.is::<ErasureConstructionError>());
        let encoder_source = construction_source
            .source()
            .expect("construction error must expose the encoder error");
        assert!(encoder_source.is::<reed_solomon_erasure::Error>());
    }

    // The lifecycle transition worker relies on this arm alone to suppress the
    // closed-connection noise (`bucket_lifecycle_ops.rs`); dropping it here would
    // silently turn shutdown races back into `error!` log spam.
    #[test]
    fn is_network_or_host_down_covers_closed_network_connection() {
        assert!(is_network_or_host_down("transition failed: use of closed network connection", false));
    }

    // Regression for #952 (ECA-11): an all-`DiskNotFound` slice (every drive in
    // every set unreachable) must NOT be classified as "all not found",
    // otherwise ListObjects silently returns an empty listing and masks a full
    // availability failure as "the bucket is empty".
    #[test]
    fn is_all_not_found_rejects_all_disk_not_found() {
        let errs = vec![Some(StorageError::DiskNotFound), Some(StorageError::DiskNotFound)];
        assert!(!is_all_not_found(&errs), "all-DiskNotFound must not be treated as all-not-found");
        assert!(
            !is_all_volume_not_found(&errs),
            "all-DiskNotFound must not be treated as all-volume-not-found"
        );
        assert!(
            is_all_disk_not_found(&errs),
            "all-DiskNotFound must be recognised as a full availability failure"
        );
    }

    // Genuine not-found errors must still be classified as such so real
    // "object/version absent" semantics do not regress into availability errors.
    #[test]
    fn is_all_not_found_accepts_genuine_not_found() {
        let object_errs = vec![
            Some(StorageError::FileNotFound),
            Some(StorageError::ObjectNotFound("bucket".into(), "object".into())),
            Some(StorageError::FileVersionNotFound),
            Some(StorageError::VersionNotFound("bucket".into(), "object".into(), "vid".into())),
            Some(StorageError::VolumeNotFound),
        ];
        assert!(
            is_all_not_found(&object_errs),
            "genuine object/version/volume not-found must stay all-not-found"
        );
        assert!(!is_all_disk_not_found(&object_errs));

        let volume_errs = vec![Some(StorageError::VolumeNotFound), Some(StorageError::VolumeNotFound)];
        assert!(is_all_not_found(&volume_errs));
        assert!(
            is_all_volume_not_found(&volume_errs),
            "all-VolumeNotFound must remain all-volume-not-found"
        );
    }

    // `is_all_volume_not_found` must escalate only volume/bucket absence, not a
    // missing object under an existing volume, and must exclude DiskNotFound.
    #[test]
    fn is_all_volume_not_found_semantics() {
        assert!(is_all_volume_not_found(&[Some(StorageError::BucketNotFound("bucket".into()))]));
        assert!(
            !is_all_volume_not_found(&[Some(StorageError::FileNotFound), Some(StorageError::VolumeNotFound)]),
            "a missing object under an existing volume must not escalate to all-volume-not-found"
        );
        assert!(!is_all_volume_not_found(&[Some(StorageError::DiskNotFound)]));
    }

    // A `None` entry (a healthy set) short-circuits both predicates to false so a
    // partial outage is never mistaken for an all-not-found / all-offline slice.
    #[test]
    fn not_found_predicates_reject_partial_and_empty() {
        let partial = vec![None, Some(StorageError::DiskNotFound)];
        assert!(!is_all_not_found(&partial));
        assert!(!is_all_volume_not_found(&partial));
        assert!(!is_all_disk_not_found(&partial));

        assert!(!is_all_not_found(&[]));
        assert!(!is_all_volume_not_found(&[]));
        assert!(!is_all_disk_not_found(&[]));
    }

    #[test]
    fn test_storage_error_to_u32() {
        // Test Io error uses 0x01
        let io_error = StorageError::Io(IoError::other("test"));
        assert_eq!(io_error.to_u32(), 0x01);

        // Test other errors have correct codes
        assert_eq!(StorageError::FaultyDisk.to_u32(), 0x02);
        assert_eq!(StorageError::DiskFull.to_u32(), 0x03);
        assert_eq!(StorageError::VolumeNotFound.to_u32(), 0x04);
        assert_eq!(StorageError::VolumeExists.to_u32(), 0x05);
        assert_eq!(StorageError::FileNotFound.to_u32(), 0x06);
        assert_eq!(StorageError::DecommissionAlreadyRunning.to_u32(), 0x30);
        assert_eq!(StorageError::RebalanceAlreadyRunning.to_u32(), 0x40);
        assert_eq!(StorageError::OperationCanceled.to_u32(), 0x41);
        assert_eq!(
            StorageError::NamespaceLockQuorumUnavailable {
                mode: "write",
                bucket: "bucket".into(),
                object: "object".into(),
                required: 3,
                achieved: 2,
            }
            .to_u32(),
            0x42
        );
        assert_eq!(StorageError::QuotaExceeded { current: 1, limit: 2 }.to_u32(), 0x53);
    }

    #[test]
    fn test_storage_error_from_u32() {
        // Test Io error conversion
        assert!(matches!(StorageError::from_u32(0x01), Some(StorageError::Io(_))));

        // Test other error conversions
        assert!(matches!(StorageError::from_u32(0x02), Some(StorageError::FaultyDisk)));
        assert!(matches!(StorageError::from_u32(0x03), Some(StorageError::DiskFull)));
        assert!(matches!(StorageError::from_u32(0x04), Some(StorageError::VolumeNotFound)));
        assert!(matches!(StorageError::from_u32(0x30), Some(StorageError::DecommissionAlreadyRunning)));
        assert!(matches!(StorageError::from_u32(0x40), Some(StorageError::RebalanceAlreadyRunning)));
        assert!(matches!(StorageError::from_u32(0x41), Some(StorageError::OperationCanceled)));
        assert!(matches!(
            StorageError::from_u32(0x42),
            Some(StorageError::NamespaceLockQuorumUnavailable { .. })
        ));
        assert!(matches!(
            StorageError::from_u32(0x53),
            Some(StorageError::QuotaExceeded { current: 0, limit: 0 })
        ));

        // Test invalid code returns None
        assert!(StorageError::from_u32(0xFF).is_none());
    }

    #[test]
    fn test_storage_error_code_contract_matches_storage_api() {
        assert_eq!(StorageError::DiskFull.to_u32(), StorageErrorCode::DiskFull.as_u32());
        assert_eq!(
            StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()).to_u32(),
            StorageErrorCode::ObjectNotFound.as_u32()
        );
        assert!(matches!(
            StorageError::from_u32(StorageErrorCode::ErasureReadQuorum.as_u32()),
            Some(StorageError::ErasureReadQuorum)
        ));
        assert!(matches!(
            StorageError::from_u32(StorageErrorCode::NamespaceLockQuorumUnavailable.as_u32()),
            Some(StorageError::NamespaceLockQuorumUnavailable { .. })
        ));
        assert!(StorageError::from_u32(0x2B).is_none());
        assert!(StorageError::from_u32(0x2C).is_none());
    }

    #[test]
    fn test_storage_error_partial_eq() {
        // Test IO error comparison
        let io1 = StorageError::Io(IoError::new(ErrorKind::NotFound, "file not found"));
        let io2 = StorageError::Io(IoError::new(ErrorKind::NotFound, "file not found"));
        let io3 = StorageError::Io(IoError::new(ErrorKind::PermissionDenied, "access denied"));

        assert_eq!(io1, io2);
        assert_ne!(io1, io3);

        // Test non-IO error comparison
        let bucket1 = StorageError::BucketExists("test".to_string());
        let bucket2 = StorageError::BucketExists("different".to_string());
        assert_eq!(bucket1, bucket2); // Same error type, different parameters

        let disk_error = StorageError::DiskFull;
        assert_ne!(bucket1, disk_error);
    }

    #[test]
    fn test_error_running_state_helpers() {
        assert!(is_err_decommission_running(&StorageError::DecommissionAlreadyRunning));
        assert!(!is_err_decommission_running(&StorageError::RebalanceAlreadyRunning));

        assert!(is_err_rebalance_running(&StorageError::RebalanceAlreadyRunning));
        assert!(!is_err_rebalance_running(&StorageError::DecommissionAlreadyRunning));
        assert!(is_err_operation_canceled(&StorageError::OperationCanceled));
        assert!(!is_err_operation_canceled(&StorageError::RebalanceAlreadyRunning));
        assert!(is_err_not_initialized(&StorageError::other("errServerNotInitialized")));
        assert!(is_err_not_initialized(&StorageError::other("ServerNotInitialized")));
        assert!(!is_err_not_initialized(&StorageError::DecommissionAlreadyRunning));
    }

    #[test]
    fn test_classify_system_path_failure_reason() {
        assert_eq!(classify_system_path_failure_reason(&StorageError::ConfigNotFound), "config_not_found");
        assert_eq!(classify_system_path_failure_reason(&StorageError::ErasureReadQuorum), "read_quorum");
        assert_eq!(
            classify_system_path_failure_reason(&StorageError::InsufficientReadQuorum(
                "bucket".to_string(),
                "object".to_string()
            )),
            "read_quorum"
        );
        assert_eq!(
            classify_system_path_failure_reason(&StorageError::Io(IoError::new(ErrorKind::TimedOut, "probe"))),
            "timeout"
        );
        assert_eq!(
            classify_system_path_failure_reason(&StorageError::Io(IoError::new(ErrorKind::PermissionDenied, "probe"))),
            "io"
        );
        assert_eq!(classify_system_path_failure_reason(&StorageError::DiskFull), "other");
    }

    #[test]
    fn test_storage_error_from_disk_error() {
        // Test conversion from DiskError
        let disk_io = DiskError::Io(IoError::other("disk io error"));
        let storage_error: StorageError = disk_io.into();
        assert!(matches!(storage_error, StorageError::Io(_)));

        let disk_full = DiskError::DiskFull;
        let storage_error: StorageError = disk_full.into();
        assert_eq!(storage_error, StorageError::DiskFull);

        let file_not_found = DiskError::FileNotFound;
        let storage_error: StorageError = file_not_found.into();
        assert_eq!(storage_error, StorageError::FileNotFound);
    }

    #[test]
    fn test_disk_error_specific_variants_do_not_degrade_to_io() {
        let cases = vec![
            (DiskError::MaxVersionsExceeded, StorageError::MaxVersionsExceeded),
            (DiskError::InconsistentDisk, StorageError::InconsistentDisk),
            (DiskError::UnsupportedDisk, StorageError::UnsupportedDisk),
            (DiskError::DiskNotDir, StorageError::DiskNotDir),
            (DiskError::DiskOngoingReq, StorageError::DiskOngoingReq),
            (DiskError::PathNotFound, StorageError::PathNotFound),
            (DiskError::BitrotHashAlgoInvalid, StorageError::BitrotHashAlgoInvalid),
            (DiskError::CrossDeviceLink, StorageError::CrossDeviceLink),
            (DiskError::LessData, StorageError::LessData),
            (DiskError::MoreData, StorageError::MoreData),
            (DiskError::OutdatedXLMeta, StorageError::OutdatedXLMeta),
            (DiskError::PartMissingOrCorrupt, StorageError::PartMissingOrCorrupt),
            (DiskError::ShortWrite, StorageError::ShortWrite),
            (DiskError::SourceStalled, StorageError::SourceStalled),
            (DiskError::Timeout, StorageError::Timeout),
            (DiskError::InvalidPath, StorageError::InvalidPath),
        ];

        for (disk_error, expected_storage_error) in cases {
            let storage_error: StorageError = disk_error.into();

            assert!(
                !matches!(&storage_error, StorageError::Io(_)),
                "expected {expected_storage_error:?}, got generic Io"
            );
            assert_eq!(storage_error, expected_storage_error);
        }
    }

    #[test]
    fn test_storage_error_disk_preservation_codes_roundtrip() {
        let errors = vec![
            StorageError::MaxVersionsExceeded,
            StorageError::InconsistentDisk,
            StorageError::UnsupportedDisk,
            StorageError::DiskNotDir,
            StorageError::DiskOngoingReq,
            StorageError::PathNotFound,
            StorageError::BitrotHashAlgoInvalid,
            StorageError::CrossDeviceLink,
            StorageError::LessData,
            StorageError::MoreData,
            StorageError::OutdatedXLMeta,
            StorageError::PartMissingOrCorrupt,
            StorageError::ShortWrite,
            StorageError::SourceStalled,
            StorageError::Timeout,
            StorageError::InvalidPath,
        ];

        for original_error in errors {
            let code = original_error.to_u32();
            let recovered_error =
                StorageError::from_u32(code).unwrap_or_else(|| panic!("failed to recover error from code: {code:#x}"));

            assert_eq!(std::mem::discriminant(&original_error), std::mem::discriminant(&recovered_error));
        }
    }

    // Every DiskError variant must survive DiskError -> StorageError -> DiskError
    // unchanged. A variant that degrades to `DiskError::Io` on the way back loses
    // its identity for quorum aggregation (`reduce_errs` classifies by variant
    // equality), so ignore-list entries such as FaultyRemoteDisk and
    // DiskAccessDenied would silently stop matching.
    #[test]
    fn test_disk_error_storage_error_round_trip_identity_all_variants() {
        // DiskError codes are contiguous from 0x01, so enumerating via from_u32
        // covers every variant and picks up newly appended ones automatically.
        let all_variants: Vec<DiskError> = (1u32..).map_while(DiskError::from_u32).collect();
        assert!(
            all_variants.len() >= 42,
            "DiskError variant enumeration shrank: got {}, expected at least 42",
            all_variants.len()
        );

        for original in all_variants {
            let storage_error: StorageError = original.clone().into();
            let round_tripped: DiskError = storage_error
                .narrow_to_disk()
                .expect("every disk-representable StorageError must narrow back");

            assert_eq!(
                std::mem::discriminant(&original),
                std::mem::discriminant(&round_tripped),
                "round trip changed variant: {original:?} -> {round_tripped:?}"
            );
            assert_eq!(original, round_tripped, "round trip not identical for {original:?}");
        }

        // Io is the only payload-carrying variant: a representative kind and
        // message must both survive the round trip.
        let io_original = DiskError::Io(IoError::new(ErrorKind::PermissionDenied, "denied"));
        let storage_error: StorageError = io_original.clone().into();
        let io_round_tripped: DiskError = storage_error.narrow_to_disk().expect("Io narrows through the bridge");
        assert_eq!(io_original, io_round_tripped);
        match io_round_tripped {
            DiskError::Io(inner) => {
                assert_eq!(inner.kind(), ErrorKind::PermissionDenied);
                assert_eq!(inner.to_string(), "denied");
            }
            other => panic!("expected DiskError::Io, got {other:?}"),
        }
    }

    #[test]
    fn test_storage_error_from_io_error() {
        // Test direct IO error conversion
        let io_error = IoError::new(ErrorKind::NotFound, "test error");
        let storage_error: StorageError = io_error.into();
        assert!(matches!(storage_error, StorageError::Io(_)));

        // Test IO error containing DiskError
        let disk_error = DiskError::DiskFull;
        let io_with_disk_error = IoError::other(disk_error);
        let storage_error: StorageError = io_with_disk_error.into();
        assert_eq!(storage_error, StorageError::DiskFull);

        // Test IO error containing StorageError
        let original_storage_error = StorageError::BucketNotFound("test".to_string());
        let io_with_storage_error = IoError::other(original_storage_error.clone());
        let recovered_storage_error: StorageError = io_with_storage_error.into();
        assert_eq!(recovered_storage_error, original_storage_error);
    }

    #[test]
    fn test_storage_error_to_io_error() {
        // Test conversion to IO error
        let storage_error = StorageError::DiskFull;
        let io_error: IoError = storage_error.into();
        assert_eq!(io_error.kind(), ErrorKind::Other);

        // Test IO error round trip
        let original_io = IoError::new(ErrorKind::PermissionDenied, "access denied");
        let storage_error = StorageError::Io(original_io);
        let converted_io: IoError = storage_error.into();
        assert_eq!(converted_io.kind(), ErrorKind::PermissionDenied);
    }

    #[test]
    fn test_bucket_and_object_errors() {
        let bucket_not_found = StorageError::BucketNotFound("mybucket".to_string());
        let object_not_found = StorageError::ObjectNotFound("mybucket".to_string(), "myobject".to_string());
        let version_not_found = StorageError::VersionNotFound("mybucket".to_string(), "myobject".to_string(), "v1".to_string());

        // Test different error codes
        assert_ne!(bucket_not_found.to_u32(), object_not_found.to_u32());
        assert_ne!(object_not_found.to_u32(), version_not_found.to_u32());

        // Test error messages contain expected information
        assert!(bucket_not_found.to_string().contains("mybucket"));
        assert!(object_not_found.to_string().contains("mybucket"));
        assert!(object_not_found.to_string().contains("myobject"));
        assert!(version_not_found.to_string().contains("v1"));
    }

    #[test]
    fn test_upload_id_errors() {
        let invalid_upload = StorageError::InvalidUploadID("bucket".to_string(), "object".to_string(), "uploadid".to_string());
        let malformed_upload = StorageError::MalformedUploadID("badid".to_string());

        assert_ne!(invalid_upload.to_u32(), malformed_upload.to_u32());
        assert!(invalid_upload.to_string().contains("uploadid"));
        assert!(malformed_upload.to_string().contains("badid"));
    }

    #[test]
    fn test_round_trip_conversion() {
        // Test that to_u32 and from_u32 are consistent for all variants
        let test_errors = vec![
            StorageError::FaultyDisk,
            StorageError::DiskFull,
            StorageError::VolumeNotFound,
            StorageError::BucketExists("test".to_string()),
            StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()),
            StorageError::DecommissionAlreadyRunning,
            StorageError::RebalanceAlreadyRunning,
            StorageError::OperationCanceled,
            StorageError::QuotaExceeded { current: 1, limit: 2 },
        ];

        for original_error in test_errors {
            let code = original_error.to_u32();
            if let Some(recovered_error) = StorageError::from_u32(code) {
                // For errors with parameters, we only check the variant type
                assert_eq!(std::mem::discriminant(&original_error), std::mem::discriminant(&recovered_error));
            } else {
                panic!("Failed to recover error from code: {code:#x}");
            }
        }
    }

    #[test]
    fn test_storage_error_io_roundtrip() {
        // Test StorageError -> std::io::Error -> StorageError roundtrip conversion
        let original_storage_errors = vec![
            StorageError::FileNotFound,
            StorageError::VolumeNotFound,
            StorageError::DiskFull,
            StorageError::FileCorrupt,
            StorageError::MethodNotAllowed,
            StorageError::BucketExists("test-bucket".to_string()),
            StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()),
        ];

        for original_error in original_storage_errors {
            // Convert to io::Error and back
            let io_error: std::io::Error = original_error.clone().into();
            let recovered_error: StorageError = io_error.into();

            // Check that conversion preserves the essential error information
            match &original_error {
                StorageError::Io(_) => {
                    // Io errors should maintain their inner structure
                    assert!(matches!(recovered_error, StorageError::Io(_)));
                }
                _ => {
                    // Other errors should be recoverable via downcast or match to equivalent type
                    assert_eq!(original_error.to_u32(), recovered_error.to_u32());
                }
            }
        }
    }

    #[test]
    fn test_io_error_with_storage_error_inside() {
        // Test that io::Error containing StorageError can be properly converted back
        let original_storage_error = StorageError::FileNotFound;
        let io_with_storage_error = std::io::Error::other(original_storage_error.clone());

        // Convert io::Error back to StorageError
        let recovered_storage_error: StorageError = io_with_storage_error.into();
        assert_eq!(original_storage_error, recovered_storage_error);
    }

    #[test]
    fn test_io_error_with_disk_error_inside() {
        // Test io::Error containing DiskError -> StorageError conversion
        let original_disk_error = DiskError::FileNotFound;
        let io_with_disk_error = std::io::Error::other(original_disk_error);

        // Convert io::Error to StorageError
        let storage_error: StorageError = io_with_disk_error.into();
        assert_eq!(storage_error, StorageError::FileNotFound);
    }

    #[test]
    fn test_nested_error_conversion_chain() {
        // Test complex conversion chain: DiskError -> StorageError -> io::Error -> StorageError
        let original_disk_error = DiskError::DiskFull;
        let storage_error1: StorageError = original_disk_error.into();
        let io_error: std::io::Error = storage_error1.into();
        let storage_error2: StorageError = io_error.into();

        assert_eq!(storage_error2, StorageError::DiskFull);
    }

    #[test]
    fn test_storage_error_different_io_kinds() {
        use std::io::ErrorKind;

        let test_cases = vec![
            (ErrorKind::NotFound, "not found"),
            (ErrorKind::PermissionDenied, "permission denied"),
            (ErrorKind::ConnectionRefused, "connection refused"),
            (ErrorKind::TimedOut, "timed out"),
            (ErrorKind::InvalidInput, "invalid input"),
            (ErrorKind::BrokenPipe, "broken pipe"),
        ];

        for (kind, message) in test_cases {
            let io_error = std::io::Error::new(kind, message);
            let storage_error: StorageError = io_error.into();

            // Should become StorageError::Io with the same kind and message
            match storage_error {
                StorageError::Io(inner_io) => {
                    assert_eq!(inner_io.kind(), kind);
                    assert!(inner_io.to_string().contains(message));
                }
                _ => panic!("Expected StorageError::Io variant for kind: {kind:?}"),
            }
        }
    }

    #[test]
    fn test_storage_error_to_io_error_preserves_information() {
        let test_cases = vec![
            StorageError::FileNotFound,
            StorageError::VolumeNotFound,
            StorageError::DiskFull,
            StorageError::FileCorrupt,
            StorageError::MethodNotAllowed,
            StorageError::StorageFull,
            StorageError::SlowDown,
            StorageError::BucketExists("test-bucket".to_string()),
        ];

        for storage_error in test_cases {
            let io_error: std::io::Error = storage_error.clone().into();

            // Error message should be preserved
            assert!(io_error.to_string().contains(&storage_error.to_string()));

            // Should be able to downcast back to StorageError
            let recovered_error = io_error.downcast::<StorageError>();
            assert!(recovered_error.is_ok());
            assert_eq!(recovered_error.unwrap(), storage_error);
        }
    }

    #[test]
    fn test_storage_error_io_variant_preservation() {
        // Test StorageError::Io variant preserves original io::Error
        let original_io = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof");
        let storage_error = StorageError::Io(original_io);

        let converted_io: std::io::Error = storage_error.into();
        assert_eq!(converted_io.kind(), std::io::ErrorKind::UnexpectedEof);
        assert!(converted_io.to_string().contains("unexpected eof"));
    }

    #[test]
    fn test_from_filemeta_error_conversions() {
        // Test conversions from rustfs_filemeta::Error
        use rustfs_filemeta::Error as FilemetaError;

        let filemeta_errors = vec![
            (FilemetaError::FileNotFound, StorageError::FileNotFound),
            (FilemetaError::FileVersionNotFound, StorageError::FileVersionNotFound),
            (FilemetaError::FileCorrupt, StorageError::FileCorrupt),
            (FilemetaError::MethodNotAllowed, StorageError::MethodNotAllowed),
            (FilemetaError::VolumeNotFound, StorageError::VolumeNotFound),
            (FilemetaError::DoneForNow, StorageError::DoneForNow),
            (FilemetaError::Unexpected, StorageError::Unexpected),
        ];

        for (filemeta_error, expected_storage_error) in filemeta_errors {
            let converted_storage_error: StorageError = filemeta_error.into();
            assert_eq!(converted_storage_error, expected_storage_error);

            // Test reverse conversion
            let converted_back: rustfs_filemeta::Error = converted_storage_error
                .narrow_to_filemeta()
                .expect("matched variants must narrow to filemeta");
            let expected_back: rustfs_filemeta::Error = expected_storage_error
                .narrow_to_filemeta()
                .expect("matched variants must narrow to filemeta");
            assert_eq!(converted_back, expected_back);
        }
    }

    #[test]
    fn test_error_message_consistency() {
        let storage_errors = vec![
            StorageError::BucketNotFound("test-bucket".to_string()),
            StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()),
            StorageError::VersionNotFound("bucket".to_string(), "object".to_string(), "v1".to_string()),
            StorageError::InvalidUploadID("bucket".to_string(), "object".to_string(), "upload123".to_string()),
        ];

        for storage_error in storage_errors {
            let original_message = storage_error.to_string();
            let io_error: std::io::Error = storage_error.clone().into();

            // The io::Error should contain the original error message or info
            assert!(io_error.to_string().contains(&original_message));
        }
    }

    #[test]
    fn test_error_equality_after_conversion() {
        let storage_errors = vec![
            StorageError::FileNotFound,
            StorageError::VolumeNotFound,
            StorageError::DiskFull,
            StorageError::MethodNotAllowed,
        ];

        for original_error in storage_errors {
            // Test that equality is preserved through conversion
            let io_error: std::io::Error = original_error.clone().into();
            let recovered_error: StorageError = io_error.into();

            assert_eq!(original_error, recovered_error);
        }
    }

    #[test]
    fn test_storage_error_display_snapshot() {
        // Snapshot test to detect unexpected changes in error display format
        let errors = vec![
            StorageError::BucketNotFound("test-bucket".to_string()),
            StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()),
            StorageError::VersionNotFound("bucket".to_string(), "object".to_string(), "v1".to_string()),
            StorageError::InvalidUploadID("bucket".to_string(), "object".to_string(), "upload123".to_string()),
            StorageError::DiskFull,
            StorageError::FaultyDisk,
            StorageError::FileNotFound,
            StorageError::VolumeNotFound,
            StorageError::ErasureReadQuorum,
            StorageError::ErasureWriteQuorum,
            StorageError::DecommissionAlreadyRunning,
            StorageError::RebalanceAlreadyRunning,
            StorageError::OperationCanceled,
            StorageError::NamespaceLockQuorumUnavailable {
                mode: "write",
                bucket: "bucket".into(),
                object: "object".into(),
                required: 3,
                achieved: 2,
            },
        ];

        let error_messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
        insta::assert_yaml_snapshot!("storage_error_display", error_messages);
    }
}
