/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you 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 org.apache.james.jmap.draft.methods.integration.cucumber;

import static org.apache.james.jmap.JmapURIBuilder.baseUri;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import javax.inject.Inject;

import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;
import org.apache.http.client.utils.URIBuilder;
import org.apache.james.core.Username;
import org.apache.james.jmap.AccessToken;
import org.apache.james.jmap.draft.model.AttachmentAccessToken;
import org.apache.james.mailbox.MessageManager.AppendCommand;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.AttachmentId;
import org.apache.james.mailbox.model.ComposedMessageId;
import org.apache.james.mailbox.model.MailboxConstants;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mime4j.codec.DecoderUtil;
import org.apache.james.util.io.InputStreamUtils;

import com.google.common.base.CharMatcher;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import cucumber.runtime.java.guice.ScenarioScoped;

@ScenarioScoped
public class DownloadStepdefs {

    private static final String ONE_ATTACHMENT_EML_ATTACHMENT_BLOB_ID = "913ee2d903a68c3e8cb51169d34cf9ec257323c1bee254c0c7ab820930fdb8e7";
    private static final String EXPIRED_ATTACHMENT_TOKEN = "[email protected]_"
            + "2016-06-29T13:41:22.124Z_"
            + "DiZa0O14MjLWrAA8P6MG35Gt5CBp7mt5U1EH/M++rIoZK7nlGJ4dPW0dvZD7h4m3o5b/Yd8DXU5x2x4+s0HOOKzD7X0RMlsU7JHJMNLvTvRGWF/C+MUyC8Zce7DtnRVPEQX2uAZhL2PBABV07Vpa8kH+NxoS9CL955Bc1Obr4G+KN2JorADlocFQA6ElXryF5YS/HPZSvq1MTC6aJIP0ku8WRpRnbwgwJnn26YpcHXcJjbkCBtd9/BhlMV6xNd2hTBkfZmYdoNo+UKBaXWzLxAlbLuxjpxwvDNJfOEyWFPgHDoRvzP+G7KzhVWjanHAHrhF0GilEa/MKpOI1qHBSwA==";
    private static final String INVALID_ATTACHMENT_TOKEN = "[email protected]_"
            + "2015-06-29T13:41:22.124Z_"
            + "DiZa0O14MjLWrAA8P6MG35Gt5CBp7mt5U1EH/M++rIoZK7nlGJ4dPW0dvZD7h4m3o5b/Yd8DXU5x2x4+s0HOOKzD7X0RMlsU7JHJMNLvTvRGWF/C+MUyC8Zce7DtnRVPEQX2uAZhL2PBABV07Vpa8kH+NxoS9CL955Bc1Obr4G+KN2JorADlocFQA6ElXryF5YS/HPZSvq1MTC6aJIP0ku8WRpRnbwgwJnn26YpcHXcJjbkCBtd9/BhlMV6xNd2hTBkfZmYdoNo+UKBaXWzLxAlbLuxjpxwvDNJfOEyWFPgHDoRvzP+G7KzhVWjanHAHrhF0GilEa/MKpOI1qHBSwA==";
    private static final String UTF8_CONTENT_DIPOSITION_START = "Content-Disposition: attachment; filename*=\"";

    private final UserStepdefs userStepdefs;
    private final MainStepdefs mainStepdefs;
    private final GetMessagesMethodStepdefs getMessagesMethodStepdefs;
    private HttpResponse response;
    private Multimap<String, String> attachmentsByMessageId;
    private Map<String, String> blobIdByAttachmentId;
    private Map<String, MessageId> inputToMessageId;
    private Map<AttachmentAccessTokenKey, AttachmentAccessToken> attachmentAccessTokens;

    @Inject
    private DownloadStepdefs(MainStepdefs mainStepdefs, UserStepdefs userStepdefs, GetMessagesMethodStepdefs getMessagesMethodStepdefs) {
        this.mainStepdefs = mainStepdefs;
        this.userStepdefs = userStepdefs;
        this.getMessagesMethodStepdefs = getMessagesMethodStepdefs;
        this.attachmentsByMessageId = ArrayListMultimap.create();
        this.blobIdByAttachmentId = new HashMap<>();
        this.attachmentAccessTokens = new HashMap<>();
        this.inputToMessageId = new HashMap<>();
    }

