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

//! Write-side state machine: open_write, commit_write, the
//! write_dispatch chain that flushes a part once part_buffer reaches
//! part_size, abort_upload_with_auth, close_streaming, and
//! multipart_copy. Also, the cancellation-safety primitives
//! (build_write_tombstone, should_abort_on_drop) that the Drop impl
//! in driver.rs consumes.

use super::attrs::{s3_attrs_to_sftp, sftp_attrs_to_user_metadata};
use super::constants::limits::{
    COMMIT_WRITE_BACKOFF_MS, COMMIT_WRITE_MAX_RETRIES, S3_MAX_MULTIPART_PARTS, S3_MAX_PART_SIZE, S3_MIN_PART_SIZE,
};
use super::driver::SftpDriver;
use super::errors::{SftpError, is_not_found_error, s3_error_to_sftp};
use super::paths::{parse_s3_path, sanitise_control_bytes};
use super::state::{CompletedPart, HandleState, MultipartUpload, WritePhase};
use crate::common::client::s3::StorageBackend;
use crate::common::gateway::S3Action;
use bytes::Bytes;
use futures_util::stream;
use russh_sftp::protocol::{FileAttributes, Handle, OpenFlags, StatusCode};
use rustfs_utils::MaskedAccessKey;
use s3s::dto::{
    AbortMultipartUploadInput, CompleteMultipartUploadInput, CompletedMultipartUpload, CompletedPart as S3CompletedPart,
    CopySource, CreateMultipartUploadInput, PutObjectInput, StreamingBlob, UploadPartCopyInput, UploadPartInput,
};

const LOG_COMPONENT_PROTOCOLS: &str = "protocols";
const LOG_SUBSYSTEM_SFTP_WRITE: &str = "sftp_write";
const EVENT_SFTP_WRITE_STATE: &str = "sftp_write_state";
const EVENT_SFTP_MULTIPART_STATE: &str = "sftp_multipart_state";
const EVENT_SFTP_ABORT_STATE: &str = "sftp_abort_state";
const EVENT_SFTP_MULTIPART_COPY_STATE: &str = "sftp_multipart_copy_state";

/// Running byte count for a write handle's current phase. Buffering is
/// part_buffer.len(). Streaming is (parts_done * part_size) +
/// part_buffer.len(). Failed is treated as zero in saturating mode and
/// an error in strict mode. The saturating flag controls what happens
/// on u64 overflow: strict returns Err, saturating returns u64::MAX.
/// Strict is used by the offset precondition check (entry to the
/// write_dispatch chain). Saturating is used when refreshing attrs.size
/// at the tail of the same chain. See write_dispatch for the full call
/// graph.
pub(super) fn write_dispatch_byte_count(phase: &WritePhase, part_size: u64, saturating: bool) -> Result<u64, SftpError> {
    match phase {
        WritePhase::Buffering { part_buffer } => Ok(part_buffer.len() as u64),
        WritePhase::Streaming {
            part_buffer,
            next_part_number,
            ..
        } => {
            let parts_done = (*next_part_number - 1) as u64;
            let sum = parts_done
                .checked_mul(part_size)
                .and_then(|base| base.checked_add(part_buffer.len() as u64));
            match (sum, saturating) {
                (Some(value), _) => Ok(value),
                (None, true) => Ok(u64::MAX),
                (None, false) => {
                    tracing::warn!(
                        event = EVENT_SFTP_WRITE_STATE,
                        component = LOG_COMPONENT_PROTOCOLS,
                        subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                        result = "byte_count_overflow",
                        "SFTP write byte-count overflow"
                    );
                    Err(SftpError::code(StatusCode::Failure))
                }
            }
        }
        WritePhase::Failed { .. } => {
            if saturating {
                Ok(0)
            } else {
                tracing::warn!(
                    event = EVENT_SFTP_WRITE_STATE,
                    component = LOG_COMPONENT_PROTOCOLS,
                    subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                    result = "handle_poisoned",
                    "SFTP write handle poisoned"
                );
                Err(SftpError::code(StatusCode::Failure))
            }
        }
    }
}

/// Append incoming bytes to whichever buffer the current phase carries.
/// Failed is unreachable here: write_dispatch's offset check rejects
/// Failed before this call. The Failed arm logs and returns Failure
/// rather than panicking so a broken invariant cannot abort the
/// process. See write_dispatch for the full call graph.
pub(super) fn write_dispatch_append_bytes(phase: &mut WritePhase, data: &[u8]) -> Result<(), SftpError> {
    match phase {
        WritePhase::Buffering { part_buffer } | WritePhase::Streaming { part_buffer, .. } => {
            part_buffer.extend_from_slice(data);
            Ok(())
        }
        WritePhase::Failed { .. } => {
            tracing::error!(
                event = EVENT_SFTP_WRITE_STATE,
                component = LOG_COMPONENT_PROTOCOLS,
                subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                result = "append_on_failed_handle",
                "SFTP write append rejected on failed handle"
            );
            Err(SftpError::code(StatusCode::Failure))
        }
    }
}

/// Drain-loop predicate. Returns true when the current phase's
/// part_buffer holds at least part_size bytes. Failed returns false so
/// the loop exits on a poisoned handle. See write_dispatch for the
/// full call graph.
pub(super) fn write_dispatch_has_full_part(phase: &WritePhase, part_size: u64) -> bool {
    match phase {
        WritePhase::Buffering { part_buffer } | WritePhase::Streaming { part_buffer, .. } => {
            (part_buffer.len() as u64) >= part_size
        }
        WritePhase::Failed { .. } => false,
    }
}

/// Size value reported by FSTAT for a write handle in the given phase.
///
/// Buffering returns the current buffer length. Streaming returns the
/// running total of bytes received so far (parts uploaded plus any
/// bytes still buffered), saturating at u64::MAX on arithmetic
/// overflow. Failed returns the cached attrs.size from the last
/// successful write: a client polling FSTAT after a write failure
/// reads the byte count that landed rather than zero.
pub(super) fn fstat_reported_size(phase: &WritePhase, part_size: u64, cached_size: u64) -> u64 {
    match phase {
        WritePhase::Buffering { part_buffer } => part_buffer.len() as u64,
        WritePhase::Streaming {
            part_buffer,
            next_part_number,
            ..
        } => {
            let parts_done = (*next_part_number - 1) as u64;
            parts_done
                .checked_mul(part_size)
                .and_then(|base| base.checked_add(part_buffer.len() as u64))
                .unwrap_or(u64::MAX)
        }
        WritePhase::Failed { .. } => cached_size,
    }
}

/// Construct a cancellation-safety tombstone for a live multipart
/// upload. A tombstone is a HandleState::Write whose phase is Failed,
/// carrying the in-flight upload_id and the cached abort_authorized
/// flag. Drop reads both fields to issue AbortMultipartUpload on
/// session teardown.
///
/// The write() and close() handlers remove a HandleState from the
/// handle table, await an S3 backend call, and re-insert. If the
/// handler future is cancelled or panics between remove and re-insert,
/// the real state is dropped before Drop ever sees it. To prevent the
/// upload_id from orphaning, every remove-await-reinsert site inserts
/// a tombstone under the same handle id before the await. Drop's
/// drain loop picks up WritePhase::Failed entries with
/// abort_authorized == true and fires AbortMultipartUpload. A
/// successful future overwrites the tombstone with the real state
/// synchronously after the await returns; no further await runs in
/// between, so cancellation cannot fire in that window.
///
/// WritePhase::Failed already means "upload poisoned by a prior
/// UploadPart failure; abort at close." Tombstones reuse the variant
/// for "upload in progress; abort if the caller's future vanishes."
/// Drop runs the same AbortMultipartUpload for both.
///
/// attrs is required by the HandleState::Write variant layout but
/// Drop does not read it. Callers pass a clone of the live attrs.
pub(super) fn build_write_tombstone(
    bucket: &str,
    key: &str,
    attrs: &FileAttributes,
    upload_id: String,
    abort_authorized: bool,
) -> HandleState {
    HandleState::Write {
        bucket: bucket.to_string(),
        key: key.to_string(),
        attrs: attrs.clone(),
        open_attrs: FileAttributes::empty(),
        phase: WritePhase::Failed {
            upload_id,
            abort_authorized,
        },
    }
}

/// Predicate for the SFTPv3 draft section 6.3 rule that SSH_FXF_EXCL
/// and SSH_FXF_TRUNC are modifiers of SSH_FXF_CREAT. Returns true when
/// either modifier is set without CREAT, which the open() handler
/// translates into BadMessage at the protocol boundary.
pub(super) fn rejects_excl_or_trunc_without_create(pflags: OpenFlags) -> bool {
    !pflags.contains(OpenFlags::CREATE) && (pflags.contains(OpenFlags::EXCLUDE) || pflags.contains(OpenFlags::TRUNCATE))
}

/// Decide whether Drop should abort the given write handle's phase.
/// Returns Some(upload_id) when there is a live upload AND the cached
/// abort_authorized flag says the principal is permitted to call
/// AbortMultipartUpload. Returns None for Buffering (no upload_id) and
/// for Streaming or Failed with abort_authorized == false (IAM denies
/// abort; cleanup falls to the bucket AbortIncompleteMultipartUpload
/// lifecycle rule).
pub(super) fn should_abort_on_drop(phase: &WritePhase) -> Option<&str> {
    match phase {
        WritePhase::Streaming {
            upload_id,
            abort_authorized: true,
            ..
        } => Some(upload_id.as_str()),
        WritePhase::Failed {
            upload_id,
            abort_authorized: true,
        } => Some(upload_id.as_str()),
        _ => None,
    }
}

