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

#![cfg(test)]

//! Object Lock E2E Tests
//!
//! These tests verify the complete Object Lock workflow including:
//! - COMPLIANCE mode blocks deletion
//! - GOVERNANCE mode blocks deletion without bypass header
//! - GOVERNANCE mode allows deletion with bypass header
//! - Legal Hold blocks deletion
//! - PutObjectRetention modification restrictions
//! - Default bucket retention is applied to new objects

use std::borrow::Borrow;

use super::common::*;
use aws_sdk_s3::Client;
use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
use aws_sdk_s3::primitives::{ByteStream, DateTimeFormat};
use aws_sdk_s3::types::{
    CompletedMultipartUpload, CompletedPart, Delete, MetadataDirective, ObjectIdentifier, ObjectLockLegalHoldStatus,
    ObjectLockMode, ObjectLockRetentionMode,
};
use chrono::{DateTime, Duration, Utc};
use tracing::info;

/// Initialize test logging
fn init_logging() {
    let _ = tracing_subscriber::fmt()
        .with_env_filter("e2e_test=debug,rustfs=info")
        .try_init();
}

async fn put_bucket_deny_policy(
    client: &Client,
    bucket: &str,
    sid: &str,
    action: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let policy = serde_json::json!({
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": sid,
            "Effect": "Deny",
            "Principal": "*",
            "Action": action,
            "Resource": format!("arn:aws:s3:::{}/*", bucket)
        }]
    })
    .to_string();

    client.put_bucket_policy().bucket(bucket).policy(policy).send().await?;
    Ok(())
}

fn retention_timestamp(days: i64) -> aws_sdk_s3::primitives::DateTime {
    let retain_until = future_retain_until(days).format("%Y-%m-%dT%H:%M:%SZ").to_string();
    aws_sdk_s3::primitives::DateTime::from_str(&retain_until, aws_sdk_s3::primitives::DateTimeFormat::DateTime)
        .expect("retention timestamp should parse")
}

fn assert_access_denied<T, E, R>(result: Result<T, R>, context: &str)
where
    T: std::fmt::Debug,
    E: ProvideErrorMetadata + std::fmt::Debug,
    R: Borrow<SdkError<E>> + std::fmt::Debug,
{
    let error = result.expect_err(context);
    let sdk_error = error.borrow();
    assert_eq!(
        sdk_error.raw_response().map(|response| response.status().as_u16()),
        Some(403),
        "{context}: expected HTTP 403, got: {error:?}"
    );
    assert_eq!(
        sdk_error.as_service_error().and_then(ProvideErrorMetadata::code),
        Some("AccessDenied"),
        "{context}: expected AccessDenied, got: {error:?}"
    );
}

fn assert_invalid_object_lock_retention_pair<T, E, R>(result: Result<T, R>, context: &str)
where
    T: std::fmt::Debug,
    E: ProvideErrorMetadata + std::fmt::Debug,
    R: Borrow<SdkError<E>> + std::fmt::Debug,
{
    let error = result.expect_err(context);
    let sdk_error = error.borrow();
    assert_eq!(
        sdk_error.raw_response().map(|response| response.status().as_u16()),
        Some(400),
        "{context}: expected HTTP 400, got: {error:?}"
    );
    let service_error = sdk_error.as_service_error().expect("expected an S3 service error");
    assert_eq!(
        service_error.code(),
        Some("InvalidRequest"),
        "{context}: expected InvalidRequest, got: {error:?}"
    );
    assert_eq!(
        service_error.message(),
        Some("x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied"),
        "{context}: unexpected error message: {error:?}"
    );
}

fn parse_s3_datetime(value: &aws_sdk_s3::primitives::DateTime) -> DateTime<Utc> {
    let formatted = value
        .fmt(DateTimeFormat::DateTime)
        .expect("S3 timestamp should format as RFC3339");
    DateTime::parse_from_rfc3339(&formatted)
        .expect("S3 timestamp should parse as RFC3339")
        .with_timezone(&Utc)
}

// ============================================================================
// DeleteObject Tests
// ============================================================================

#[tokio::test]
async fn test_delete_object_blocked_by_compliance_retention() {
    init_logging();
    info!("🧪 Test: DeleteObject blocked by COMPLIANCE retention");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-compliance-delete";
    let key = "locked-object";
    let data = b"test data for compliance mode";

    // Create bucket with Object Lock enabled
    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with COMPLIANCE retention (30 days in future)
    let retain_until = future_retain_until(30);
    let version_id = put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Compliance, retain_until)
        .await
        .unwrap();

    // Attempt to delete - should fail
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await,
        "Delete should fail for COMPLIANCE locked object",
    );

    // Even with bypass header, COMPLIANCE should not allow deletion
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&version_id), true).await,
        "Delete with bypass should still fail for COMPLIANCE mode",
    );

    info!("✅ Test passed: COMPLIANCE retention blocks deletion");
}

#[tokio::test]
async fn test_delete_object_blocked_by_governance_without_bypass() {
    init_logging();
    info!("🧪 Test: DeleteObject blocked by GOVERNANCE retention without bypass");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-governance-no-bypass";
    let key = "governance-locked-object";
    let data = b"test data for governance mode";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with GOVERNANCE retention
    let retain_until = future_retain_until(30);
    let version_id = put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Governance, retain_until)
        .await
        .unwrap();

    // Attempt to delete without bypass - should fail
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await,
        "Delete without bypass should fail for GOVERNANCE locked object",
    );

    info!("✅ Test passed: GOVERNANCE retention blocks deletion without bypass");
}

#[tokio::test]
async fn test_delete_object_allowed_by_governance_with_bypass() {
    init_logging();
    info!("🧪 Test: DeleteObject allowed by GOVERNANCE retention with bypass");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-governance-with-bypass";
    let key = "governance-bypass-object";
    let data = b"test data for governance bypass";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with GOVERNANCE retention
    let retain_until = future_retain_until(30);
    let version_id = put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Governance, retain_until)
        .await
        .unwrap();

    // Delete with bypass header - should succeed
    let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), true).await;
    assert!(delete_result.is_ok(), "Delete with bypass should succeed for GOVERNANCE mode");

    // Verify object is deleted
    let head_error = client
        .head_object()
        .bucket(bucket)
        .key(key)
        .version_id(&version_id)
        .send()
        .await
        .expect_err("Object should be deleted");
    assert_eq!(
        head_error.raw_response().map(|response| response.status().as_u16()),
        Some(404),
        "deleted version should return HTTP 404: {head_error:?}"
    );

    info!("✅ Test passed: GOVERNANCE retention allows deletion with bypass");
}

