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

//! Regression tests for Issue #2427: object keys with a leading slash.
//!
//! AWS S3 accepts keys such as `/foo/bar` (request path `PUT /bucket//foo/bar`),
//! and some SDK wrappers produce such keys. RustFS normalizes these MinIO-style:
//! leading slashes are stripped and duplicate slashes after a leading slash are
//! collapsed, so `//foo/bar` is stored and served as `foo/bar`.

#[cfg(test)]
mod tests {
    use crate::common::{RustFSTestEnvironment, init_logging};
    use aws_sdk_s3::Client;
    use aws_sdk_s3::primitives::ByteStream;
    use std::error::Error;
    use tracing::info;

    async fn create_bucket(client: &Client, bucket: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
        client.create_bucket().bucket(bucket).send().await?;
        Ok(())
    }

    /// PUT with a leading-slash key must succeed and the object must be
    /// readable under the normalized key (leading slash stripped).
    #[tokio::test]
    async fn test_put_object_with_leading_slash_key() -> Result<(), Box<dyn Error + Send + Sync>> {
        init_logging();
        info!("Starting test: PUT object with leading slash in key (Issue #2427)");

        let mut env = RustFSTestEnvironment::new().await?;
        env.start_rustfs_server(vec![]).await?;

        let client = env.create_s3_client();
        let bucket = "test-leading-slash";
        create_bucket(&client, bucket).await?;

        // Key "/foo/bar" produces the request path "PUT /bucket//foo/bar".
        let content = b"leading slash content";
        client
            .put_object()
            .bucket(bucket)
            .key("/foo/bar")
            .body(ByteStream::from_static(content))
            .send()
            .await
            .expect("PUT with leading-slash key must succeed");
        info!("PUT with leading-slash key succeeded");

        // The object is stored under the normalized key "foo/bar".
        let output = client
            .get_object()
            .bucket(bucket)
            .key("foo/bar")
            .send()
            .await
            .expect("GET with normalized key must succeed");
        let body = output.body.collect().await?.into_bytes();
        assert_eq!(body.as_ref(), content, "content mismatch for normalized key");

        // GET with the original leading-slash key is normalized to the same object.
        let output = client
            .get_object()
            .bucket(bucket)
            .key("/foo/bar")
            .send()
            .await
            .expect("GET with leading-slash key must succeed");
        let body = output.body.collect().await?.into_bytes();
        assert_eq!(body.as_ref(), content, "content mismatch for leading-slash key");

        // LIST must report the normalized key only.
        let output = client.list_objects_v2().bucket(bucket).send().await?;
        let keys: Vec<&str> = output.contents().iter().filter_map(|o| o.key()).collect();
        assert_eq!(keys, vec!["foo/bar"], "LIST must contain only the normalized key");

        env.stop_server();
        info!("Test completed successfully");
        Ok(())
    }

    /// Duplicate and repeated slashes after a leading slash collapse MinIO-style.
    #[tokio::test]
    async fn test_put_object_with_duplicate_slashes_normalized() -> Result<(), Box<dyn Error + Send + Sync>> {
        init_logging();
        info!("Starting test: duplicate slash normalization (Issue #2427)");

        let mut env = RustFSTestEnvironment::new().await?;
        env.start_rustfs_server(vec![]).await?;

        let client = env.create_s3_client();
        let bucket = "test-duplicate-slash";
        create_bucket(&client, bucket).await?;

        // (request key, normalized stored key)
        let cases = [("//keyname", "keyname"), ("/dir///sub//file", "dir/sub/file")];

        for (raw_key, normalized_key) in cases {
            let content = format!("content for {raw_key}");
            client
                .put_object()
                .bucket(bucket)
                .key(raw_key)
                .body(ByteStream::from(content.clone().into_bytes()))
                .send()
                .await
                .unwrap_or_else(|e| panic!("PUT with key '{raw_key}' must succeed: {e:?}"));

            let output = client
                .get_object()
                .bucket(bucket)
                .key(normalized_key)
                .send()
                .await
                .unwrap_or_else(|e| panic!("GET with normalized key '{normalized_key}' must succeed: {e:?}"));
            let body = output.body.collect().await?.into_bytes();
            assert_eq!(body.as_ref(), content.as_bytes(), "content mismatch for key '{raw_key}'");
            info!("PUT '{}' stored and readable as '{}'", raw_key, normalized_key);
        }

        // DELETE through the raw key removes the normalized object.
        client.delete_object().bucket(bucket).key("//keyname").send().await?;
        let error = client
            .get_object()
            .bucket(bucket)
            .key("keyname")
            .send()
            .await
            .expect_err("object must be gone after DELETE with raw key");
        assert_eq!(
            error.raw_response().map(|response| response.status().as_u16()),
            Some(404),
            "GET after DELETE with raw key must return HTTP 404, got {error:?}"
        );

        env.stop_server();
        info!("Test completed successfully");
        Ok(())
    }
}