    @Given("^\"([^\"]*)\" mailbox \"([^\"]*)\" contains a message \"([^\"]*)\"$")
    public void appendMessageToMailbox(String user, String mailbox, String messageId) throws Throwable {
        MailboxPath mailboxPath = MailboxPath.forUser(Username.of(user), mailbox);

        ComposedMessageId composedMessageId = mainStepdefs.mailboxProbe.appendMessage(user, mailboxPath,
            AppendCommand.from(ClassLoader.getSystemResourceAsStream("eml/oneAttachment.eml")));

        inputToMessageId.put(messageId, composedMessageId.getMessageId());
    }

    @Given("^\"([^\"]*)\" mailbox \"([^\"]*)\" contains a big message \"([^\"]*)\"$")
    public void appendBigMessageToMailbox(String user, String mailbox, String messageId) throws Throwable {
        MailboxPath mailboxPath = MailboxPath.forUser(Username.of(user), mailbox);

        ComposedMessageId composedMessageId = mainStepdefs.mailboxProbe.appendMessage(user, mailboxPath,
            AppendCommand.from(new ByteArrayInputStream(
                Strings.repeat("header: 0123456789\r\n", 128 * 1024)
                    .getBytes(StandardCharsets.UTF_8))));

        inputToMessageId.put(messageId, composedMessageId.getMessageId());
    }

    @Given("^\"([^\"]*)\" mailbox \"([^\"]*)\" contains a message \"([^\"]*)\" with an attachment \"([^\"]*)\"$")
    public void appendMessageWithAttachmentToMailbox(String user, String mailbox, String messageId, String attachmentId) throws Throwable {
        MailboxPath mailboxPath = MailboxPath.forUser(Username.of(user), mailbox);

        ComposedMessageId composedMessageId = mainStepdefs.mailboxProbe.appendMessage(user, mailboxPath,
            AppendCommand.from(ClassLoader.getSystemResourceAsStream("eml/oneAttachment.eml")));

        retrieveAndSaveAttachmentDetails(user, messageId, attachmentId, composedMessageId);
    }

    @Given("^\"([^\"]*)\" mailbox \"([^\"]*)\" contains a message \"([^\"]*)\" with an attachment \"([^\"]*)\" having \"([^\"]*)\" contentType$")
    public void appendMessageWithAttachmentToMailbox(String user, String mailbox, String messageId, String attachmentId, String contentType) throws Throwable {
        MailboxPath mailboxPath = MailboxPath.forUser(Username.of(user), mailbox);

        InputStream message = InputStreamUtils.concat(
            ClassLoader.getSystemResourceAsStream("eml/oneAttachment-part1.eml"),
            new ByteArrayInputStream(contentType.getBytes(StandardCharsets.UTF_8)),
            ClassLoader.getSystemResourceAsStream("eml/oneAttachment-part2.eml"));

        ComposedMessageId composedMessageId = mainStepdefs.mailboxProbe.appendMessage(user, mailboxPath, AppendCommand.from(message));

        retrieveAndSaveAttachmentDetails(user, messageId, attachmentId, composedMessageId);
    }

    @Given("^\"([^\"]*)\" mailbox \"([^\"]*)\" contains a message \"([^\"]*)\" with an inlined attachment \"([^\"]*)\"$")
    public void appendMessageWithInlinedAttachmentToMailbox(String user, String mailbox, String messageId, String attachmentId) throws Throwable {
        MailboxPath mailboxPath = MailboxPath.forUser(Username.of(user), mailbox);

        ComposedMessageId composedMessageId = mainStepdefs.mailboxProbe.appendMessage(user, mailboxPath,
            AppendCommand.from(ClassLoader.getSystemResourceAsStream("eml/oneInlinedImage.eml")));

        retrieveAndSaveAttachmentDetails(user, messageId, attachmentId, composedMessageId);
    }