#[tokio::test]
async fn test_delete_object_creates_delete_marker_for_retained_current_version() {
    init_logging();
    info!("🧪 Test: DeleteObject creates delete marker for retained current version");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-retention-delete-marker";
    let key = "retained-object";
    let data = b"test data for retained current version";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    let retain_until = future_retain_until(30);
    let retained_version_id =
        put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Governance, retain_until)
            .await
            .unwrap();

    let delete_marker_output = client.delete_object().bucket(bucket).key(key).send().await.unwrap();
    assert_eq!(delete_marker_output.delete_marker(), Some(true));

    let delete_marker_version_id = delete_marker_output
        .version_id()
        .expect("delete marker should have a version id")
        .to_string();

    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await,
        "Retained version should still reject direct deletion",
    );

    delete_object_with_bypass(&client, bucket, key, Some(&delete_marker_version_id), false)
        .await
        .unwrap();

    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await,
        "Retained version should remain protected after delete marker removal",
    );

    delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true)
        .await
        .unwrap();

    info!("✅ Test passed: Delete marker is allowed while retained version stays protected");
}

#[tokio::test]
async fn test_delete_object_blocked_by_legal_hold() {
    init_logging();
    info!("🧪 Test: DeleteObject blocked by Legal Hold");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-legal-hold-delete";
    let key = "legal-hold-object";
    let data = b"test data for legal hold";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with legal hold ON
    let version_id = put_object_with_legal_hold(&client, bucket, key, data, ObjectLockLegalHoldStatus::On)
        .await
        .unwrap();

    // Attempt to delete - should fail (legal hold cannot be bypassed)
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await,
        "Delete should fail for legal hold object",
    );

    // Even with bypass header, legal hold should block deletion
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&version_id), true).await,
        "Delete with bypass should still fail for legal hold",
    );

    info!("✅ Test passed: Legal Hold blocks deletion");
}

#[tokio::test]
async fn test_delete_object_allowed_with_legal_hold_off() {
    init_logging();
    info!("🧪 Test: DeleteObject allowed with Legal Hold OFF");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-legal-hold-off-delete";
    let key = "legal-hold-off-object";
    let data = b"test data for legal hold off";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    let version_id = put_object_with_legal_hold(&client, bucket, key, data, ObjectLockLegalHoldStatus::Off)
        .await
        .unwrap();

    let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await;
    assert!(delete_result.is_ok(), "Delete should succeed when legal hold is OFF");

    let head_error = client
        .head_object()
        .bucket(bucket)
        .key(key)
        .version_id(&version_id)
        .send()
        .await
        .expect_err("Object should be deleted when legal hold is OFF");
    assert_eq!(
        head_error.raw_response().map(|response| response.status().as_u16()),
        Some(404),
        "deleted version should return HTTP 404: {head_error:?}"
    );

    info!("✅ Test passed: Legal Hold OFF allows deletion");
}

#[tokio::test]
async fn test_delete_object_after_legal_hold_removed() {
    init_logging();
    info!("🧪 Test: DeleteObject succeeds after Legal Hold is removed");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-legal-hold-remove";
    let key = "legal-hold-remove-object";
    let data = b"test data for legal hold removal";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with legal hold ON
    let version_id = put_object_with_legal_hold(&client, bucket, key, data, ObjectLockLegalHoldStatus::On)
        .await
        .unwrap();

    // Remove legal hold
    put_object_legal_hold(&client, bucket, key, Some(&version_id), ObjectLockLegalHoldStatus::Off)
        .await
        .unwrap();

    // Now deletion should succeed
    let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await;
    assert!(delete_result.is_ok(), "Delete should succeed after legal hold is removed");

    info!("✅ Test passed: Deletion succeeds after Legal Hold removal");
}

#[tokio::test]
async fn test_get_object_legal_hold_returns_updated_status() {
    init_logging();
    info!("🧪 Test: GetObjectLegalHold returns updated status");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-get-legal-hold";
    let key = "legal-hold-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let version_id = put_object_with_legal_hold(&client, bucket, key, b"test data", ObjectLockLegalHoldStatus::On)
        .await
        .unwrap();

    let on_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key(key)
        .version_id(&version_id)
        .send()
        .await
        .unwrap();
    assert_eq!(
        on_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("ON")
    );

    put_object_legal_hold(&client, bucket, key, Some(&version_id), ObjectLockLegalHoldStatus::Off)
        .await
        .unwrap();

    let off_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key(key)
        .version_id(&version_id)
        .send()
        .await
        .unwrap();
    assert_eq!(
        off_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("OFF")
    );
}

#[tokio::test]
async fn test_get_object_retention_returns_configured_values() {
    init_logging();
    info!("🧪 Test: GetObjectRetention returns configured values");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-get-retention";
    let key = "retained-object";
    let retain_until = future_retain_until(30);
    let retain_until_expected = retain_until.format("%Y-%m-%dT%H:%M:%SZ").to_string();

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let version_id =
        put_object_with_retention(&client, bucket, key, b"test data", ObjectLockRetentionMode::Governance, retain_until)
            .await
            .unwrap();

    let retention = client
        .get_object_retention()
        .bucket(bucket)
        .key(key)
        .version_id(&version_id)
        .send()
        .await
        .unwrap();
    let retention = retention.retention().expect("retention should be present");

    assert_eq!(retention.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
    assert_eq!(
        retention
            .retain_until_date()
            .expect("retain_until_date should be present")
            .fmt(aws_sdk_s3::primitives::DateTimeFormat::DateTime)
            .unwrap(),
        retain_until_expected
    );
}

// ============================================================================
// Put/Copy/Multipart Legal Hold Tests
// ============================================================================

// AWS semantics (PR #3179): Object Lock buckets are always versioned, so a
// plain PUT/copy/multipart write to a held or retained key succeeds by
// creating a new current version. The lock protects the existing version
// from deletion; it never blocks new versions.
#[tokio::test]
async fn test_put_object_overwrite_creates_new_version_under_legal_hold() {
    init_logging();
    info!("🧪 Test: PutObject overwrite of a legal-hold version creates a new version");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-put-overwrite-legal-hold";
    let key = "overwrite-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    let held_version_id = put_object_with_legal_hold(&client, bucket, key, b"locked-body", ObjectLockLegalHoldStatus::On)
        .await
        .unwrap();

    let overwrite_output = client
        .put_object()
        .bucket(bucket)
        .key(key)
        .body(ByteStream::from(b"replacement-body".to_vec()))
        .send()
        .await
        .expect("PutObject overwrite should succeed by creating a new version");

    let new_version_id = overwrite_output
        .version_id()
        .expect("versioned put should return a version id")
        .to_string();
    assert_ne!(new_version_id, held_version_id, "overwrite must create a distinct version");

    let current_body = client
        .get_object()
        .bucket(bucket)
        .key(key)
        .send()
        .await
        .unwrap()
        .body
        .collect()
        .await
        .unwrap()
        .into_bytes();
    assert_eq!(current_body.as_ref(), b"replacement-body");

    let held_body = client
        .get_object()
        .bucket(bucket)
        .key(key)
        .version_id(&held_version_id)
        .send()
        .await
        .unwrap()
        .body
        .collect()
        .await
        .unwrap()
        .into_bytes();
    assert_eq!(held_body.as_ref(), b"locked-body", "held version must keep its original content");

    let held_legal_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key(key)
        .version_id(&held_version_id)
        .send()
        .await
        .unwrap();
    assert_eq!(
        held_legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("ON"),
        "held version must keep its legal hold after the overwrite"
    );

    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&held_version_id), false).await,
        "held version must stay delete-protected after the overwrite",
    );
}

