use bytes::Bytes;

use crate::{
    error::Error,
    types::{self, xml},
};

const S3_XMLNS: &str = "http://s3.amazonaws.com/doc/2006-03-01/";

pub(crate) fn parse_error_xml(body: &str) -> Option<xml::XmlError> {
    if body.trim().is_empty() {
        return None;
    }

    let fragment = extract_error_fragment(body)?;
    let mut parsed = quick_xml::de::from_str::<xml::XmlError>(fragment).ok()?;
    if parsed.request_id.is_none() {
        parsed.request_id = extract_tag_text(body, "RequestId");
    }
    if parsed.host_id.is_none() {
        parsed.host_id = extract_tag_text(body, "HostId");
    }
    normalize_error_fields(parsed)
}

fn extract_error_fragment(body: &str) -> Option<&str> {
    let start = find_error_start(body)?;
    let open_end = body[start..].find('>')? + start;
    let close_start = body[open_end + 1..].find("</Error>")? + open_end + 1;
    let close_end = close_start + "</Error>".len();
    body.get(start..close_end)
}

fn find_error_start(body: &str) -> Option<usize> {
    body.find("<Error>")
        .or_else(|| body.find("<Error "))
        .or_else(|| body.find("<Error\n"))
        .or_else(|| body.find("<Error\r"))
        .or_else(|| body.find("<Error\t"))
}

fn extract_tag_text(body: &str, tag: &str) -> Option<String> {
    let open = format!("<{tag}>");
    let close = format!("</{tag}>");
    let start = body.find(&open)? + open.len();
    let end = body[start..].find(&close)? + start;
    let value = body.get(start..end)?.trim();
    if value.is_empty() {
        return None;
    }
    Some(value.to_string())
}

fn normalize_error_fields(mut error: xml::XmlError) -> Option<xml::XmlError> {
    trim_optional_string(&mut error.code);
    trim_optional_string(&mut error.message);
    trim_optional_string(&mut error.request_id);
    trim_optional_string(&mut error.host_id);

    if error.code.is_none()
        && error.message.is_none()
        && error.request_id.is_none()
        && error.host_id.is_none()
    {
        return None;
    }

    Some(error)
}

fn trim_optional_string(value: &mut Option<String>) {
    let trimmed = value
        .as_deref()
        .map(str::trim)
        .filter(|v| !v.is_empty())
        .map(ToOwned::to_owned);
    *value = trimmed;
}

pub(crate) fn parse_list_objects_v2(body: &str) -> Result<types::ListObjectsV2Output, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlListBucketResult>(body).map_err(|e| {
        Error::decode(
            "failed to parse ListObjectsV2 XML response",
            Some(Box::new(e)),
        )
    })?;
    types::ListObjectsV2Output::try_from(parsed)
}

pub(crate) fn parse_list_buckets(body: &str) -> Result<types::ListBucketsOutput, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlListAllMyBucketsResult>(body).map_err(|e| {
        Error::decode(
            "failed to parse ListBuckets XML response",
            Some(Box::new(e)),
        )
    })?;
    Ok(types::ListBucketsOutput::from(parsed))
}

pub(crate) fn parse_bucket_versioning(
    body: &str,
) -> Result<types::BucketVersioningConfiguration, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlVersioningConfiguration>(body).map_err(|e| {
        Error::decode(
            "failed to parse GetBucketVersioning XML response",
            Some(Box::new(e)),
        )
    })?;

    Ok(types::BucketVersioningConfiguration {
        status: parsed.status.as_deref().and_then(parse_versioning_status),
        mfa_delete: parsed.mfa_delete.as_deref().and_then(parse_mfa_delete),
    })
}

pub(crate) fn parse_bucket_lifecycle(
    body: &str,
) -> Result<types::BucketLifecycleConfiguration, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlLifecycleConfiguration>(body).map_err(|e| {
        Error::decode(
            "failed to parse GetBucketLifecycle XML response",
            Some(Box::new(e)),
        )
    })?;

    Ok(types::BucketLifecycleConfiguration {
        rules: parsed
            .rules
            .into_iter()
            .filter_map(|r| {
                let status = match r.status.as_str() {
                    "Enabled" => types::BucketLifecycleStatus::Enabled,
                    "Disabled" => types::BucketLifecycleStatus::Disabled,
                    _ => return None,
                };

                let prefix = r
                    .filter
                    .and_then(|f| f.prefix)
                    .or(r.prefix)
                    .filter(|v| !v.is_empty());
                let (expiration_days, expiration_date) = match r.expiration {
                    Some(exp) => (exp.days, exp.date),
                    None => (None, None),
                };

                Some(types::BucketLifecycleRule {
                    id: r.id,
                    status,
                    prefix,
                    expiration_days,
                    expiration_date,
                })
            })
            .collect(),
    })
}

