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

//! CopyObject checksum compatibility tests. Covers all supported algorithms,
//! source-checksum preservation, explicit override, and fail-closed handling of
//! unsupported algorithms before destination mutation.

#[cfg(test)]
mod tests {
    use crate::common::{RustFSTestEnvironment, init_logging};
    use aws_sdk_s3::config::{Credentials, Region, RequestChecksumCalculation};
    use aws_sdk_s3::error::ProvideErrorMetadata;
    use aws_sdk_s3::primitives::ByteStream;
    use aws_sdk_s3::types::{
        BucketVersioningStatus, ChecksumAlgorithm, ChecksumMode, ChecksumType, CompletedMultipartUpload, CompletedPart,
        VersioningConfiguration,
    };
    use aws_smithy_http_client::Builder as SmithyHttpClientBuilder;
    use base64_simd::STANDARD as BASE64;
    use rustfs_rio::{Checksum, ChecksumType as RioChecksumType};
    use sha2::{Digest, Sha256};
    use tracing::info;

    async fn create_versioned_bucket(client: &aws_sdk_s3::Client, bucket: &str) {
        client
            .create_bucket()
            .bucket(bucket)
            .send()
            .await
            .expect("Failed to create bucket");
        client
            .put_bucket_versioning()
            .bucket(bucket)
            .versioning_configuration(
                VersioningConfiguration::builder()
                    .status(BucketVersioningStatus::Enabled)
                    .build(),
            )
            .send()
            .await
            .expect("Failed to enable versioning");
    }

    fn create_s3_client_no_auto_checksum(env: &RustFSTestEnvironment) -> aws_sdk_s3::Client {
        let credentials = Credentials::new(&env.access_key, &env.secret_key, None, None, "copy-checksum-e2e");
        let config = aws_sdk_s3::Config::builder()
            .credentials_provider(credentials)
            .region(Region::new("us-east-1"))
            .endpoint_url(format!("http://{}", env.address))
            .force_path_style(true)
            .behavior_version_latest()
            .request_checksum_calculation(RequestChecksumCalculation::WhenRequired)
            .http_client(SmithyHttpClientBuilder::new().build_http())
            .build();
        aws_sdk_s3::Client::from_conf(config)
    }

    fn algorithms() -> [(ChecksumAlgorithm, RioChecksumType); 10] {
        [
            (ChecksumAlgorithm::Crc32, RioChecksumType::CRC32),
            (ChecksumAlgorithm::Crc32C, RioChecksumType::CRC32C),
            (ChecksumAlgorithm::Crc64Nvme, RioChecksumType::CRC64_NVME),
            (ChecksumAlgorithm::Sha1, RioChecksumType::SHA1),
            (ChecksumAlgorithm::Sha256, RioChecksumType::SHA256),
            (ChecksumAlgorithm::Md5, RioChecksumType::MD5),
            (ChecksumAlgorithm::Sha512, RioChecksumType::SHA512),
            (ChecksumAlgorithm::Xxhash3, RioChecksumType::XXHASH3),
            (ChecksumAlgorithm::Xxhash64, RioChecksumType::XXHASH64),
            (ChecksumAlgorithm::Xxhash128, RioChecksumType::XXHASH128),
        ]
    }

    fn result_checksums(result: &aws_sdk_s3::types::CopyObjectResult) -> [Option<&str>; 10] {
        [
            result.checksum_crc32(),
            result.checksum_crc32_c(),
            result.checksum_crc64_nvme(),
            result.checksum_sha1(),
            result.checksum_sha256(),
            result.checksum_md5(),
            result.checksum_sha512(),
            result.checksum_xxhash3(),
            result.checksum_xxhash64(),
            result.checksum_xxhash128(),
        ]
    }

