package cz.moneta.test.harness.support.rest;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import cz.moneta.test.harness.connectors.rest.ExtendedRestResponse;
import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.endpoints.RestEndpoint;
import cz.moneta.test.harness.support.util.FileReader;
import cz.moneta.test.harness.support.util.Template;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MultivaluedMap;
import net.javacrumbs.jsonunit.assertj.JsonAssert;
import net.javacrumbs.jsonunit.assertj.JsonAssertions;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * RawRestRequest provides a simplified api for executing RESTful calls. Its typical usage would be:
 * <p/>
 * <pre>{@code
 * ResultObject result = RawRestRequest.jsonBuilder(someEndpointInstance)
 *       .withPath("/service/method/...")
 *       .withHeader("Authorization", "Basic 123456")
 *       .withPojoPayload(somePojoInstance)
 *       .post(ResultObject.class)
 *       .andAssertFieldValue("someField", "someValue")
 * }</pre>
 */
@SuppressWarnings("unchecked")
public final class RawRestRequest {

    private RawRestRequest() {
    }

    /**
     * Factory method for JSON-based requests
     */
    public static Path jsonBuilder(RestEndpoint endpoint) {
        return new Builder(endpoint,
                new ObjectMapper()
                        .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
                        .registerModules(new ParameterNamesModule(), new Jdk8Module(), new JavaTimeModule()),
                Entity::json);
    }

    /**
     * Factory method for XML-based requests
     */
    public static Path xmlBuilder(RestEndpoint endpoint) {
        return new Builder(endpoint, new XmlMapper(), Entity::xml);
    }

    /**
     * Factory method for custom media type requests
     */
    public static Path customXmlBuilder(RestEndpoint endpoint, String mediaType) {
        return new Builder(endpoint, new XmlMapper(), p -> Entity.entity(p, mediaType));
    }

    public static Path customJsonBuilder(RestEndpoint endpoint, String mediaType) {
        return new Builder(endpoint,
                new ObjectMapper()
                        .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
                        .registerModules(new ParameterNamesModule(), new Jdk8Module(), new JavaTimeModule()),
                p -> Entity.entity(p, mediaType));
    }

    public interface Path {

        /**
         * The relative request path i.e. URL without the protocol, host and port parts
         */
        Request withPath(String path);

        /**
         * Prefix to relative path to by added during setting path variable
         * <p>
         * Default value will be empty String
         */
        Path setPathPrefix(String pathPrefix);
    }

    public interface Request extends Json, Get, Delete {
        Request withHeader(String key, Object value);

        /**
         * Rresolves a GET, DELETE ... request path parameter
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path/{a_path_parameter}/further/path")
         *     .withPathParam("a_path_parameter", 123)
         *     .get()
         * }</pre>
         * <p>
         * Results in the following URL being called.
         * <br/>
         * {@code https://some_application_host/some/path/123/further/path}
         *
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path/{a_path_parameter}/further/path")
         *     .withPathParam("a_path_parameter", 123)
         *     .delete()
         * }</pre>
         * <p>
         * Results in the following URL being called.
         * <br/>
         * {@code https://some_application_host/some/path/123/further/path}
         */
        Request withPathParam(String name, Object value);

        /**
         * Adds a GET request parameter to URL
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .withParam("value", 123)
         *     .get()
         * }</pre>
         * <p>
         * Results in the following URL being called.
         * <br/>
         * {@code https://some_application_host/some/path?value=123}
         */
        Request withParam(String name, Object value);
    }

    public interface Json {

        /**
         * Creates payload from string.
         * <p/>
         * Example:
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .withHeader("Authorization", "Bearer 123465")
         *     .withPayload("{" +
         *             "  \"SPECIAL_CODE\": \"100\"," +
         *             "  \"RS_persons\": [" +
         *             "    {" +
         *             "      \"my_string\": \"abcd\"," +
         *             "      \"name\": \"Adam\"," +
         *             "      \"city\": \"Jihlava\"" +
         *             "    }," +
         *             "    {" +
         *             "      \"my_string\": \"aaaaaa\"," +
         *             "      \"name\": \"David\"," +
         *             "      \"city\": \"Plzeň\"" +
         *             "    }" +
         *             "  ]" +
         *             "}")
         *     .post()
         * }</pre>
         */
        Post withPayload(String payload);