pub(crate) fn parse_bucket_cors(body: &str) -> Result<types::BucketCorsConfiguration, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlCorsConfiguration>(body).map_err(|e| {
        Error::decode(
            "failed to parse GetBucketCors XML response",
            Some(Box::new(e)),
        )
    })?;

    Ok(types::BucketCorsConfiguration {
        rules: parsed
            .rules
            .into_iter()
            .map(|r| types::BucketCorsRule {
                id: r.id,
                allowed_origins: r.allowed_origins,
                allowed_methods: r
                    .allowed_methods
                    .into_iter()
                    .map(parse_cors_method)
                    .collect(),
                allowed_headers: r.allowed_headers,
                expose_headers: r.expose_headers,
                max_age_seconds: r.max_age_seconds,
            })
            .collect(),
    })
}

pub(crate) fn parse_bucket_tagging(body: &str) -> Result<types::BucketTagging, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlTagging>(body).map_err(|e| {
        Error::decode(
            "failed to parse GetBucketTagging XML response",
            Some(Box::new(e)),
        )
    })?;

    let tags = parsed
        .tag_set
        .map(|ts| {
            ts.tags
                .into_iter()
                .map(|t| types::Tag {
                    key: t.key,
                    value: t.value,
                })
                .collect()
        })
        .unwrap_or_default();

    Ok(types::BucketTagging { tags })
}

pub(crate) fn parse_bucket_encryption(
    body: &str,
) -> Result<types::BucketEncryptionConfiguration, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlServerSideEncryptionConfiguration>(body)
        .map_err(|e| {
            Error::decode(
                "failed to parse GetBucketEncryption XML response",
                Some(Box::new(e)),
            )
        })?;

    let rules = parsed
        .rules
        .into_iter()
        .filter_map(|r| {
            let apply = r.apply?;
            Some(types::BucketEncryptionRule {
                apply: types::ApplyServerSideEncryptionByDefault {
                    sse_algorithm: parse_sse_algorithm(&apply.sse_algorithm),
                    kms_master_key_id: apply.kms_master_key_id,
                },
                bucket_key_enabled: r.bucket_key_enabled,
            })
        })
        .collect();

    Ok(types::BucketEncryptionConfiguration { rules })
}

pub(crate) fn parse_bucket_public_access_block(
    body: &str,
) -> Result<types::BucketPublicAccessBlockConfiguration, Error> {
    let parsed =
        quick_xml::de::from_str::<xml::XmlPublicAccessBlockConfiguration>(body).map_err(|e| {
            Error::decode(
                "failed to parse GetPublicAccessBlock XML response",
                Some(Box::new(e)),
            )
        })?;

    Ok(types::BucketPublicAccessBlockConfiguration {
        block_public_acls: parsed.block_public_acls.unwrap_or(false),
        ignore_public_acls: parsed.ignore_public_acls.unwrap_or(false),
        block_public_policy: parsed.block_public_policy.unwrap_or(false),
        restrict_public_buckets: parsed.restrict_public_buckets.unwrap_or(false),
    })
}

pub(crate) fn parse_delete_objects(body: &str) -> Result<types::DeleteObjectsOutput, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlDeleteResult>(body).map_err(|e| {
        Error::decode(
            "failed to parse DeleteObjects XML response",
            Some(Box::new(e)),
        )
    })?;
    Ok(types::DeleteObjectsOutput::from(parsed))
}

pub(crate) fn parse_copy_object(body: &str) -> Result<types::CopyObjectOutput, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlCopyObjectResult>(body)
        .map_err(|e| Error::decode("failed to parse CopyObject XML response", Some(Box::new(e))))?;
    Ok(types::CopyObjectOutput::from(parsed))
}

#[cfg(feature = "multipart")]
pub(crate) fn parse_create_multipart_upload(
    body: &str,
) -> Result<types::CreateMultipartUploadOutput, Error> {
    let parsed =
        quick_xml::de::from_str::<xml::XmlInitiateMultipartUploadResult>(body).map_err(|e| {
            Error::decode(
                "failed to parse CreateMultipartUpload XML response",
                Some(Box::new(e)),
            )
        })?;
    Ok(types::CreateMultipartUploadOutput::from(parsed))
}

#[cfg(feature = "multipart")]
pub(crate) fn parse_complete_multipart_upload(
    body: &str,
) -> Result<types::CompleteMultipartUploadOutput, Error> {
    let parsed =
        quick_xml::de::from_str::<xml::XmlCompleteMultipartUploadResult>(body).map_err(|e| {
            Error::decode(
                "failed to parse CompleteMultipartUpload XML response",
                Some(Box::new(e)),
            )
        })?;
    Ok(types::CompleteMultipartUploadOutput::from(parsed))
}

#[cfg(feature = "multipart")]
pub(crate) fn parse_list_parts(body: &str) -> Result<types::ListPartsOutput, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlListPartsResult>(body)
        .map_err(|e| Error::decode("failed to parse ListParts XML response", Some(Box::new(e))))?;
    Ok(types::ListPartsOutput::from(parsed))
}