    public void retrieveAndSaveAttachmentDetails(String user, String messageId, String attachmentId, ComposedMessageId composedMessageId) throws MailboxException {
        AttachmentId mailboxAttachmentId = mainStepdefs.messageIdProbe
            .retrieveAttachmentIds(composedMessageId.getMessageId(), Username.of(user))
            .get(0);

        inputToMessageId.put(messageId, composedMessageId.getMessageId());
        attachmentsByMessageId.put(messageId, attachmentId);
        blobIdByAttachmentId.put(attachmentId, mailboxAttachmentId.getId());
    }

    @Given("^\"([^\"]*)\" mailbox \"([^\"]*)\" contains a message \"([^\"]*)\" with multiple same inlined attachments \"([^\"]*)\"$")
    public void appendMessageWithSameInlinedAttachmentsToMailbox(String user, String mailbox, String messageName, String attachmentId) throws Throwable {
        MailboxPath mailboxPath = MailboxPath.forUser(Username.of(user), mailbox);

        ComposedMessageId composedMessageId = mainStepdefs.mailboxProbe.appendMessage(user, mailboxPath,
            AppendCommand.from(ClassLoader.getSystemResourceAsStream("eml/sameInlinedImages.eml")));

        retrieveAndSaveAttachmentDetails(user, messageName, attachmentId, composedMessageId);
    }

    @When("^\"([^\"]*)\" checks for the availability of the attachment endpoint$")
    public void optionDownload(String username) throws Throwable {
        AccessToken accessToken = userStepdefs.authenticate(username);
        URI target = baseUri(mainStepdefs.jmapServer).setPath("/download/" + ONE_ATTACHMENT_EML_ATTACHMENT_BLOB_ID).build();
        Request request = Request.Options(target);
        if (accessToken != null) {
            request.addHeader("Authorization", accessToken.asString());
        }
        response = request.execute().returnResponse();
    }

    @When("^\"([^\"]*)\" downloads \"([^\"]*)\"$")
    public void downloads(String username, String blobId) throws Throwable {
        String attachmentIdOrMessageId = Optional.ofNullable(blobIdByAttachmentId.get(blobId))
            .orElse(Optional.ofNullable(inputToMessageId.get(blobId))
                .map(MessageId::serialize)
                .orElse(null));

        downLoad(username, attachmentIdOrMessageId);
    }

    @When("^\"([^\"]*)\" downloads \"([^\"]*)\" using query parameter strategy$")
    public void downloadsUsingQueryParameter(String username, String blobId) throws Throwable {
        String attachmentIdOrMessageId = Optional.ofNullable(blobIdByAttachmentId.get(blobId))
            .orElse(Optional.ofNullable(inputToMessageId.get(blobId))
                .map(MessageId::serialize)
                .orElse(null));
        URIBuilder uriBuilder = baseUri(mainStepdefs.jmapServer).setPath("/download/" + attachmentIdOrMessageId);
        response = queryParameterDownloadRequest(uriBuilder, attachmentIdOrMessageId, username).execute().returnResponse();
    }

    @When("^un-authenticated user downloads \"([^\"]*)\"$")
    public void downloadsUnAuthenticated(String blobId) throws Throwable {
        String attachmentIdOrMessageId = Optional.ofNullable(blobIdByAttachmentId.get(blobId))
            .orElse(Optional.ofNullable(inputToMessageId.get(blobId))
                .map(MessageId::serialize)
                .orElse(null));

        response = Request.Get(
            baseUri(mainStepdefs.jmapServer)
                .setPath("/download/" + attachmentIdOrMessageId)
                .build())
            .execute()
            .returnResponse();
    }

    @When("^\"([^\"]*)\" downloads the message by its blobId$")
    public void downloads(String username) throws Throwable {
        downLoad(username, getMessagesMethodStepdefs.getBlobId());
    }

    private void downLoad(String username, String blobId) throws IOException, URISyntaxException {
        URIBuilder uriBuilder = baseUri(mainStepdefs.jmapServer).setPath("/download/" + blobId);
        response = authenticatedDownloadRequest(uriBuilder, blobId, username).execute().returnResponse();
    }

