/**
 * Copyright (C) 2016 Etaia AS ([email protected])
 *
 * 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.
 */
package com.hubrick.vertx.s3.client;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
import com.hubrick.vertx.s3.AbstractFunctionalTest;
import com.hubrick.vertx.s3.S3TestCredentials;
import com.hubrick.vertx.s3.exception.HttpErrorException;
import com.hubrick.vertx.s3.model.AccessControlPolicy;
import com.hubrick.vertx.s3.model.CannedAcl;
import com.hubrick.vertx.s3.model.ErrorCode;
import com.hubrick.vertx.s3.model.Part;
import com.hubrick.vertx.s3.model.Response;
import com.hubrick.vertx.s3.model.header.CommonResponseHeaders;
import com.hubrick.vertx.s3.model.header.CompleteMultipartUploadResponseHeaders;
import com.hubrick.vertx.s3.model.header.ContinueMultipartUploadResponseHeaders;
import com.hubrick.vertx.s3.model.header.InitMultipartUploadResponseHeaders;
import com.hubrick.vertx.s3.model.request.AbortMultipartUploadRequest;
import com.hubrick.vertx.s3.model.request.AclHeadersRequest;
import com.hubrick.vertx.s3.model.request.CompleteMultipartUploadRequest;
import com.hubrick.vertx.s3.model.request.ContinueMultipartUploadRequest;
import com.hubrick.vertx.s3.model.request.CopyObjectRequest;
import com.hubrick.vertx.s3.model.request.DeleteObjectRequest;
import com.hubrick.vertx.s3.model.request.GetBucketRequest;
import com.hubrick.vertx.s3.model.request.GetObjectRequest;
import com.hubrick.vertx.s3.model.request.HeadObjectRequest;
import com.hubrick.vertx.s3.model.request.InitMultipartUploadRequest;
import com.hubrick.vertx.s3.model.request.PutObjectAclRequest;
import com.hubrick.vertx.s3.model.request.PutObjectRequest;
import com.hubrick.vertx.s3.model.response.CompleteMultipartUploadResponse;
import com.hubrick.vertx.s3.model.response.MultipartUploadWriteStream;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import org.apache.commons.lang3.ArrayUtils;
import org.junit.Before;
import org.mockserver.model.BinaryBody;
import org.mockserver.model.Body;
import org.mockserver.model.Header;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.StringBody;
import org.mockserver.model.XmlBody;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.hubrick.vertx.s3.VertxMatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

/**
 * @author marcus
 * @since 1.0.0
 */
public abstract class AbstractS3ClientTest extends AbstractFunctionalTest {

    public static final String HOSTNAME = "localhost";

    private S3Client s3Client;

    @Before
    public void setUp() throws Exception {
        final S3ClientOptions clientOptions = new S3ClientOptions();
        clientOptions.setDefaultHost(HOSTNAME);
        clientOptions.setDefaultPort(MOCKSERVER_PORT);
        clientOptions.setMaxPoolSize(10);
        clientOptions.setAwsRegion(S3TestCredentials.REGION);
        clientOptions.setAwsServiceName(S3TestCredentials.SERVICE_NAME);
        clientOptions.setHostnameOverride(HOSTNAME);

        augmentClientOptions(clientOptions);

        s3Client = new S3Client(
                vertx,
                clientOptions,
                Clock.fixed(Instant.ofEpochSecond(1478782934), ZoneId.of("UTC")));

    }

    protected abstract void augmentClientOptions(final S3ClientOptions clientOptions);

    void addCredentials(final S3ClientOptions clientOptions) {
        clientOptions.setAwsAccessKey(S3TestCredentials.AWS_ACCESS_KEY);
        clientOptions.setAwsSecretKey(S3TestCredentials.AWS_SECRET_KEY);
    }