        Patch withPayloadPatch(String payload);

        /**
         * Creates payload from POJO.
         * <p/>
         * Example:
         * <pre>{@code
         * DummyRequest request = DummyRequest.builder()
         *     .specialCode("100")
         *     .rsPersons(Arrays.asList(
         *         new Dummy("abcd", "Adam", "Jihlava"),
         *         new Dummy("aaaaaa", "David", "Plzeň")))
         *     .build();
         *
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .withHeader("Authorization", "Bearer 123456")
         *     .withPojoPayload(request)
         *     .post()
         * }</pre>
         */
        Post withPojoPayload(Object payload);

        /**
         * Reads payload from a file.
         * <p/>
         * Example:
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .withHeader("Authorization", "Bearer 123456")
         *     .withPayloadFromFile("some_request.json")
         *     .post()
         * }</pre>
         */
        Post withPayloadFromFile(String uri);

        Post withPayloadFromTemplate(Template template);
      
        /**
         * Returns the Request payload
         * Good for logging
         */
        String getPayload();
    }

    public interface Get {

        /**
         * Sends the actual GET request and returns the {@link Response} object
         */
        Response get();

        /**
         * Sends the actual GET request and maps the response onto the response type specified
         *
         * @param responseType the required response type
         */
        <RESP> RESP get(Class<RESP> responseType);
    }

    public interface Delete {

        /**
         * Sends the actual GET request and returns the {@link Response} object
         */
        Response delete();

        /**
         * Sends the actual GET request and maps the response onto the response type specified
         *
         * @param responseType the required response type
         */
        <RESP> RESP delete(Class<RESP> responseType);
    }

    public interface Post {

        /**
         * Adds a field to an existing rest request.
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .withHeader("Authorization", "Bearer 123456")
         *
         *     // let's start with an empty request; it works with any pre-defined requests too, of course
         *     .withPayload("{}")
         *
         *     // this adds a collection named someArrayField to the root of the request
         *     .addField("someArrayField", Arrays.asList(1, 2, 3)))
         *
         *     // this adds an empty object named someObjectField to the root of the request, please note that
         *     // we are adding a plain Object, but any POJO or a Collection could be used just as well
         *     .addField("someObjectField", new Object())
         *
         *     // let's add a String field to the newly created field, please note that we're not adding it to the root
         *     // but rather to someObjectField
         *     .addField("someObjectField", "aStringField", "some string value")
         *
         *      // let's add an array field with three items
         *     .addField("someObjectField", "aCollectionField", Arrays.asList(new Object(), new Object(), new Object()))
         *
         *      // let's add a field to the third item of the array; please note the dot syntax while accessing nested
         *      // fields and the bracket syntax while accessing elements within a collection
         *     .addField("someObjectField.aCollectionField[2]", "aNumberField", 123)
         *     .post()
         * }</pre>
         * <p>
         * The resulting request now looks as follows:
         * <pre>{@code
         * {
         *     "someArrayField": [1, 2, 3],
         *     "someObjectField": {
         *         "aStringField": "some string value",
         *         "aCollectionField": [
         *             {},
         *             {},
         *             {"aNumberField": 123}
         *         ]
         *     }
         * }
         * }</pre>
         *
         * @param path      path within the requesst relative to its root; use empty string (i.e. "") to add a field
         *                  directly to the root of the request
         * @param fieldName the name of the field being added
         * @param value     the value of the field being added; it may be of any type (i.e. a String, POJO, Collection...)
         */
        Post addField(String path, String fieldName, Object value);