    private Request authenticatedDownloadRequest(URIBuilder uriBuilder, String blobId, String username) throws URISyntaxException {
        AccessToken accessToken = userStepdefs.authenticate(username);
        AttachmentAccessTokenKey key = new AttachmentAccessTokenKey(username, blobId);
        if (attachmentAccessTokens.containsKey(key)) {
            uriBuilder.addParameter("access_token", attachmentAccessTokens.get(key).serialize());
        }
        Request request = Request.Get(uriBuilder.build());
        if (accessToken != null) {
            request.addHeader("Authorization", accessToken.asString());
        }
        return request;
    }

    private Request queryParameterDownloadRequest(URIBuilder uriBuilder, String blobId, String username) throws URISyntaxException {
        AccessToken accessToken = userStepdefs.authenticate(username);
        AttachmentAccessTokenKey key = new AttachmentAccessTokenKey(username, blobId);
        uriBuilder.addParameter("access_token", attachmentAccessTokens.get(key).serialize());
        return Request.Get(uriBuilder.build());
    }

    @When("^\"([^\"]*)\" is trusted for attachment \"([^\"]*)\"$")
    public void attachmentAccessTokenFor(String username, String attachmentId) throws Throwable {
        userStepdefs.connectUser(username);
        trustForBlobId(blobIdByAttachmentId.get(attachmentId), username);
    }

    private static class AttachmentAccessTokenKey {

        private String username;
        private String blobId;

        public AttachmentAccessTokenKey(String username, String blobId) {
            this.username = username;
            this.blobId = blobId;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof AttachmentAccessTokenKey) {
                AttachmentAccessTokenKey other = (AttachmentAccessTokenKey) obj;
                return Objects.equal(username, other.username)
                    && Objects.equal(blobId, other.blobId);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(username, blobId);
        }

        @Override
        public String toString() {
            return MoreObjects
                    .toStringHelper(this)
                    .add("username", username)
                    .add("blobId", blobId)
                    .toString();
        }
    }

    private void trustForBlobId(String blobId, String username) throws Exception {
        Response tokenGenerationResponse = Request.Post(baseUri(mainStepdefs.jmapServer).setPath("/download/" + blobId).build())
            .addHeader("Authorization", userStepdefs.authenticate(username).asString())
            .execute();
        String serializedAttachmentAccessToken = tokenGenerationResponse.returnContent().asString();
        attachmentAccessTokens.put(
                new AttachmentAccessTokenKey(username, blobId),
                AttachmentAccessToken.from(
                    serializedAttachmentAccessToken,
                    blobId));
    }

    @When("^\"([^\"]*)\" downloads \"(?:[^\"]*)\" with a valid authentication token but a bad blobId$")
    public void downloadsWithValidToken(String username) {
        userStepdefs.execWithUser(username, () -> {
            URIBuilder uriBuilder = baseUri(mainStepdefs.jmapServer).setPath("/download/badblobId");
            response = Request.Get(uriBuilder.build())
                .addHeader("Authorization", userStepdefs.authenticate(username).asString())
                .execute()
                .returnResponse();

        });
    }

    @When("^\"(?:[^\"]*)\" downloads \"([^\"]*)\" without any authentication token$")
    public void getDownloadWithoutToken(String attachmentId) throws Exception {
        String blobId = blobIdByAttachmentId.get(attachmentId);
        response = Request.Get(baseUri(mainStepdefs.jmapServer).setPath("/download/" + blobId).build())
            .execute()
            .returnResponse();
    }

    @When("^\"(?:[^\"]*)\" downloads \"([^\"]*)\" with an empty authentication token$")
    public void getDownloadWithEmptyToken(String attachmentId) throws Exception {
        String blobId = blobIdByAttachmentId.get(attachmentId);
        response = Request.Get(
                baseUri(mainStepdefs.jmapServer)
                    .setPath("/download/" + blobId)
                    .addParameter("access_token", "")
                    .build())
                .execute()
                .returnResponse();
    }

    @When("^\"(?:[^\"]*)\" downloads \"([^\"]*)\" with a bad authentication token$")
    public void getDownloadWithBadToken(String attachmentId) throws Exception {
        String blobId = blobIdByAttachmentId.get(attachmentId);
        response = Request.Get(
                baseUri(mainStepdefs.jmapServer)
                    .setPath("/download/" + blobId)
                    .addParameter("access_token", "bad")
                    .build())
                .execute()
                .returnResponse();
    }