    fn head_checksums(output: &aws_sdk_s3::operation::head_object::HeadObjectOutput) -> [Option<&str>; 10] {
        [
            output.checksum_crc32(),
            output.checksum_crc32_c(),
            output.checksum_crc64_nvme(),
            output.checksum_sha1(),
            output.checksum_sha256(),
            output.checksum_md5(),
            output.checksum_sha512(),
            output.checksum_xxhash3(),
            output.checksum_xxhash64(),
            output.checksum_xxhash128(),
        ]
    }

    #[tokio::test]
    async fn test_copy_supports_all_checksum_algorithms() {
        init_logging();

        let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
        env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");

        let client = create_s3_client_no_auto_checksum(&env);
        let src_bucket = "copy-all-checksums-src";
        let dst_bucket = "copy-all-checksums-dst";
        let src_key = "objects/source.bin";
        let content = b"deterministic CopyObject payload for all ten checksum algorithms";

        create_versioned_bucket(&client, src_bucket).await;
        create_versioned_bucket(&client, dst_bucket).await;
        client
            .put_object()
            .bucket(src_bucket)
            .key(src_key)
            .body(ByteStream::from_static(content))
            .send()
            .await
            .expect("PUT source failed");

        for (index, (sdk_algorithm, rio_algorithm)) in algorithms().into_iter().enumerate() {
            let expected = Checksum::new_from_data(rio_algorithm, content)
                .expect("supported checksum must be computable")
                .encoded;
            let dst_key = format!("objects/destination-{index}.bin");
            let copy = client
                .copy_object()
                .bucket(dst_bucket)
                .key(&dst_key)
                .copy_source(format!("{src_bucket}/{src_key}"))
                .checksum_algorithm(sdk_algorithm)
                .send()
                .await
                .expect("CopyObject with supported checksum must succeed");
            let result = copy.copy_object_result().expect("CopyObject result");
            let checksums = result_checksums(result);
            assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: response checksum");
            assert_eq!(
                checksums.iter().filter(|checksum| checksum.is_some()).count(),
                1,
                "{rio_algorithm}: only the requested checksum may be returned"
            );

            let head = client
                .head_object()
                .bucket(dst_bucket)
                .key(&dst_key)
                .checksum_mode(ChecksumMode::Enabled)
                .send()
                .await
                .expect("HEAD destination failed");
            let checksums = head_checksums(&head);
            assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: persisted checksum");
            assert_eq!(
                checksums.iter().filter(|checksum| checksum.is_some()).count(),
                1,
                "{rio_algorithm}: destination must persist only the requested checksum"
            );

            let body = client
                .get_object()
                .bucket(dst_bucket)
                .key(&dst_key)
                .send()
                .await
                .expect("GET destination failed")
                .body
                .collect()
                .await
                .expect("collect destination body")
                .into_bytes();
            assert_eq!(body.as_ref(), content, "{rio_algorithm}: full copied body");
        }

        env.stop_server();
    }

    #[tokio::test]
    async fn test_copy_without_algorithm_preserves_every_supported_source_checksum() {
        init_logging();

        let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
        env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");

        let client = create_s3_client_no_auto_checksum(&env);
        let src_bucket = "copy-preserve-all-src";
        let dst_bucket = "copy-preserve-all-dst";
        let content = b"source checksum preservation payload for all ten algorithms";

        create_versioned_bucket(&client, src_bucket).await;
        create_versioned_bucket(&client, dst_bucket).await;

        for (index, (_sdk_algorithm, rio_algorithm)) in algorithms().into_iter().enumerate() {
            let expected = Checksum::new_from_data(rio_algorithm, content)
                .expect("supported checksum must be computable")
                .encoded;
            let checksum_header = rio_algorithm.key().expect("supported checksum header");
            let request_checksum = expected.clone();
            let src_key = format!("objects/source-{index}.bin");
            let dst_key = format!("objects/destination-{index}.bin");
            client
                .put_object()
                .bucket(src_bucket)
                .key(&src_key)
                .body(ByteStream::from_static(content))
                .customize()
                .mutate_request(move |request| {
                    request.headers_mut().insert(checksum_header, request_checksum.clone());
                })
                .send()
                .await
                .expect("PUT checksummed source failed");

            let copy = client
                .copy_object()
                .bucket(dst_bucket)
                .key(&dst_key)
                .copy_source(format!("{src_bucket}/{src_key}"))
                .send()
                .await
                .expect("CopyObject without algorithm must succeed");
            let result = copy.copy_object_result().expect("CopyObject result");
            let checksums = result_checksums(result);
            assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: preserved response checksum");
            assert_eq!(checksums.iter().filter(|checksum| checksum.is_some()).count(), 1);

            let head = client
                .head_object()
                .bucket(dst_bucket)
                .key(&dst_key)
                .checksum_mode(ChecksumMode::Enabled)
                .send()
                .await
                .expect("HEAD destination failed");
            let checksums = head_checksums(&head);
            assert_eq!(checksums[index], Some(expected.as_str()), "{rio_algorithm}: preserved stored checksum");
            assert_eq!(checksums.iter().filter(|checksum| checksum.is_some()).count(), 1);
        }

        env.stop_server();
    }