        /**
         * Adds a field to the root of the rest request. See {@link Builder#addField(String, String, Object)} for usage
         */
        Post addField(String fieldName, Object value);

        /**
         * Adds an irem to an existing array.
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .withHeader("Authorization", "Bearer 123456")
         *     .withPayload("{
         *         "anArray": [
         *             {"field1": 123},
         *             {"field2": 456},
         *             {"nestedArray": ["one", "two", "three"]},
         *         ]
         *     }")
         *     .appendToArray("anArray[2].nestedArray", "four")
         * }</pre>
         * <p>
         * Please note the bracket syntax when accessing elements within a collection and the dot syntax while
         * accessing nested fields.
         * <p/>
         * The resulting request looks as follows:
         * <pre>{@code
         * {
         *     "anArray": [
         *         {"field1": 123},
         *         {"field2": 456},
         *         {"nestedArray": ["one", "two", "three", "four"]},
         *     ]
         * }
         * }</pre>
         */
        Post appendToArray(String path, Object value);

        /**
         * Sends the actual POST request and returns the {@link Response} object
         */
        Response post();

        Response postExtended();

        /**
         * Sends the actual POST request and maps the response onto the response type specified
         *
         * @param responseType the required response type
         */
        <RESP> RESP post(Class<RESP> responseType);

        <RESP> ExtendedRestResponse<RESP> postExtended(Class<RESP> responseType);
    }

    public interface Patch {

        /**
         * Sends the actual PATCH request and returns the {@link Response} object
         */
        Response patch();

        /**
         * Sends the actual PATCH request and maps the response onto the response type specified
         *
         * @param responseType the required response type
         */
        <RESP> RESP patch(Class<RESP> responseType);
    }

    public interface Response {

        /**
         * Makes an assertion on response field value. Please note the dot and bracket syntaxes while accessing
         * nested elements
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .get()
         *     .andAssertFieldValue("aField.aCollection[3].aCollectionElementField.anotherNestedField", 456)
         * }</pre>
         * The example asserts that the following structure is present within the response:
         * <pre>{@code
         * {
         *     ...
         *     "aField": [
         *         ...,
         *         ...,
         *         ...,
         *         {
         *             ...,
         *             "aCollectionElementField": {
         *                 ...,
         *                 "anotherNestedField": 456 <<<
         *             }
         *         }
         *
         *     ]
         * }
         * }</pre>
         */
        Response andAssertFieldValue(String field, String value);

        /**
         * Makes an assertion on the presence of the given element. Please note the dot and bracket syntaxes while
         * accessing nested elements
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .get()
         *     .andAssertPresent("aField.aCollection[3].aCollectionElementField.anotherNestedField")
         * }</pre>
         * The example asserts that the following structure is present within the response:
         * <pre>{@code
         * {
         *     ...
         *     "aField": [
         *         ...,
         *         ...,
         *         ...,
         *         {
         *             ...,
         *             "aCollectionElementField": {
         *                 ...,
         *                 "anotherNestedField": 456 <<<
         *             }
         *         }
         *
         *     ]
         * }
         * }</pre>
         */
        Response andAssertPresent(String field);

        /**
         * Makes an assertion on the absence of an element. Basically, a negated {@link #andAssertPresent}
         */
        Response andAssertNotPresent(String field);

        List<Object> getHeader(String header);

        Response assertHeaderPresent(String header);

        /**
         * Makes an assertion on HTTP response status
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .get()
         *     .andAssertStatus(200)
         * }</pre>
         */
        Response andAssertStatus(int status);