#[cfg(feature = "multipart")]
pub(crate) fn parse_upload_part_copy(body: &str) -> Result<types::UploadPartCopyOutput, Error> {
    let parsed = quick_xml::de::from_str::<xml::XmlCopyPartResult>(body).map_err(|e| {
        Error::decode(
            "failed to parse UploadPartCopy XML response",
            Some(Box::new(e)),
        )
    })?;
    Ok(types::UploadPartCopyOutput::from(parsed))
}

pub(crate) fn encode_create_bucket_configuration(region: &str) -> Result<Bytes, Error> {
    if region.trim().is_empty() {
        return Err(Error::invalid_config(
            "create bucket location constraint must not be empty",
        ));
    }

    #[derive(serde::Serialize)]
    #[serde(rename = "CreateBucketConfiguration")]
    struct XmlCreateBucketConfiguration<'a> {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "LocationConstraint")]
        location_constraint: &'a str,
    }

    let xml = quick_xml::se::to_string(&XmlCreateBucketConfiguration {
        xmlns: S3_XMLNS,
        location_constraint: region,
    })
    .map_err(|e| {
        Error::decode(
            "failed to encode CreateBucketConfiguration XML",
            Some(Box::new(e)),
        )
    })?;
    Ok(Bytes::from(xml))
}

#[cfg(feature = "multipart")]
pub(crate) fn encode_complete_multipart_upload(
    parts: &[types::CompletedPart],
) -> Result<Bytes, Error> {
    if parts.is_empty() {
        return Err(Error::invalid_config(
            "complete multipart upload requires at least one part",
        ));
    }

    #[derive(serde::Serialize)]
    #[serde(rename = "CompleteMultipartUpload")]
    struct XmlOut<'a> {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "Part")]
        parts: Vec<XmlPart<'a>>,
    }

    #[derive(serde::Serialize)]
    struct XmlPart<'a> {
        #[serde(rename = "PartNumber")]
        part_number: u32,
        #[serde(rename = "ETag")]
        etag: &'a str,
    }

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        parts: parts
            .iter()
            .map(|p| XmlPart {
                part_number: p.part_number,
                etag: &p.etag,
            })
            .collect(),
    })
    .map_err(|e| {
        Error::decode(
            "failed to encode CompleteMultipartUpload XML",
            Some(Box::new(e)),
        )
    })?;

    Ok(Bytes::from(xml))
}

pub(crate) fn encode_delete_objects(
    objects: &[types::DeleteObjectIdentifier],
    quiet: bool,
) -> Result<Bytes, Error> {
    if objects.is_empty() {
        return Err(Error::invalid_config(
            "delete_objects requires at least one object",
        ));
    }

    #[derive(serde::Serialize)]
    #[serde(rename = "Delete")]
    struct XmlOut<'a> {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "Object")]
        objects: Vec<XmlObject<'a>>,
        #[serde(rename = "Quiet")]
        quiet: bool,
    }

    #[derive(serde::Serialize)]
    struct XmlObject<'a> {
        #[serde(rename = "Key")]
        key: &'a str,
        #[serde(rename = "VersionId", skip_serializing_if = "Option::is_none")]
        version_id: Option<&'a str>,
    }

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        objects: objects
            .iter()
            .map(|o| XmlObject {
                key: &o.key,
                version_id: o.version_id.as_deref(),
            })
            .collect(),
        quiet,
    })
    .map_err(|e| Error::decode("failed to encode DeleteObjects XML", Some(Box::new(e))))?;

    Ok(Bytes::from(xml))
}

pub(crate) fn encode_bucket_versioning(
    configuration: &types::BucketVersioningConfiguration,
) -> Result<Bytes, Error> {
    if configuration.status.is_none() {
        return Err(Error::invalid_config(
            "bucket versioning configuration must include status",
        ));
    }

    #[derive(serde::Serialize)]
    #[serde(rename = "VersioningConfiguration")]
    struct XmlOut {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "Status", skip_serializing_if = "Option::is_none")]
        status: Option<&'static str>,
        #[serde(rename = "MfaDelete", skip_serializing_if = "Option::is_none")]
        mfa_delete: Option<&'static str>,
    }

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        status: configuration.status.map(versioning_status_str),
        mfa_delete: configuration.mfa_delete.map(mfa_delete_str),
    })
    .map_err(|e| {
        Error::decode(
            "failed to encode VersioningConfiguration XML",
            Some(Box::new(e)),
        )
    })?;
    Ok(Bytes::from(xml))
}