    #[tokio::test]
    async fn test_copy_without_algorithm_preserves_composite_checksum_type() {
        init_logging();

        let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
        env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");

        let client = create_s3_client_no_auto_checksum(&env);
        let bucket = "copy-preserve-composite";
        let source_key = "objects/multipart-source.bin";
        let destination_key = "objects/copied-multipart.bin";
        let content = b"multipart source checksum must remain composite";

        create_versioned_bucket(&client, bucket).await;
        let created = client
            .create_multipart_upload()
            .bucket(bucket)
            .key(source_key)
            .checksum_algorithm(ChecksumAlgorithm::Sha256)
            .send()
            .await
            .expect("CreateMultipartUpload failed");
        let upload_id = created.upload_id().expect("multipart upload ID");
        let checksum = Checksum::new_from_data(RioChecksumType::SHA256, content)
            .expect("SHA256 checksum")
            .encoded;
        let uploaded = client
            .upload_part()
            .bucket(bucket)
            .key(source_key)
            .upload_id(upload_id)
            .part_number(1)
            .checksum_sha256(&checksum)
            .body(ByteStream::from_static(content))
            .send()
            .await
            .expect("UploadPart failed");
        let completed_part = CompletedPart::builder()
            .part_number(1)
            .e_tag(uploaded.e_tag().expect("part ETag"))
            .checksum_sha256(uploaded.checksum_sha256().expect("part checksum"))
            .build();
        client
            .complete_multipart_upload()
            .bucket(bucket)
            .key(source_key)
            .upload_id(upload_id)
            .multipart_upload(CompletedMultipartUpload::builder().parts(completed_part).build())
            .send()
            .await
            .expect("CompleteMultipartUpload failed");

        let source_head = client
            .head_object()
            .bucket(bucket)
            .key(source_key)
            .checksum_mode(ChecksumMode::Enabled)
            .send()
            .await
            .expect("HEAD multipart source failed");
        let source_checksum = source_head.checksum_sha256().expect("multipart source checksum");
        assert_eq!(source_head.checksum_type(), Some(&ChecksumType::Composite));

        let copied = client
            .copy_object()
            .bucket(bucket)
            .key(destination_key)
            .copy_source(format!("{bucket}/{source_key}"))
            .send()
            .await
            .expect("CopyObject without algorithm failed");
        let result = copied.copy_object_result().expect("CopyObject result");
        assert_eq!(result.checksum_sha256(), Some(source_checksum));
        assert_eq!(result.checksum_type(), Some(&ChecksumType::Composite));

        let destination_head = client
            .head_object()
            .bucket(bucket)
            .key(destination_key)
            .checksum_mode(ChecksumMode::Enabled)
            .send()
            .await
            .expect("HEAD copied multipart object failed");
        assert_eq!(destination_head.checksum_sha256(), Some(source_checksum));
        assert_eq!(destination_head.checksum_type(), Some(&ChecksumType::Composite));

        env.stop_server();
    }