        /**
         * Extracts the value of a particular response element. Please note the dot and bracket syntaxes while
         * accessing nested elements
         * <pre>{@code
         * RawRestRequest.jsonBuilder(harness.getEndpoint(SomeRestEndpoint.class))
         *     .withPath("some/path")
         *     .get()
         *     .extract("aField.aCollection[3].aCollectionElementField.anotherNestedField")
         *     .asText()
         * }</pre>
         * Would have yielded "456" from the following hypothetical response
         * <pre>{@code
         * {
         *     ...
         *     "aField": [
         *         ...,
         *         ...,
         *         ...,
         *         {
         *             ...,
         *             "aCollectionElementField": {
         *                 ...,
         *                 "anotherNestedField": 456 <<<
         *             }
         *         }
         *
         *     ]
         * }
         * }</pre>
         */
        JsonNode extract(String fieldName);

        /**
         * Returns the HTTP response status
         */
        Integer getStatus();
      
        /**
         * Returns the Request payload
         * Good for logging
         */
        String getPayload();

        /**
         * Expecting base64 string in format "data:application/pdf;base64,JVBERi0xLj..."
         * For more variability it needs more development :)
         */
        Response saveBase64FieldAsFile(String field);

        <RESP> RESP mapResponseIntoObject(Class<RESP> responseObjectClass);

        JsonAssert.ConfigurableJsonAssert andAssertWithAssertJ();
    }

    public static class Builder implements Path, Request, Json, Post, Get, Delete, Patch, Response {

        private static final Pattern ARRAY_NODE_PATTERN = Pattern.compile("(.*?)\\[([0-9]*?)\\]");

        private final RestEndpoint endpoint;
        private final ObjectMapper jackson;
        private final Function<String, Entity<?>> type;
        private final Map<String, Object> headers = new HashMap<>();
        private final Map<String, Object> params = new HashMap<>();
        private final Map<String, Object> pathParams = new HashMap<>();
        private String path;
        private String pathPrefix = "";
        private String payload;
        private JsonNode response;
        private Integer status;
        private MultivaluedMap<String, Object> responseHeaders;

        private Builder(RestEndpoint endpoint, ObjectMapper jackson, Function<String, Entity<?>> type) {
            this.endpoint = endpoint;
            this.jackson = jackson;
            this.type = type;
        }

        private static JsonNode extractNode(String path, JsonNode rootNode) {
            return Arrays.stream(path.split("\\."))
                    .filter(StringUtils::isNotEmpty)
                    .reduce(rootNode,
                            (r, p) -> {
                                Matcher matcher = ARRAY_NODE_PATTERN.matcher(p);
                                if (matcher.find()) {
                                    return r.path(matcher.group(1)).path(Integer.valueOf(matcher.group(2)));
                                } else {
                                    return r.path(p);
                                }
                            },
                            (j1, j2) -> j1);
        }

        public Request withPath(String path) {
            this.path = this.pathPrefix + path;
            return this;
        }

        public Path setPathPrefix(String pathPrefix) {
            this.pathPrefix = pathPrefix;
            return this;
        }

        public Post withPayload(String payload) {
            this.payload = payload;
            return this;
        }

        public Patch withPayloadPatch(String payload) {
            this.payload = payload;
            return this;
        }

        public Post withPojoPayload(Object payload) {
            try {
                this.payload = jackson.writeValueAsString(payload);
            } catch (JsonProcessingException e) {
                throw new IllegalArgumentException("Failed to parse payload: "
                        + (payload == null ? null : payload.toString()));
            }
            return this;
        }

        public Post withPayloadFromFile(String path) {
            this.payload = FileReader.readFileFromResources(path);
            return this;
        }

        public Post withPayloadFromTemplate(Template template) {
           this.payload = template.render();
           return this;
        }

        public Request withHeader(String key, Object value) {
            this.headers.put(key, value);
            return this;
        }

        public Post addField(String fieldName, Object value) {
            return addField("", fieldName, value);
        }

        public Post addField(String path, String fieldName, Object value) {
            handleNode(path, node -> {
                JsonNode newNode = jackson.valueToTree(value);
                ((ObjectNode) node).set(fieldName, newNode);
            });
            return this;
        }