pub(crate) fn encode_bucket_lifecycle(
    configuration: &types::BucketLifecycleConfiguration,
) -> Result<Bytes, Error> {
    if configuration.rules.is_empty() {
        return Err(Error::invalid_config(
            "bucket lifecycle configuration must include at least one rule",
        ));
    }

    #[derive(serde::Serialize)]
    #[serde(rename = "LifecycleConfiguration")]
    struct XmlOut {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "Rule")]
        rules: Vec<XmlRuleOut>,
    }

    #[derive(serde::Serialize)]
    struct XmlRuleOut {
        #[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
        id: Option<String>,
        #[serde(rename = "Status")]
        status: &'static str,
        #[serde(rename = "Filter", skip_serializing_if = "Option::is_none")]
        filter: Option<XmlFilterOut>,
        #[serde(rename = "Expiration", skip_serializing_if = "Option::is_none")]
        expiration: Option<XmlExpirationOut>,
    }

    #[derive(serde::Serialize)]
    struct XmlFilterOut {
        #[serde(rename = "Prefix", skip_serializing_if = "Option::is_none")]
        prefix: Option<String>,
    }

    #[derive(serde::Serialize)]
    struct XmlExpirationOut {
        #[serde(rename = "Days", skip_serializing_if = "Option::is_none")]
        days: Option<u32>,
        #[serde(rename = "Date", skip_serializing_if = "Option::is_none")]
        date: Option<String>,
    }

    let rules = configuration
        .rules
        .iter()
        .map(|r| XmlRuleOut {
            id: r.id.clone(),
            status: lifecycle_status_str(r.status),
            filter: if r.prefix.is_some() {
                Some(XmlFilterOut {
                    prefix: r.prefix.clone(),
                })
            } else {
                None
            },
            expiration: if r.expiration_days.is_some() || r.expiration_date.is_some() {
                Some(XmlExpirationOut {
                    days: r.expiration_days,
                    date: r.expiration_date.clone(),
                })
            } else {
                None
            },
        })
        .collect::<Vec<_>>();

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        rules,
    })
    .map_err(|e| {
        Error::decode(
            "failed to encode LifecycleConfiguration XML",
            Some(Box::new(e)),
        )
    })?;
    Ok(Bytes::from(xml))
}

pub(crate) fn encode_bucket_cors(
    configuration: &types::BucketCorsConfiguration,
) -> Result<Bytes, Error> {
    if configuration.rules.is_empty() {
        return Err(Error::invalid_config(
            "bucket cors configuration must include at least one rule",
        ));
    }

    #[derive(serde::Serialize)]
    #[serde(rename = "CORSConfiguration")]
    struct XmlOut {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "CORSRule")]
        rules: Vec<XmlRuleOut>,
    }

    #[derive(serde::Serialize)]
    struct XmlRuleOut {
        #[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
        id: Option<String>,
        #[serde(rename = "AllowedOrigin")]
        allowed_origins: Vec<String>,
        #[serde(rename = "AllowedMethod")]
        allowed_methods: Vec<String>,
        #[serde(rename = "AllowedHeader", skip_serializing_if = "Vec::is_empty")]
        allowed_headers: Vec<String>,
        #[serde(rename = "ExposeHeader", skip_serializing_if = "Vec::is_empty")]
        expose_headers: Vec<String>,
        #[serde(rename = "MaxAgeSeconds", skip_serializing_if = "Option::is_none")]
        max_age_seconds: Option<u32>,
    }

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        rules: configuration
            .rules
            .iter()
            .map(|r| XmlRuleOut {
                id: r.id.clone(),
                allowed_origins: r.allowed_origins.clone(),
                allowed_methods: r
                    .allowed_methods
                    .iter()
                    .map(|m| m.as_str().to_string())
                    .collect(),
                allowed_headers: r.allowed_headers.clone(),
                expose_headers: r.expose_headers.clone(),
                max_age_seconds: r.max_age_seconds,
            })
            .collect(),
    })
    .map_err(|e| Error::decode("failed to encode CORSConfiguration XML", Some(Box::new(e))))?;
    Ok(Bytes::from(xml))
}

pub(crate) fn encode_bucket_tagging(tagging: &types::BucketTagging) -> Result<Bytes, Error> {
    #[derive(serde::Serialize)]
    #[serde(rename = "Tagging")]
    struct XmlOut {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "TagSet")]
        tag_set: XmlTagSet,
    }

    #[derive(serde::Serialize)]
    struct XmlTagSet {
        #[serde(rename = "Tag")]
        tags: Vec<XmlTag>,
    }

    #[derive(serde::Serialize)]
    struct XmlTag {
        #[serde(rename = "Key")]
        key: String,
        #[serde(rename = "Value")]
        value: String,
    }

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        tag_set: XmlTagSet {
            tags: tagging
                .tags
                .iter()
                .map(|t| XmlTag {
                    key: t.key.clone(),
                    value: t.value.clone(),
                })
                .collect(),
        },
    })
    .map_err(|e| Error::decode("failed to encode Tagging XML", Some(Box::new(e))))?;
    Ok(Bytes::from(xml))
}