    #[tokio::test]
    async fn test_copy_rejects_unknown_algorithm_without_destination_mutation() {
        init_logging();

        let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
        env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");

        let client = env.create_s3_client();
        let bucket = "copy-reject-unknown-checksum";
        let src_key = "objects/source.bin";
        let dst_key = "objects/destination.bin";
        let source = b"source must never replace destination";
        let destination = b"pre-existing destination must remain byte-for-byte unchanged";
        let expected = Checksum::new_from_data(RioChecksumType::SHA256, destination)
            .expect("SHA256 checksum")
            .encoded;

        create_versioned_bucket(&client, bucket).await;
        client
            .put_object()
            .bucket(bucket)
            .key(src_key)
            .body(ByteStream::from_static(source))
            .send()
            .await
            .expect("PUT source failed");
        let original = client
            .put_object()
            .bucket(bucket)
            .key(dst_key)
            .metadata("state", "original")
            .checksum_algorithm(ChecksumAlgorithm::Sha256)
            .body(ByteStream::from_static(destination))
            .send()
            .await
            .expect("PUT destination failed");
        let original_version = original.version_id().expect("versioned PUT must return a version id");

        let missing_source_error = client
            .copy_object()
            .bucket(bucket)
            .key(dst_key)
            .copy_source(format!("{bucket}/objects/missing-source.bin"))
            .checksum_algorithm(ChecksumAlgorithm::from("BLAKE3"))
            .send()
            .await
            .expect_err("checksum validation must precede source lookup");
        assert_eq!(
            missing_source_error.as_service_error().and_then(|value| value.code()),
            Some("InvalidArgument")
        );
        assert_eq!(missing_source_error.raw_response().map(|response| response.status().as_u16()), Some(400));

        let error = client
            .copy_object()
            .bucket(bucket)
            .key(dst_key)
            .copy_source(format!("{bucket}/{src_key}"))
            .checksum_algorithm(ChecksumAlgorithm::from("BLAKE3"))
            .send()
            .await
            .expect_err("unsupported checksum algorithm must fail");
        assert_eq!(error.as_service_error().and_then(|value| value.code()), Some("InvalidArgument"));
        assert_eq!(error.raw_response().map(|response| response.status().as_u16()), Some(400));

        let head = client
            .head_object()
            .bucket(bucket)
            .key(dst_key)
            .checksum_mode(ChecksumMode::Enabled)
            .send()
            .await
            .expect("HEAD unchanged destination");
        assert_eq!(head.version_id(), Some(original_version));
        assert_eq!(
            head.metadata().and_then(|metadata| metadata.get("state").map(String::as_str)),
            Some("original")
        );
        assert_eq!(head.checksum_sha256(), Some(expected.as_str()));

        let body = client
            .get_object()
            .bucket(bucket)
            .key(dst_key)
            .send()
            .await
            .expect("GET unchanged destination")
            .body
            .collect()
            .await
            .expect("collect unchanged destination")
            .into_bytes();
        assert_eq!(body.as_ref(), destination);

        env.stop_server();
    }