impl<S: StorageBackend + Send + Sync + 'static> SftpDriver<S> {
    /// Write-side OPEN: enforce read-only mode, authorise PutObject,
    /// and require WRITE | CREATE | TRUNCATE (with optional EXCLUDE).
    /// No bytes are sent to S3 here. The upload happens at CLOSE with
    /// a single PutObject call carrying the buffered payload.
    ///
    /// The streaming write path overwrites the entire object at close
    /// so the only flag combination that matches that semantic is
    /// CREATE | TRUNCATE. WRITE without CREATE or TRUNCATE,
    /// WRITE | CREATE without TRUNCATE, and other combinations would
    /// silently mistranslate partial-write or open-without-truncate
    /// intent into truncate-and-replace, with data loss for clients
    /// that requested the former. Those combinations return
    /// OpUnsupported at OPEN so the client sees a clean error rather
    /// than a corrupted object at CLOSE.
    ///
    /// EXCLUDE adds a HeadObject existence check so the OPEN fails
    /// when the key already exists. The check is best-effort. A
    /// second client racing the same path can win the PutObject
    /// between this HEAD and the eventual CLOSE. The SFTPv3 draft
    /// does not guarantee atomicity here and S3 has no native CAS
    /// primitive, so the race is accepted.
    pub(super) async fn open_write(
        &mut self,
        id: u32,
        filename: &str,
        pflags: OpenFlags,
        open_attrs: FileAttributes,
    ) -> Result<Handle, SftpError> {
        self.enforce_server_readonly()?;

        let (bucket, key) = parse_s3_path(filename)?;
        let Some(object_key) = key else {
            return Err(SftpError::code(StatusCode::NoSuchFile));
        };
        if bucket.is_empty() {
            return Err(SftpError::code(StatusCode::NoSuchFile));
        }

        self.authorize(&S3Action::PutObject, &bucket, Some(&object_key)).await?;

        let creat = pflags.contains(OpenFlags::CREATE);
        let trunc = pflags.contains(OpenFlags::TRUNCATE);
        let excl = pflags.contains(OpenFlags::EXCLUDE);

        // Reject any flag combination that would not be honoured by a
        // single PutObject at CLOSE. See the doc comment above for the
        // full rationale.
        if !creat || !trunc {
            return Err(SftpError::code(StatusCode::OpUnsupported));
        }

        if excl {
            // EXCLUDE (SSH_FXF_EXCL): check whether the object already
            // exists. HEAD returning Ok means the key is taken. A
            // not-found error means the key is free. Any other error is
            // propagated rather than misinterpreted as "does not exist".
            match self
                .run_backend_with_err(
                    "head_object",
                    self.storage
                        .head_object(&bucket, &object_key, self.access_key(), self.secret_key()),
                )
                .await?
            {
                Ok(_) => return Err(SftpError::code(StatusCode::Failure)),
                Err(e) if is_not_found_error(&e) => {}
                Err(e) => return Err(s3_error_to_sftp("head_object", e)),
            }
        }

        let mut attrs = s3_attrs_to_sftp(0, None, false);
        if open_attrs.mtime.is_some() {
            attrs.mtime = open_attrs.mtime;
            attrs.atime = open_attrs.mtime;
        }
        if open_attrs.permissions.is_some() {
            attrs.permissions = open_attrs.permissions;
        }
        if open_attrs.uid.is_some() {
            attrs.uid = open_attrs.uid;
        }
        if open_attrs.gid.is_some() {
            attrs.gid = open_attrs.gid;
        }
        let handle = self.allocate_handle(HandleState::Write {
            bucket,
            key: object_key,
            attrs,
            open_attrs,
            phase: WritePhase::Buffering { part_buffer: Vec::new() },
        })?;
        Ok(Handle { id, handle })
    }

    /// Upload the buffered bytes to S3 with a single PutObject call.
    /// An empty buffer still issues a PutObject, so an open followed
    /// by a close with no WRITE in between creates a zero-byte object
    /// (matching POSIX create-and-close semantics).
    ///
    /// PutObject is retried on transient backend errors recognised by
    /// rustfs_utils::retry::is_s3code_in_message_retryable (SlowDown,
    /// RequestTimeout, Throttling, InternalError, etc.). Up to
    /// COMMIT_WRITE_MAX_RETRIES retries with the
    /// COMMIT_WRITE_BACKOFF_MS exponential schedule. Terminal errors
    /// (AccessDenied, NoSuchBucket, etc.) return immediately. The
    /// buffer is held as a Bytes across retries so each attempt
    /// rebuilds the stream from a cheap clone of the same payload
    /// without a second heap allocation. A timeout on the underlying
    /// run_backend deadline still propagates immediately as Failure
    /// rather than retrying, because a stuck backend is not a
    /// transient classification the retry set targets.
    pub(super) async fn commit_write(
        &self,
        bucket: &str,
        key: &str,
        attrs: &FileAttributes,
        buffer: Vec<u8>,
    ) -> Result<(), SftpError> {
        let size = buffer.len() as i64;
        let body_bytes = Bytes::from(buffer);

        for attempt in 0..=COMMIT_WRITE_MAX_RETRIES {
            if attempt > 0 {
                tokio::time::sleep(std::time::Duration::from_millis(COMMIT_WRITE_BACKOFF_MS[attempt - 1])).await;
                tracing::warn!(
                    event = EVENT_SFTP_WRITE_STATE,
                    component = LOG_COMPONENT_PROTOCOLS,
                    subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                    bucket = %sanitise_control_bytes(bucket),
                    key = %sanitise_control_bytes(key),
                    attempt = attempt,
                    state = "retrying_put_object",
                    "SFTP put_object retry scheduled",
                );
            }

            let body = body_bytes.clone();
            let stream = stream::once(async move { Ok::<Bytes, std::io::Error>(body) });
            let streaming = StreamingBlob::wrap(stream);
            let input = PutObjectInput::builder()
                .bucket(bucket.to_string())
                .key(key.to_string())
                .content_length(Some(size))
                .metadata(sftp_attrs_to_user_metadata(attrs))
                .body(Some(streaming))
                .build()
                .map_err(|e| s3_error_to_sftp("build_put_object", e))?;

            let outcome = self
                .run_backend_with_err("put_object", self.storage.put_object(input, self.access_key(), self.secret_key()))
                .await?;

            let backend_err = match outcome {
                Ok(_) => return Ok(()),
                Err(e) => e,
            };

            let msg = backend_err.to_string();
            if attempt < COMMIT_WRITE_MAX_RETRIES && rustfs_utils::retry::is_s3code_in_message_retryable(&msg) {
                continue;
            }
            return Err(s3_error_to_sftp("put_object", backend_err));
        }

        // Defensive fallback. The for is exhaustive over
        // 0..=COMMIT_WRITE_MAX_RETRIES and every iteration either
        // returns or continues; the continue branch is gated on
        // attempt < COMMIT_WRITE_MAX_RETRIES, so the final iteration
        // always returns. If a future change to the loop bound breaks
        // that proof, log and surface Failure rather than panicking
        // the session task.
        tracing::error!(
            event = EVENT_SFTP_WRITE_STATE,
            component = LOG_COMPONENT_PROTOCOLS,
            subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
            bucket = %sanitise_control_bytes(bucket),
            key = %sanitise_control_bytes(key),
            result = "retry_loop_fell_through",
            "SFTP put_object retry loop fell through",
        );
        Err(SftpError::code(StatusCode::Failure))
    }

    /// Upload exactly one part of an in-progress multipart upload.
    /// Returns a CompletedPart on success. Authorization for UploadPart
    /// is checked before each call.
    pub(super) async fn upload_multipart_bytes(
        &self,
        bucket: &str,
        key: &str,
        upload_id: &str,
        part_number: i32,
        part_bytes: Vec<u8>,
    ) -> Result<CompletedPart, SftpError> {
        self.authorize(&S3Action::UploadPart, bucket, Some(key)).await?;

        let part_len = part_bytes.len() as i64;
        let body_bytes = Bytes::from(part_bytes);
        let body_stream = stream::once(async move { Ok::<Bytes, std::io::Error>(body_bytes) });
        let streaming = StreamingBlob::wrap(body_stream);

        let input = UploadPartInput::builder()
            .bucket(bucket.to_string())
            .key(key.to_string())
            .upload_id(upload_id.to_string())
            .part_number(part_number)
            .content_length(Some(part_len))
            .body(Some(streaming))
            .build()
            .map_err(|e| s3_error_to_sftp("build_upload_part", e))?;

        let out = self
            .run_backend("upload_part", self.storage.upload_part(input, self.access_key(), self.secret_key()))
            .await?;

        let e_tag = out.e_tag.ok_or_else(|| {
            tracing::warn!(
                event = EVENT_SFTP_MULTIPART_STATE,
                component = LOG_COMPONENT_PROTOCOLS,
                subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                upload_id = %upload_id,
                part_number = part_number,
                result = "etag_missing",
                "SFTP multipart part missing etag"
            );
            SftpError::code(StatusCode::Failure)
        })?;

        Ok(CompletedPart { part_number, e_tag })
    }

    /// Issue CreateMultipartUpload for the given bucket and key. Returns
    /// the upload_id plus a cached authorisation flag for the matching
    /// AbortMultipartUpload call. Authorisation for CreateMultipartUpload
    /// is issued before the backend call. SFTP does not carry S3 object
    /// metadata (content type, storage class, SSE config) so the input
    /// is built with only bucket and key.
    ///
    /// The Abort probe exists because the Drop impl on SftpDriver is
    /// synchronous and cannot later await an auth check. Caching the
    /// decision here lets Drop honour a Deny policy on AbortMultipartUpload
    /// without regressing the abort-on-disconnect invariant for every
    /// principal. close() consults the same cached flag so the two
    /// paths agree on policy outcome.
    ///
    /// On probe failure (Deny on AbortMultipartUpload, IAM unreachable,
    /// or any other authorize_operation Err), set abort_authorized =
    /// false. The upload still proceeds. Rationale: the admin
    /// configured a Deny Abort policy deliberately (append-only / WORM
    /// patterns). Fail-closed would refuse uploads from such principals
    /// entirely, which is not the admin's intent. Orphaned parts left
    /// behind by a Drop skip are cleaned up by the bucket
    /// AbortIncompleteMultipartUpload lifecycle rule, which operators
    /// using this policy pattern must configure.
    ///
    /// Condition-key policies (aws:SourceIp, aws:MultiFactorAuthPresent,
    /// aws:CurrentTime, object tags, etc.) are not evaluated here or
    /// anywhere else on the SFTP path: authorize_operation in gateway.rs
    /// passes an empty conditions map. Only unconditional Allow/Deny is
    /// honoured. This is a gateway-wide limitation, not specific to
    /// this cache.
    pub(super) async fn start_multipart_upload(
        &self,
        bucket: &str,
        key: &str,
        attrs: &FileAttributes,
    ) -> Result<MultipartUpload, SftpError> {
        self.authorize(&S3Action::CreateMultipartUpload, bucket, Some(key)).await?;

        // Probe AbortMultipartUpload authorisation immediately after
        // Create. The probe has no backend side effect. Its result is
        // cached on the resulting WritePhase::Streaming variant and read
        // by Drop and by close()'s abort paths. Routing through
        // self.authorize wraps the IAM call in the same per-call
        // deadline as every other authorize on the SFTP path; an IAM
        // hang here would otherwise wedge the caller indefinitely with
        // a live upload_id already at S3. is_ok() collapses
        // AccessDenied and IamUnavailable to false; only an explicit
        // Allow yields true.
        let abort_authorized = self
            .authorize(&S3Action::AbortMultipartUpload, bucket, Some(key))
            .await
            .is_ok();

        let input = CreateMultipartUploadInput::builder()
            .bucket(bucket.to_string())
            .key(key.to_string())
            .metadata(sftp_attrs_to_user_metadata(attrs))
            .build()
            .map_err(|e| s3_error_to_sftp("build_create_multipart_upload", e))?;

        let out = self
            .run_backend(
                "create_multipart_upload",
                self.storage
                    .create_multipart_upload(input, self.access_key(), self.secret_key()),
            )
            .await?;

        let upload_id = out.upload_id.ok_or_else(|| {
            tracing::warn!(
                event = EVENT_SFTP_MULTIPART_STATE,
                component = LOG_COMPONENT_PROTOCOLS,
                subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                bucket = %sanitise_control_bytes(bucket),
                key = %sanitise_control_bytes(key),
                result = "upload_id_missing",
                "SFTP multipart upload missing upload_id"
            );
            SftpError::code(StatusCode::Failure)
        })?;
        Ok(MultipartUpload {
            upload_id,
            abort_authorized,
        })
    }

    /// Finalise a multipart upload by calling CompleteMultipartUpload
    /// with the collected parts. Authorisation for
    /// CompleteMultipartUpload is issued before the backend call.
    pub(super) async fn finish_multipart_upload(
        &self,
        bucket: &str,
        key: &str,
        upload_id: &str,
        uploaded_parts: Vec<CompletedPart>,
    ) -> Result<(), SftpError> {
        self.authorize(&S3Action::CompleteMultipartUpload, bucket, Some(key)).await?;

        let parts: Vec<S3CompletedPart> = uploaded_parts
            .into_iter()
            .map(|p| S3CompletedPart {
                part_number: Some(p.part_number),
                e_tag: Some(p.e_tag),
                ..Default::default()
            })
            .collect();

        let input = CompleteMultipartUploadInput::builder()
            .bucket(bucket.to_string())
            .key(key.to_string())
            .upload_id(upload_id.to_string())
            .multipart_upload(Some(CompletedMultipartUpload { parts: Some(parts) }))
            .build()
            .map_err(|e| s3_error_to_sftp("build_complete_multipart_upload", e))?;

        let result = self
            .run_backend(
                "complete_multipart_upload",
                self.storage
                    .complete_multipart_upload(input, self.access_key(), self.secret_key()),
            )
            .await;
        result?;
        Ok(())
    }

    /// Run one WRITE packet against an extracted HandleState. The handle
    /// has already been removed from the table by the caller. The caller
    /// reinserts it after dispatch. Returns Err to convert to an SFTP
    /// status at the call site. On upload_part failure the phase is
    /// transitioned to Failed before returning.
    ///
    /// Cancellation-safety: the caller has removed the handle from the
    /// table, so an await inside this function holds the live upload_id
    /// only in the caller's local state. A tombstone must be in the
    /// table before the first such await. For a handle entering in the
    /// Streaming or Failed phase the caller installs the tombstone
    /// before calling this method. For a handle entering in Buffering,
    /// write_dispatch_begin_streaming installs the tombstone immediately
    /// after the synchronous transition to Streaming, before any
    /// subsequent await.
    ///
    /// Helper chain (each helper's doc references back here):
    ///
    ///     write_dispatch
    ///       write_dispatch_byte_count (strict)        [offset precondition]
    ///       write_dispatch_append_bytes               [add incoming bytes]
    ///       loop while write_dispatch_has_full_part:
    ///         write_dispatch_begin_streaming          [Buffering to Streaming]
    ///           start_multipart_upload                [S3 CreateMultipartUpload]
    ///         write_dispatch_flush_one_part           [drain + upload one part]
    ///           upload_multipart_bytes                [S3 UploadPart]
    ///       write_dispatch_byte_count (saturating)    [update attrs.size]
    pub(super) async fn write_dispatch(
        &mut self,
        handle: &str,
        state: &mut HandleState,
        offset: u64,
        data: Vec<u8>,
    ) -> Result<(), SftpError> {
        let HandleState::Write {
            bucket,
            key,
            attrs,
            open_attrs,
            phase,
        } = state
        else {
            return Err(SftpError::code(StatusCode::Failure));
        };
        let part_size = self.part_size;

        let current_len = write_dispatch_byte_count(phase, part_size, false)?;
        if offset != current_len {
            tracing::warn!(
                event = EVENT_SFTP_WRITE_STATE,
                component = LOG_COMPONENT_PROTOCOLS,
                subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                offset,
                buffered = current_len,
                result = "non_sequential_offset",
                "sftp write state changed"
            );
            return Err(SftpError::code(StatusCode::Failure));
        }

        // Own the bucket and key before the drain loop so &self helpers
        // can await without conflicting with the live &mut phase borrow.
        let bucket_owned = bucket.clone();
        let key_owned = key.clone();

        write_dispatch_append_bytes(phase, &data)?;

        while write_dispatch_has_full_part(phase, part_size) {
            if matches!(phase, WritePhase::Buffering { .. }) {
                self.write_dispatch_begin_streaming(handle, phase, &bucket_owned, &key_owned, open_attrs)
                    .await?;
            }
            self.write_dispatch_flush_one_part(phase, &bucket_owned, &key_owned, part_size)
                .await?;
        }

        attrs.size = Some(write_dispatch_byte_count(phase, part_size, true).unwrap_or(u64::MAX));
        Ok(())
    }

    /// Buffering -> Streaming transition. Issues CreateMultipartUpload,
    /// then moves the existing part_buffer into the new Streaming
    /// variant. If CreateMultipartUpload fails the phase stays in
    /// Buffering and the error propagates. The next full-part flush
    /// retries the transition (transient S3 error invisible to the
    /// client).
    ///
    /// CreateMultipartUpload is awaited before mem::take on the
    /// buffer. The reverse order would lose the buffered bytes on
    /// transient failure. See write_dispatch for the full call graph.
    pub(super) async fn write_dispatch_begin_streaming(
        &mut self,
        handle: &str,
        phase: &mut WritePhase,
        bucket: &str,
        key: &str,
        attrs: &FileAttributes,
    ) -> Result<(), SftpError> {
        if !matches!(phase, WritePhase::Buffering { .. }) {
            return Ok(());
        }
        // start_multipart_upload returns a MultipartUpload containing
        // the upload_id and the cached
        // authorize_operation(AbortMultipartUpload) result. The pair is
        // stored on the Streaming variant so Drop (which cannot await)
        // has a pre-decided policy answer.
        let mp = self.start_multipart_upload(bucket, key, attrs).await?;
        let existing_buffer = match phase {
            WritePhase::Buffering { part_buffer } => std::mem::take(part_buffer),
            _ => {
                tracing::error!(
                    event = EVENT_SFTP_MULTIPART_STATE,
                    component = LOG_COMPONENT_PROTOCOLS,
                    subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                    result = "buffering_phase_lost",
                    "SFTP multipart buffering phase lost"
                );
                return Err(SftpError::code(StatusCode::Failure));
            }
        };
        *phase = WritePhase::Streaming {
            upload_id: mp.upload_id.clone(),
            abort_authorized: mp.abort_authorized,
            part_buffer: existing_buffer,
            uploaded_parts: Vec::new(),
            next_part_number: 1,
        };
        // Between this point and the caller re-inserting the real
        // state, the upload_id exists only in a local variable here,
        // not in the handle table. Insert a tombstone so Drop can still
        // abort if the next UploadPart await is cancelled. The
        // synchronous window between start_multipart_upload returning
        // Ok and this insert contains no await, so cancellation cannot
        // fire in it.
        let tombstone = build_write_tombstone(bucket, key, attrs, mp.upload_id, mp.abort_authorized);
        self.handles.insert(handle.to_string(), tombstone);
        Ok(())
    }

    /// Drain exactly part_size bytes from a Streaming phase and upload
    /// them as one part. On success: records the returned CompletedPart
    /// and increments next_part_number. On failure: transitions to
    /// Failed carrying the live upload_id so close() can abort. Also
    /// poisons to Failed if next_part_number would exceed the S3 parts
    /// cap.
    ///
    /// Drain, upload, and the record-or-poison step are one atomic
    /// unit. Splitting them would create a window where bytes have
    /// left part_buffer but no Failed transition has occurred. See
    /// write_dispatch for the full call graph.
    pub(super) async fn write_dispatch_flush_one_part(
        &self,
        phase: &mut WritePhase,
        bucket: &str,
        key: &str,
        part_size: u64,
    ) -> Result<(), SftpError> {
        // Capture abort_authorized alongside upload_id inside the
        // Streaming match arm. The bool is Copy so the capture is
        // cheap. Both Streaming -> Failed transitions below must
        // carry this flag forward so Drop and close() continue to
        // honour the cached policy answer on the Failed handle.
        let (upload_id_for_call, abort_authorized_for_call, part_number_for_call, drained) = match phase {
            WritePhase::Streaming {
                upload_id,
                abort_authorized,
                part_buffer,
                next_part_number,
                ..
            } => {
                if *next_part_number > S3_MAX_MULTIPART_PARTS {
                    tracing::warn!(
                        event = EVENT_SFTP_MULTIPART_STATE,
                        component = LOG_COMPONENT_PROTOCOLS,
                        subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                        bucket = %bucket,
                        key = %key,
                        limit = S3_MAX_MULTIPART_PARTS,
                        result = "parts_limit_exceeded",
                        "SFTP multipart parts limit exceeded",
                    );
                    let upload_id_for_fail = upload_id.clone();
                    let abort_authorized_for_fail = *abort_authorized;
                    *phase = WritePhase::Failed {
                        upload_id: upload_id_for_fail,
                        abort_authorized: abort_authorized_for_fail,
                    };
                    return Err(SftpError::code(StatusCode::Failure));
                }
                let drained: Vec<u8> = part_buffer.drain(..part_size as usize).collect();
                (upload_id.clone(), *abort_authorized, *next_part_number, drained)
            }
            _ => {
                tracing::error!(
                    event = EVENT_SFTP_MULTIPART_STATE,
                    component = LOG_COMPONENT_PROTOCOLS,
                    subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                    result = "flush_without_streaming",
                    "SFTP multipart flush requested without streaming state"
                );
                return Err(SftpError::code(StatusCode::Failure));
            }
        };

        match self
            .upload_multipart_bytes(bucket, key, &upload_id_for_call, part_number_for_call, drained)
            .await
        {
            Ok(completed) => match phase {
                WritePhase::Streaming {
                    uploaded_parts,
                    next_part_number,
                    ..
                } => {
                    uploaded_parts.push(completed);
                    *next_part_number += 1;
                    Ok(())
                }
                _ => {
                    tracing::error!(
                        event = EVENT_SFTP_MULTIPART_STATE,
                        component = LOG_COMPONENT_PROTOCOLS,
                        subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                        result = "post_upload_phase_missing",
                        "SFTP multipart post-upload phase missing"
                    );
                    Err(SftpError::code(StatusCode::Failure))
                }
            },
            Err(err) => {
                // UploadPart failed. The drained bytes are lost: the
                // handle is now out of sync with its sequential
                // offset invariant. Transition to Failed so close can
                // issue AbortMultipartUpload. Carry the captured
                // abort_authorized into the new variant. The policy
                // decision does not change because the upload failed.
                *phase = WritePhase::Failed {
                    upload_id: upload_id_for_call,
                    abort_authorized: abort_authorized_for_call,
                };
                Err(err)
            }
        }
    }

    /// Issue AbortMultipartUpload for the given upload_id. Authorisation
    /// for AbortMultipartUpload is issued before the backend call. SFTP
    /// does not use cross-account or conditional-abort fields, so the
    /// input is built with bucket, key, and upload_id only.
    pub(super) async fn abort_upload_with_auth(&self, bucket: &str, key: &str, upload_id: &str) -> Result<(), SftpError> {
        self.authorize(&S3Action::AbortMultipartUpload, bucket, Some(key)).await?;

        let input = AbortMultipartUploadInput::builder()
            .bucket(bucket.to_string())
            .key(key.to_string())
            .upload_id(upload_id.to_string())
            .build()
            .map_err(|e| s3_error_to_sftp("build_abort_multipart_upload", e))?;

        self.run_backend(
            "abort_multipart_upload",
            self.storage
                .abort_multipart_upload(input, self.access_key(), self.secret_key()),
        )
        .await?;
        Ok(())
    }

    /// Issue AbortMultipartUpload if abort_authorized is true.
    /// Otherwise log a skip with bucket, key, upload_id, principal, and
    /// the supplied context. context is a short free-form label (for
    /// example "parts-limit breach" or "Failed handle") embedded in
    /// both the abort-error log and the skip log so operators can tell
    /// which arm of close() produced the record.
    pub(super) async fn close_abort_or_skip(
        &self,
        bucket: &str,
        key: &str,
        upload_id: &str,
        abort_authorized: bool,
        context: &str,
    ) {
        if abort_authorized {
            if let Err(abort_err) = self.abort_upload_with_auth(bucket, key, upload_id).await {
                tracing::warn!(
                    event = EVENT_SFTP_ABORT_STATE,
                    component = LOG_COMPONENT_PROTOCOLS,
                    subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                    bucket = %bucket,
                    key = %key,
                    upload_id = %upload_id,
                    err = ?abort_err,
                    context = context,
                    result = "abort_failed",
                    "SFTP multipart abort failed",
                );
            }
        } else {
            tracing::warn!(
                event = EVENT_SFTP_ABORT_STATE,
                component = LOG_COMPONENT_PROTOCOLS,
                subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                bucket = %bucket,
                key = %key,
                upload_id = %upload_id,
                access_key = %MaskedAccessKey(self.access_key()),
                context = context,
                result = "abort_skipped_unauthorized",
                "SFTP multipart abort skipped by policy",
            );
        }
    }

    /// close() arm handler for a handle in the Streaming phase. Flushes
    /// the trailing partial part if one exists, then calls
    /// CompleteMultipartUpload. On any failure inside this sequence the
    /// upload is rolled back via close_abort_or_skip, honouring the
    /// cached abort_authorized flag.
    ///
    /// S3 allows the last part of a multipart upload to be smaller than
    /// the minimum part size, so the trailing UploadPart does not need
    /// a minimum-size guard. A zero-length trailing buffer is skipped
    /// entirely because an earlier flush already emitted the final full
    /// part.
    ///
    /// Before issuing the trailing UploadPart, the S3 parts-per-upload
    /// cap is enforced. The flush-loop guard catches this for full-part
    /// flushes, but the close-time tail can hit next_part_number ==
    /// S3_MAX_MULTIPART_PARTS + 1 when the upload's size is exactly
    /// (S3_MAX_MULTIPART_PARTS * part_size) + tail. Without this guard
    /// the trailing call is guaranteed to be rejected by S3 with
    /// InvalidPart. Aborting here reports the size-overflow reason at
    /// the SFTP boundary and skips the round-trip that would fail.
    ///
    /// Every abort call site routes through close_abort_or_skip, which
    /// consults abort_authorized. False means the principal's IAM policy
    /// denies AbortMultipartUpload. Honouring that keeps close() aligned
    /// with Drop. Staged parts then fall to the bucket's
    /// AbortIncompleteMultipartUpload lifecycle rule for cleanup.
    ///
    /// Processes the Streaming field set by value. Passing
    /// WritePhase::Streaming by move would push the destructuring back
    /// inside the body and hide the field-by-field correspondence at
    /// the call site, so #[allow(clippy::too_many_arguments)] stays.
    #[allow(clippy::too_many_arguments)]
    pub(super) async fn close_streaming(
        &self,
        bucket: &str,
        key: &str,
        upload_id: String,
        abort_authorized: bool,
        part_buffer: Vec<u8>,
        mut uploaded_parts: Vec<CompletedPart>,
        next_part_number: i32,
    ) -> Result<(), SftpError> {
        if !part_buffer.is_empty() {
            if next_part_number > S3_MAX_MULTIPART_PARTS {
                tracing::warn!(
                    event = EVENT_SFTP_MULTIPART_STATE,
                    component = LOG_COMPONENT_PROTOCOLS,
                    subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                    bucket = %bucket,
                    key = %key,
                    upload_id = %upload_id,
                    limit = S3_MAX_MULTIPART_PARTS,
                    result = "final_part_limit_exceeded",
                    "SFTP multipart final-part limit exceeded",
                );
                self.close_abort_or_skip(bucket, key, &upload_id, abort_authorized, "parts-limit breach")
                    .await;
                return Err(SftpError::code(StatusCode::Failure));
            }
            match self
                .upload_multipart_bytes(bucket, key, &upload_id, next_part_number, part_buffer)
                .await
            {
                Ok(completed) => uploaded_parts.push(completed),
                Err(err) => {
                    self.close_abort_or_skip(bucket, key, &upload_id, abort_authorized, "final-part upload failure")
                        .await;
                    return Err(err);
                }
            }
        }

        if let Err(err) = self.finish_multipart_upload(bucket, key, &upload_id, uploaded_parts).await {
            self.close_abort_or_skip(bucket, key, &upload_id, abort_authorized, "CompleteMultipartUpload failure")
                .await;
            return Err(err);
        }
        Ok(())
    }

    /// Copy an object larger than S3_COPY_OBJECT_MAX_SIZE (5 GiB) via
    /// UploadPartCopy. Server-side only: no bytes transit the SFTP
    /// server. On any failure the destination multipart upload is
    /// aborted so no partial state is left behind. The source object
    /// is not touched.
    pub(super) async fn multipart_copy(
        &self,
        src_bucket: &str,
        src_key: &str,
        dst_bucket: &str,
        dst_key: &str,
        content_length: u64,
    ) -> Result<(), SftpError> {
        // Pick effective_part_size so any object up to the 5 TiB S3
        // limit divides into at most S3_MAX_MULTIPART_PARTS parts. The
        // ceil-div ensures the final part is not over the limit. The
        // min(S3_MAX_PART_SIZE) clamp protects against an out-of-spec
        // content_length: with S3_MAX_PART_SIZE = S3_COPY_OBJECT_MAX_SIZE
        // = 5 GiB and S3_MAX_MULTIPART_PARTS = 10000, the upper bound on
        // a copyable object is 50 TiB, an order of magnitude above the
        // 5 TiB S3 single-object cap. If the backend ever reports a
        // larger content_length the guard surfaces it as Failure here
        // rather than letting S3 reject UploadPartCopy with InvalidRange.
        let effective_part_size = {
            let max_parts = S3_MAX_MULTIPART_PARTS as u64;
            let configured = self.part_size;
            let needed = content_length.div_ceil(max_parts);
            let target = needed.max(configured).max(S3_MIN_PART_SIZE);
            if target > S3_MAX_PART_SIZE {
                tracing::warn!(
                    event = EVENT_SFTP_MULTIPART_COPY_STATE,
                    component = LOG_COMPONENT_PROTOCOLS,
                    subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                    bucket = %dst_bucket,
                    key = %sanitise_control_bytes(dst_key),
                    content_length,
                    target,
                    result = "part_size_exceeded",
                    "sftp multipart copy state changed"
                );
                return Err(SftpError::code(StatusCode::Failure));
            }
            target
        };

        // multipart_copy manages the destination upload lifecycle
        // directly: any failure routes through close_abort_or_skip
        // rather than relying on the Drop tombstone path. The cached
        // abort_authorized flag is carried on the MultipartUpload so
        // close_abort_or_skip can honour a Deny-Abort policy without a
        // second IAM probe per error path.
        let mp = self
            .start_multipart_upload(dst_bucket, dst_key, &FileAttributes::empty())
            .await?;

        let result: Result<Vec<CompletedPart>, SftpError> = async {
            let mut uploaded_parts = Vec::new();
            let mut part_number: i32 = 1;
            let mut offset: u64 = 0;
            while offset < content_length {
                let end = offset.saturating_add(effective_part_size).min(content_length);
                let range = format!("bytes={}-{}", offset, end - 1);

                self.authorize(&S3Action::UploadPart, dst_bucket, Some(dst_key)).await?;

                let input = UploadPartCopyInput::builder()
                    .bucket(dst_bucket.to_string())
                    .key(dst_key.to_string())
                    .upload_id(mp.upload_id.clone())
                    .part_number(part_number)
                    .copy_source(CopySource::Bucket {
                        bucket: src_bucket.to_string().into(),
                        key: src_key.to_string().into(),
                        version_id: None,
                    })
                    .copy_source_range(Some(range))
                    .build()
                    .map_err(|e| s3_error_to_sftp("build_upload_part_copy", e))?;

                let out = self
                    .run_backend(
                        "upload_part_copy",
                        self.storage.upload_part_copy(input, self.access_key(), self.secret_key()),
                    )
                    .await?;

                let e_tag = out.copy_part_result.and_then(|r| r.e_tag).ok_or_else(|| {
                    tracing::warn!(
                        event = EVENT_SFTP_MULTIPART_COPY_STATE,
                        component = LOG_COMPONENT_PROTOCOLS,
                        subsystem = LOG_SUBSYSTEM_SFTP_WRITE,
                        upload_id = %mp.upload_id,
                        part_number = part_number,
                        result = "etag_missing",
                        "SFTP multipart copy part missing etag"
                    );
                    SftpError::code(StatusCode::Failure)
                })?;

                uploaded_parts.push(CompletedPart { part_number, e_tag });
                part_number += 1;
                offset = end;
            }
            Ok(uploaded_parts)
        }
        .await;

        match result {
            Ok(parts) => {
                if let Err(err) = self.finish_multipart_upload(dst_bucket, dst_key, &mp.upload_id, parts).await {
                    self.close_abort_or_skip(
                        dst_bucket,
                        dst_key,
                        &mp.upload_id,
                        mp.abort_authorized,
                        "complete-multipart-copy failure",
                    )
                    .await;
                    return Err(err);
                }
                Ok(())
            }
            Err(err) => {
                self.close_abort_or_skip(dst_bucket, dst_key, &mp.upload_id, mp.abort_authorized, "copy failure")
                    .await;
                Err(err)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::super::constants::limits::S3_MAX_MULTIPART_PARTS;
    use super::super::test_support::{TEST_PART_SIZE, build_driver, build_driver_with_timeout, write_handle};
    use super::*;
    use crate::common::dummy_storage::{AbortCall, DummyBackend, DummyError};
    use crate::common::gateway::with_test_auth_override;
    use russh_sftp::protocol::{FileAttributes, OpenFlags, StatusCode};
    use s3s::dto::ETag;
    use std::sync::Arc;
    use std::time::Duration;
    use tokio::sync::Notify;

    #[test]
    fn should_abort_on_drop_buffering_is_none() {
        let phase = WritePhase::Buffering { part_buffer: Vec::new() };
        assert!(should_abort_on_drop(&phase).is_none());
    }

    #[test]
    fn should_abort_on_drop_streaming_authorized_returns_upload_id() {
        let phase = WritePhase::Streaming {
            upload_id: "UP-7".to_string(),
            abort_authorized: true,
            part_buffer: Vec::new(),
            uploaded_parts: Vec::new(),
            next_part_number: 1,
        };
        assert_eq!(should_abort_on_drop(&phase), Some("UP-7"));
    }

    #[test]
    fn should_abort_on_drop_streaming_denied_is_none() {
        let phase = WritePhase::Streaming {
            upload_id: "UP-8".to_string(),
            abort_authorized: false,
            part_buffer: Vec::new(),
            uploaded_parts: Vec::new(),
            next_part_number: 1,
        };
        assert!(should_abort_on_drop(&phase).is_none());
    }

    #[test]
    fn should_abort_on_drop_failed_authorized_returns_upload_id() {
        let phase = WritePhase::Failed {
            upload_id: "UP-9".to_string(),
            abort_authorized: true,
        };
        assert_eq!(should_abort_on_drop(&phase), Some("UP-9"));
    }

    #[test]
    fn should_abort_on_drop_failed_denied_is_none() {
        let phase = WritePhase::Failed {
            upload_id: "UP-10".to_string(),
            abort_authorized: false,
        };
        assert!(should_abort_on_drop(&phase).is_none());
    }

    // Tombstone construction: the cancellation-safety mechanism relies
    // on the tombstone being recognised by the Drop drain loop. The
    // two invariants these tests pin:
    //   1. The tombstone carries the caller's upload_id verbatim.
    //   2. should_abort_on_drop returns Some(upload_id) when
    //      abort_authorized is true, so Drop picks the tombstone up.

    #[test]
    fn tombstone_carries_upload_id_and_authorization() {
        let attrs = FileAttributes::default();
        let state = build_write_tombstone("b", "k", &attrs, "UP-T1".to_string(), true);
        let HandleState::Write {
            bucket,
            key,
            phase: WritePhase::Failed {
                upload_id,
                abort_authorized,
            },
            ..
        } = state
        else {
            panic!("tombstone must be HandleState::Write with Failed phase");
        };
        assert_eq!(bucket, "b");
        assert_eq!(key, "k");
        assert_eq!(upload_id, "UP-T1");
        assert!(abort_authorized);
    }

    #[test]
    fn tombstone_is_picked_up_by_drop_when_authorized() {
        let attrs = FileAttributes::default();
        let state = build_write_tombstone("b", "k", &attrs, "UP-T2".to_string(), true);
        let HandleState::Write { phase, .. } = state else {
            panic!("tombstone must be HandleState::Write");
        };
        assert_eq!(should_abort_on_drop(&phase), Some("UP-T2"));
    }

    #[test]
    fn tombstone_is_skipped_by_drop_when_abort_denied() {
        // Principal with Deny on s3:AbortMultipartUpload: Drop must
        // skip the call. The tombstone still exists in the map so
        // operators see the orphaned upload_id in the skip log. The
        // bucket lifecycle rule reclaims parts.
        let attrs = FileAttributes::default();
        let state = build_write_tombstone("b", "k", &attrs, "UP-T3".to_string(), false);
        let HandleState::Write { phase, .. } = state else {
            panic!("tombstone must be HandleState::Write");
        };
        assert!(should_abort_on_drop(&phase).is_none());
    }

    // SFTPv3 draft section 6.3 rule: EXCL and TRUNC are modifiers of
    // CREAT. The open() handler boundary check lives in a free function
    // so these tests exercise it without a StorageBackend mock. WRITE
    // without CREATE or TRUNCATE passes the boundary check.
    // open_write rejects that combination at the next gate with
    // OpUnsupported because the streaming write path requires
    // CREATE | TRUNCATE.

    #[test]
    fn open_flags_excl_without_create_is_rejected() {
        assert!(rejects_excl_or_trunc_without_create(OpenFlags::EXCLUDE | OpenFlags::WRITE));
    }

    #[test]
    fn open_flags_trunc_without_create_is_rejected() {
        assert!(rejects_excl_or_trunc_without_create(OpenFlags::TRUNCATE | OpenFlags::WRITE));
    }

    #[test]
    fn open_flags_excl_with_create_is_allowed() {
        assert!(!rejects_excl_or_trunc_without_create(
            OpenFlags::CREATE | OpenFlags::EXCLUDE | OpenFlags::WRITE
        ));
    }

    #[test]
    fn open_flags_trunc_with_create_is_allowed() {
        assert!(!rejects_excl_or_trunc_without_create(
            OpenFlags::CREATE | OpenFlags::TRUNCATE | OpenFlags::WRITE
        ));
    }

    #[test]
    fn open_flags_plain_read_or_write_is_allowed() {
        assert!(!rejects_excl_or_trunc_without_create(OpenFlags::READ));
        assert!(!rejects_excl_or_trunc_without_create(OpenFlags::WRITE));
    }

    // write_dispatch_byte_count: covers the three phases and the
    // strict-vs-saturating overflow behaviour.

    #[test]
    fn byte_count_buffering_returns_buffer_len() {
        // 777 is arbitrary. The test would fail if the Buffering arm
        // returned 0 or a derived value instead of the buffer length.
        let phase = WritePhase::Buffering {
            part_buffer: vec![0u8; 777],
        };
        assert_eq!(write_dispatch_byte_count(&phase, 5 * 1024 * 1024, true).unwrap(), 777);
        assert_eq!(write_dispatch_byte_count(&phase, 5 * 1024 * 1024, false).unwrap(), 777);
    }

    #[test]
    fn byte_count_streaming_overflow_strict_errs() {
        // next_part_number = i32::MAX and part_size = u64::MAX causes
        // checked_mul to overflow. Strict mode must propagate the
        // overflow rather than silently saturate.
        let phase = WritePhase::Streaming {
            upload_id: "X".to_string(),
            abort_authorized: true,
            part_buffer: Vec::new(),
            uploaded_parts: Vec::new(),
            next_part_number: i32::MAX,
        };
        assert!(write_dispatch_byte_count(&phase, u64::MAX, false).is_err());
    }

    #[test]
    fn byte_count_streaming_overflow_saturates_to_u64_max() {
        // Same overflow inputs as above but saturating mode. Would
        // fail if the function returned 0 or dropped the saturation.
        let phase = WritePhase::Streaming {
            upload_id: "X".to_string(),
            abort_authorized: true,
            part_buffer: Vec::new(),
            uploaded_parts: Vec::new(),
            next_part_number: i32::MAX,
        };
        assert_eq!(write_dispatch_byte_count(&phase, u64::MAX, true).unwrap(), u64::MAX);
    }

    #[test]
    fn byte_count_failed_strict_errs() {
        // Strict mode must refuse to calculate a size for a Failed
        // handle. The sequential-offset check relies on this to
        // reject further writes.
        let phase = WritePhase::Failed {
            upload_id: "X".to_string(),
            abort_authorized: true,
        };
        assert!(write_dispatch_byte_count(&phase, 5 * 1024 * 1024, false).is_err());
    }

    #[test]
    fn byte_count_failed_saturating_returns_zero() {
        // Saturating mode returns 0 for Failed so the post-flush
        // attrs.size update is infallible.
        let phase = WritePhase::Failed {
            upload_id: "X".to_string(),
            abort_authorized: true,
        };
        assert_eq!(write_dispatch_byte_count(&phase, 5 * 1024 * 1024, true).unwrap(), 0);
    }

    // write_dispatch_has_full_part: boundary behaviour plus the
    // Failed-variant short-circuit.

    #[test]
    fn has_full_part_boundary_at_exact_part_size() {
        let part_size: u64 = 1024;
        let at = WritePhase::Buffering {
            part_buffer: vec![0u8; 1024],
        };
        let below = WritePhase::Buffering {
            part_buffer: vec![0u8; 1023],
        };
        let above = WritePhase::Buffering {
            part_buffer: vec![0u8; 1025],
        };
        // The predicate uses >=, so exactly part_size is true.
        assert!(write_dispatch_has_full_part(&at, part_size));
        assert!(!write_dispatch_has_full_part(&below, part_size));
        assert!(write_dispatch_has_full_part(&above, part_size));
    }

    #[test]
    fn has_full_part_failed_returns_false() {
        // Failed carries no buffer so the predicate must not yield
        // true and keep the drain loop spinning.
        let phase = WritePhase::Failed {
            upload_id: "X".to_string(),
            abort_authorized: true,
        };
        assert!(!write_dispatch_has_full_part(&phase, 0));
        assert!(!write_dispatch_has_full_part(&phase, u64::MAX));
    }

    // write_dispatch_append_bytes: Buffering arm, Failed arm.

    #[test]
    fn append_bytes_buffering_arm_extends() {
        let mut phase = WritePhase::Buffering {
            part_buffer: vec![1u8, 2, 3],
        };
        write_dispatch_append_bytes(&mut phase, &[9u8, 9, 9]).unwrap();
        match &phase {
            WritePhase::Buffering { part_buffer } => {
                assert_eq!(part_buffer.as_slice(), &[1, 2, 3, 9, 9, 9]);
            }
            WritePhase::Streaming { .. } => panic!("append_bytes promoted Buffering to Streaming"),
            WritePhase::Failed { .. } => panic!("append_bytes poisoned Buffering to Failed"),
        }
    }

    #[test]
    fn append_bytes_failed_arm_returns_err() {
        let mut phase = WritePhase::Failed {
            upload_id: "X".to_string(),
            abort_authorized: true,
        };
        assert!(write_dispatch_append_bytes(&mut phase, &[1u8, 2, 3]).is_err());
        // The phase must remain Failed after the rejected call.
        // Any silent promotion or buffer attachment would be a bug.
        assert!(matches!(phase, WritePhase::Failed { .. }));
    }

    // fstat_reported_size: covers the Buffering/Streaming arithmetic
    // and the Failed-preserves-cached-size rule.

    #[test]
    fn fstat_reported_size_buffering_uses_buffer_length() {
        let phase = WritePhase::Buffering {
            part_buffer: vec![0u8; 512],
        };
        assert_eq!(fstat_reported_size(&phase, 5 * 1024 * 1024, 0), 512);
    }

    #[test]
    fn fstat_reported_size_streaming_combines_parts_and_buffer() {
        let part_size: u64 = 5 * 1024 * 1024;
        let phase = WritePhase::Streaming {
            upload_id: "X".to_string(),
            abort_authorized: true,
            part_buffer: vec![0u8; 1024],
            uploaded_parts: Vec::new(),
            // next_part_number==3 means parts 1 and 2 have been flushed.
            next_part_number: 3,
        };
        assert_eq!(fstat_reported_size(&phase, part_size, 0), 2 * part_size + 1024);
    }

    #[test]
    fn fstat_reported_size_streaming_saturates_on_overflow() {
        let phase = WritePhase::Streaming {
            upload_id: "X".to_string(),
            abort_authorized: true,
            part_buffer: Vec::new(),
            uploaded_parts: Vec::new(),
            next_part_number: i32::MAX,
        };
        assert_eq!(fstat_reported_size(&phase, u64::MAX, 0), u64::MAX);
    }

    #[test]
    fn fstat_reported_size_failed_returns_cached_size() {
        let phase = WritePhase::Failed {
            upload_id: "X".to_string(),
            abort_authorized: false,
        };
        assert_eq!(fstat_reported_size(&phase, 1_000_000, 42_000), 42_000);
        // And that cached_size == 0 is still reported as 0 rather than
        // some derived value, so clients see "nothing confirmed to S3"
        // rather than a misleading partial count.
        assert_eq!(fstat_reported_size(&phase, 1_000_000, 0), 0);
    }

    // Adversarial-input coverage for the write-dispatch running-total
    // helper. The helper composes two checked operations on u64:
    // parts_done * part_size (from the Streaming arm's
    // next_part_number and the configured part_size) and the
    // part_buffer length. The proptest block below biases part_size
    // and next_part_number toward boundaries so
    // parts_done * part_size reaches u64::MAX, then asserts the two
    // modes preserve their documented contracts. Strict mode returns
    // Err(Failure) on overflow and Err(Failure) on the Failed arm.
    // Saturating mode returns Ok(u64::MAX) on overflow and Ok(0) on
    // the Failed arm. Both modes return Ok(buffer_len) for Buffering.
    // The part_buffer length is kept small to bound the per-case Vec
    // allocation. Boundary stress lives in part_size and
    // next_part_number.
    proptest::proptest! {
        #![proptest_config(proptest::prelude::ProptestConfig {
            cases: 10_000,
            .. proptest::prelude::ProptestConfig::default()
        })]

        #[test]
        fn write_dispatch_byte_count_preserves_overflow_contract(
            part_buffer_len in 0usize..=1024,
            next_part_number in proptest::prop_oneof![
                proptest::prelude::Just(1i32),
                proptest::prelude::Just(2i32),
                proptest::prelude::Just(i32::MAX - 1),
                proptest::prelude::Just(i32::MAX),
                1i32..=i32::MAX,
            ],
            part_size in proptest::prop_oneof![
                proptest::prelude::Just(0u64),
                proptest::prelude::Just(1u64),
                proptest::prelude::Just(2u64),
                proptest::prelude::Just(u64::MAX / 3),
                proptest::prelude::Just(u64::MAX / 2),
                proptest::prelude::Just(u64::MAX - 1),
                proptest::prelude::Just(u64::MAX),
                1u64..=(16 * 1024 * 1024),
                proptest::prelude::any::<u64>(),
            ],
            phase_variant in 0u8..3,
            saturating in proptest::prelude::any::<bool>(),
        ) {
            let part_buffer = vec![0u8; part_buffer_len];
            let buffer_len_u64 = part_buffer_len as u64;
            let phase = match phase_variant {
                0 => WritePhase::Buffering {
                    part_buffer,
                },
                1 => WritePhase::Streaming {
                    upload_id: "UP-proptest".to_string(),
                    abort_authorized: true,
                    part_buffer,
                    uploaded_parts: Vec::new(),
                    next_part_number,
                },
                _ => WritePhase::Failed {
                    upload_id: "UP-proptest".to_string(),
                    abort_authorized: false,
                },
            };

            let result = write_dispatch_byte_count(&phase, part_size, saturating);

            match &phase {
                WritePhase::Buffering { .. } => {
                    let value = result.map_err(|e| e.0).expect("Buffering arm must yield Ok");
                    proptest::prop_assert_eq!(value, buffer_len_u64);
                }
                WritePhase::Streaming { .. } => {
                    let parts_done = (next_part_number - 1) as u64;
                    let expected = parts_done
                        .checked_mul(part_size)
                        .and_then(|base| base.checked_add(buffer_len_u64));
                    match expected {
                        Some(sum) => {
                            let value = result
                                .map_err(|e| e.0)
                                .expect("Streaming arm must yield Ok when the sum fits in u64");
                            proptest::prop_assert_eq!(value, sum);
                        }
                        None => {
                            if saturating {
                                let value = result
                                    .map_err(|e| e.0)
                                    .expect("Saturating Streaming overflow must return Ok(u64::MAX)");
                                proptest::prop_assert_eq!(value, u64::MAX);
                            } else {
                                proptest::prop_assert!(
                                    matches!(
                                        &result,
                                        Err(err) if matches!(err.0, StatusCode::Failure)
                                    ),
                                    "Strict Streaming overflow must return Err(Failure), got {:?}",
                                    result,
                                );
                            }
                        }
                    }
                }
                WritePhase::Failed { .. } => {
                    if saturating {
                        let value = result
                            .map_err(|e| e.0)
                            .expect("Saturating Failed arm must return Ok(0)");
                        proptest::prop_assert_eq!(value, 0u64);
                    } else {
                        proptest::prop_assert!(
                            matches!(
                                &result,
                                Err(err) if matches!(err.0, StatusCode::Failure)
                            ),
                            "Strict Failed arm must return Err(Failure), got {:?}",
                            result,
                        );
                    }
                }
            }
        }
    }
    /// Streaming with abort_authorized=true at parts limit: transition to Failed, flag preserved.
    #[tokio::test]
    async fn flush_one_part_parts_limit_keeps_abort_authorized_true() {
        let backend = Arc::new(DummyBackend::new());
        let driver = build_driver(backend, TEST_PART_SIZE);
        let mut phase = WritePhase::Streaming {
            upload_id: "UP-OVER".to_string(),
            abort_authorized: true,
            part_buffer: vec![0u8; TEST_PART_SIZE as usize],
            uploaded_parts: Vec::new(),
            next_part_number: S3_MAX_MULTIPART_PARTS + 1,
        };
        let err = driver
            .write_dispatch_flush_one_part(&mut phase, "b", "k", TEST_PART_SIZE)
            .await
            .expect_err("parts-limit breach must return Err");
        assert!(matches!(err.0, StatusCode::Failure));
        let WritePhase::Failed {
            upload_id,
            abort_authorized,
        } = phase
        else {
            panic!("phase must transition to Failed on parts-limit breach");
        };
        assert_eq!(upload_id, "UP-OVER");
        assert!(abort_authorized, "Failed variant must carry abort_authorized=true forward");
    }

    /// Streaming with abort_authorized=false at parts limit: transition to Failed, flag preserved.
    #[tokio::test]
    async fn flush_one_part_parts_limit_keeps_abort_authorized_false() {
        let backend = Arc::new(DummyBackend::new());
        let driver = build_driver(backend, TEST_PART_SIZE);
        let mut phase = WritePhase::Streaming {
            upload_id: "UP-OVER-DENY".to_string(),
            abort_authorized: false,
            part_buffer: vec![0u8; TEST_PART_SIZE as usize],
            uploaded_parts: Vec::new(),
            next_part_number: S3_MAX_MULTIPART_PARTS + 1,
        };
        let err = driver
            .write_dispatch_flush_one_part(&mut phase, "b", "k", TEST_PART_SIZE)
            .await
            .expect_err("parts-limit breach must return Err even when abort_authorized=false");
        assert!(matches!(err.0, StatusCode::Failure));
        let WritePhase::Failed { abort_authorized, .. } = phase else {
            panic!("phase must transition to Failed");
        };
        assert!(!abort_authorized, "Deny-cached abort_authorized must survive the transition");
    }

    // --- UploadPart failure inside write_dispatch_flush_one_part ---

    /// UploadPart backend error: transition to Failed carrying upload_id and abort_authorized.
    #[tokio::test]
    async fn flush_one_part_upload_err_transitions_to_failed() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_upload_part_err(DummyError::Injected("upload_part backend failure".to_string()));
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);
        let mut phase = WritePhase::Streaming {
            upload_id: "UP-FLUSH-ERR".to_string(),
            abort_authorized: true,
            part_buffer: vec![0u8; TEST_PART_SIZE as usize],
            uploaded_parts: Vec::new(),
            next_part_number: 1,
        };
        let err =
            with_test_auth_override(|_, _, _| true, driver.write_dispatch_flush_one_part(&mut phase, "b", "k", TEST_PART_SIZE))
                .await
                .expect_err("upload_part failure must propagate as Err");
        assert!(matches!(err.0, StatusCode::Failure));
        let WritePhase::Failed {
            upload_id,
            abort_authorized,
        } = phase
        else {
            panic!("phase must transition to Failed after UploadPart error");
        };
        assert_eq!(upload_id, "UP-FLUSH-ERR");
        assert!(abort_authorized, "abort_authorized must be carried into Failed");
        assert_eq!(
            backend.upload_part_calls().len(),
            1,
            "the failed UploadPart must still have been dispatched once"
        );
    }

    // --- write_dispatch_begin_streaming ---

    /// begin_streaming installs a tombstone before any subsequent await
    /// so a cancelled future leaves a recoverable handle for Drop to abort.
    #[tokio::test]
    async fn begin_streaming_installs_tombstone_before_await() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_create_multipart_upload_ok("UP-BEG-1");
        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);

        // Pre-populate the handle map so the tombstone lands under the
        // same id we query afterwards.
        let handle_id = driver
            .allocate_handle(write_handle("b", "k", WritePhase::Buffering { part_buffer: Vec::new() }))
            .expect("allocate");

        // The test owns the phase local so we can observe the
        // Buffering->Streaming transition independently of the handle
        // table the driver maintains.
        let mut phase = WritePhase::Buffering {
            part_buffer: vec![1, 2, 3, 4],
        };

        with_test_auth_override(
            |_, _, _| true,
            driver.write_dispatch_begin_streaming(&handle_id, &mut phase, "b", "k", &FileAttributes::empty()),
        )
        .await
        .expect("begin_streaming must succeed on queued Create Ok");

        // Tombstone invariant: the driver.handles entry under handle_id
        // is now a Failed-variant HandleState carrying the upload_id.
        let tombstone = driver.handles.get(&handle_id).expect("tombstone present in handle map");
        let HandleState::Write {
            phase: WritePhase::Failed { upload_id, .. },
            ..
        } = tombstone
        else {
            panic!("tombstone must be a Write handle with Failed phase");
        };
        assert_eq!(upload_id, "UP-BEG-1");

        // Local phase transitioned to Streaming and preserved the buffered bytes.
        let WritePhase::Streaming {
            upload_id: streaming_upload_id,
            part_buffer,
            next_part_number,
            ..
        } = phase
        else {
            panic!("local phase must be Streaming after begin_streaming");
        };
        assert_eq!(streaming_upload_id, "UP-BEG-1");
        assert_eq!(part_buffer, vec![1, 2, 3, 4], "buffered bytes must survive the transition");
        assert_eq!(next_part_number, 1, "next_part_number starts at 1 on entry to Streaming");
    }

    // --- start_multipart_upload ---

    #[tokio::test]
    async fn start_multipart_upload_caches_abort_authorized_true_on_allow() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_create_multipart_upload_ok("UP-ALLOW");
        let driver = build_driver(backend, TEST_PART_SIZE);

        let mp = with_test_auth_override(|_, _, _| true, driver.start_multipart_upload("b", "k", &FileAttributes::empty()))
            .await
            .expect("start_multipart_upload must succeed on Allow");
        assert_eq!(mp.upload_id, "UP-ALLOW");
        assert!(mp.abort_authorized, "Allow on AbortMultipartUpload probe must cache as true");
    }

    #[tokio::test]
    async fn start_multipart_upload_preserves_open_attrs_as_metadata() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_create_multipart_upload_ok("UP-ATTRS");
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);
        let attrs = FileAttributes {
            size: None,
            uid: Some(1000),
            gid: Some(1001),
            user: None,
            group: None,
            permissions: Some(0o100640),
            atime: None,
            mtime: Some(1_777_992_333),
        };

        with_test_auth_override(|_, _, _| true, driver.start_multipart_upload("b", "k", &attrs))
            .await
            .expect("start_multipart_upload must succeed");

        let calls = backend.create_multipart_calls();
        assert_eq!(calls.len(), 1);
        let metadata = calls[0].metadata.as_ref().expect("OPEN attrs must become S3 user metadata");
        assert_eq!(metadata.get("mtime").map(String::as_str), Some("1777992333"));
        assert_eq!(metadata.get("mode").map(String::as_str), Some("33184"));
        assert_eq!(metadata.get("uid").map(String::as_str), Some("1000"));
        assert_eq!(metadata.get("gid").map(String::as_str), Some("1001"));
    }

    #[tokio::test]
    async fn start_multipart_upload_omits_metadata_when_open_attrs_empty() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_create_multipart_upload_ok("UP-NO-ATTRS");
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        with_test_auth_override(|_, _, _| true, driver.start_multipart_upload("b", "k", &FileAttributes::empty()))
            .await
            .expect("start_multipart_upload must succeed");

        let calls = backend.create_multipart_calls();
        assert_eq!(calls.len(), 1);
        assert!(calls[0].metadata.is_none(), "empty OPEN attrs must not write default metadata");
    }

    #[tokio::test]
    async fn start_multipart_upload_caches_abort_authorized_false_on_deny_abort() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_create_multipart_upload_ok("UP-DENY-ABORT");
        let driver = build_driver(backend, TEST_PART_SIZE);

        // Allow CreateMultipartUpload, deny AbortMultipartUpload. Mirrors
        // a WORM-shaped IAM policy a principal can meet in production.
        let mp = with_test_auth_override(
            |action, _bucket, _object| !matches!(action, S3Action::AbortMultipartUpload),
            driver.start_multipart_upload("b", "k", &FileAttributes::empty()),
        )
        .await
        .expect("Create Allow must succeed even when Abort is Deny");
        assert_eq!(mp.upload_id, "UP-DENY-ABORT");
        assert!(
            !mp.abort_authorized,
            "Deny on AbortMultipartUpload probe must cache as false so Drop skips the abort"
        );
    }

    #[tokio::test]
    async fn start_multipart_upload_returns_err_when_create_authorize_denies() {
        let backend = Arc::new(DummyBackend::new());
        // No queued response: if the driver bypassed the authorize gate
        // it would hit DummyError::Unconfigured, which is not what we
        // assert here. The PermissionDenied from auth_err is the
        // expected outcome.
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |action, _, _| !matches!(action, S3Action::CreateMultipartUpload),
            driver.start_multipart_upload("b", "k", &FileAttributes::empty()),
        )
        .await
        .expect_err("Deny on CreateMultipartUpload must fail fast");
        assert!(matches!(err.0, StatusCode::PermissionDenied));
        assert!(
            backend.upload_part_calls().is_empty(),
            "Create authorize failure must not reach any backend call"
        );
    }

    #[tokio::test]
    async fn multipart_copy_starts_upload_without_sftp_open_metadata() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_create_multipart_upload_ok("UP-COPY");
        backend.queue_upload_part_copy_ok("etag-copy-1");
        backend.queue_complete_multipart_upload_ok();
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        with_test_auth_override(
            |_, _, _| true,
            driver.multipart_copy("src-bucket", "src-key", "dst-bucket", "dst-key", TEST_PART_SIZE),
        )
        .await
        .expect("multipart copy must complete");

        let create_calls = backend.create_multipart_calls();
        assert_eq!(create_calls.len(), 1);
        assert_eq!(create_calls[0].bucket, "dst-bucket");
        assert_eq!(create_calls[0].key, "dst-key");
        assert!(
            create_calls[0].metadata.is_none(),
            "server-side multipart copy is not an SFTP OPEN path and must not write OPEN metadata"
        );

        let complete_calls = backend.complete_multipart_calls();
        assert_eq!(complete_calls.len(), 1);
        assert_eq!(complete_calls[0].upload_id, "UP-COPY");
        assert_eq!(complete_calls[0].part_count, 1);
        assert!(backend.abort_multipart_calls().is_empty(), "successful multipart copy must not abort");
    }

    // --- upload_multipart_bytes ---

    #[tokio::test]
    async fn upload_multipart_bytes_returns_err_when_response_lacks_etag() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_upload_part_ok_without_etag();
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let result =
            with_test_auth_override(|_, _, _| true, driver.upload_multipart_bytes("b", "k", "UP-NO-ETAG", 1, vec![0u8; 8])).await;
        let Err(err) = result else {
            panic!("missing ETag must not silently succeed");
        };
        assert!(matches!(err.0, StatusCode::Failure));
    }

    #[tokio::test]
    async fn upload_multipart_bytes_records_part_and_etag_on_success() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_upload_part_ok("etag-part-1");
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let completed =
            with_test_auth_override(|_, _, _| true, driver.upload_multipart_bytes("b", "k", "UP-OK", 1, vec![0u8; 8]))
                .await
                .expect("upload_part Ok must succeed");
        assert_eq!(completed.part_number, 1);
        let ETag::Strong(etag) = completed.e_tag else {
            panic!("DummyBackend queued a Strong ETag");
        };
        assert_eq!(etag, "etag-part-1");

        let calls = backend.upload_part_calls();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].upload_id, "UP-OK");
        assert_eq!(calls[0].part_number, 1);
        assert_eq!(calls[0].content_length, Some(8));
    }

    // --- close_streaming branches ---

    #[tokio::test]
    async fn close_streaming_parts_limit_breach_skips_abort_when_deny_cached() {
        let backend = Arc::new(DummyBackend::new());
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        // abort_authorized=false so close_abort_or_skip takes the skip
        // branch. Skip branch never calls the backend, so no authorize
        // override is needed. The returned Err propagates up.
        let err = driver
            .close_streaming(
                "b",
                "k",
                "UP-CAP-DENY".to_string(),
                false,
                vec![1u8; 16],
                Vec::new(),
                S3_MAX_MULTIPART_PARTS + 1,
            )
            .await
            .expect_err("parts-limit breach must return Err");
        assert!(matches!(err.0, StatusCode::Failure));
        assert!(
            backend.abort_multipart_calls().is_empty(),
            "Deny-cached abort_authorized must take the skip-log path, not the backend call"
        );
    }

    #[tokio::test]
    async fn close_streaming_parts_limit_breach_calls_abort_when_allow_cached() {
        let backend = Arc::new(DummyBackend::new());
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |_, _, _| true,
            driver.close_streaming(
                "b",
                "k",
                "UP-CAP-ALLOW".to_string(),
                true,
                vec![1u8; 16],
                Vec::new(),
                S3_MAX_MULTIPART_PARTS + 1,
            ),
        )
        .await
        .expect_err("parts-limit breach returns Err even when abort is Allow");
        assert!(matches!(err.0, StatusCode::Failure));
        let calls = backend.abort_multipart_calls();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].upload_id, "UP-CAP-ALLOW");
    }

    #[tokio::test]
    async fn close_streaming_complete_multipart_ok_returns_ok_without_abort() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_upload_part_ok("etag-tail");
        backend.queue_complete_multipart_upload_ok();
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        with_test_auth_override(
            |_, _, _| true,
            driver.close_streaming("b", "k", "UP-COMPLETE-OK".to_string(), true, vec![0u8; 16], Vec::new(), 1),
        )
        .await
        .expect("close_streaming must return Ok on Complete success");
        assert!(backend.abort_multipart_calls().is_empty(), "successful Complete must not fire an abort");
        assert_eq!(backend.complete_multipart_calls().len(), 1);
    }

    #[tokio::test]
    async fn close_streaming_complete_multipart_err_calls_abort() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_upload_part_ok("etag-tail");
        backend.queue_complete_multipart_upload_err(DummyError::Injected("complete failed".to_string()));
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |_, _, _| true,
            driver.close_streaming("b", "k", "UP-COMPLETE-ERR".to_string(), true, vec![0u8; 16], Vec::new(), 1),
        )
        .await
        .expect_err("Complete failure must propagate");
        assert!(matches!(err.0, StatusCode::Failure));
        let aborts = backend.abort_multipart_calls();
        assert_eq!(aborts.len(), 1, "Complete failure must trigger one abort");
        assert_eq!(aborts[0].upload_id, "UP-COMPLETE-ERR");
    }

    #[tokio::test]
    async fn close_streaming_trailing_upload_part_err_calls_abort() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_upload_part_err(DummyError::Injected("trailing upload failed".to_string()));
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |_, _, _| true,
            driver.close_streaming("b", "k", "UP-TAIL-ERR".to_string(), true, vec![0u8; 16], Vec::new(), 1),
        )
        .await
        .expect_err("trailing UploadPart failure must propagate");
        assert!(matches!(err.0, StatusCode::Failure));
        let aborts = backend.abort_multipart_calls();
        assert_eq!(aborts.len(), 1, "trailing UploadPart failure must trigger one abort");
        assert_eq!(aborts[0].upload_id, "UP-TAIL-ERR");
        assert!(
            backend.complete_multipart_calls().is_empty(),
            "Complete must not be called when the tail UploadPart failed"
        );
    }

    #[tokio::test]
    async fn commit_write_preserves_open_attrs_as_metadata() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_put_object_ok();
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);
        let attrs = FileAttributes {
            size: None,
            uid: Some(501),
            gid: Some(20),
            user: None,
            group: None,
            permissions: Some(0o100600),
            atime: None,
            mtime: Some(1_777_992_348),
        };

        driver
            .commit_write("b", "k", &attrs, b"hello".to_vec())
            .await
            .expect("commit_write must succeed");

        let calls = backend.put_object_calls();
        assert_eq!(calls.len(), 1);
        let metadata = calls[0].metadata.as_ref().expect("OPEN attrs must become S3 user metadata");
        assert_eq!(metadata.get("mtime").map(String::as_str), Some("1777992348"));
        assert_eq!(metadata.get("mode").map(String::as_str), Some("33152"));
        assert_eq!(metadata.get("uid").map(String::as_str), Some("501"));
        assert_eq!(metadata.get("gid").map(String::as_str), Some("20"));
    }

    #[tokio::test]
    async fn commit_write_omits_metadata_when_open_attrs_empty() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_put_object_ok();
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        driver
            .commit_write("b", "k", &FileAttributes::empty(), b"hello".to_vec())
            .await
            .expect("commit_write must succeed");

        let calls = backend.put_object_calls();
        assert_eq!(calls.len(), 1);
        assert!(calls[0].metadata.is_none(), "empty OPEN attrs must not write default metadata");
    }

    // --- open_write strict-flag gate ---

    #[tokio::test]
    async fn open_write_write_only_returns_op_unsupported() {
        // OpenFlags::WRITE without CREATE or TRUNCATE is rejected at
        // OPEN. No HEAD call is issued because the gate is flag-only.
        let backend = Arc::new(DummyBackend::new());
        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |_, _, _| true,
            driver.open_write(7, "/bucket/key", OpenFlags::WRITE, FileAttributes::default()),
        )
        .await
        .expect_err("WRITE without CREATE or TRUNCATE must be rejected");
        assert!(matches!(err.0, StatusCode::OpUnsupported));
        assert!(
            backend.head_object_calls().is_empty(),
            "no HEAD call should be issued on the strict-flag rejection path"
        );
    }

    #[tokio::test]
    async fn open_write_create_without_trunc_returns_op_unsupported() {
        // WRITE | CREATE without TRUNCATE is rejected at OPEN. The
        // streaming write path cannot honour create-or-modify-existing
        // semantics, so the rejection is unconditional and no HEAD is
        // issued.
        let backend = Arc::new(DummyBackend::new());
        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |_, _, _| true,
            driver.open_write(7, "/bucket/key", OpenFlags::WRITE | OpenFlags::CREATE, FileAttributes::default()),
        )
        .await
        .expect_err("WRITE | CREATE without TRUNCATE must be rejected");
        assert!(matches!(err.0, StatusCode::OpUnsupported));
        assert!(
            backend.head_object_calls().is_empty(),
            "no HEAD call should be issued on the strict-flag rejection path"
        );
    }

    #[tokio::test]
    async fn open_write_create_and_trunc_succeeds_on_missing_file() {
        // WRITE | CREATE | TRUNCATE on a missing file: no HEAD
        // response is queued. If the OPEN attempted a HEAD it would
        // trigger a DummyBackend panic. The successful return proves
        // the non-EXCL accept path allocates a handle without
        // consulting backend object state.
        let backend = Arc::new(DummyBackend::new());
        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let handle = with_test_auth_override(
            |_, _, _| true,
            driver.open_write(
                9,
                "/bucket/missing_key",
                OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
                FileAttributes::default(),
            ),
        )
        .await
        .expect("WRITE | CREATE | TRUNCATE on a missing file must allocate a handle");
        assert!(!handle.handle.is_empty());
        assert!(backend.head_object_calls().is_empty(), "the non-EXCL accept path must not HEAD");
    }

    #[tokio::test]
    async fn open_write_create_and_trunc_succeeds_on_existing_file() {
        // WRITE | CREATE | TRUNCATE on an existing file: queue a
        // HEAD-Ok response. The OPEN succeeds and the queued HEAD
        // response is not consumed, proving the non-EXCL accept path
        // is independent of backend object state.
        let backend = Arc::new(DummyBackend::new());
        backend.queue_head_object_ok(42, None);
        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let handle = with_test_auth_override(
            |_, _, _| true,
            driver.open_write(
                10,
                "/bucket/existing_key",
                OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
                FileAttributes::default(),
            ),
        )
        .await
        .expect("WRITE | CREATE | TRUNCATE on an existing file must allocate a handle");
        assert!(!handle.handle.is_empty());
        assert!(backend.head_object_calls().is_empty(), "the non-EXCL accept path must not HEAD");
    }

    // --- open_write EXCL ---

    #[tokio::test]
    async fn open_write_excl_rejects_existing_object_with_failure() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_head_object_ok(42, None);
        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |_, _, _| true,
            driver.open_write(
                7,
                "/bucket/key",
                OpenFlags::CREATE | OpenFlags::TRUNCATE | OpenFlags::EXCLUDE | OpenFlags::WRITE,
                FileAttributes::default(),
            ),
        )
        .await
        .expect_err("EXCL on an existing object must fail");
        assert!(matches!(err.0, StatusCode::Failure));
        let heads = backend.head_object_calls();
        assert_eq!(heads.len(), 1);
        assert_eq!(heads[0].bucket, "bucket");
        assert_eq!(heads[0].key, "key");
    }

    #[tokio::test]
    async fn open_write_excl_allows_creation_when_head_object_not_found() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_head_object_not_found();
        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let handle = with_test_auth_override(
            |_, _, _| true,
            driver.open_write(
                9,
                "/bucket/key2",
                OpenFlags::CREATE | OpenFlags::TRUNCATE | OpenFlags::EXCLUDE | OpenFlags::WRITE,
                FileAttributes::default(),
            ),
        )
        .await
        .expect("EXCL on a missing key must allow creation");
        assert!(!handle.handle.is_empty());
        assert_eq!(backend.head_object_calls().len(), 1, "EXCL must HEAD exactly once");
    }

    // --- abort_upload_with_auth Deny ---

    #[tokio::test]
    async fn abort_upload_with_auth_deny_returns_err_without_backend_call() {
        let backend = Arc::new(DummyBackend::new());
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = with_test_auth_override(
            |action, _, _| !matches!(action, S3Action::AbortMultipartUpload),
            driver.abort_upload_with_auth("b", "k", "UP-ABORT-DENY"),
        )
        .await
        .expect_err("Deny on abort must fail before reaching the backend");
        assert!(matches!(err.0, StatusCode::PermissionDenied));
        assert!(backend.abort_multipart_calls().is_empty(), "Deny path must not call the backend");
    }

    // --- full-flow cancellation test ---

    /// Cancel a stalled UploadPart future, drop the driver, verify Drop
    /// finds the tombstone and fires AbortMultipartUpload exactly once.
    #[tokio::test]
    async fn cancel_mid_upload_part_drop_aborts() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_create_multipart_upload_ok("UP-CANCEL");
        let entered = Arc::new(tokio::sync::Notify::new());
        backend.stall_upload_part(entered.clone());

        let mut driver = build_driver(backend.clone(), TEST_PART_SIZE);
        let handle_id = driver
            .allocate_handle(write_handle("b", "k", WritePhase::Buffering { part_buffer: Vec::new() }))
            .expect("allocate");
        let mut state = driver.handles.remove(&handle_id).expect("remove");

        // One full part's worth of bytes so write_dispatch transitions
        // Buffering->Streaming (installs the tombstone synchronously)
        // and then calls upload_part, which the DummyBackend stalls.
        let data = vec![0u8; TEST_PART_SIZE as usize];
        let write_fut = driver.write_dispatch(&handle_id, &mut state, 0, data);

        with_test_auth_override(|_, _, _| true, async {
            tokio::select! {
                biased;
                _ = entered.notified() => {
                    // upload_part has been entered. Fall through so
                    // write_fut is dropped on exit from this block.
                }
                _ = write_fut => {
                    panic!("write_dispatch must stall inside upload_part, not complete");
                }
            }
        })
        .await;

        // state is dropped unrestored. The tombstone that
        // write_dispatch_begin_streaming installed remains in driver.handles.
        drop(state);

        // Confirm the tombstone is actually sitting in the map before Drop runs.
        let pre_drop = driver.handles.get(&handle_id).expect("tombstone must survive cancellation");
        let HandleState::Write {
            phase: WritePhase::Failed {
                upload_id,
                abort_authorized,
            },
            ..
        } = pre_drop
        else {
            panic!("surviving handle must be a Failed tombstone");
        };
        assert_eq!(upload_id, "UP-CANCEL");
        assert!(*abort_authorized, "Allow cached at Create must make Drop fire abort");

        // Dropping the driver spawns an abort task on the current-thread
        // runtime. yield_now lets the spawned task poll to completion
        // against the DummyBackend (which returns Ok synchronously).
        drop(driver);
        tokio::task::yield_now().await;
        tokio::task::yield_now().await;

        let aborts = backend.abort_multipart_calls();
        assert_eq!(aborts.len(), 1, "Drop must fire exactly one abort for the surviving tombstone");
        let AbortCall { bucket, key, upload_id } = &aborts[0];
        assert_eq!(bucket, "b");
        assert_eq!(key, "k");
        assert_eq!(upload_id, "UP-CANCEL");
    }

    // --- compliance matrix cross-reference tests ---
    //
    // Row 5 SSH_FXP_READ: boundary cases (pre-IAM) plus happy path.
    // Row 8 SSH_FXP_FSTAT: cached attrs on File and running count on Write.
    // Row 10 SSH_FXP_FSETSTAT: unconditional ok_status.

    /// commit_write against a stalling put_object must return Failure
    /// within the configured backend deadline. Anchors the C1 contract:
    /// a backend that accepted the request and never returned a body
    /// surfaces as Failure on the wire rather than blocking the
    /// session indefinitely. The 1 s deadline keeps the test runtime
    /// under two seconds while still covering the deadline-elapsed
    /// branch end to end (driver setup, run_backend wrap, stalling
    /// put_object, Failure emission).
    #[tokio::test(flavor = "current_thread")]
    async fn commit_write_returns_failure_when_put_object_stalls_past_deadline() {
        let backend = Arc::new(DummyBackend::new());
        let entered = Arc::new(Notify::new());
        backend.stall_put_object(entered.clone());

        let driver = build_driver_with_timeout(backend, TEST_PART_SIZE, 1);

        // Outer guard timeout is generously above the inner deadline
        // so the assertion failure mode distinguishes "driver did not
        // honour the deadline" (outer fires) from "deadline fired but
        // mapped to the wrong status" (Ok(Err) with non-Failure).
        let outcome = tokio::time::timeout(
            Duration::from_secs(10),
            driver.commit_write("b", "k", &FileAttributes::default(), b"hello".to_vec()),
        )
        .await;

        let inner = outcome.expect("driver deadline must fire before the outer 10 s guard");
        let err = inner.expect_err("stalling backend must surface as Err");
        assert!(matches!(err.0, StatusCode::Failure));

        // The stall path notifies entered exactly once when put_object
        // first runs. Confirming the notify fired proves the stall
        // path was actually exercised (rather than the test passing
        // because some earlier validation rejected the call).
        entered.notified().await;
    }

    /// run_backend_with_err exposes the original backend Err to the
    /// caller so EXCLUDE create and HeadObject-then-list fallback
    /// paths can keep their is_not_found_error filters. The
    /// commit_write integration test covers the timeout path. The
    /// error pass-through is covered here.
    #[tokio::test(flavor = "current_thread")]
    async fn run_backend_with_err_passes_backend_error_through_unchanged() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_head_object_err(DummyError::AccessDenied("pinned".to_string()));
        let driver = build_driver(backend, TEST_PART_SIZE);

        let result = driver
            .run_backend_with_err("head_object", driver.storage.head_object("b", "k", "ak", "sk"))
            .await;

        match result {
            Ok(Err(e)) => assert!(matches!(e, DummyError::AccessDenied(_))),
            other => panic!("expected backend Err passed through; got {other:?}"),
        }
    }

    // --- bounded retry around PutObject in commit_write ---

    /// commit_write retries the PutObject call on a transient backend
    /// error and returns Ok once a retry succeeds. SlowDown is in the
    /// rustfs_utils retryable code set, so two SlowDown errors
    /// followed by an Ok exercises the retry loop end to end. The
    /// real backoff schedule (250 + 500 ms) keeps this test under a
    /// second of wall-clock.
    #[tokio::test(flavor = "current_thread")]
    async fn commit_write_retries_on_slow_down_and_succeeds_on_recovery() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_put_object_err(DummyError::Injected("SlowDown: backoff".into()));
        backend.queue_put_object_err(DummyError::Injected("SlowDown: backoff".into()));
        backend.queue_put_object_ok();
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        driver
            .commit_write("b", "k", &FileAttributes::default(), b"hello".to_vec())
            .await
            .expect("commit_write must succeed once a retry returns Ok");
        assert_eq!(
            backend.put_object_queue_len(),
            0,
            "all three queued responses must have been consumed (two retryable Errs plus one Ok)"
        );
    }

    /// commit_write surfaces Failure when the backend returns a
    /// retryable error past the cap. COMMIT_WRITE_MAX_RETRIES + 1
    /// SlowDown responses cover the initial attempt plus every retry,
    /// proving the loop bounds.
    #[tokio::test(flavor = "current_thread")]
    async fn commit_write_returns_failure_after_retry_cap_exhausted() {
        let backend = Arc::new(DummyBackend::new());
        for _ in 0..=COMMIT_WRITE_MAX_RETRIES {
            backend.queue_put_object_err(DummyError::Injected("SlowDown: backoff".into()));
        }
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = driver
            .commit_write("b", "k", &FileAttributes::default(), b"hello".to_vec())
            .await
            .expect_err("commit_write must surface the final retryable error after the cap");
        assert!(matches!(err.0, StatusCode::Failure));
        assert_eq!(
            backend.put_object_queue_len(),
            0,
            "every queued retryable response must have been consumed by the cap-exhausting attempts"
        );
    }

    /// commit_write must not retry on a terminal error like
    /// AccessDenied. The first call returns AccessDenied; no second
    /// call is issued, and the wire status is PermissionDenied (the
    /// s3_error_to_sftp mapping), not Failure.
    #[tokio::test(flavor = "current_thread")]
    async fn commit_write_does_not_retry_on_access_denied() {
        let backend = Arc::new(DummyBackend::new());
        backend.queue_put_object_err(DummyError::AccessDenied("policy".into()));
        // A second response so a retry attempt would surface a
        // wrong-status assertion failure rather than the
        // configured-miss default. If the loop wrongly retries, the
        // second pop is the Ok below and the test sees Ok instead of
        // PermissionDenied.
        backend.queue_put_object_ok();
        let driver = build_driver(backend.clone(), TEST_PART_SIZE);

        let err = driver
            .commit_write("b", "k", &FileAttributes::default(), b"hello".to_vec())
            .await
            .expect_err("commit_write must surface AccessDenied without retrying");
        assert!(matches!(err.0, StatusCode::PermissionDenied));
        assert_eq!(
            backend.put_object_queue_len(),
            1,
            "non-retryable error must not consume a second queued response"
        );
    }
}