    void mockGetObject(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "GET",
                "/bucket/key",
                200,
                "response".getBytes(),
                expectedHeaders
        );
    }

    void mockGetObjectErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "GET",
                "/bucket/key",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                expectedHeaders
        );
    }


    void verifyGetObject(TestContext testContext) {
        final Async async = testContext.async();
        s3Client.getObject("bucket", "key", new GetObjectRequest(),
                (getObjectResponse) -> {
                    assertThat(testContext, getObjectResponse.getHeader(), notNullValue());
                    assertThat(testContext, getObjectResponse.getData(), notNullValue());

                    getObjectResponse.getData().handler(buffer -> {
                        assertThat(testContext, new String(buffer.getBytes(), StandardCharsets.UTF_8), is("response"));
                        async.complete();

                    });
                },
                testContext::fail);
    }

    void verifyGetObjectErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.getObject("bucket", "key", new GetObjectRequest(),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mockGetObjectAcl(AccessControlPolicy accessControlPolicy, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("acl", ImmutableList.of("")),
                "GET",
                "/bucket/key",
                200,
                null,
                new StringBody("<AccessControlPolicy><Owner><ID>" + accessControlPolicy.getOwner().getId() + "</ID><DisplayName>" + accessControlPolicy.getOwner().getDisplayName() + "</DisplayName></Owner>" +
                        "<AccessControlList><Grant><Grantee><ID>" + accessControlPolicy.getGrants().get(0).getGrantee().getId() + "</ID><DisplayName>" + accessControlPolicy.getGrants().get(0).getGrantee().getDisplayName() + "</DisplayName></Grantee><Permission>" + accessControlPolicy.getGrants().get(0).getPermission() + "</Permission></Grant></AccessControlList></AccessControlPolicy>"
                ),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void mockGetObjectAclErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("acl", ImmutableList.of("")),
                "GET",
                "/bucket/key",
                403,
                null,
                new BinaryBody(Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml"))),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void verifyGetObjectAcl(TestContext testContext, AccessControlPolicy accessControlPolicy) {
        final Async async = testContext.async();
        s3Client.getObjectAcl("bucket", "key",
                (getObjectResponse) -> {
                    assertThat(testContext, getObjectResponse.getHeader(), notNullValue());
                    assertThat(testContext, getObjectResponse.getData(), notNullValue());
                    assertThat(testContext, getObjectResponse.getData(), is(accessControlPolicy));

                    async.complete();
                },
                testContext::fail);
    }

    void verifyGetObjectAclErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.getObjectAcl("bucket", "key",
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mockHeadObject(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "HEAD",
                "/bucket/key",
                200,
                "0".getBytes(),
                expectedHeaders
        );
    }

    void mockHeadObjectErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "HEAD",
                "/bucket/key",
                403,
                "0".getBytes(),
                expectedHeaders
        );
    }

    void verifyHeadObject(TestContext testContext) {
        final Async async = testContext.async();
        s3Client.headObject("bucket", "key", new HeadObjectRequest(),
                (response) -> {
                    assertThat(testContext, response, notNullValue());
                    async.complete();
                },
                testContext::fail
        );
    }

    void verifyHeadObjectErrorResponse(final TestContext testContext) {
        final Async async = testContext.async();
        s3Client.headObject("bucket", "key", new HeadObjectRequest(),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse(), nullValue());
                    async.complete();
                }
        );
    }

    void mockPutObject(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "PUT",
                "/bucket/key",
                200,
                new StringBody("test"),
                new StringBody("<>"),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void mockPutObjectErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "PUT",
                "/bucket/key",
                403,
                new StringBody("test"),
                new BinaryBody(Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml"))),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void verifyPutObject(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.putObject("bucket", "key", new PutObjectRequest(Buffer.buffer("test")),
                (putResponseHeaders) -> {
                    assertThat(testContext, putResponseHeaders, notNullValue());
                    async.complete();
                },
                testContext::fail);
    }

    void verifyPutObjectErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.putObject("bucket", "key", new PutObjectRequest(Buffer.buffer("test")),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mockPutObjectAclWithHeaders(Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("acl", ImmutableList.of("")),
                "PUT",
                "/bucket/key",
                200,
                "test".getBytes(),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void verifyPutObjectAclWithHeaders(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.putObjectAcl("bucket", "key", new PutObjectAclRequest(new AclHeadersRequest().withAmzAcl(CannedAcl.PRIVATE)),
                (putResponseHeaders) -> {
                    assertThat(testContext, putResponseHeaders, notNullValue());
                    async.complete();
                },
                testContext::fail);
    }

    void mockPutObjectAclWithHeadersErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("acl", ImmutableList.of("")),
                "PUT",
                "/bucket/key",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void verifyPutObjectAclWithHeadersErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.putObjectAcl("bucket", "key", new PutObjectAclRequest(new AclHeadersRequest().withAmzAcl(CannedAcl.PRIVATE)),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mockPutObjectAclWithBody(AccessControlPolicy accessControlPolicy, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("acl", ImmutableList.of("")),
                "PUT",
                "/bucket/key",
                200,
                new XmlBody("<AccessControlPolicy><Owner><ID>" + accessControlPolicy.getOwner().getId() + "</ID><DisplayName>" + accessControlPolicy.getOwner().getDisplayName() + "</DisplayName></Owner>" +
                        "<AccessControlList><Grant><Grantee><ID>" + accessControlPolicy.getGrants().get(0).getGrantee().getId() + "</ID><DisplayName>" + accessControlPolicy.getGrants().get(0).getGrantee().getDisplayName() + "</DisplayName></Grantee><Permission>" + accessControlPolicy.getGrants().get(0).getPermission() + "</Permission></Grant></AccessControlList></AccessControlPolicy>"
                ),
                new StringBody("<>"),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void verifyPutObjectAclWithBody(AccessControlPolicy accessControlPolicy, final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.putObjectAcl("bucket", "key", new PutObjectAclRequest(accessControlPolicy),
                (putResponseHeaders) -> {
                    assertThat(testContext, putResponseHeaders, notNullValue());
                    async.complete();
                },
                testContext::fail);
    }

    void mockPutObjectAclWithBodyErrorResponse(AccessControlPolicy accessControlPolicy, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("acl", ImmutableList.of("")),
                "PUT",
                "/bucket/key",
                403,
                new XmlBody("<AccessControlPolicy><Owner><ID>" + accessControlPolicy.getOwner().getId() + "</ID><DisplayName>" + accessControlPolicy.getOwner().getDisplayName() + "</DisplayName></Owner>" +
                        "<AccessControlList><Grant><Grantee><ID>" + accessControlPolicy.getGrants().get(0).getGrantee().getId() + "</ID><DisplayName>" + accessControlPolicy.getGrants().get(0).getGrantee().getDisplayName() + "</DisplayName></Grantee><Permission>" + accessControlPolicy.getGrants().get(0).getPermission() + "</Permission></Grant></AccessControlList></AccessControlPolicy>"
                ),
                new BinaryBody(Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml"))),
                Collections.emptyList(),
                expectedHeaders
        );
    }

    void verifyPutObjectAclWithBodyErrorResponse(AccessControlPolicy accessControlPolicy, final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.putObjectAcl("bucket", "key", new PutObjectAclRequest(accessControlPolicy),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mockInitMultipartUpload(String uploadId, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("uploads", ImmutableList.of("")),
                "POST",
                "/bucket/key",
                200,
                ("<InitiateMultipartUploadResult><Bucket>bucket</Bucket><Key>key</Key><UploadId>" + uploadId + "</UploadId></InitiateMultipartUploadResult>").getBytes(),
                expectedHeaders
        );
    }

    void mockInitMultipartUploadErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("uploads", ImmutableList.of("")),
                "POST",
                "/bucket/key",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                expectedHeaders
        );
    }

    void verifyInitMultipartUpload(final TestContext testContext) {

        final Async async = testContext.async();
        callInitMultipartUpload(testContext, event -> {
            assertThat(testContext, event.getData(), notNullValue());
            assertThat(testContext, event.getHeader(), notNullValue());

            async.complete();
        });
    }

    void verifyInitMultipartUploadErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.initMultipartUpload("bucket", "key", new InitMultipartUploadRequest(),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void callInitMultipartUpload(final TestContext testContext, Handler<Response<InitMultipartUploadResponseHeaders, MultipartUploadWriteStream>> handler) {

        s3Client.initMultipartUpload(
                "bucket",
                "key",
                new InitMultipartUploadRequest(),
                handler,
                testContext::fail
        );
    }


    void mockContinueMultipartUpload(Integer partNumber, String uploadId, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("partNumber", ImmutableList.of(partNumber.toString()), "uploadId", ImmutableList.of(uploadId)),
                "PUT",
                "/bucket/key",
                200,
                "<>".getBytes(),
                ImmutableList.of(new Header("ETag", "someetag")),
                expectedHeaders
        );
    }

    void mockContinueMultipartUploadErrorResponse(Integer partNumber, String uploadId, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("partNumber", ImmutableList.of(partNumber.toString()), "uploadId", ImmutableList.of(uploadId)),
                "PUT",
                "/bucket/key",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                expectedHeaders
        );
    }

    void verifyContinueMultipartUpload(final TestContext testContext, Integer partNumber, String uploadId) {

        final Async async = testContext.async();
        callContinueMultipartUpload(testContext, partNumber, uploadId, event -> {
            assertThat(testContext, event.getData(), nullValue());
            assertThat(testContext, event.getHeader(), notNullValue());

            async.complete();
        });
    }

    void verifyContinueMultipartUploadErrorResponse(final TestContext testContext, Integer partNumber, String uploadId) {

        final Async async = testContext.async();
        s3Client.continueMultipartUpload(
                "bucket",
                "key",
                new ContinueMultipartUploadRequest(Buffer.buffer("whatever".getBytes()), partNumber, uploadId),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void callContinueMultipartUpload(final TestContext testContext, Integer partNumber, String uploadId, Handler<Response<ContinueMultipartUploadResponseHeaders, Void>> handler) {

        s3Client.continueMultipartUpload(
                "bucket",
                "key",
                new ContinueMultipartUploadRequest(Buffer.buffer("some data".getBytes()), partNumber, uploadId),
                handler,
                testContext::fail
        );
    }

    void mockCompleteMultipartUpload(String uploadId, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("uploadId", ImmutableList.of(uploadId)),
                "POST",
                "/bucket/key",
                200,
                "<CompleteMultipartUploadResult><Location>whatever</Location><Bucket>bucket</Bucket><Key>key</Key><ETag>whatever</ETag></CompleteMultipartUploadResult>".getBytes(),
                expectedHeaders
        );
    }

    void mockCompleteMultipartUploadErrorResponse(String uploadId, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("uploadId", ImmutableList.of(uploadId)),
                "POST",
                "/bucket/key",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                expectedHeaders
        );
    }

    void verifyCompleteMultipartUpload(final TestContext testContext, String uploadId) {

        final Async async = testContext.async();
        callCompleteMultipartUpload(testContext, Collections.emptyList(), uploadId, event -> {
            assertThat(testContext, event.getData(), notNullValue());
            assertThat(testContext, event.getHeader(), notNullValue());

            async.complete();
        });
    }

    void verifyCompleteMultipartUploadErrorResponse(final TestContext testContext, String uploadId) {

        final Async async = testContext.async();
        s3Client.completeMultipartUpload(
                "bucket",
                "key",
                new CompleteMultipartUploadRequest(uploadId, Collections.emptyList()),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void callCompleteMultipartUpload(final TestContext testContext, List<Part> parts, String uploadId, Handler<Response<CompleteMultipartUploadResponseHeaders, CompleteMultipartUploadResponse>> handler) {

        s3Client.completeMultipartUpload(
                "bucket",
                "key",
                new CompleteMultipartUploadRequest(uploadId, parts),
                handler,
                testContext::fail
        );
    }

    void mockAbortMultipartUpload(String uploadId, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("uploadId", ImmutableList.of(uploadId)),
                "DELETE",
                "/bucket/key",
                200,
                "<>".getBytes(),
                expectedHeaders
        );
    }

    void mockAbortMultipartUploadErrorResponse(String uploadId, Header... expectedHeaders) throws IOException {
        mock(
                ImmutableMap.of("uploadId", ImmutableList.of(uploadId)),
                "DELETE",
                "/bucket/key",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                expectedHeaders
        );
    }

    void verifyAbortMultipartUpload(final TestContext testContext, String uploadId) {
        final Async async = testContext.async();
        callAbortMultipartUpload(testContext, uploadId, event -> {
            assertThat(testContext, event.getData(), nullValue());
            assertThat(testContext, event.getHeader(), notNullValue());

            async.complete();
        });
    }

    void verifyAbortMultipartUploadErrorResponse(final TestContext testContext, String uploadId) {

        final Async async = testContext.async();
        s3Client.abortMultipartUpload(
                "bucket",
                "key",
                new AbortMultipartUploadRequest(uploadId),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void callAbortMultipartUpload(final TestContext testContext, String uploadId, Handler<Response<CommonResponseHeaders, Void>> handler) {

        s3Client.abortMultipartUpload(
                "bucket",
                "key",
                new AbortMultipartUploadRequest(uploadId),
                handler,
                testContext::fail
        );
    }

    void mockDeleteObject(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "DELETE",
                "/bucket/key",
                200,
                "<>".getBytes(),
                expectedHeaders
        );
    }

    void mockDeleteObjectErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "DELETE",
                "/bucket/key",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                expectedHeaders
        );
    }

    void verifyDeleteObject(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.deleteObject("bucket", "key", new DeleteObjectRequest(),
                (commonResponseHeaders) -> {
                    assertThat(testContext, commonResponseHeaders, notNullValue());
                    async.complete();
                },
                testContext::fail);
    }

    void verifyDeleteObjectErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.deleteObject(
                "bucket", "key", new DeleteObjectRequest(),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mockCopyObject(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "PUT",
                "/destinationBucket/destinationKey",
                200,
                "<CopyObjectResult></CopyObjectResult>".getBytes(),
                ArrayUtils.add(expectedHeaders, Header.header("X-Amz-Copy-Source", "/sourceBucket/sourceKey"))
        );
    }

    void mockCopyObjectErrorResponse(Header... expectedHeaders) throws IOException {
        mock(
                Collections.emptyMap(),
                "PUT",
                "/destinationBucket/destinationKey",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                ArrayUtils.add(expectedHeaders, Header.header("X-Amz-Copy-Source", "/sourceBucket/sourceKey"))
        );
    }

    void verifyCopyObject(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.copyObject(
                "sourceBucket", "sourceKey",
                "destinationBucket", "destinationKey",
                new CopyObjectRequest(),
                (copyObjectResponse) -> {
                    assertThat(testContext, copyObjectResponse.getHeader(), notNullValue());
                    assertThat(testContext, copyObjectResponse.getData(), notNullValue());
                    async.complete();
                },
                testContext::fail);
    }

    void verifyCopyObjectErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.copyObject(
                "sourceBucket", "sourceKey",
                "destinationBucket", "destinationKey",
                new CopyObjectRequest(),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mockGetBucket(Map<String, List<String>> expectedQueryParams, Header... expectedHeaders) throws IOException {
        mock(
                expectedQueryParams,
                "GET",
                "/sourceBucket",
                200,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/listBucketResult.xml")),
                expectedHeaders
        );
    }

    void mockGetBucketErrorResponse(Map<String, List<String>> expectedQueryParams, Header... expectedHeaders) throws IOException {
        mock(
                expectedQueryParams,
                "GET",
                "/sourceBucket",
                403,
                Resources.toByteArray(Resources.getResource(AbstractS3ClientTest.class, "/response/errorResponse.xml")),
                expectedHeaders
        );
    }

    void verifyGetBucket(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.getBucket(
                "sourceBucket",
                new GetBucketRequest()
                        .withContinuationToken("14HF6Dfbr92F1EYlZIrMwxPYKQl5lD/mbwiw5+Nlrn1lYIZX3YGzo16Dgz+dxbxFeNGmLsnzwnbbuQM0CMl0krVwh8TBj8nCmNtq/iQCK6gzln8z3U4C71Mh2HyEMHcMgrZGR/akosVql7/AIctj6rA=="),
                (getBucketRespone) -> {
                    assertThat(testContext, getBucketRespone, notNullValue());
                    assertThat(testContext, getBucketRespone.getHeader().getContentType(), is("application/xml;charset=UTF-8"));
                    assertThat(testContext, getBucketRespone.getData().getContentsList(), hasSize(5));
                    assertThat(testContext, getBucketRespone.getData().getName(), is("bucket"));

                    async.complete();
                },
                testContext::fail
        );
    }

    void verifyGetBucketErrorResponse(final TestContext testContext) {

        final Async async = testContext.async();
        s3Client.getBucket(
                "sourceBucket",
                new GetBucketRequest()
                        .withContinuationToken("14HF6Dfbr92F1EYlZIrMwxPYKQl5lD/mbwiw5+Nlrn1lYIZX3YGzo16Dgz+dxbxFeNGmLsnzwnbbuQM0CMl0krVwh8TBj8nCmNtq/iQCK6gzln8z3U4C71Mh2HyEMHcMgrZGR/akosVql7/AIctj6rA=="),
                (result) -> {
                    testContext.fail("Exceptions should be thrown");
                },
                error -> {
                    assertThat(testContext, error, instanceOf(HttpErrorException.class));

                    final HttpErrorException httpErrorException = (HttpErrorException) error;
                    assertThat(testContext, httpErrorException.getStatus(), is(403));
                    assertThat(testContext, httpErrorException.getErrorResponse().getCode(), is(ErrorCode.SIGNATURE_DOES_NOT_MATCH));
                    async.complete();
                }
        );
    }

    void mock(Map<String, List<String>> expectedQueryParams, String method, String path, Integer statusCode, byte[] responseBody, Header... expectedHeaders) throws IOException {
        mock(expectedQueryParams, method, path, statusCode, null, new BinaryBody(responseBody), Collections.emptyList(), expectedHeaders);
    }

    void mock(Map<String, List<String>> expectedQueryParams, String method, String path, Integer statusCode, byte[] responseBody, List<Header> responseHeaders, Header... expectedHeaders) throws IOException {
        mock(expectedQueryParams, method, path, statusCode, null, new BinaryBody(responseBody), responseHeaders, expectedHeaders);
    }

    void mock(Map<String, List<String>> expectedQueryParams, String method, String path, Integer statusCode, Body requestBody, Body responseBody, List<Header> responseHeaders, Header... expectedHeaders) throws IOException {
        final HttpRequest httpRequest = request()
                .withMethod(method)
                .withPath(path)
                .withHeaders(expectedHeaders)
                .withQueryStringParameters(expectedQueryParams);

        if (requestBody != null) {
            httpRequest.withBody(requestBody);
        }

        getMockServerClient().when(
                httpRequest
        ).respond(
                response()
                        .withStatusCode(statusCode)
                        .withHeaders(responseHeaders)
                        .withHeader(Header.header("Content-Type", "application/xml;charset=UTF-8"))
                        .withBody(responseBody)
        );
    }
}