    @When("^\"(?:[^\"]*)\" downloads \"([^\"]*)\" with an invalid authentication token$")
    public void getDownloadWithUnknownToken(String attachmentId) throws Exception {
        String blobId = blobIdByAttachmentId.get(attachmentId);
        response = Request.Get(
                baseUri(mainStepdefs.jmapServer)
                    .setPath("/download/" + blobId)
                    .addParameter("access_token", INVALID_ATTACHMENT_TOKEN)
                    .build())
                .execute()
                .returnResponse();
    }

    @When("^\"([^\"]*)\" downloads \"([^\"]*)\" without blobId parameter$")
    public void getDownloadWithoutBlobId(String username, String attachmentId) throws Throwable {
        String blobId = blobIdByAttachmentId.get(attachmentId);

        URIBuilder uriBuilder = baseUri(mainStepdefs.jmapServer).setPath("/download/");
        trustForBlobId(blobId, username);
        AttachmentAccessTokenKey key = new AttachmentAccessTokenKey(username, blobId);
        uriBuilder.addParameter("access_token", attachmentAccessTokens.get(key).serialize());
        response = Request.Get(uriBuilder.build())
            .execute()
            .returnResponse();
    }

    @When("^\"([^\"]*)\" downloads \"([^\"]*)\" with wrong blobId$")
    public void getDownloadWithWrongBlobId(String username, String attachmentId) throws Throwable {
        String blobId = blobIdByAttachmentId.get(attachmentId);

        URIBuilder uriBuilder = baseUri(mainStepdefs.jmapServer).setPath("/download/badbadbadbadbadbadbadbadbadbadbadbadbadb");
        trustForBlobId(blobId, username);
        AttachmentAccessTokenKey key = new AttachmentAccessTokenKey(username, blobId);
        uriBuilder.addParameter("access_token", attachmentAccessTokens.get(key).serialize());
        response = Request.Get(uriBuilder.build())
            .execute()
            .returnResponse();
    }

    @When("^\"([^\"]*)\" asks for a token for attachment \"([^\"]*)\"$")
    public void postDownload(String username, String attachmentId) throws Throwable {
        String blobId = blobIdByAttachmentId.get(attachmentId);
        AccessToken accessToken = userStepdefs.authenticate(username);
        response = Request.Post(baseUri(mainStepdefs.jmapServer).setPath("/download/" + blobId).build())
                .addHeader("Authorization", accessToken.asString())
                .execute()
                .returnResponse();
    }

    @When("^\"([^\"]*)\" downloads \"([^\"]*)\" with \"([^\"]*)\" name$")
    public void downloadsWithName(String username, String attachmentId, String name) throws Exception {
        String blobId = blobIdByAttachmentId.get(attachmentId);
        URIBuilder uriBuilder = baseUri(mainStepdefs.jmapServer).setPath("/download/" + blobId + "/" + name);
        response = authenticatedDownloadRequest(uriBuilder, blobId, username)
                .execute()
                .returnResponse();
    }

    @When("^\"(?:[^\"]*)\" downloads \"([^\"]*)\" with an expired token$")
    public void getDownloadWithExpiredToken(String attachmentId) throws Exception {
        String blobId = blobIdByAttachmentId.get(attachmentId);
        response = Request.Get(baseUri(mainStepdefs.jmapServer).setPath("/download/" + blobId)
                .addParameter("access_token", EXPIRED_ATTACHMENT_TOKEN)
                .build())
            .execute()
            .returnResponse();
    }

    @When("^\"([^\"]*)\" delete mailbox \"([^\"]*)\"$")
    public void deleteMailboxButNotAttachment(String username, String mailboxName) {
        mainStepdefs.mailboxProbe.deleteMailbox(MailboxConstants.USER_NAMESPACE, username, mailboxName);
    }

    @Then("^the user should be authorized$")
    public void httpStatusDifferentFromUnauthorized() {
        assertThat(response.getStatusLine().getStatusCode()).isIn(200, 404);
    }