        public Post appendToArray(String path, Object value) {
            handleNode(path, node -> {
                JsonNode newNode = jackson.valueToTree(value);
                ((ArrayNode) node).add(newNode);
            });
            return this;
        }

        private void handleNode(String path, Consumer<JsonNode> nodeHandler) {
            try {
                JsonNode rootNode = jackson.readTree(payload);
                nodeHandler.accept(extractNode(path, rootNode));
                this.payload = jackson.writeValueAsString(rootNode);
            } catch (IOException e) {
                throw new IllegalStateException(String.format("Failed to parse payload: %s", payload), e);
            }
        }

        public Request withParam(String name, Object value) {
            params.put(name, value);
            return this;
        }

        public <RESP> RESP get(Class<RESP> responseType) {
            return doGet(responseType).getRight();
        }

        public Response get() {
            Pair<Integer, String> response = doGet(String.class);
            this.status = response.getLeft();
            this.response = parseResponse(response.getRight());
            return this;
        }

        private <RESP> Pair<Integer, RESP> doGet(Class<RESP> responseType) {
            return endpoint.get(resolvePathParams(path), params, responseType, headers);
        }

        public Request withPathParam(String name, Object value) {
            pathParams.put(name, value);
            return this;
        }

        public <RESP> RESP delete(Class<RESP> responseType) {
            return doDelete(responseType).getRight();
        }

        public Response delete() {
            Pair<Integer, String> response = doDelete(String.class);
            this.status = response.getLeft();
            this.response = parseResponse(response.getRight());
            return this;
        }

        private <RESP> Pair<Integer, RESP> doDelete(Class<RESP> responseType) {
            return endpoint.delete(resolvePathParams(path), params, responseType, headers);
        }

        private String resolvePathParams(String path) {
            return pathParams.entrySet().stream()
                    .<Function<String, String>>map(e -> s -> s.replace("{" + e.getKey() + "}", String.valueOf(e.getValue())))
                    .reduce(Function.identity(), Function::andThen)
                    .apply(path);
        }

        public <RESP> RESP post(Class<RESP> responseType) {
            return doPost(responseType).getRight();
        }

        public <RESP> ExtendedRestResponse<RESP> postExtended(Class<RESP> responseType) {
            return doExtendedPost(responseType).getRight();
        }

        public Response post() {
            Pair<Integer, String> response = doPost(String.class);
            this.status = response.getLeft();
            this.response = parseResponse(response.getRight());
            return this;
        }

        public Response postExtended() {
            Pair<Integer, ExtendedRestResponse<String>> response = doExtendedPost(String.class);
            this.status = response.getLeft();
            this.response = parseResponse(response.getRight().getResponseBody());
            this.responseHeaders = response.getRight().getResponseHeaders();
            return this;
        }

        private <RESP> Pair<Integer, RESP> doPost(Class<RESP> responseType) {
            return endpoint.post(path, type.apply(payload), responseType, headers, false);
        }

        private <RESP> Pair<Integer, ExtendedRestResponse<RESP>> doExtendedPost(Class<RESP> responseType) {
            return endpoint.postExtended(path, type.apply(payload), responseType, headers);
        }

        public <RESP> RESP patch(Class<RESP> responseType) {
            return doPatch(responseType).getRight();
        }

        public Response patch() {
            Pair<Integer, String> response = doPatch(String.class);
            this.status = response.getLeft();
            this.response = parseResponse(response.getRight());
            return this;
        }

        private <RESP> Pair<Integer, RESP> doPatch(Class<RESP> responseType) {
            return endpoint.patch(path, type.apply(payload), responseType, headers, false);
        }

        private JsonNode parseResponse(String rawResponse) {
            try {
                return jackson.readTree(rawResponse);
            } catch (IOException e) {
                throw new IllegalArgumentException(String.format("Failed to parse response: %s", rawResponse), e);
            }
        }