#[tokio::test]
async fn test_copy_object_applies_requested_legal_hold() {
    init_logging();
    info!("🧪 Test: CopyObject applies requested Legal Hold");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-copy-object-legal-hold";
    let src_key = "src-object";
    let dst_key = "dst-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    client
        .put_object()
        .bucket(bucket)
        .key(src_key)
        .body(ByteStream::from(b"copy-source".to_vec()))
        .send()
        .await
        .unwrap();

    client
        .copy_object()
        .copy_source(format!("{bucket}/{src_key}"))
        .bucket(bucket)
        .key(dst_key)
        .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::On)
        .send()
        .await
        .unwrap();

    let legal_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key(dst_key)
        .send()
        .await
        .unwrap();

    assert_eq!(
        legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("ON")
    );
}

#[tokio::test]
async fn test_copy_object_does_not_inherit_source_legal_hold() {
    init_logging();
    info!("🧪 Test: CopyObject does not inherit source Legal Hold");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-copy-object-legal-hold-inherit";
    let src_key = "held-source";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    put_object_with_legal_hold(&client, bucket, src_key, b"copy-source", ObjectLockLegalHoldStatus::On)
        .await
        .unwrap();

    client
        .copy_object()
        .copy_source(format!("{bucket}/{src_key}"))
        .bucket(bucket)
        .key("implicit-copy")
        .send()
        .await
        .unwrap();

    let implicit_legal_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key("implicit-copy")
        .send()
        .await
        .unwrap();
    assert_eq!(
        implicit_legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("OFF")
    );

    client
        .copy_object()
        .copy_source(format!("{bucket}/{src_key}"))
        .bucket(bucket)
        .key("explicit-on-copy")
        .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::On)
        .send()
        .await
        .unwrap();

    let explicit_on_legal_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key("explicit-on-copy")
        .send()
        .await
        .unwrap();
    assert_eq!(
        explicit_on_legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("ON")
    );

    client
        .copy_object()
        .copy_source(format!("{bucket}/{src_key}"))
        .bucket(bucket)
        .key("explicit-off-copy")
        .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::Off)
        .send()
        .await
        .unwrap();

    let explicit_off_legal_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key("explicit-off-copy")
        .send()
        .await
        .unwrap();
    assert_eq!(
        explicit_off_legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("OFF")
    );
}

#[tokio::test]
async fn test_copy_object_overwrite_creates_new_version_under_legal_hold() {
    init_logging();
    info!("🧪 Test: CopyObject overwrite of a legal-hold destination creates a new version");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-copy-overwrite-legal-hold";
    let src_key = "src-object";
    let dst_key = "dst-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    client
        .put_object()
        .bucket(bucket)
        .key(src_key)
        .body(ByteStream::from(b"copy-source".to_vec()))
        .send()
        .await
        .unwrap();

    let held_version_id =
        put_object_with_legal_hold(&client, bucket, dst_key, b"locked-destination", ObjectLockLegalHoldStatus::On)
            .await
            .unwrap();

    let copy_output = client
        .copy_object()
        .copy_source(format!("{bucket}/{src_key}"))
        .bucket(bucket)
        .key(dst_key)
        .send()
        .await
        .expect("CopyObject overwrite should succeed by creating a new destination version");

    let new_version_id = copy_output
        .version_id()
        .expect("versioned copy should return a version id")
        .to_string();
    assert_ne!(new_version_id, held_version_id, "copy must create a distinct destination version");

    let current_body = client
        .get_object()
        .bucket(bucket)
        .key(dst_key)
        .send()
        .await
        .unwrap()
        .body
        .collect()
        .await
        .unwrap()
        .into_bytes();
    assert_eq!(current_body.as_ref(), b"copy-source");

    let held_legal_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key(dst_key)
        .version_id(&held_version_id)
        .send()
        .await
        .unwrap();
    assert_eq!(
        held_legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("ON"),
        "held destination version must keep its legal hold after the copy"
    );

    assert_access_denied(
        delete_object_with_bypass(&client, bucket, dst_key, Some(&held_version_id), false).await,
        "held destination version must stay delete-protected",
    );
}

#[tokio::test]
async fn test_create_multipart_upload_applies_requested_legal_hold() {
    init_logging();
    info!("🧪 Test: CreateMultipartUpload applies requested Legal Hold");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-multipart-legal-hold";
    let key = "multipart-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let create_output = client
        .create_multipart_upload()
        .bucket(bucket)
        .key(key)
        .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::On)
        .send()
        .await
        .unwrap();

    let upload_id = create_output.upload_id().unwrap();
    let upload_part_output = client
        .upload_part()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .part_number(1)
        .body(ByteStream::from(b"multipart-body".to_vec()))
        .send()
        .await
        .unwrap();

    let completed_upload = CompletedMultipartUpload::builder()
        .parts(
            CompletedPart::builder()
                .part_number(1)
                .e_tag(upload_part_output.e_tag().unwrap_or_default())
                .build(),
        )
        .build();

    client
        .complete_multipart_upload()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .multipart_upload(completed_upload)
        .send()
        .await
        .unwrap();

    let legal_hold = client.get_object_legal_hold().bucket(bucket).key(key).send().await.unwrap();

    assert_eq!(
        legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("ON")
    );
}

#[tokio::test]
async fn test_create_multipart_upload_creates_new_version_under_compliance_retention() {
    init_logging();
    info!("🧪 Test: CreateMultipartUpload over a COMPLIANCE-retained key creates a new version");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-multipart-create-compliance";
    let key = "protected-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let retained_version_id = put_object_with_retention(
        &client,
        bucket,
        key,
        b"locked-destination",
        ObjectLockRetentionMode::Compliance,
        future_retain_until(30),
    )
    .await
    .unwrap();

    let create_output = client
        .create_multipart_upload()
        .bucket(bucket)
        .key(key)
        .send()
        .await
        .expect("CreateMultipartUpload should succeed; completion will create a new version");

    let upload_id = create_output.upload_id().unwrap();
    let upload_part_output = client
        .upload_part()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .part_number(1)
        .body(ByteStream::from(b"multipart-body".to_vec()))
        .send()
        .await
        .unwrap();

    let completed_upload = CompletedMultipartUpload::builder()
        .parts(
            CompletedPart::builder()
                .part_number(1)
                .e_tag(upload_part_output.e_tag().unwrap_or_default())
                .build(),
        )
        .build();

    let complete_output = client
        .complete_multipart_upload()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .multipart_upload(completed_upload)
        .send()
        .await
        .expect("CompleteMultipartUpload should succeed by creating a new version");

    let new_version_id = complete_output
        .version_id()
        .expect("versioned multipart completion should return a version id")
        .to_string();
    assert_ne!(new_version_id, retained_version_id, "completion must create a distinct version");

    // COMPLIANCE retention on the previous version survives the overwrite and
    // cannot be bypassed.
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true).await,
        "retained version must stay delete-protected even with governance bypass",
    );
}