    @Then("^\"([^\"]*)\" be authorized$")
    public void httpStatusDifferentFromUnauthorized(String username) {
        userStepdefs.execWithUser(username, this::httpStatusDifferentFromUnauthorized);
    }

    @Then("^the user should not be authorized$")
    public void httpUnauthorizedStatus() {
        assertThat(response.getStatusLine().getStatusCode()).isEqualTo(401);
    }

    @Then("^\"([^\"]*)\" should not be authorized$")
    public void httpUnauthorizedStatus(String username) {
        userStepdefs.execWithUser(username, this::httpUnauthorizedStatus);
    }

    @Then("^the user should receive a bad request response$")
    public void httpBadRequestStatus() {
        assertThat(response.getStatusLine().getStatusCode()).isEqualTo(400);
    }

    @Then("^\"([^\"]*)\" should receive a bad request response$")
    public void httpBadRequestStatus(String username) {
        userStepdefs.execWithUser(username, this::httpBadRequestStatus);
    }

    @Then("^(?:he|she|the user) can read that blob")
    public void httpOkStatusAndExpectedContent() throws IOException {
        assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
        assertThat(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)).isNotEmpty();
    }

    @Then("^the user should receive a not found response$")
    public void httpNotFoundStatus() {
        assertThat(response.getStatusLine().getStatusCode()).isEqualTo(404);
    }

    @Then("^\"([^\"]*)\" should receive a not found response$")
    public void httpNotFoundStatus(String username) {
        userStepdefs.execWithUser(username, this::httpNotFoundStatus);
    }

    @Then("^the user should receive an attachment access token$")
    public void accessTokenResponse() throws Throwable {
        assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
        assertThat(response.getHeaders("Content-Type")).extracting(Header::getValue).containsExactly("text/plain");
        assertThat(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)).isNotEmpty();
    }

    @Then("^\"([^\"]*)\" should receive an attachment access token$")
    public void accessTokenResponse(String username) {
        userStepdefs.execWithUser(username, this::accessTokenResponse);
    }

    @Then("^the attachment is named \"([^\"]*)\"$")
    public void assertContentDisposition(String name) {
        if (!CharMatcher.ascii().matchesAllOf(name)) {
            assertEncodedFilenameMatches(name);
        } else {
            assertThat(response.getFirstHeader("Content-Disposition").getValue()).isEqualTo("attachment; filename=\"" + name + "\"");
        }
    }

    @Then("^the blob size is (\\d+)$")
    public void assertContentLength(int size) {
        assertThat(response.getFirstHeader("Content-Length").getValue()).isEqualTo(String.valueOf(size));
    }

    @Then("^CORS headers are positioned$")
    public void assertCorsHeader() {
        assertThat(response.getFirstHeader("Access-Control-Allow-Origin").getValue()).isEqualTo("*");
        assertThat(response.getFirstHeader("Access-Control-Allow-Methods").getValue()).isEqualTo("GET, POST, DELETE, PUT");
        assertThat(response.getFirstHeader("Access-Control-Allow-Headers").getValue()).isEqualTo("Content-Type, Authorization, Accept");
    }

    @Then("^the Content-Type is \"([^\"]*)\"$")
    public void assertContentType(String contentType) {
        assertThat(response.getFirstHeader("Content-Type").getValue()).isEqualTo(contentType);
    }

    private void assertEncodedFilenameMatches(String name) {
        String contentDispositionHeader = response.getHeaders("Content-Disposition")[0].toString();
        assertThat(contentDispositionHeader).startsWith(UTF8_CONTENT_DIPOSITION_START);

        String expectedFilename = decode(extractFilename(contentDispositionHeader));
        assertThat(name).isEqualTo(expectedFilename);
    }

    private String extractFilename(String contentDispositionHeader) {
        return contentDispositionHeader.substring(UTF8_CONTENT_DIPOSITION_START.length(),
                contentDispositionHeader.length() - 1);
    }

    private String decode(String name) {
        return DecoderUtil.decodeEncodedWords(name, StandardCharsets.UTF_8);
    }
}