        public Response andAssertFieldValue(String path, String value) {
            if (responseHeaders != null) {
                Assertions.assertEquals(value, extract(path).asText(), String.format("Input: '%s', \n Output: '%s' \n activityId -> %s",
                        this.payload,
                        this.response.toString(),
                        this.responseHeaders.get("activityId")));
            } else {
                Assertions.assertEquals(value, extract(path).asText(), String.format("Input: '%s', \n Output: '%s'",
                        this.payload,
                        this.response.toString()));
            }
            return this;
        }

        public Response andAssertPresent(String path) {
            JsonNode node = extract(path);
            if (node.isMissingNode()) {
                throw new AssertionError(String.format("Expected path is missing: %s", path));
            }
            return this;
        }

        public Response andAssertNotPresent(String path) {
            JsonNode node = extract(path);
            if (!node.isMissingNode()) {
                throw new AssertionError(String.format("Unexpected path is present: %s, value: %s", path, node.asText()));
            }
            return this;
        }

        @Override
        public List<Object> getHeader(String header) {
            if (!this.responseHeaders.containsKey(header)) {
                return null;
            }
            return this.responseHeaders.get(header);
        }

        public Response assertHeaderPresent(String header) {
            if (!this.responseHeaders.containsKey(header)) {
                throw new AssertionError(String.format("Expected header is missing in response: %s", header));
            }
            return this;
        }

        public Response andAssertStatus(int expected) {
            if (this.status == 204 && this.responseHeaders == null) { //204 = no content, response and header is null
                Assertions.assertEquals(expected, this.status, String.format("Input: '%s'", this.payload));
                return this;
            }

            if (this.status == 204) { //204 = no content, response is null
                Assertions.assertEquals(expected, this.status, String.format("Input: '%s', \n activityId -> %s",
                        this.payload,
                        this.responseHeaders.get("activityId")));
                return this;
            }

            if (this.responseHeaders != null) {
                Assertions.assertEquals(expected, this.status, String.format("Input: '%s', \n Output: '%s' \n activityId -> %s",
                        this.payload,
                        this.response.toString(),
                        this.responseHeaders.get("activityId")));
            } else {
                Assertions.assertEquals(expected, this.status, String.format("Input: '%s', \n Output: '%s'",
                        this.payload,
                        this.response.toString()));

            }
            return this;
        }

        public JsonNode extract(String path) {
            return extractNode(path, this.response);
        }

        public Integer getStatus() {
            return status;
        }
      
        public String getPayload() { 
            return payload; 
        }

        public Response saveBase64FieldAsFile(String field) {
            String downloadsPath = endpoint.getStore().getConfig("harness.downloads.path", "target/downloads");
            try {
                String[] base64Text = extract(field).asText().split(",", 2);
                AllowedContentTypes type = AllowedContentTypes.getByMimeType(base64Text[0].replace("data:", "").replace(";base64", ""));
                String finalFileName = endpoint.getStore().get(HarnessConfigConstants.TEST_SHORT_ID) + "-" + System.currentTimeMillis() + "." + type.getExtension();
                File file = Paths.get(downloadsPath, finalFileName).toFile();
                FileUtils.writeByteArrayToFile(file, Base64.getDecoder().decode(base64Text[1]));
            } catch (IndexOutOfBoundsException e) {
                throw new IllegalStateException("Unable to parse input Base64 text", e);
            } catch (IOException e) {
                throw new IllegalStateException("Unable to save downloaded file to " + downloadsPath, e);
            }
            return this;
        }

        public <RESP> RESP mapResponseIntoObject(Class<RESP> responseObjectClass) {
            String responseString = this.response.toString();
            try {
                return jackson.readValue(responseString, responseObjectClass);
            } catch (IOException possiblyErrorResponse) {
                throw new AssertionError(String.format("HTTP POST response looks different than expected: %s", responseString), possiblyErrorResponse);
            }
        }

        public JsonAssert.ConfigurableJsonAssert andAssertWithAssertJ() {
            return JsonAssertions.assertThatJson(this.response);
        }
    }
}