    /// Requested algorithm: a CopyObject asking for SHA256 must compute it over the copied
    /// bytes, return it in `CopyObjectResult.ChecksumSHA256`, and persist it so a checksum-mode
    /// HEAD on the destination returns the identical value.
    #[tokio::test]
    async fn test_copy_with_checksum_algorithm_returns_and_persists_sha256() {
        init_logging();
        info!("Issue #4996: CopyObject with ChecksumAlgorithm=SHA256 must return and persist the checksum");

        let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
        env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");

        let client = env.create_s3_client();
        let src_bucket = "copy-checksum-req-src";
        let dst_bucket = "copy-checksum-req-dst";
        let src_key = "objects/source.bin";
        let dst_key = "objects/dest.bin";

        create_versioned_bucket(&client, src_bucket).await;
        create_versioned_bucket(&client, dst_bucket).await;

        let content = b"deterministic synthetic payload for copy-object checksum #4996";
        let expected_sha256 = BASE64.encode_to_string(Sha256::digest(content));

        client
            .put_object()
            .bucket(src_bucket)
            .key(src_key)
            .body(ByteStream::from_static(content))
            .send()
            .await
            .expect("PUT source failed");

        let copy_out = client
            .copy_object()
            .bucket(dst_bucket)
            .key(dst_key)
            .copy_source(format!("{src_bucket}/{src_key}"))
            .checksum_algorithm(ChecksumAlgorithm::Sha256)
            .send()
            .await
            .expect("CopyObject with ChecksumAlgorithm must succeed");

        // (3) The response must carry the freshly computed SHA-256 of the copied bytes.
        let result = copy_out
            .copy_object_result()
            .expect("issue #4996: CopyObject must return a CopyObjectResult");
        assert_eq!(
            result.checksum_sha256(),
            Some(expected_sha256.as_str()),
            "issue #4996: CopyObjectResult.ChecksumSHA256 must equal the SHA-256 of the copied bytes"
        );

        // (4) A checksum-mode HEAD on the destination must return the same SHA-256.
        let head = client
            .head_object()
            .bucket(dst_bucket)
            .key(dst_key)
            .checksum_mode(ChecksumMode::Enabled)
            .send()
            .await
            .expect("HEAD destination failed");
        assert_eq!(
            head.checksum_sha256(),
            Some(expected_sha256.as_str()),
            "issue #4996: destination checksum-mode HEAD must return the same SHA-256 the copy reported"
        );

        env.stop_server();
    }

    /// No algorithm requested: when the source object already carries a checksum, the copy must
    /// preserve it on the destination (AWS default), visible via a checksum-mode HEAD.
    #[tokio::test]
    async fn test_copy_without_algorithm_preserves_source_checksum() {
        init_logging();
        info!("Issue #4996: CopyObject without ChecksumAlgorithm must preserve the source object's checksum");

        let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
        env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");

        let client = env.create_s3_client();
        let src_bucket = "copy-checksum-preserve-src";
        let dst_bucket = "copy-checksum-preserve-dst";
        let src_key = "objects/source.bin";
        let dst_key = "objects/dest.bin";

        create_versioned_bucket(&client, src_bucket).await;
        create_versioned_bucket(&client, dst_bucket).await;

        let content = b"another deterministic payload whose source checksum must survive the copy";
        let expected_sha256 = BASE64.encode_to_string(Sha256::digest(content));

        // Store the source WITH a SHA-256 checksum so it has one to preserve.
        let put_src = client
            .put_object()
            .bucket(src_bucket)
            .key(src_key)
            .checksum_algorithm(ChecksumAlgorithm::Sha256)
            .body(ByteStream::from_static(content))
            .send()
            .await
            .expect("PUT source with checksum failed");
        assert_eq!(
            put_src.checksum_sha256(),
            Some(expected_sha256.as_str()),
            "source PUT must report the SHA-256 it stored"
        );

        // Copy WITHOUT specifying a checksum algorithm.
        let copy_out = client
            .copy_object()
            .bucket(dst_bucket)
            .key(dst_key)
            .copy_source(format!("{src_bucket}/{src_key}"))
            .send()
            .await
            .expect("CopyObject without ChecksumAlgorithm must succeed");

        // The response should echo the preserved source checksum.
        let result = copy_out
            .copy_object_result()
            .expect("issue #4996: CopyObject must return a CopyObjectResult");
        assert_eq!(
            result.checksum_sha256(),
            Some(expected_sha256.as_str()),
            "issue #4996: a no-algorithm copy must preserve and report the source object's SHA-256"
        );

        // And a checksum-mode HEAD on the destination must return that same preserved SHA-256.
        let head = client
            .head_object()
            .bucket(dst_bucket)
            .key(dst_key)
            .checksum_mode(ChecksumMode::Enabled)
            .send()
            .await
            .expect("HEAD destination failed");
        assert_eq!(
            head.checksum_sha256(),
            Some(expected_sha256.as_str()),
            "issue #4996: destination checksum-mode HEAD must return the preserved source SHA-256"
        );

        env.stop_server();
    }