#[tokio::test]
async fn test_delete_completed_multipart_object_blocked_by_legal_hold() {
    init_logging();
    info!("🧪 Test: Delete completed multipart object blocked by Legal Hold");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-multipart-delete-legal-hold";
    let key = "multipart-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let create_output = client
        .create_multipart_upload()
        .bucket(bucket)
        .key(key)
        .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::On)
        .send()
        .await
        .unwrap();

    let upload_id = create_output.upload_id().unwrap();
    let upload_part_output = client
        .upload_part()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .part_number(1)
        .body(ByteStream::from(b"multipart-body".to_vec()))
        .send()
        .await
        .unwrap();

    let completed_upload = CompletedMultipartUpload::builder()
        .parts(
            CompletedPart::builder()
                .part_number(1)
                .e_tag(upload_part_output.e_tag().unwrap_or_default())
                .build(),
        )
        .build();

    let complete_output = client
        .complete_multipart_upload()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .multipart_upload(completed_upload)
        .send()
        .await
        .unwrap();

    let version_id = complete_output.version_id().expect("multipart object should be versioned");
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await,
        "Delete should fail for multipart object protected by legal hold",
    );
}

#[tokio::test]
async fn test_delete_completed_multipart_object_blocked_by_retention() {
    init_logging();
    info!("🧪 Test: Delete completed multipart object blocked by retention");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-multipart-delete-retention";
    let key = "multipart-object";
    let retain_until = retention_timestamp(30);

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let create_output = client
        .create_multipart_upload()
        .bucket(bucket)
        .key(key)
        .object_lock_mode(aws_sdk_s3::types::ObjectLockMode::Compliance)
        .object_lock_retain_until_date(retain_until)
        .send()
        .await
        .unwrap();

    let upload_id = create_output.upload_id().unwrap();
    let upload_part_output = client
        .upload_part()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .part_number(1)
        .body(ByteStream::from(b"multipart-body".to_vec()))
        .send()
        .await
        .unwrap();

    let completed_upload = CompletedMultipartUpload::builder()
        .parts(
            CompletedPart::builder()
                .part_number(1)
                .e_tag(upload_part_output.e_tag().unwrap_or_default())
                .build(),
        )
        .build();

    let complete_output = client
        .complete_multipart_upload()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .multipart_upload(completed_upload)
        .send()
        .await
        .unwrap();

    let version_id = complete_output.version_id().expect("multipart object should be versioned");
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await,
        "Delete should fail for multipart object protected by retention",
    );
}

#[tokio::test]
async fn test_complete_multipart_upload_creates_new_version_under_legal_hold() {
    init_logging();
    info!("🧪 Test: CompleteMultipartUpload creates a new version when the current version is under Legal Hold");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-complete-multipart-legal-hold";
    let key = "multipart-race-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let create_output = client.create_multipart_upload().bucket(bucket).key(key).send().await.unwrap();

    let upload_id = create_output.upload_id().unwrap();
    let upload_part_output = client
        .upload_part()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .part_number(1)
        .body(ByteStream::from(b"multipart-body".to_vec()))
        .send()
        .await
        .unwrap();

    let held_version_id =
        put_object_with_legal_hold(&client, bucket, key, b"locked-current-version", ObjectLockLegalHoldStatus::On)
            .await
            .unwrap();

    let completed_upload = CompletedMultipartUpload::builder()
        .parts(
            CompletedPart::builder()
                .part_number(1)
                .e_tag(upload_part_output.e_tag().unwrap_or_default())
                .build(),
        )
        .build();

    let complete_output = client
        .complete_multipart_upload()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .multipart_upload(completed_upload)
        .send()
        .await
        .expect("CompleteMultipartUpload should succeed by creating a new version over the held one");

    let new_version_id = complete_output
        .version_id()
        .expect("versioned multipart completion should return a version id")
        .to_string();
    assert_ne!(new_version_id, held_version_id, "completion must create a distinct version");

    let held_legal_hold = client
        .get_object_legal_hold()
        .bucket(bucket)
        .key(key)
        .version_id(&held_version_id)
        .send()
        .await
        .unwrap();
    assert_eq!(
        held_legal_hold
            .legal_hold()
            .and_then(|value| value.status())
            .map(|value| value.as_str()),
        Some("ON"),
        "held version must keep its legal hold after multipart completion"
    );

    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&held_version_id), false).await,
        "held version must stay delete-protected",
    );
}

#[tokio::test]
async fn test_complete_multipart_upload_creates_new_version_under_compliance_retention() {
    init_logging();
    info!("🧪 Test: CompleteMultipartUpload creates a new version when the current version is under COMPLIANCE retention");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-complete-multipart-compliance";
    let key = "multipart-race-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    let create_output = client.create_multipart_upload().bucket(bucket).key(key).send().await.unwrap();

    let upload_id = create_output.upload_id().unwrap();
    let upload_part_output = client
        .upload_part()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .part_number(1)
        .body(ByteStream::from(b"multipart-body".to_vec()))
        .send()
        .await
        .unwrap();

    let retained_version_id = put_object_with_retention(
        &client,
        bucket,
        key,
        b"locked-current-version",
        ObjectLockRetentionMode::Compliance,
        future_retain_until(30),
    )
    .await
    .unwrap();

    let completed_upload = CompletedMultipartUpload::builder()
        .parts(
            CompletedPart::builder()
                .part_number(1)
                .e_tag(upload_part_output.e_tag().unwrap_or_default())
                .build(),
        )
        .build();

    let complete_output = client
        .complete_multipart_upload()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .multipart_upload(completed_upload)
        .send()
        .await
        .expect("CompleteMultipartUpload should succeed by creating a new version over the retained one");

    let new_version_id = complete_output
        .version_id()
        .expect("versioned multipart completion should return a version id")
        .to_string();
    assert_ne!(new_version_id, retained_version_id, "completion must create a distinct version");

    // COMPLIANCE retention on the previous version survives the overwrite and
    // cannot be bypassed.
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true).await,
        "retained version must stay delete-protected even with governance bypass",
    );
}

#[tokio::test]
async fn test_write_paths_require_put_object_legal_hold_permission() {
    init_logging();
    info!("🧪 Test: write paths require PutObjectLegalHold permission");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-legal-hold-permissions";
    let src_key = "src-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    client
        .put_object()
        .bucket(bucket)
        .key(src_key)
        .body(ByteStream::from(b"copy-source".to_vec()))
        .send()
        .await
        .unwrap();

    put_bucket_deny_policy(&client, bucket, "DenyPutObjectLegalHold", "s3:PutObjectLegalHold")
        .await
        .unwrap();

    assert_access_denied(
        client
            .put_object()
            .bucket(bucket)
            .key("put-target")
            .body(ByteStream::from(b"put-body".to_vec()))
            .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::On)
            .send()
            .await,
        "PutObject with legal hold should require s3:PutObjectLegalHold",
    );

    assert_access_denied(
        client
            .copy_object()
            .copy_source(format!("{bucket}/{src_key}"))
            .bucket(bucket)
            .key("copy-target")
            .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::On)
            .send()
            .await,
        "CopyObject with legal hold should require s3:PutObjectLegalHold",
    );

    assert_access_denied(
        client
            .create_multipart_upload()
            .bucket(bucket)
            .key("multipart-target")
            .object_lock_legal_hold_status(ObjectLockLegalHoldStatus::On)
            .send()
            .await,
        "CreateMultipartUpload with legal hold should require s3:PutObjectLegalHold",
    );
}