pub(crate) fn encode_bucket_encryption(
    configuration: &types::BucketEncryptionConfiguration,
) -> Result<Bytes, Error> {
    if configuration.rules.is_empty() {
        return Err(Error::invalid_config(
            "bucket encryption configuration must include at least one rule",
        ));
    }

    #[derive(serde::Serialize)]
    #[serde(rename = "ServerSideEncryptionConfiguration")]
    struct XmlOut {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "Rule")]
        rules: Vec<XmlRuleOut>,
    }

    #[derive(serde::Serialize)]
    struct XmlRuleOut {
        #[serde(rename = "ApplyServerSideEncryptionByDefault")]
        apply: XmlApplyOut,
        #[serde(rename = "BucketKeyEnabled", skip_serializing_if = "Option::is_none")]
        bucket_key_enabled: Option<bool>,
    }

    #[derive(serde::Serialize)]
    struct XmlApplyOut {
        #[serde(rename = "SSEAlgorithm")]
        sse_algorithm: String,
        #[serde(rename = "KMSMasterKeyID", skip_serializing_if = "Option::is_none")]
        kms_master_key_id: Option<String>,
    }

    let rules = configuration
        .rules
        .iter()
        .map(|r| XmlRuleOut {
            apply: XmlApplyOut {
                sse_algorithm: r.apply.sse_algorithm.as_str().to_string(),
                kms_master_key_id: r.apply.kms_master_key_id.clone(),
            },
            bucket_key_enabled: r.bucket_key_enabled,
        })
        .collect();

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        rules,
    })
    .map_err(|e| {
        Error::decode(
            "failed to encode ServerSideEncryptionConfiguration XML",
            Some(Box::new(e)),
        )
    })?;
    Ok(Bytes::from(xml))
}

pub(crate) fn encode_bucket_public_access_block(
    configuration: &types::BucketPublicAccessBlockConfiguration,
) -> Result<Bytes, Error> {
    #[derive(serde::Serialize)]
    #[serde(rename = "PublicAccessBlockConfiguration")]
    struct XmlOut {
        #[serde(rename = "@xmlns")]
        xmlns: &'static str,
        #[serde(rename = "BlockPublicAcls")]
        block_public_acls: bool,
        #[serde(rename = "IgnorePublicAcls")]
        ignore_public_acls: bool,
        #[serde(rename = "BlockPublicPolicy")]
        block_public_policy: bool,
        #[serde(rename = "RestrictPublicBuckets")]
        restrict_public_buckets: bool,
    }

    let xml = quick_xml::se::to_string(&XmlOut {
        xmlns: S3_XMLNS,
        block_public_acls: configuration.block_public_acls,
        ignore_public_acls: configuration.ignore_public_acls,
        block_public_policy: configuration.block_public_policy,
        restrict_public_buckets: configuration.restrict_public_buckets,
    })
    .map_err(|e| {
        Error::decode(
            "failed to encode PublicAccessBlockConfiguration XML",
            Some(Box::new(e)),
        )
    })?;
    Ok(Bytes::from(xml))
}

fn parse_versioning_status(value: &str) -> Option<types::BucketVersioningStatus> {
    match value {
        "Enabled" => Some(types::BucketVersioningStatus::Enabled),
        "Suspended" => Some(types::BucketVersioningStatus::Suspended),
        _ => None,
    }
}

fn versioning_status_str(value: types::BucketVersioningStatus) -> &'static str {
    match value {
        types::BucketVersioningStatus::Enabled => "Enabled",
        types::BucketVersioningStatus::Suspended => "Suspended",
    }
}

fn parse_mfa_delete(value: &str) -> Option<types::BucketMfaDeleteStatus> {
    match value {
        "Enabled" => Some(types::BucketMfaDeleteStatus::Enabled),
        "Disabled" => Some(types::BucketMfaDeleteStatus::Disabled),
        _ => None,
    }
}

fn mfa_delete_str(value: types::BucketMfaDeleteStatus) -> &'static str {
    match value {
        types::BucketMfaDeleteStatus::Enabled => "Enabled",
        types::BucketMfaDeleteStatus::Disabled => "Disabled",
    }
}

fn lifecycle_status_str(value: types::BucketLifecycleStatus) -> &'static str {
    match value {
        types::BucketLifecycleStatus::Enabled => "Enabled",
        types::BucketLifecycleStatus::Disabled => "Disabled",
    }
}

fn parse_cors_method(value: String) -> types::CorsMethod {
    match value.as_str() {
        "GET" => types::CorsMethod::Get,
        "PUT" => types::CorsMethod::Put,
        "POST" => types::CorsMethod::Post,
        "DELETE" => types::CorsMethod::Delete,
        "HEAD" => types::CorsMethod::Head,
        other => types::CorsMethod::Other(other.to_string()),
    }
}