    /// Requested algorithm differs from the source's: a source stored with SHA256, copied while
    /// requesting CRC32, must return/persist the freshly computed CRC32 and must NOT carry the
    /// source's SHA256 through. Guards the request-over-source precedence and the destination's
    /// checksum-not-inherited path, and exercises the CRC32 code path (a different branch of
    /// ChecksumType::from_string than SHA256).
    #[tokio::test]
    async fn test_copy_requested_algorithm_overrides_source_checksum() {
        init_logging();
        info!("Issue #4996: a requested CopyObject checksum algorithm must override the source object's algorithm");

        let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
        env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");

        let client = env.create_s3_client();
        let src_bucket = "copy-checksum-override-src";
        let dst_bucket = "copy-checksum-override-dst";
        let src_key = "objects/source.bin";
        let ref_key = "objects/reference-crc32.bin";
        let dst_key = "objects/dest.bin";

        create_versioned_bucket(&client, src_bucket).await;
        create_versioned_bucket(&client, dst_bucket).await;

        let content = b"payload whose copy must be re-checksummed with a different algorithm";
        let expected_sha256 = BASE64.encode_to_string(Sha256::digest(content));

        // Source is stored WITH a SHA-256 checksum.
        client
            .put_object()
            .bucket(src_bucket)
            .key(src_key)
            .checksum_algorithm(ChecksumAlgorithm::Sha256)
            .body(ByteStream::from_static(content))
            .send()
            .await
            .expect("PUT source with SHA256 failed");

        // Establish the canonical CRC32 the server computes for this content via a reference PUT,
        // so the copy's CRC32 can be asserted against an exact server-computed value.
        let ref_put = client
            .put_object()
            .bucket(src_bucket)
            .key(ref_key)
            .checksum_algorithm(ChecksumAlgorithm::Crc32)
            .body(ByteStream::from_static(content))
            .send()
            .await
            .expect("reference PUT with CRC32 failed");
        let expected_crc32 = ref_put
            .checksum_crc32()
            .expect("reference PUT must report a CRC32")
            .to_string();

        // Copy the SHA256 source while requesting CRC32.
        let copy_out = client
            .copy_object()
            .bucket(dst_bucket)
            .key(dst_key)
            .copy_source(format!("{src_bucket}/{src_key}"))
            .checksum_algorithm(ChecksumAlgorithm::Crc32)
            .send()
            .await
            .expect("CopyObject requesting a different algorithm must succeed");

        let result = copy_out
            .copy_object_result()
            .expect("issue #4996: CopyObject must return a CopyObjectResult");
        // The requested CRC32 must be computed and returned.
        assert_eq!(
            result.checksum_crc32(),
            Some(expected_crc32.as_str()),
            "issue #4996: a requested CRC32 must be computed fresh over the copied bytes"
        );
        // The source's SHA256 must NOT leak through — the requested algorithm wins.
        assert_eq!(
            result.checksum_sha256(),
            None,
            "issue #4996: the source object's SHA256 must not be inherited when a different algorithm is requested"
        );
        assert_ne!(
            result.checksum_crc32(),
            Some(expected_sha256.as_str()),
            "sanity: CRC32 field must not carry the SHA256 value"
        );

        // The destination must persist CRC32 (and only CRC32) for a checksum-mode HEAD.
        let head = client
            .head_object()
            .bucket(dst_bucket)
            .key(dst_key)
            .checksum_mode(ChecksumMode::Enabled)
            .send()
            .await
            .expect("HEAD destination failed");
        assert_eq!(
            head.checksum_crc32(),
            Some(expected_crc32.as_str()),
            "issue #4996: destination checksum-mode HEAD must return the requested CRC32"
        );
        assert_eq!(
            head.checksum_sha256(),
            None,
            "issue #4996: destination must not report the source's SHA256 after an override copy"
        );

        env.stop_server();
    }
}