#[tokio::test]
async fn test_write_paths_require_put_object_retention_permission() {
    init_logging();
    info!("🧪 Test: write paths require PutObjectRetention permission");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-retention-permissions";
    let src_key = "src-object";
    let retain_until = retention_timestamp(30);

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    client
        .put_object()
        .bucket(bucket)
        .key(src_key)
        .body(ByteStream::from(b"copy-source".to_vec()))
        .send()
        .await
        .unwrap();

    put_bucket_deny_policy(&client, bucket, "DenyPutObjectRetention", "s3:PutObjectRetention")
        .await
        .unwrap();

    assert_access_denied(
        client
            .put_object()
            .bucket(bucket)
            .key("put-target")
            .body(ByteStream::from(b"put-body".to_vec()))
            .object_lock_mode(aws_sdk_s3::types::ObjectLockMode::Governance)
            .object_lock_retain_until_date(retain_until)
            .send()
            .await,
        "PutObject with retention should require s3:PutObjectRetention",
    );

    assert_access_denied(
        client
            .copy_object()
            .copy_source(format!("{bucket}/{src_key}"))
            .bucket(bucket)
            .key("copy-target")
            .object_lock_mode(aws_sdk_s3::types::ObjectLockMode::Governance)
            .object_lock_retain_until_date(retain_until)
            .send()
            .await,
        "CopyObject with retention should require s3:PutObjectRetention",
    );

    assert_access_denied(
        client
            .create_multipart_upload()
            .bucket(bucket)
            .key("multipart-target")
            .object_lock_mode(aws_sdk_s3::types::ObjectLockMode::Governance)
            .object_lock_retain_until_date(retain_until)
            .send()
            .await,
        "CreateMultipartUpload with retention should require s3:PutObjectRetention",
    );
}

// ============================================================================
// DeleteObjects (Batch Delete) Tests
// ============================================================================

#[tokio::test]
async fn test_delete_objects_mixed_locked_unlocked() {
    init_logging();
    info!("🧪 Test: DeleteObjects with mixed locked and unlocked objects");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-batch-delete-mixed";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put unlocked object
    let unlocked_key = "unlocked-object";
    client
        .put_object()
        .bucket(bucket)
        .key(unlocked_key)
        .body(ByteStream::from(b"unlocked data".to_vec()))
        .send()
        .await
        .unwrap();

    // Put locked object with COMPLIANCE
    let locked_key = "locked-object";
    let retain_until = future_retain_until(30);
    let locked_version = put_object_with_retention(
        &client,
        bucket,
        locked_key,
        b"locked data",
        ObjectLockRetentionMode::Compliance,
        retain_until,
    )
    .await
    .unwrap();

    // Batch delete both objects
    let delete = Delete::builder()
        .objects(ObjectIdentifier::builder().key(unlocked_key).build().unwrap())
        .objects(
            ObjectIdentifier::builder()
                .key(locked_key)
                .version_id(&locked_version)
                .build()
                .unwrap(),
        )
        .build()
        .unwrap();

    let result = client.delete_objects().bucket(bucket).delete(delete).send().await.unwrap();

    // Unlocked object should be deleted
    let deleted_count = result.deleted().len();
    let error_count = result.errors().len();

    info!("Deleted: {}, Errors: {}", deleted_count, error_count);

    // Should have 1 successful delete (unlocked) and 1 error (locked)
    assert_eq!(deleted_count, 1, "One object should be deleted");
    assert_eq!(error_count, 1, "One object should have error (locked)");

    // Verify locked object still exists
    let head_result = client
        .head_object()
        .bucket(bucket)
        .key(locked_key)
        .version_id(&locked_version)
        .send()
        .await;
    assert!(head_result.is_ok(), "Locked object should still exist");

    info!("✅ Test passed: Batch delete correctly handles mixed locked/unlocked objects");
}

// ============================================================================
// PutObjectRetention Tests
// ============================================================================

#[tokio::test]
async fn test_put_retention_compliance_cannot_shorten() {
    init_logging();
    info!("🧪 Test: PutObjectRetention cannot shorten COMPLIANCE retention");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-retention-shorten";
    let key = "compliance-shorten-object";
    let data = b"test data";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with COMPLIANCE retention for 60 days
    let retain_until_60 = future_retain_until(60);
    let version_id = put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Compliance, retain_until_60)
        .await
        .unwrap();

    // Try to shorten to 30 days - should fail
    let retain_until_30 = future_retain_until(30);
    let shorten_result = put_object_retention(
        &client,
        bucket,
        key,
        Some(&version_id),
        ObjectLockRetentionMode::Compliance,
        retain_until_30,
        false,
    )
    .await;

    assert_access_denied(shorten_result, "Shortening COMPLIANCE retention should fail");

    info!("✅ Test passed: Cannot shorten COMPLIANCE retention");
}

#[tokio::test]
async fn test_put_retention_compliance_can_extend() {
    init_logging();
    info!("🧪 Test: PutObjectRetention can extend COMPLIANCE retention");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-retention-extend";
    let key = "compliance-extend-object";
    let data = b"test data";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with COMPLIANCE retention for 30 days
    let retain_until_30 = future_retain_until(30);
    let version_id = put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Compliance, retain_until_30)
        .await
        .unwrap();

    // Extend to 60 days - should succeed
    let retain_until_60 = future_retain_until(60);
    let extend_result = put_object_retention(
        &client,
        bucket,
        key,
        Some(&version_id),
        ObjectLockRetentionMode::Compliance,
        retain_until_60,
        false,
    )
    .await;

    assert!(extend_result.is_ok(), "Extending COMPLIANCE retention should succeed");

    info!("✅ Test passed: Can extend COMPLIANCE retention");
}

#[tokio::test]
async fn test_put_retention_governance_extend_without_bypass() {
    init_logging();
    info!("🧪 Test: PutObjectRetention on GOVERNANCE can extend without bypass");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-governance-extend";
    let key = "governance-extend-object";
    let data = b"test data";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with GOVERNANCE retention for 30 days
    let retain_until_30 = future_retain_until(30);
    let version_id = put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Governance, retain_until_30)
        .await
        .unwrap();

    // Extend to 60 days without bypass - should succeed (AWS S3 behavior)
    let retain_until_60 = future_retain_until(60);
    let extend_without_bypass = put_object_retention(
        &client,
        bucket,
        key,
        Some(&version_id),
        ObjectLockRetentionMode::Governance,
        retain_until_60,
        false,
    )
    .await;

    assert!(
        extend_without_bypass.is_ok(),
        "Extending GOVERNANCE retention without bypass should succeed"
    );

    info!("✅ Test passed: GOVERNANCE retention can be extended without bypass");
}