fn parse_sse_algorithm(value: &str) -> types::SseAlgorithm {
    match value {
        "AES256" => types::SseAlgorithm::Aes256,
        "aws:kms" => types::SseAlgorithm::AwsKms,
        "aws:kms:dsse" => types::SseAlgorithm::AwsKmsDsse,
        other => types::SseAlgorithm::Other(other.to_string()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::DeleteObjectIdentifier;

    #[test]
    fn parses_error_xml() {
        let xml = r#"
<Error>
  <Code>NoSuchKey</Code>
  <Message>The specified key does not exist.</Message>
  <RequestId>req-123</RequestId>
  <HostId>host-456</HostId>
</Error>
"#;

        let err = parse_error_xml(xml).unwrap();
        assert_eq!(err.code.as_deref(), Some("NoSuchKey"));
        assert_eq!(
            err.message.as_deref(),
            Some("The specified key does not exist.")
        );
        assert_eq!(err.request_id.as_deref(), Some("req-123"));
        assert_eq!(err.host_id.as_deref(), Some("host-456"));
    }

    #[test]
    fn parses_wrapped_error_response_xml() {
        let xml = r#"
<ErrorResponse>
  <Error>
    <Code>AccessDenied</Code>
    <Message>Access denied</Message>
  </Error>
  <RequestId>req-outer</RequestId>
</ErrorResponse>
"#;

        let err = parse_error_xml(xml).expect("wrapped error response should parse");
        assert_eq!(err.code.as_deref(), Some("AccessDenied"));
        assert_eq!(err.message.as_deref(), Some("Access denied"));
        assert_eq!(err.request_id.as_deref(), Some("req-outer"));
    }

    #[test]
    fn parses_error_xml_with_request_id_only() {
        let xml = r#"
<Error>
  <RequestId>req-only</RequestId>
</Error>
"#;
        let err = parse_error_xml(xml).expect("request-id-only error should parse");
        assert_eq!(err.code, None);
        assert_eq!(err.message, None);
        assert_eq!(err.request_id.as_deref(), Some("req-only"));
    }

    #[test]
    fn ignores_non_error_xml_root() {
        let xml = r#"
<ListBucketResult>
  <Name>bucket-a</Name>
</ListBucketResult>
"#;
        assert!(parse_error_xml(xml).is_none());
    }

    #[test]
    fn parses_list_buckets() {
        let xml = r#"
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Owner>
    <ID>owner-id</ID>
    <DisplayName>owner</DisplayName>
  </Owner>
  <Buckets>
    <Bucket>
      <Name>bucket-a</Name>
      <CreationDate>2020-01-01T00:00:00.000Z</CreationDate>
    </Bucket>
  </Buckets>
</ListAllMyBucketsResult>
"#;

        let out = parse_list_buckets(xml).unwrap();
        assert_eq!(out.owner.unwrap().id.as_deref(), Some("owner-id"));
        assert_eq!(out.buckets.len(), 1);
        assert_eq!(out.buckets[0].name, "bucket-a");
    }

    #[test]
    fn parses_list_objects_v2() {
        let xml = r#"
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>bucket-a</Name>
  <KeyCount>1</KeyCount>
  <MaxKeys>1000</MaxKeys>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>logs/app.txt</Key>
    <ETag>"etag-1"</ETag>
    <Size>7</Size>
  </Contents>
</ListBucketResult>
"#;

        let out = parse_list_objects_v2(xml).unwrap();
        assert_eq!(out.name, "bucket-a");
        assert_eq!(out.key_count, Some(1));
        assert_eq!(out.contents.len(), 1);
        assert_eq!(out.contents[0].key, "logs/app.txt");
        assert_eq!(out.contents[0].etag.as_deref(), Some("\"etag-1\""));
    }

    #[test]
    fn parses_bucket_versioning() {
        let xml = r#"
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Status>Enabled</Status>
  <MfaDelete>Disabled</MfaDelete>
</VersioningConfiguration>
"#;

        let cfg = parse_bucket_versioning(xml).unwrap();
        assert_eq!(cfg.status, Some(types::BucketVersioningStatus::Enabled));
        assert_eq!(cfg.mfa_delete, Some(types::BucketMfaDeleteStatus::Disabled));
    }

    #[test]
    fn parses_bucket_lifecycle() {
        let xml = r#"
<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Rule>
    <ID>rule-1</ID>
    <Status>Enabled</Status>
    <Filter>
      <Prefix>logs/</Prefix>
    </Filter>
    <Expiration>
      <Days>30</Days>
    </Expiration>
  </Rule>
</LifecycleConfiguration>
"#;

        let cfg = parse_bucket_lifecycle(xml).unwrap();
        assert_eq!(cfg.rules.len(), 1);
        let r = &cfg.rules[0];
        assert_eq!(r.id.as_deref(), Some("rule-1"));
        assert_eq!(r.status, types::BucketLifecycleStatus::Enabled);
        assert_eq!(r.prefix.as_deref(), Some("logs/"));
        assert_eq!(r.expiration_days, Some(30));
    }

    #[test]
    fn parses_bucket_cors() {
        let xml = r#"
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <ID>rule-1</ID>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PATCH</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
  </CORSRule>
</CORSConfiguration>
"#;

        let cfg = parse_bucket_cors(xml).unwrap();
        assert_eq!(cfg.rules.len(), 1);
        let r = &cfg.rules[0];
        assert_eq!(r.id.as_deref(), Some("rule-1"));
        assert_eq!(r.allowed_origins, vec!["*".to_string()]);
        assert_eq!(
            r.allowed_methods,
            vec![
                types::CorsMethod::Get,
                types::CorsMethod::Other("PATCH".to_string())
            ]
        );
        assert_eq!(r.allowed_headers, vec!["*".to_string()]);
        assert_eq!(r.expose_headers, vec!["ETag".to_string()]);
        assert_eq!(r.max_age_seconds, Some(3000));
    }

    #[test]
    fn parses_bucket_tagging() {
        let xml = r#"
<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <TagSet>
    <Tag>
      <Key>k</Key>
      <Value>v</Value>
    </Tag>
  </TagSet>
</Tagging>
"#;

        let cfg = parse_bucket_tagging(xml).unwrap();
        assert_eq!(cfg.tags.len(), 1);
        assert_eq!(cfg.tags[0].key, "k");
        assert_eq!(cfg.tags[0].value, "v");
    }

    #[test]
    fn parses_bucket_encryption() {
        let xml = r#"
<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Rule>
    <ApplyServerSideEncryptionByDefault>
      <SSEAlgorithm>aws:kms</SSEAlgorithm>
      <KMSMasterKeyID>key-id</KMSMasterKeyID>
    </ApplyServerSideEncryptionByDefault>
    <BucketKeyEnabled>true</BucketKeyEnabled>
  </Rule>
</ServerSideEncryptionConfiguration>
"#;

        let cfg = parse_bucket_encryption(xml).unwrap();
        assert_eq!(cfg.rules.len(), 1);
        assert_eq!(
            cfg.rules[0].apply.sse_algorithm,
            types::SseAlgorithm::AwsKms
        );
        assert_eq!(
            cfg.rules[0].apply.kms_master_key_id.as_deref(),
            Some("key-id")
        );
        assert_eq!(cfg.rules[0].bucket_key_enabled, Some(true));
    }

    #[test]
    fn parses_bucket_public_access_block() {
        let xml = r#"
<PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <BlockPublicAcls>true</BlockPublicAcls>
  <IgnorePublicAcls>false</IgnorePublicAcls>
  <BlockPublicPolicy>true</BlockPublicPolicy>
  <RestrictPublicBuckets>false</RestrictPublicBuckets>
</PublicAccessBlockConfiguration>
"#;

        let cfg = parse_bucket_public_access_block(xml).unwrap();
        assert!(cfg.block_public_acls);
        assert!(!cfg.ignore_public_acls);
        assert!(cfg.block_public_policy);
        assert!(!cfg.restrict_public_buckets);
    }

    #[test]
    fn parses_delete_objects() {
        let xml = r#"
<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Deleted>
    <Key>a</Key>
  </Deleted>
  <Error>
    <Key>b</Key>
    <Code>AccessDenied</Code>
    <Message>Access Denied</Message>
  </Error>
</DeleteResult>
"#;

        let out = parse_delete_objects(xml).unwrap();
        assert_eq!(out.deleted.len(), 1);
        assert_eq!(out.deleted[0].key.as_deref(), Some("a"));
        assert_eq!(out.errors.len(), 1);
        assert_eq!(out.errors[0].key.as_deref(), Some("b"));
        assert_eq!(out.errors[0].code.as_deref(), Some("AccessDenied"));
    }

    #[test]
    fn parses_copy_object() {
        let xml = r#"
<CopyObjectResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <LastModified>2020-01-01T00:00:00.000Z</LastModified>
  <ETag>"etag"</ETag>
</CopyObjectResult>
"#;

        let out = parse_copy_object(xml).unwrap();
        assert_eq!(out.etag.as_deref(), Some("\"etag\""));
        assert_eq!(
            out.last_modified.as_deref(),
            Some("2020-01-01T00:00:00.000Z")
        );
    }

    #[test]
    fn encodes_delete_objects_request() {
        let objects = vec![
            DeleteObjectIdentifier::new("a.txt"),
            DeleteObjectIdentifier::new("b.txt").with_version_id("v1"),
        ];
        let xml = encode_delete_objects(&objects, true).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();

        assert!(xml.contains("<Delete"));
        assert!(xml.contains("<Quiet>true</Quiet>"));
        assert!(xml.contains("<Key>a.txt</Key>"));
        assert!(xml.contains("<Key>b.txt</Key>"));
        assert!(xml.contains("<VersionId>v1</VersionId>"));
    }

    #[test]
    fn encodes_bucket_versioning() {
        let cfg = types::BucketVersioningConfiguration {
            status: Some(types::BucketVersioningStatus::Enabled),
            mfa_delete: Some(types::BucketMfaDeleteStatus::Disabled),
        };
        let xml = encode_bucket_versioning(&cfg).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<VersioningConfiguration"));
        assert!(xml.contains("xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\""));
        assert!(xml.contains("<Status>Enabled</Status>"));
        assert!(xml.contains("<MfaDelete>Disabled</MfaDelete>"));
    }

    #[test]
    fn encodes_create_bucket_configuration() {
        let xml = encode_create_bucket_configuration("eu-central-1").unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<CreateBucketConfiguration"));
        assert!(xml.contains("xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\""));
        assert!(xml.contains("<LocationConstraint>eu-central-1</LocationConstraint>"));
    }

    #[test]
    fn encodes_bucket_lifecycle() {
        let cfg = types::BucketLifecycleConfiguration {
            rules: vec![types::BucketLifecycleRule {
                id: Some("rule-1".to_string()),
                status: types::BucketLifecycleStatus::Enabled,
                prefix: Some("logs/".to_string()),
                expiration_days: Some(30),
                expiration_date: None,
            }],
        };

        let xml = encode_bucket_lifecycle(&cfg).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<LifecycleConfiguration"));
        assert!(xml.contains("<Rule>"));
        assert!(xml.contains("<ID>rule-1</ID>"));
        assert!(xml.contains("<Status>Enabled</Status>"));
        assert!(xml.contains("<Prefix>logs/</Prefix>"));
        assert!(xml.contains("<Days>30</Days>"));
    }

    #[test]
    fn encodes_bucket_cors() {
        let cfg = types::BucketCorsConfiguration {
            rules: vec![types::BucketCorsRule {
                id: Some("rule-1".to_string()),
                allowed_origins: vec!["*".to_string()],
                allowed_methods: vec![types::CorsMethod::Get, types::CorsMethod::Put],
                allowed_headers: vec!["*".to_string()],
                expose_headers: vec!["ETag".to_string()],
                max_age_seconds: Some(3000),
            }],
        };

        let xml = encode_bucket_cors(&cfg).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<CORSConfiguration"));
        assert!(xml.contains("<AllowedOrigin>*</AllowedOrigin>"));
        assert!(xml.contains("<AllowedMethod>GET</AllowedMethod>"));
        assert!(xml.contains("<AllowedMethod>PUT</AllowedMethod>"));
    }

    #[test]
    fn encodes_bucket_tagging() {
        let cfg = types::BucketTagging {
            tags: vec![types::Tag {
                key: "k".to_string(),
                value: "v".to_string(),
            }],
        };
        let xml = encode_bucket_tagging(&cfg).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<Tagging"));
        assert!(xml.contains("<Key>k</Key>"));
        assert!(xml.contains("<Value>v</Value>"));
    }

    #[test]
    fn encodes_bucket_encryption() {
        let cfg = types::BucketEncryptionConfiguration {
            rules: vec![types::BucketEncryptionRule {
                apply: types::ApplyServerSideEncryptionByDefault {
                    sse_algorithm: types::SseAlgorithm::AwsKms,
                    kms_master_key_id: Some("key-id".to_string()),
                },
                bucket_key_enabled: Some(true),
            }],
        };
        let xml = encode_bucket_encryption(&cfg).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<ServerSideEncryptionConfiguration"));
        assert!(xml.contains("<SSEAlgorithm>aws:kms</SSEAlgorithm>"));
        assert!(xml.contains("<KMSMasterKeyID>key-id</KMSMasterKeyID>"));
        assert!(xml.contains("<BucketKeyEnabled>true</BucketKeyEnabled>"));
    }

    #[test]
    fn encodes_bucket_public_access_block() {
        let cfg = types::BucketPublicAccessBlockConfiguration {
            block_public_acls: true,
            ignore_public_acls: false,
            block_public_policy: true,
            restrict_public_buckets: false,
        };
        let xml = encode_bucket_public_access_block(&cfg).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<PublicAccessBlockConfiguration"));
        assert!(xml.contains("<BlockPublicAcls>true</BlockPublicAcls>"));
        assert!(xml.contains("<IgnorePublicAcls>false</IgnorePublicAcls>"));
    }

    #[cfg(feature = "multipart")]
    #[test]
    fn encodes_complete_multipart_upload() {
        let parts = vec![
            types::CompletedPart {
                part_number: 1,
                etag: "\"etag1\"".to_string(),
            },
            types::CompletedPart {
                part_number: 2,
                etag: "\"etag2\"".to_string(),
            },
        ];
        let xml = encode_complete_multipart_upload(&parts).unwrap();
        let xml = String::from_utf8_lossy(&xml).to_string();
        assert!(xml.contains("<CompleteMultipartUpload"));
        assert!(xml.contains("<PartNumber>1</PartNumber>"));
        assert!(xml.contains("<ETag>\"etag1\"</ETag>"));
    }
}