#[tokio::test]
async fn test_put_retention_governance_shorten_requires_bypass() {
    init_logging();
    info!("🧪 Test: PutObjectRetention on GOVERNANCE requires bypass to shorten");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-governance-shorten";
    let key = "governance-shorten-object";
    let data = b"test data";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with GOVERNANCE retention for 60 days
    let retain_until_60 = future_retain_until(60);
    let version_id = put_object_with_retention(&client, bucket, key, data, ObjectLockRetentionMode::Governance, retain_until_60)
        .await
        .unwrap();

    // Try to shorten to 30 days without bypass - should fail
    let retain_until_30 = future_retain_until(30);
    let shorten_without_bypass = put_object_retention(
        &client,
        bucket,
        key,
        Some(&version_id),
        ObjectLockRetentionMode::Governance,
        retain_until_30,
        false,
    )
    .await;

    assert_access_denied(shorten_without_bypass, "Shortening GOVERNANCE retention without bypass should fail");

    // Shorten with bypass - should succeed
    let shorten_with_bypass = put_object_retention(
        &client,
        bucket,
        key,
        Some(&version_id),
        ObjectLockRetentionMode::Governance,
        retain_until_30,
        true,
    )
    .await;

    assert!(shorten_with_bypass.is_ok(), "Shortening GOVERNANCE retention with bypass should succeed");

    info!("✅ Test passed: GOVERNANCE retention shortening requires bypass");
}

// ============================================================================
// Default Retention Tests
// ============================================================================

#[tokio::test]
async fn test_default_retention_applied_to_new_objects() {
    init_logging();
    info!("🧪 Test: Default retention is applied to new objects");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-default-retention";
    let key = "object-with-default-retention";
    let data = b"test data";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Set default retention: GOVERNANCE for 30 days
    put_object_lock_configuration(&client, bucket, ObjectLockRetentionMode::Governance, Some(30), None)
        .await
        .unwrap();

    // Put object without explicit retention
    let response = client
        .put_object()
        .bucket(bucket)
        .key(key)
        .body(ByteStream::from(data.to_vec()))
        .send()
        .await
        .unwrap();

    let version_id = response.version_id().unwrap();

    // Try to delete without bypass - should fail due to default retention
    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await,
        "Delete should fail for object with default retention applied",
    );

    let retention = client
        .get_object_retention()
        .bucket(bucket)
        .key(key)
        .version_id(version_id)
        .send()
        .await
        .unwrap();
    let retention = retention.retention().expect("default retention should be readable");
    assert_eq!(retention.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
    assert!(
        retention.retain_until_date().is_some(),
        "default retention should write a retain-until date"
    );

    let head = client
        .head_object()
        .bucket(bucket)
        .key(key)
        .version_id(version_id)
        .send()
        .await
        .unwrap();
    assert_eq!(head.object_lock_mode().map(|value| value.as_str()), Some("GOVERNANCE"));
    assert!(
        head.object_lock_retain_until_date().is_some(),
        "HeadObject should expose the default retention retain-until date"
    );

    info!("✅ Test passed: Default retention is applied to new objects");
}

#[tokio::test]
async fn test_delete_object_creates_delete_marker_for_default_retained_current_version() {
    init_logging();
    info!("🧪 Test: DeleteObject creates delete marker for default-retained current version");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-default-retention-delete-marker";
    let key = "default-retained-object";
    let data = b"test data for default-retained current version";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    put_object_lock_configuration(&client, bucket, ObjectLockRetentionMode::Governance, Some(30), None)
        .await
        .unwrap();

    let put_output = client
        .put_object()
        .bucket(bucket)
        .key(key)
        .body(ByteStream::from(data.to_vec()))
        .send()
        .await
        .unwrap();
    let retained_version_id = put_output
        .version_id()
        .expect("default-retained object should have a version id")
        .to_string();

    let retention = client
        .get_object_retention()
        .bucket(bucket)
        .key(key)
        .version_id(&retained_version_id)
        .send()
        .await
        .unwrap();
    let retention = retention.retention().expect("default retention should be readable");
    assert_eq!(retention.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
    assert!(
        retention.retain_until_date().is_some(),
        "default retention should write a retain-until date"
    );

    let delete_marker_output = client.delete_object().bucket(bucket).key(key).send().await.unwrap();
    assert_eq!(delete_marker_output.delete_marker(), Some(true));
    let delete_marker_version_id = delete_marker_output
        .version_id()
        .expect("delete marker should have a version id")
        .to_string();

    assert_access_denied(
        delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await,
        "Default-retained version should still reject direct deletion",
    );

    let retention_after_delete_marker = client
        .get_object_retention()
        .bucket(bucket)
        .key(key)
        .version_id(&retained_version_id)
        .send()
        .await
        .unwrap();
    let retention_after_delete_marker = retention_after_delete_marker
        .retention()
        .expect("default retention should remain readable by version id after delete marker creation");
    assert_eq!(retention_after_delete_marker.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
    assert!(
        retention_after_delete_marker.retain_until_date().is_some(),
        "retained version should keep its retain-until date after delete marker creation"
    );

    delete_object_with_bypass(&client, bucket, key, Some(&delete_marker_version_id), false)
        .await
        .unwrap();
    delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true)
        .await
        .unwrap();

    info!("✅ Test passed: Delete marker is allowed while default-retained version stays protected");
}

#[tokio::test]
async fn test_put_copy_and_multipart_reject_incomplete_retention_headers() {
    init_logging();
    info!("🧪 Test: write paths reject incomplete Object Lock retention headers");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-incomplete-retention";
    let src_key = "copy-source";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    client
        .put_object()
        .bucket(bucket)
        .key(src_key)
        .body(ByteStream::from(b"copy-source".to_vec()))
        .send()
        .await
        .unwrap();

    put_object_lock_configuration(&client, bucket, ObjectLockRetentionMode::Governance, Some(30), None)
        .await
        .unwrap();

    assert_invalid_object_lock_retention_pair(
        client
            .put_object()
            .bucket(bucket)
            .key("put-mode-only")
            .body(ByteStream::from(b"put-body".to_vec()))
            .object_lock_mode(ObjectLockMode::Governance)
            .send()
            .await,
        "PutObject with mode only should fail",
    );

    assert_invalid_object_lock_retention_pair(
        client
            .put_object()
            .bucket(bucket)
            .key("put-date-only")
            .body(ByteStream::from(b"put-body".to_vec()))
            .object_lock_retain_until_date(retention_timestamp(30))
            .send()
            .await,
        "PutObject with retain-until-date only should fail",
    );

    assert_invalid_object_lock_retention_pair(
        client
            .copy_object()
            .copy_source(format!("{bucket}/{src_key}"))
            .bucket(bucket)
            .key("copy-mode-only")
            .object_lock_mode(ObjectLockMode::Governance)
            .send()
            .await,
        "CopyObject with mode only should fail",
    );

    assert_invalid_object_lock_retention_pair(
        client
            .copy_object()
            .copy_source(format!("{bucket}/{src_key}"))
            .bucket(bucket)
            .key("copy-date-only")
            .object_lock_retain_until_date(retention_timestamp(30))
            .send()
            .await,
        "CopyObject with retain-until-date only should fail",
    );

    assert_invalid_object_lock_retention_pair(
        client
            .create_multipart_upload()
            .bucket(bucket)
            .key("multipart-mode-only")
            .object_lock_mode(ObjectLockMode::Governance)
            .send()
            .await,
        "CreateMultipartUpload with mode only should fail",
    );

    assert_invalid_object_lock_retention_pair(
        client
            .create_multipart_upload()
            .bucket(bucket)
            .key("multipart-date-only")
            .object_lock_retain_until_date(retention_timestamp(30))
            .send()
            .await,
        "CreateMultipartUpload with retain-until-date only should fail",
    );
}

#[tokio::test]
async fn test_copy_object_retention_uses_destination_policy() {
    init_logging();
    info!("🧪 Test: CopyObject retention follows destination policy");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let src_bucket = "test-copy-retention-src";
    let dst_bucket = "test-copy-retention-dst";
    let no_default_bucket = "test-copy-retention-nodef";
    let src_key = "retained-source";

    env.create_object_lock_bucket(src_bucket).await.unwrap();
    env.create_object_lock_bucket(dst_bucket).await.unwrap();
    env.create_object_lock_bucket(no_default_bucket).await.unwrap();

    let client = env.s3_client();
    put_object_lock_configuration(&client, dst_bucket, ObjectLockRetentionMode::Governance, Some(1), None)
        .await
        .unwrap();

    put_object_with_retention(
        &client,
        src_bucket,
        src_key,
        b"copy-source",
        ObjectLockRetentionMode::Compliance,
        future_retain_until(30),
    )
    .await
    .unwrap();

    let copy_started = Utc::now();
    client
        .copy_object()
        .copy_source(format!("{src_bucket}/{src_key}"))
        .bucket(dst_bucket)
        .key("default-copy")
        .send()
        .await
        .unwrap();

    let retention = client
        .get_object_retention()
        .bucket(dst_bucket)
        .key("default-copy")
        .send()
        .await
        .unwrap();
    let retention = retention
        .retention()
        .expect("destination default retention should be present");
    assert_eq!(retention.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
    let retain_until = parse_s3_datetime(retention.retain_until_date().expect("retain-until date should be present"));
    assert!(
        retain_until < copy_started + Duration::days(3),
        "destination default retention should not inherit the source's longer retention"
    );

    client
        .copy_object()
        .copy_source(format!("{src_bucket}/{src_key}"))
        .bucket(dst_bucket)
        .key("replace-copy")
        .metadata_directive(MetadataDirective::Replace)
        .send()
        .await
        .unwrap();

    let replace_retention = client
        .get_object_retention()
        .bucket(dst_bucket)
        .key("replace-copy")
        .send()
        .await
        .unwrap();
    assert_eq!(
        replace_retention
            .retention()
            .and_then(|value| value.mode())
            .map(|value| value.as_str()),
        Some("GOVERNANCE")
    );

    client
        .copy_object()
        .copy_source(format!("{src_bucket}/{src_key}"))
        .bucket(dst_bucket)
        .key("explicit-copy")
        .object_lock_mode(ObjectLockMode::Compliance)
        .object_lock_retain_until_date(retention_timestamp(30))
        .send()
        .await
        .unwrap();

    let explicit_retention = client
        .get_object_retention()
        .bucket(dst_bucket)
        .key("explicit-copy")
        .send()
        .await
        .unwrap();
    assert_eq!(
        explicit_retention
            .retention()
            .and_then(|value| value.mode())
            .map(|value| value.as_str()),
        Some("COMPLIANCE")
    );
    let explicit_retain_until = parse_s3_datetime(
        explicit_retention
            .retention()
            .and_then(|value| value.retain_until_date())
            .expect("explicit retain-until date should be present"),
    );
    assert!(
        explicit_retain_until > Utc::now() + Duration::days(20),
        "explicit retention should override the shorter bucket default"
    );

    client
        .copy_object()
        .copy_source(format!("{src_bucket}/{src_key}"))
        .bucket(no_default_bucket)
        .key("no-default-copy")
        .send()
        .await
        .unwrap();

    let no_default_retention = client
        .get_object_retention()
        .bucket(no_default_bucket)
        .key("no-default-copy")
        .send()
        .await
        .unwrap();
    let no_default_retention = no_default_retention
        .retention()
        .expect("retention response should be present");
    assert!(no_default_retention.mode().is_none());
    assert!(no_default_retention.retain_until_date().is_none());

    let retained_version_id = put_object_with_retention(
        &client,
        dst_bucket,
        "locked-destination",
        b"locked-target",
        ObjectLockRetentionMode::Compliance,
        future_retain_until(30),
    )
    .await
    .unwrap();

    let overwrite_output = client
        .copy_object()
        .copy_source(format!("{src_bucket}/{src_key}"))
        .bucket(dst_bucket)
        .key("locked-destination")
        .send()
        .await
        .expect("CopyObject overwrite should succeed by creating a new destination version");
    let overwrite_version_id = overwrite_output
        .version_id()
        .expect("versioned copy should return a version id")
        .to_string();
    assert_ne!(
        overwrite_version_id, retained_version_id,
        "copy must create a distinct destination version"
    );

    // COMPLIANCE retention on the previous destination version survives the
    // overwrite and cannot be bypassed.
    assert_access_denied(
        delete_object_with_bypass(&client, dst_bucket, "locked-destination", Some(&retained_version_id), true).await,
        "retained destination version must stay delete-protected even with governance bypass",
    );
}

#[tokio::test]
async fn test_multipart_default_retention_fixed_at_create() {
    init_logging();
    info!("🧪 Test: multipart default retention is fixed at CreateMultipartUpload");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-multipart-default-drift";
    let key = "multipart-object";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();
    put_object_lock_configuration(&client, bucket, ObjectLockRetentionMode::Governance, Some(1), None)
        .await
        .unwrap();

    let create_started = Utc::now();
    let create_output = client.create_multipart_upload().bucket(bucket).key(key).send().await.unwrap();
    let upload_id = create_output.upload_id().unwrap();

    let upload_part_output = client
        .upload_part()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .part_number(1)
        .body(ByteStream::from(b"multipart-body".to_vec()))
        .send()
        .await
        .unwrap();

    put_object_lock_configuration(&client, bucket, ObjectLockRetentionMode::Governance, Some(10), None)
        .await
        .unwrap();

    let completed_upload = CompletedMultipartUpload::builder()
        .parts(
            CompletedPart::builder()
                .part_number(1)
                .e_tag(upload_part_output.e_tag().unwrap_or_default())
                .build(),
        )
        .build();

    client
        .complete_multipart_upload()
        .bucket(bucket)
        .key(key)
        .upload_id(upload_id)
        .multipart_upload(completed_upload)
        .send()
        .await
        .unwrap();

    let retention = client.get_object_retention().bucket(bucket).key(key).send().await.unwrap();
    let retention = retention.retention().expect("multipart default retention should be present");
    assert_eq!(retention.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
    let retain_until = parse_s3_datetime(retention.retain_until_date().expect("retain-until date should be present"));
    assert!(
        retain_until < create_started + Duration::days(3),
        "CompleteMultipartUpload should keep the default retention calculated at create time"
    );
}

// ============================================================================
// Versioning Auto-Enable Tests
// ============================================================================

#[tokio::test]
async fn test_unretained_object_lock_object_delete_and_bucket_cleanup() {
    init_logging();
    info!("🧪 Test: Unretained Object Lock object delete and bucket cleanup (Issue #5339)");

    let mut env = ObjectLockTestEnvironment::new()
        .await
        .expect("failed to create Object Lock test environment");
    env.start_rustfs().await.expect("failed to start RustFS");

    let bucket = "test-object-lock-delete-cleanup";
    let key = "unretained-object";

    env.create_object_lock_bucket(bucket)
        .await
        .expect("failed to create Object Lock bucket");
    let client = env.s3_client();

    let put_response = client
        .put_object()
        .bucket(bucket)
        .key(key)
        .body(ByteStream::from_static(b"unretained data"))
        .send()
        .await
        .expect("failed to upload unretained object");
    let object_version_id = put_response
        .version_id()
        .expect("Object Lock buckets must create versioned objects")
        .to_string();

    let delete_response = client
        .delete_object()
        .bucket(bucket)
        .key(key)
        .send()
        .await
        .expect("failed to create delete marker");
    assert_eq!(delete_response.delete_marker(), Some(true));
    let delete_marker_version_id = delete_response
        .version_id()
        .expect("Deleting without a version ID must create a delete marker")
        .to_string();

    let get_error = client
        .get_object()
        .bucket(bucket)
        .key(key)
        .send()
        .await
        .expect_err("GET must not return an object hidden by a delete marker");
    assert_eq!(get_error.raw_response().map(|response| response.status().as_u16()), Some(404));
    assert_eq!(get_error.as_service_error().and_then(|error| error.code()), Some("NoSuchKey"));

    let listed_objects = client
        .list_objects_v2()
        .bucket(bucket)
        .send()
        .await
        .expect("failed to list current objects");
    assert!(
        listed_objects.contents().iter().all(|object| object.key() != Some(key)),
        "ListObjectsV2 must hide objects whose latest version is a delete marker"
    );

    let listed_versions = client
        .list_object_versions()
        .bucket(bucket)
        .send()
        .await
        .expect("failed to list object versions");
    assert!(
        listed_versions
            .versions()
            .iter()
            .any(|version| version.key() == Some(key) && version.version_id() == Some(object_version_id.as_str())),
        "The data version must remain until it is explicitly deleted"
    );
    assert!(
        listed_versions
            .delete_markers()
            .iter()
            .any(|marker| marker.key() == Some(key) && marker.version_id() == Some(delete_marker_version_id.as_str())),
        "ListObjectVersions must expose the delete marker"
    );

    client
        .delete_object()
        .bucket(bucket)
        .key(key)
        .version_id(object_version_id)
        .send()
        .await
        .expect("failed to delete the data version");
    client
        .delete_object()
        .bucket(bucket)
        .key(key)
        .version_id(delete_marker_version_id)
        .send()
        .await
        .expect("failed to delete the delete marker");

    let remaining_versions = client
        .list_object_versions()
        .bucket(bucket)
        .send()
        .await
        .expect("failed to list versions after cleanup");
    assert!(remaining_versions.versions().is_empty());
    assert!(remaining_versions.delete_markers().is_empty());

    client
        .delete_bucket()
        .bucket(bucket)
        .send()
        .await
        .expect("Deleting every version must remove xl.meta so the bucket can be deleted normally");
}

#[tokio::test]
async fn test_versioning_auto_enabled_with_object_lock() {
    init_logging();
    info!("🧪 Test: Versioning is auto-enabled when Object Lock is configured");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-versioning-auto-enable";

    // Create bucket with Object Lock enabled
    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Check versioning status - should be Enabled
    let versioning = client.get_bucket_versioning().bucket(bucket).send().await.unwrap();

    // Object Lock buckets must have versioning enabled
    // Note: Some S3 implementations may report MfaDelete status as well
    let status = versioning.status();
    info!("Versioning status: {:?}", status);

    // Put an object and verify it gets a version ID
    let key = "versioned-object";
    let response = client
        .put_object()
        .bucket(bucket)
        .key(key)
        .body(ByteStream::from(b"v1".to_vec()))
        .send()
        .await
        .unwrap();

    let version1 = response.version_id();
    assert!(version1.is_some(), "Object should have a version ID");

    // Put another version
    let response2 = client
        .put_object()
        .bucket(bucket)
        .key(key)
        .body(ByteStream::from(b"v2".to_vec()))
        .send()
        .await
        .unwrap();

    let version2 = response2.version_id();
    assert!(version2.is_some(), "Second object should have a version ID");
    assert_ne!(version1, version2, "Version IDs should be different");

    info!("✅ Test passed: Versioning is auto-enabled with Object Lock");
}

// ============================================================================
// Error Message Tests
// ============================================================================

#[tokio::test]
async fn test_legal_hold_and_retention_delete_errors_are_exact_and_non_mutating() {
    init_logging();
    info!("🧪 Test: Legal Hold and Retention reject deletes without mutating objects");

    let mut env = ObjectLockTestEnvironment::new().await.unwrap();
    env.start_rustfs().await.unwrap();

    let bucket = "test-error-messages";

    env.create_object_lock_bucket(bucket).await.unwrap();

    let client = env.s3_client();

    // Put object with legal hold
    let legal_hold_key = "legal-hold-object";
    let lh_version = put_object_with_legal_hold(&client, bucket, legal_hold_key, b"data", ObjectLockLegalHoldStatus::On)
        .await
        .unwrap();

    // Put object with retention
    let retention_key = "retention-object";
    let retain_until = future_retain_until(30);
    let ret_version =
        put_object_with_retention(&client, bucket, retention_key, b"data", ObjectLockRetentionMode::Compliance, retain_until)
            .await
            .unwrap();

    let lh_delete_result = client
        .delete_object()
        .bucket(bucket)
        .key(legal_hold_key)
        .version_id(&lh_version)
        .send()
        .await;
    assert_access_denied(lh_delete_result, "Legal Hold must reject deleting the protected version");

    let ret_delete_result = client
        .delete_object()
        .bucket(bucket)
        .key(retention_key)
        .version_id(&ret_version)
        .send()
        .await;
    assert_access_denied(ret_delete_result, "COMPLIANCE retention must reject deleting the protected version");

    for (key, version_id) in [(legal_hold_key, &lh_version), (retention_key, &ret_version)] {
        let body = client
            .get_object()
            .bucket(bucket)
            .key(key)
            .version_id(version_id)
            .send()
            .await
            .expect("rejected delete must leave the protected version readable")
            .body
            .collect()
            .await
            .expect("protected version body should remain readable")
            .into_bytes();
        assert_eq!(body.as_ref(), b"data", "rejected delete mutated protected object {key}");
    }

    info!("✅ Test passed: protected deletes are exact and non-mutating");
}
