/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.module.mockmvc;

import io.restassured.RestAssured;
import io.restassured.config.LogConfig;
import io.restassured.filter.log.LogDetail;
import io.restassured.http.Method;
import io.restassured.module.mockmvc.config.RestAssuredMockMvcConfig;
import io.restassured.module.mockmvc.internal.MockMvcFactory;
import io.restassured.module.mockmvc.internal.MockMvcRequestSpecificationImpl;
import io.restassured.module.mockmvc.internal.StandaloneMockMvcFactory;
import io.restassured.module.mockmvc.response.MockMvcResponse;
import io.restassured.module.mockmvc.specification.*;
import io.restassured.specification.ResponseSpecification;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MockMvcBuilder;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
import org.springframework.web.context.WebApplicationContext;

import java.net.URI;
import java.net.URL;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static io.restassured.config.LogConfig.logConfig;
import static io.restassured.internal.common.assertion.AssertParameter.notNull;

/**
 * The Spring MVC module's equivalent of {@link RestAssured}. This is the starting point of the DSL.
 *
Note that some Javadoc is copied from Spring MVC's test documentation.

 */
public class RestAssuredMockMvc {

    /**
     * Set a {@link org.springframework.test.web.servlet.MockMvc} instance that REST Assured will use when making requests unless overwritten
     * by a {@link MockMvcRequestSpecification}.
     *
     * @param mockMvc The MockMvc instance to use.
     */
    public static void mockMvc(MockMvc mockMvc) {
        RestAssuredMockMvc.mockMvcFactory = new MockMvcFactory(mockMvc);
    }

    /**
     * Define a REST Assured Mock Mvc configuration. E.g.
     *

     * given().config(newConfig().logConfig(new LogConfig(captor, true))). ..
     *

     *

     * newConfig() can be statically imported from {@link RestAssuredMockMvcConfig}.
     */
    public static RestAssuredMockMvcConfig config;
    /**
     * Specify a default request specification that will be sent with each request. E,g.
     *

     * RestAssuredMockMvc.requestSpecification = new MockMvcRequestSpecBuilder().addParam("parameter1", "value1").build();
     *

     *

     * means that for each request by Rest Assured "parameter1" will be equal to "value1".
     */
    public static MockMvcRequestSpecification requestSpecification;

    /**
     * Specify a default response specification that will be sent with each request. E,g.
     *

     * RestAssuredMockMvc.responseSpecification = new ResponseSpecBuilder().expectStatusCode(200).build();
     *

     *

     * means that for each response Rest Assured will assert that the status code is equal to 200.
     */
    public static ResponseSpecification responseSpecification = null;

    private static List<ResultHandler> resultHandlers = new ArrayList<ResultHandler>();

    private static List<RequestPostProcessor> requestPostProcessors = new ArrayList<RequestPostProcessor>();

    private static MockMvcFactory mockMvcFactory = null;

    /**
     * The base path that's used by REST assured when making requests. The base path is prepended to the request path.
     * Default value is /.
     */
    public static String basePath = "/";

    /**
     * Defines a global authentication scheme that'll be used for all requests (if not overridden). Usage example:
     *

     * RestAssured.authentication = principal(myPrincipal);
     *

     *
     * @see #principal(java.security.Principal)
     * @see #principal(Object)
     * @see #principalWithCredentials(Object, Object, String...)
     * @see #authentication(Object)
     */
    public static MockMvcAuthenticationScheme authentication;

    /**
     * This is usually the entry-point of the API if you need to specify parameters or a body in the request. For example:
     *

     *

     * given().
     *         param("x", "y").
     * when().
     *         get("/something").
     * then().
     *        statusCode(200).
     *        body("x.y", notNullValue());
     *

     * Note that this method is the same as {@link #with()} but with another syntax.
     *
     * @return A {@link MockMvcRequestSpecification}.
     */
    public static MockMvcRequestSpecification given() {
        return new MockMvcRequestSpecificationImpl(mockMvcFactory, config, resultHandlers, requestPostProcessors, basePath, requestSpecification, responseSpecification, authentication);
    }

    /**
     * This is usually the entry-point of the API if you need to specify parameters or a body in the request. For example:
     *

     *

     * given().
     *         param("x", "y").
     * when().
     *         get("/something").
     * then().
     *        statusCode(200).
     *        body("x.y", notNullValue());
     *

     *

     * Note that this method is the same as {@link #given()} but with another syntax.
     *
     * @return A {@link MockMvcRequestSpecification}.
     */
    public static MockMvcRequestSpecification with() {
        return given();
    }

    /**
     * This is usually the entry-point of the API if you need to specify parameters or a body in the request. For example:
     *

     *

     * when().
     *        get("/x").
     * then().
     *        body("x.y.z1", equalTo("Z1")).
     *        body("x.y.z2", equalTo("Z2"));
     *

     *

     * Note that if you need to add parameters, headers, cookies or other request properties use the {@link #given()} method.
     *

     *
     * @return A request sender interface that let's you call resources on the server
     */
    public static MockMvcRequestSender when() {
        return given().when();
    }

    /**
     * Build a {@link MockMvc} by registering one or more {@code @Controller}'s
     * instances and configuring Spring MVC infrastructure programmatically.
     * This allows full control over the instantiation and initialization of
     * controllers, and their dependencies, similar to plain unit tests while
     * also making it possible to test one controller at a time.
     *

     *
When this option is used, the minimum infrastructure required by the
     * {@link org.springframework.web.servlet.DispatcherServlet} to serve requests with annotated controllers is
     * automatically created, and can be customized, resulting in configuration
     * that is equivalent to what the MVC Java configuration provides except
     * using builder style methods.
     *

     *
If the Spring MVC configuration of an application is relatively
     * straight-forward, for example when using the MVC namespace or the MVC
     * Java config, then using this builder might be a good option for testing
     * a majority of controllers. A much smaller number of tests can be used
     * to focus on testing and verifying the actual Spring MVC configuration.
     *
     * @param controllersOrMockMvcConfigurers one or more {@link org.springframework.stereotype.Controller @Controller}'s to test as well
     *                                        as @{link MockMvcConfigurer}'s to apply
     */
    public static void standaloneSetup(Object... controllersOrMockMvcConfigurers) {
        mockMvcFactory = StandaloneMockMvcFactory.of(controllersOrMockMvcConfigurers);
    }

    /**
     * Build a {@link MockMvc} by using a provided {@code AbstractMockMvcBuilder}
     * for configuring Spring MVC infrastructure programmatically.
     * This allows full control over the instantiation and initialization of
     * controllers, and their dependencies, similar to plain unit tests while
     * also making it possible to test one controller at a time.
     *

     *
If the Spring MVC configuration of an application is relatively
     * straight-forward, for example when using the MVC namespace or the MVC
     * Java config, then using this builder might be a good option for testing
     * a majority of controllers. A much smaller number of tests can be used
     * to focus on testing and verifying the actual Spring MVC configuration.
     *
     * @param builder {@link org.springframework.test.web.servlet.setup.AbstractMockMvcBuilder} to build the MVC mock
     */
    public static void standaloneSetup(MockMvcBuilder builder) {
        mockMvcFactory = new MockMvcFactory(builder);
    }

    /**
     * Build a {@link MockMvc} using the given, fully initialized, i.e.
     * refreshed, {@link WebApplicationContext} and assign it to REST Assured.
     * The {@link org.springframework.web.servlet.DispatcherServlet}
     * will use the context to discover Spring MVC infrastructure and
     * application controllers in it. The context must have been configured with
     * a {@link javax.servlet.ServletContext}.
     *
     * @param context            The web application context to use
     * @param mockMvcConfigurers {@link MockMvcConfigurer}'s to be applied when creating a {@link MockMvc} instance of this WebApplicationContext (optional)
     */
    public static void webAppContextSetup(WebApplicationContext context, MockMvcConfigurer... mockMvcConfigurers) {
        DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context);  // To avoid compile-time errors
        if (mockMvcConfigurers != null && mockMvcConfigurers.length > 0) {
            for (MockMvcConfigurer mockMvcConfigurer : mockMvcConfigurers) {
                builder.apply(mockMvcConfigurer);
            }
        }
        mockMvcFactory = new MockMvcFactory(builder);
    }

    /**
     * Assign one or more {@link org.springframework.test.web.servlet.ResultHandler} that'll be executes after a request has been made.
     *
     * @param resultHandler  The result handler
     * @param resultHandlers Additional result handlers (optional)
     */
    public static void resultHandlers(ResultHandler resultHandler, ResultHandler... resultHandlers) {
        notNull(resultHandler, ResultHandler.class);
        RestAssuredMockMvc.resultHandlers.add(resultHandler);
        if (resultHandlers != null && resultHandlers.length >= 1) {
            Collections.addAll(RestAssuredMockMvc.resultHandlers, resultHandlers);
        }
    }

    /**
     * @return The defined list of result handlers
     */
    public static List<ResultHandler> resultHandlers() {
        return Collections.unmodifiableList(resultHandlers);
    }

    /**
     * Assign one or more {@link org.springframework.test.web.servlet.ResultHandler} that'll be executes after a request has been made.
     *

     * Note that it's recommended to use {@link #with(RequestPostProcessor, RequestPostProcessor...)} instead of this method when setting
     * authentication/authorization based RequestPostProcessors.
     *

     *
     * @param postProcessor            a post-processor to add
     * @param additionalPostProcessors Additional post-processors to add
     * @see MockMvcRequestSpecification#postProcessors(RequestPostProcessor, RequestPostProcessor...)
     */
    public static void postProcessors(RequestPostProcessor postProcessor, RequestPostProcessor... additionalPostProcessors) {
        notNull(postProcessor, RequestPostProcessor.class);
        RestAssuredMockMvc.requestPostProcessors.add(postProcessor);
        if (additionalPostProcessors != null && additionalPostProcessors.length >= 1) {
            Collections.addAll(RestAssuredMockMvc.requestPostProcessors, additionalPostProcessors);
        }
    }

    /**
     * @return The defined list of request post processors
     */
    public static List<RequestPostProcessor> postProcessors() {
        return Collections.unmodifiableList(requestPostProcessors);
    }

    /**
     * Reset all static configurations to their default values.
     */
    public static void reset() {
        mockMvcFactory = null;
        config = null;
        basePath = "/";
        resultHandlers.clear();
        requestPostProcessors.clear();
        responseSpecification = null;
        requestSpecification = null;
        authentication = null;
    }

    /**
     * Perform a GET request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do get("/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
     * @return The response of the GET request.
     */
    public static MockMvcResponse get(String path, Object... pathParams) {
        return given().get(path, pathParams);
    }

    /**
     * Perform a GET request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters.
     * @return The response of the GET request.
     */
    public static MockMvcResponse get(String path, Map<String, ?> pathParams) {
        return given().get(path, pathParams);
    }

    /**
     * Perform a POST request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do post("/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
     * @return The response of the request.
     */
    public static MockMvcResponse post(String path, Object... pathParams) {
        return given().post(path, pathParams);
    }

    /**
     * Perform a POST request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters.
     * @return The response of the request.
     */
    public static MockMvcResponse post(String path, Map<String, ?> pathParams) {
        return given().post(path, pathParams);
    }

    /**
     * Perform a PUT request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do put("/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
     * @return The response of the request.
     */
    public static MockMvcResponse put(String path, Object... pathParams) {
        return given().put(path, pathParams);
    }

    /**
     * Perform a DELETE request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do delete("/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
     * @return The response of the request.
     */
    public static MockMvcResponse delete(String path, Object... pathParams) {
        return given().delete(path, pathParams);
    }

    /**
     * Perform a DELETE request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters.
     * @return The response of the request.
     */
    public static MockMvcResponse delete(String path, Map<String, ?> pathParams) {
        return given().delete(path, pathParams);
    }

    /**
     * Perform a HEAD request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do head("/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
     * @return The response of the request.
     */
    public static MockMvcResponse head(String path, Object... pathParams) {
        return given().head(path, pathParams);
    }

    /**
     * Perform a HEAD request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters.
     * @return The response of the request.
     */
    public static MockMvcResponse head(String path, Map<String, ?> pathParams) {
        return given().head(path, pathParams);
    }

    /**
     * Perform a PATCH request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do head("/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
     * @return The response of the request.
     */
    public static MockMvcResponse patch(String path, Object... pathParams) {
        return given().patch(path, pathParams);
    }

    /**
     * Perform a PATCH request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters.
     * @return The response of the request.
     */
    public static MockMvcResponse patch(String path, Map<String, ?> pathParams) {
        return given().patch(path, pathParams);
    }

    /**
     * Perform a OPTIONS request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do head("/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
     * @return The response of the request.
     */
    public static MockMvcResponse options(String path, Object... pathParams) {
        return given().options(path, pathParams);
    }

    /**
     * Perform a OPTIONS request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
     * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
     *
     * @param path       The path to send the request to.
     * @param pathParams The path parameters.
     * @return The response of the request.
     */
    public static MockMvcResponse options(String path, Map<String, ?> pathParams) {
        return given().options(path, pathParams);
    }

    /**
     * Perform a GET request to a uri.
     *
     * @param uri The uri to send the request to.
     * @return The response of the GET request.
     */
    public static MockMvcResponse get(URI uri) {
        return given().get(uri);
    }

    /**
     * Perform a POST request to a uri.
     *
     * @param uri The uri to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse post(URI uri) {
        return given().post(uri);
    }

    /**
     * Perform a PUT request to a uri.
     *
     * @param uri The uri to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse put(URI uri) {
        return given().put(uri);
    }

    /**
     * Perform a DELETE request to a uri.
     *
     * @param uri The uri to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse delete(URI uri) {
        return given().delete(uri);
    }

    /**
     * Perform a HEAD request to a uri.
     *
     * @param uri The uri to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse head(URI uri) {
        return given().head(uri);
    }

    /**
     * Perform a PATCH request to a uri.
     *
     * @param uri The uri to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse patch(URI uri) {
        return given().patch(uri);
    }

    /**
     * Perform a OPTIONS request to a uri.
     *
     * @param uri The uri to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse options(URI uri) {
        return given().options(uri);
    }

    /**
     * Perform a GET request to a url.
     *
     * @param url The url to send the request to.
     * @return The response of the GET request.
     */
    public static MockMvcResponse get(URL url) {
        return given().get(url);
    }

    /**
     * Perform a POST request to a url.
     *
     * @param url The url to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse post(URL url) {
        return given().post(url);
    }

    /**
     * Perform a PUT request to a url.
     *
     * @param url The url to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse put(URL url) {
        return given().put(url);
    }

    /**
     * Perform a DELETE request to a url.
     *
     * @param url The url to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse delete(URL url) {
        return given().delete(url);
    }

    /**
     * Perform a HEAD request to a url.
     *
     * @param url The url to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse head(URL url) {
        return given().head(url);
    }

    /**
     * Perform a PATCH request to a url.
     *
     * @param url The url to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse patch(URL url) {
        return given().patch(url);
    }

    /**
     * Perform a OPTIONS request to a url.
     *
     * @param url The url to send the request to.
     * @return The response of the request.
     */
    public static MockMvcResponse options(URL url) {
        return given().options(url);
    }

    /**
     * Perform a GET request to the statically configured base path.
     *
     * @return The response of the GET request.
     */
    public static MockMvcResponse get() {
        return given().get();
    }

    /**
     * Perform a POST request to the statically configured base path.
     *
     * @return The response of the request.
     */
    public static MockMvcResponse post() {
        return given().post();
    }

    /**
     * Perform a PUT request to the statically configured base path.
     *
     * @return The response of the request.
     */
    public static MockMvcResponse put() {
        return given().put();
    }

    /**
     * Perform a DELETE request to the statically configured base path.
     *
     * @return The response of the request.
     */
    public static MockMvcResponse delete() {
        return given().delete();
    }

    /**
     * Perform a HEAD request to the statically configured base path.
     *
     * @return The response of the request.
     */
    public static MockMvcResponse head() {
        return given().head();
    }

    /**
     * Perform a PATCH request to the statically configured base path.
     *
     * @return The response of the request.
     */
    public static MockMvcResponse patch() {
        return given().patch();
    }

    /**
     * Perform a OPTIONS request to the statically configured base path.
     *
     * @return The response of the request.
     */
    public static MockMvcResponse options() {
        return given().options();
    }

    /**
         * Perform a request to the pre-configured path (by default http://localhost:8080).
         *
         * @param method The HTTP method to use
         * @return The response of the request.
         */
        public static MockMvcResponse request(Method method) {
            return given().request(method);
        }

        /**
         * Perform a custom HTTP request to the pre-configured path (by default http://localhost:8080).
         *
         * @param method The HTTP method to use
         * @return The response of the request.
         */
        public static MockMvcResponse request(String method) {
            return given().request(method);
        }

        /**
         * Perform a HTTP request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
         * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
         *
         * @param method     The HTTP method to use
         * @param path       The path to send the request to.
         * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do request(Method.TRACE,"/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
         * @return The response of the request.
         */
        public static MockMvcResponse request(Method method, String path, Object... pathParams) {
            return given().request(method, path, pathParams);
        }

        /**
         * Perform a custom HTTP request to a path. Normally the path doesn't have to be fully-qualified e.g. you don't need to
         * specify the path as <tt>http://localhost:8080/path</tt>. In this case it's enough to use <tt>/path</tt>.
         *
         * @param method     The HTTP method to use
         * @param path       The path to send the request to.
         * @param pathParams The path parameters. E.g. if path is "/book/{hotelId}/{roomNumber}" you can do request("method","/book/{hotelName}/{roomNumber}", "Hotels R Us", 22);.
         * @return The response of the request.
         */
        public static MockMvcResponse request(String method, String path, Object... pathParams) {
            return given().request(method, path, pathParams);
        }

        /**
         * Perform a request to a uri.
         *
         * @param method The HTTP method to use
         * @param uri    The uri to send the request to.
         * @return The response of the GET request.
         */
        public static MockMvcResponse request(Method method, URI uri) {
            return given().request(method, uri);
        }

        /**
         * Perform a request to a url.
         *
         * @param method The HTTP method to use
         * @param url    The url to send the request to.
         * @return The response of the GET request.
         */
        public static MockMvcResponse request(Method method, URL url) {
            return given().request(method, url);
        }

        /**
         * Perform a custom HTTP request to a uri.
         *
         * @param method The HTTP method to use
         * @param uri    The uri to send the request to.
         * @return The response of the GET request.
         */
        public static MockMvcResponse request(String method, URI uri) {
            return given().request(method, uri);
        }

        /**
         * Perform a custom HTTP request to a url.
         *
         * @param method The HTTP method to use
         * @param url    The url to send the request to.
         * @return The response of the GET request.
         */
        public static MockMvcResponse request(String method, URL url) {
            return given().request(method, url);
        }

    /**
     * Authenticate using the given principal. Used as:
     *

     * RestAssured.authentication = principal(myPrincipal);
     *

     * or in a {@link MockMvcRequestSpecBuilder}:
     *

     * MockMvcRequestSpecification req = new MockMvcRequestSpecBuilder().setAuth(principal(myPrincipal)). ..
     *

     *
     * @param principal The principal to use.
     * @return A {@link MockMvcAuthenticationScheme} instance.
     * @see MockMvcAuthenticationSpecification#principal(java.security.Principal)
     */
    public static MockMvcAuthenticationScheme principal(final Principal principal) {
        return new MockMvcAuthenticationScheme() {
            public void authenticate(MockMvcRequestSpecification mockMvcRequestSpecification) {
                mockMvcRequestSpecification.auth().principal(principal);
            }
        };
    }

    /**
     * Authenticate using the given principal. Used as:
     *

     * RestAssured.authentication = principal(myPrincipal);
     *

     * or in a {@link MockMvcRequestSpecBuilder}:
     *

     * MockMvcRequestSpecification req = new MockMvcRequestSpecBuilder().setAuth(principal(myPrincipal)). ..
     *

     *
     * @param principal The principal to use.
     * @return A {@link MockMvcAuthenticationScheme} instance.
     * @see MockMvcAuthenticationSpecification#principal(Object)
     */
    public static MockMvcAuthenticationScheme principal(final Object principal) {
        return new MockMvcAuthenticationScheme() {
            public void authenticate(MockMvcRequestSpecification mockMvcRequestSpecification) {
                mockMvcRequestSpecification.auth().principal(principal);
            }
        };
    }

    /**
     * Authenticate using the given principal and credentials. Used as:
     *

     * RestAssured.authentication = principalWithCredentials(myPrincipal, myCredentials);
     *

     * or in a {@link MockMvcRequestSpecBuilder}:
     *

     * MockMvcRequestSpecification req = new MockMvcRequestSpecBuilder().setAuth(principalWithCredentials(myPrincipal, myCredentials)). ..
     *

     *
     * @param principal   The principal to use.
     * @param credentials The credentials to use
     * @param authorities Optional list of authorities
     * @return A {@link MockMvcAuthenticationScheme} instance.
     * @see MockMvcAuthenticationSpecification#principalWithCredentials(Object, Object, String...)
     */
    public static MockMvcAuthenticationScheme principalWithCredentials(final Object principal, final Object credentials, final String... authorities) {
        return new MockMvcAuthenticationScheme() {
            public void authenticate(MockMvcRequestSpecification mockMvcRequestSpecification) {
                mockMvcRequestSpecification.auth().principalWithCredentials(principal, credentials, authorities);
            }
        };
    }

    /**
     * Authenticate using the supplied authentication instance (org.springframework.security.core.Authentication from Spring Security). Used as:
     *

     * RestAssured.authentication = authentication(myAuth);
     *

     * or in a {@link MockMvcRequestSpecBuilder}:
     *

     * MockMvcRequestSpecification req = new MockMvcRequestSpecBuilder().setAuth(authentication(myAuth)). ..
     *

     *
     * @param authentication The authentication instance to use.
     * @return A {@link MockMvcAuthenticationScheme} instance.
     * @see MockMvcAuthenticationSpecification#authentication(Object)
     */
    public static MockMvcAuthenticationScheme authentication(final Object authentication) {
        return new MockMvcAuthenticationScheme() {
            public void authenticate(MockMvcRequestSpecification mockMvcRequestSpecification) {
                mockMvcRequestSpecification.auth().authentication(authentication);
            }
        };
    }

    /**
     * Authenticate using a {@link RequestPostProcessor}.
     * This is mainly useful when you have added the spring-security-test artifact to classpath. This allows
     * you to do for example:
     *

     * RestAssured.authentication = with(user("username").password("password"));
     *

     * where user is statically imported from org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.
     *
     * @param requestPostProcessor           The first request post processor to be used for authentication
     * @param additionalRequestPostProcessor Additional request post processors to be used for authentication
     * @return A {@link MockMvcAuthenticationScheme} instance.
     */
    public static MockMvcAuthenticationScheme with(final RequestPostProcessor requestPostProcessor, final RequestPostProcessor... additionalRequestPostProcessor) {
        return new MockMvcAuthenticationScheme() {
            public void authenticate(MockMvcRequestSpecification mockMvcRequestSpecification) {
                mockMvcRequestSpecification.auth().with(requestPostProcessor, additionalRequestPostProcessor);
            }
        };
    }

    /**
     * Enable logging of both the request and the response if REST Assureds test validation fails with log detail equal to {@link LogDetail#ALL}.
     *

     *

     * This is just a shortcut for:
     *

     *

     * RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().logConfig(logConfig().enableLoggingOfRequestAndResponseIfValidationFails());
     *

     */
    public static void enableLoggingOfRequestAndResponseIfValidationFails() {
        enableLoggingOfRequestAndResponseIfValidationFails(LogDetail.ALL);
    }

    /**
     * Enable logging of both the request and the response if REST Assureds test validation fails with the specified log detail.
     *

     *

     * This is just a shortcut for:
     *

     *

     * RestAssured.config = new RestAssuredMockMvcConfig().logConfig(logConfig().enableLoggingOfRequestAndResponseIfValidationFails(logDetail));
     *

     *
     * @param logDetail The log detail to show in the log
     */
    public static void enableLoggingOfRequestAndResponseIfValidationFails(LogDetail logDetail) {
        config = config == null ? new RestAssuredMockMvcConfig() : config;
        config = config.logConfig(logConfig().enableLoggingOfRequestAndResponseIfValidationFails(logDetail));
        // Update request specification if already defined otherwise it'll override the configs.
        // Note that request spec also influence response spec when it comes to logging if validation fails due to the way filters work
        if (requestSpecification != null && requestSpecification instanceof MockMvcRequestSpecificationImpl) {
            RestAssuredMockMvcConfig restAssuredConfig = ((MockMvcRequestSpecificationImpl) requestSpecification).getRestAssuredMockMvcConfig();
            if (restAssuredConfig == null) {
                restAssuredConfig = config;
            } else {
                LogConfig logConfigForRequestSpec = restAssuredConfig.getLogConfig().enableLoggingOfRequestAndResponseIfValidationFails(logDetail);
                restAssuredConfig = restAssuredConfig.logConfig(logConfigForRequestSpec);
            }
            requestSpecification.config(restAssuredConfig);
        }
    }

    /**
     * @return The assigned config or a new config is no config is assigned
     */
    public static RestAssuredMockMvcConfig config() {
        return config == null ? new RestAssuredMockMvcConfig() : config;
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.module.mockmvc.matcher;

import io.restassured.internal.matcher.xml.XmlDtdMatcher;
import io.restassured.internal.matcher.xml.XmlXsdMatcher;
import io.restassured.matcher.ResponseAwareMatcher;
import io.restassured.matcher.RestAssuredMatchers;
import io.restassured.module.mockmvc.response.MockMvcResponse;
import org.hamcrest.Matcher;

import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;

import static org.hamcrest.Matchers.*;

/**
 * Providers Hamcrest matchers that may be useful when validating a MockMvc response.
 */
public class RestAssuredMockMvcMatchers {

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static Matcher<String> matchesXsd(String xsd) {
        return RestAssuredMatchers.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static Matcher<String> matchesXsd(InputStream xsd) {
        return RestAssuredMatchers.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static Matcher<String> matchesXsd(Reader xsd) {
        return RestAssuredMatchers.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static Matcher<String> matchesXsd(File xsd) {
        return RestAssuredMatchers.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML file in classpath matches the supplied XSD.
     *
     * @param path The path to the XSD located in classpath
     * @return The DTD matcher
     */
    public static Matcher<String> matchesXsdInClasspath(String path) {
        return XmlXsdMatcher.matchesXsdInClasspath(path);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param dtd The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(String dtd) {
        return RestAssuredMatchers.matchesDtd(dtd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param dtd The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(InputStream dtd) {
        return RestAssuredMatchers.matchesDtd(dtd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param dtd The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(File dtd) {
        return RestAssuredMatchers.matchesDtd(dtd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param url The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(URL url) {
        return RestAssuredMatchers.matchesDtd(url);
    }

    /**
     * Evaluates to true if an XML file in classpath matches the supplied DTD.
     *
     * @param path The path to the DTD file in classpath
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtdInClasspath(String path) {
        return XmlDtdMatcher.matchesDtdInClasspath(path);
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "playerId" : "my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("userId", equalToPath("playerId"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<MockMvcResponse> equalToPath(final String path) {
        return new ResponseAwareMatcher<MockMvcResponse>() {
            public Matcher<?> matcher(MockMvcResponse response) {
                return equalTo(response.path(path));
            }
        };
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "href" : "http://localhost:8080/my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("href", endsWithPath("userId"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<MockMvcResponse> endsWithPath(final String path) {
        return new ResponseAwareMatcher<MockMvcResponse>() {
            public Matcher<?> matcher(MockMvcResponse response) {
                return endsWith(response.<String>path(path));
            }
        };
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "baseUri" : "http://localhost:8080",
     *      "href" : "http://localhost:8080/my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("href", startsWithPath("baseUri"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<MockMvcResponse> startsWithPath(final String path) {
        return new ResponseAwareMatcher<MockMvcResponse>() {
            public Matcher<?> matcher(MockMvcResponse response) {
                return startsWith(response.<String>path(path));
            }
        };
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "href" : "http://localhost:8080/my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("href", containsPath("userId"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<MockMvcResponse> containsPath(final String path) {
        return new ResponseAwareMatcher<MockMvcResponse>() {
            public Matcher<?> matcher(MockMvcResponse response) {
                return containsString(response.<String>path(path));
            }
        };
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.builder;

import io.restassured.filter.Filter;
import io.restassured.http.ContentType;
import io.restassured.http.Cookies;
import io.restassured.http.Header;
import io.restassured.http.Headers;
import io.restassured.internal.ResponseParserRegistrar;
import io.restassured.internal.RestAssuredResponseImpl;
import io.restassured.response.Response;
import org.apache.commons.lang3.StringUtils;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import static io.restassured.internal.common.assertion.AssertParameter.notNull;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.trim;
import static org.apache.http.HttpHeaders.CONTENT_TYPE;

/**
 * A builder to make it easier to create new {@link Response} implementations. This is useful if you're working with {@link Filter}s and want to
 * change the response you get from the sever somehow.
 */
public class ResponseBuilder {

    private RestAssuredResponseImpl restAssuredResponse = new RestAssuredResponseImpl();

    /**
     * Clone an already existing response.
     *
     * @return Builder.
     */
    public ResponseBuilder clone(Response response) {
        if (isRestAssuredResponse(response)) {
            final RestAssuredResponseImpl raResponse = raResponse(response);
            restAssuredResponse.setContent(raResponse.getContent());
            restAssuredResponse.setHasExpectations(raResponse.getHasExpectations());
            restAssuredResponse.setDefaultContentType(raResponse.getDefaultContentType());
            restAssuredResponse.setDecoderConfig(raResponse.getDecoderConfig());
            restAssuredResponse.setSessionIdName(raResponse.getSessionIdName());
            restAssuredResponse.setConnectionManager(raResponse.getConnectionManager());
            restAssuredResponse.setConfig(raResponse.getConfig());
            restAssuredResponse.setRpr(raResponse.getRpr());
            restAssuredResponse.setLogRepository(raResponse.getLogRepository());
            restAssuredResponse.setFilterContextProperties(raResponse.getFilterContextProperties());
        } else {
            restAssuredResponse.setContent(response.asInputStream());
        }
        restAssuredResponse.setContentType(response.getContentType());
        restAssuredResponse.setCookies(response.getDetailedCookies());
        restAssuredResponse.setResponseHeaders(response.getHeaders());
        restAssuredResponse.setStatusCode(response.getStatusCode());
        restAssuredResponse.setStatusLine(response.getStatusLine());
        return this;
    }

    /**
     * Set the response body to a String
     *
     * @return Builder.
     */
    public ResponseBuilder setBody(String stringBody) {
        notNull(stringBody, "Response body");
        restAssuredResponse.setContent(stringBody);
        return this;
    }

    /**
     * Set the response body to an inputstream
     *
     * @return Builder.
     */
    public ResponseBuilder setBody(InputStream inputStream) {
        notNull(inputStream, "Response body");
        restAssuredResponse.setContent(inputStream);
        return this;
    }

    /**
     * Set the response body to an array of bytes
     *
     * @return Builder.
     */
    public ResponseBuilder setBody(byte[] bytes) {
        notNull(bytes, "Response body");
        restAssuredResponse.setContent(bytes);
        return this;
    }

    /**
     * Set response headers,  e.g:
     *

     * Header first = new Header("headerName1", "headerValue1");
     * Header second = new Header("headerName2", "headerValue2");
     * Headers headers = new Header(first, second);
     *

     *
     * @return The builder
     * @see Headers
     */
    public ResponseBuilder setHeaders(Headers headers) {
        notNull(headers, "Headers");
        restAssuredResponse.setResponseHeaders(headers);
        return this;
    }

    /**
     * Set some cookies that will be available in the response. To create cookies you can do:
     *

     * Cookie cookie1 = Cookie.Builder("username", "John").setComment("comment 1").build();
     * Cookie cookie2 = Cookie.Builder("token", 1234).setComment("comment 2").build();
     * Cookies cookies = new Cookies(cookie1, cookie2);
     *

     *
     * @return The Builder
     */
    public ResponseBuilder setCookies(Cookies cookies) {
        notNull(cookies, "Cookies");
        restAssuredResponse.setCookies(cookies);
        return this;
    }

    /**
     * Set the content type of the response
     *
     * @return The builder
     */
    public ResponseBuilder setContentType(String contentType) {
        notNull(contentType, "Content type");
        restAssuredResponse.setContentType(contentType);
        setHeader(CONTENT_TYPE, contentType);
        return this;
    }

    /**
     * Set the content type of the response
     *
     * @return The builder
     */
    public ResponseBuilder setContentType(ContentType contentType) {
        notNull(contentType, ContentType.class);
        return setContentType(contentType.toString());
    }

    /**
     * Set a specific header
     *
     * @return The builder
     */
    public ResponseBuilder setHeader(String name, String value) {
        notNull(name, "Header name");
        notNull(value, "Header value");

        List newHeaders = new ArrayList(restAssuredResponse.headers().asList());
        newHeaders.add(new Header(name, value));
        restAssuredResponse.setResponseHeaders(new Headers(newHeaders));

        if (trim(name).equalsIgnoreCase(CONTENT_TYPE)) {
            restAssuredResponse.setContentType(value);
        }
        return this;
    }

    /**
     * Set the status line of the response.
     *
     * @return The builder
     */
    public ResponseBuilder setStatusLine(String statusLine) {
        notNull(statusLine, "Status line");
        restAssuredResponse.setStatusLine(statusLine);
        return this;
    }

    /**
     * Set the status code of the response.
     *
     * @return The builder
     */
    public ResponseBuilder setStatusCode(int statusCode) {
        restAssuredResponse.setStatusCode(statusCode);
        return this;
    }

    /**
     * Build the actual response
     *
     * @return The response object
     */
    public Response build() {
        final int statusCode = restAssuredResponse.statusCode();

        if (statusCode < 100 || statusCode >= 600) {
            throw new IllegalArgumentException(format("Status code must be greater than 100 and less than 600, was %d.", statusCode));
        }

        if (StringUtils.isBlank(restAssuredResponse.statusLine())) {
            restAssuredResponse.setStatusLine(restAssuredResponse.statusCode());
        }

        restAssuredResponse.setRpr(new ResponseParserRegistrar());
        return restAssuredResponse;
    }

    private boolean isRestAssuredResponse(Response response) {
        return response instanceof RestAssuredResponseImpl;
    }

    private RestAssuredResponseImpl raResponse(Response response) {
        return (RestAssuredResponseImpl) response;
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.matcher;

import io.restassured.http.Cookie;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.core.CombinableMatcher;

import java.util.Date;

import static org.hamcrest.Matchers.equalTo;

/**
 * Fluent Hamcrest matcher for assertions related to {@link Cookie} instances.
 * Can be instantiated via {@link RestAssuredMatchers} factory method.
 */
public class DetailedCookieMatcher extends CombinableMatcher<Cookie> {

    DetailedCookieMatcher() {
        this(Matchers.notNullValue());
    }

    private DetailedCookieMatcher(Matcher<? super Cookie> m) {
        super(m);
    }

    /**
     * Verifies whether value of cookie is equal to specified argument.
     * @param expectedValue expected value of cookie
     * @return A {@link DetailedCookieMatcher} instance with and-composed value property assertion
     */
    public DetailedCookieMatcher value(String expectedValue) {
        return value(equalTo(expectedValue));
    }

    /**
     * Verifies whether value of cookie satisfies specified matcher.
     * @param valueMatcher value assertion
     * @return A {@link DetailedCookieMatcher} instance with and-composed value property assertion
     */
    public DetailedCookieMatcher value(Matcher<? super String> valueMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("value", valueMatcher)));
    }

    /**
     * Verifies whether comment of cookie is equal to specified argument.
     * @param expectedComment expected comment of cookie
     * @return A {@link DetailedCookieMatcher} instance with and-composed comment property assertion
     */
    public DetailedCookieMatcher comment(String expectedComment) {
        return comment(equalTo(expectedComment));
    }
    /**
     * Verifies whether comment of cookie satisfies specified matcher.
     * @param commentMatcher comment assertion
     * @return A {@link DetailedCookieMatcher} instance with and-composed comment property assertion
     */
    public DetailedCookieMatcher comment(Matcher<? super String> commentMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("comment", commentMatcher)));
    }

    /**
     * Verifies whether expiry date of cookie is equal to specified argument.
     * @param expectedExpiryDate expected expiry date of cookie
     * @return A {@link DetailedCookieMatcher} instance with and-composed expiry date property assertion
     */
    public DetailedCookieMatcher expiryDate(Date expectedExpiryDate) {
        return expiryDate(equalTo(expectedExpiryDate));
    }

    /**
     * Verifies whether expiry date of cookie satisfies specified matcher.
     * @param expiryDateMatcher expiry date assertion
     * @return A {@link DetailedCookieMatcher} instance with and-composed expiry date property assertion
     */
    public DetailedCookieMatcher expiryDate(Matcher<? super Date> expiryDateMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("expiryDate", expiryDateMatcher)));
    }

    /**
     * Verifies whether domain of cookie is equal to specified argument.
     * @param expectedDomain expected domain of cookie
     * @return A {@link DetailedCookieMatcher} instance with and-composed domain property assertion
     */
    public DetailedCookieMatcher domain(String expectedDomain) {
        return domain(equalTo(expectedDomain));
    }

    /**
     * Verifies whether domain of cookie satisfies specified matcher.
     * @param domainMatcher assertion for domain property
     * @return A {@link DetailedCookieMatcher} instance with and-composed domain property assertion
     */
    public DetailedCookieMatcher domain(Matcher<? super String> domainMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("domain", domainMatcher)));
    }

    /**
     * Verifies whether path of cookie is equal to specified argument.
     * @param expectedPath expected path of cookie
     * @return A {@link DetailedCookieMatcher} instance with and-composed path property assertion
     */
    public DetailedCookieMatcher path(String expectedPath) {
        return path(equalTo(expectedPath));
    }

    /**
     * Verifies whether path of cookie satisfies specified matcher.
     * @param pathMatcher assertion for path property
     * @return A {@link DetailedCookieMatcher} instance with and-composed path property assertion
     */
    public DetailedCookieMatcher path(Matcher<? super String> pathMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("path", pathMatcher)));
    }

    /**
     * Verifies whether secured property of cookie is equal to specified argument.
     * @param expectedSecuredValue expected value of secured property
     * @return A {@link DetailedCookieMatcher} instance with and-composed secured property assertion
     */
    public DetailedCookieMatcher secured(boolean expectedSecuredValue) {
        return secured(equalTo(expectedSecuredValue));
    }

    /**
     * Verifies whether secured property of cookie satisfies specified matcher.
     * @param securedMatcher assertion for secured property
     * @return A {@link DetailedCookieMatcher} instance with and-composed secured property assertion
     */
    public DetailedCookieMatcher secured(Matcher<? super Boolean> securedMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("secured", securedMatcher)));
    }

    /**
     * Verifies whether http-only property of cookie is equal to specified argument.
     * @param expectedHttpOnlyValue expected http-only property value
     * @return A {@link DetailedCookieMatcher} instance with and-composed http-only property assertion
     */
    public DetailedCookieMatcher httpOnly(boolean expectedHttpOnlyValue) {
        return httpOnly(equalTo(expectedHttpOnlyValue));
    }

    /**
     * Verifies whether http-only property of cookie satisfies specified matcher.
     * @param httpOnlyMatcher assertion for domain property
     * @return A {@link DetailedCookieMatcher} instance with and-composed http-only property assertion
     */
    public DetailedCookieMatcher httpOnly(Matcher<? super Boolean> httpOnlyMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("httpOnly", httpOnlyMatcher)));
    }

    /**
     * Verifies whether version of cookie is equal to specified argument.
     * @param expectedVersionValue expected version of cookie
     * @return A {@link DetailedCookieMatcher} instance with and-composed version property assertion
     */
    public DetailedCookieMatcher version(int expectedVersionValue) {
        return version(equalTo(expectedVersionValue));
    }

    /**
     * Verifies whether version of cookie satisfies specified matcher.
     * @param versionMatcher assertion for version property
     * @return A {@link DetailedCookieMatcher} instance with and-composed version property assertion
     */
    public DetailedCookieMatcher version(Matcher<? super Integer> versionMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("version", versionMatcher)));
    }

    /**
     * Verifies whether max age of cookie is equal to specified argument.
     * @param expectedMaxAgeValue expected max age of cookie
     * @return A {@link DetailedCookieMatcher} instance with and-composed max age property assertion
     */
    public DetailedCookieMatcher maxAge(int expectedMaxAgeValue) {
        return maxAge(equalTo(expectedMaxAgeValue));
    }

    /**
     * Verifies whether max age of cookie satisfies specified matcher.
     * @param maxAgeMatcher assertion for max age property
     * @return A {@link DetailedCookieMatcher} instance with and-composed max age property assertion
     */
    public DetailedCookieMatcher maxAge(Matcher<? super Integer> maxAgeMatcher) {
        return new DetailedCookieMatcher(and(Matchers.hasProperty("maxAge", maxAgeMatcher)));
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.builder;

import io.restassured.RestAssured;
import io.restassured.config.LogConfig;
import io.restassured.config.RestAssuredConfig;
import io.restassured.filter.log.LogDetail;
import io.restassured.http.ContentType;
import io.restassured.internal.ResponseParserRegistrar;
import io.restassured.internal.ResponseSpecificationImpl;
import io.restassured.internal.SpecificationMerger;
import io.restassured.internal.log.LogRepository;
import io.restassured.matcher.DetailedCookieMatcher;
import io.restassured.parsing.Parser;
import io.restassured.specification.Argument;
import io.restassured.specification.ResponseSpecification;
import org.hamcrest.Matcher;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static io.restassured.RestAssured.rootPath;
import static io.restassured.internal.common.assertion.AssertParameter.notNull;

/**
 * You can use the builder to construct a response specification. The specification can be used as e.g.
 *

 * ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
 * RequestSpecification requestSpec = new RequestSpecBuilder().addParam("parameter1", "value1").build();
 *
 * given(responseSpec, requestSpec).post("/something");
 *

 *

 * or
 *

 * ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
 *
 * expect().
 *         spec(responseSpec).
 *         body("x.y.z", equalTo("something")).
 * when().
 *        get("/something");
 *

 */
public class ResponseSpecBuilder {
    private final ResponseSpecification spec;

    public ResponseSpecBuilder() {
        spec = new ResponseSpecificationImpl(rootPath, null, getResponseParserRegistrar(), restAssuredConfig(), new LogRepository());
    }

    /**
     * Expect that the response status code matches the given Hamcrest matcher.
     *
     * @param expectedStatusCode The expected status code matcher.
     * @return The builder
     */
    public ResponseSpecBuilder expectStatusCode(Matcher<Integer> expectedStatusCode) {
        spec.statusCode(expectedStatusCode);
        return this;
    }

    /**
     * Expect that the response status code matches an integer.
     *
     * @param expectedStatusCode The expected status code.
     * @return The builder
     */
    public ResponseSpecBuilder expectStatusCode(int expectedStatusCode) {
        spec.statusCode(expectedStatusCode);
        return this;
    }

    /**
     * Expect that the response status line matches the given Hamcrest matcher.
     *
     * @param expectedStatusLine The expected status line matcher.
     * @return The builder
     */
    public ResponseSpecBuilder expectStatusLine(Matcher<String> expectedStatusLine) {
        spec.statusLine(expectedStatusLine);
        return this;
    }

    /**
     * Expect that the response status line matches the given String.
     *
     * @param expectedStatusLine The expected status line.
     * @return The builder
     */
    public ResponseSpecBuilder expectStatusLine(String expectedStatusLine) {
        spec.statusLine(expectedStatusLine);
        return this;
    }

    /**
     * Expect that response headers matches those specified in a Map.
     *

     * E.g. expect that the response of the GET request to "/something" contains header <tt>headerName1=headerValue1</tt>
     * and <tt>headerName2=headerValue2</tt>:
     *

     * Map expectedHeaders = new HashMap();
     * expectedHeaders.put("headerName1", "headerValue1"));
     * expectedHeaders.put("headerName2", "headerValue2");
     *

     *

     *

     *

     * You can also use Hamcrest matchers:
     *

     * Map expectedHeaders = new HashMap();
     * expectedHeaders.put("Content-Type", containsString("charset=UTF-8"));
     * expectedHeaders.put("Content-Length", "160");
     *

     *

     *
     * @param expectedHeaders The Map of expected response headers
     * @return The builder
     */
    public ResponseSpecBuilder expectHeaders(Map<String, Object> expectedHeaders) {
        spec.headers(expectedHeaders);
        return this;
    }

    /**
     * Expect that a response header matches the supplied header name and hamcrest matcher.
     *
     * @param headerName           The name of the expected header
     * @param expectedValueMatcher The Hamcrest matcher that must conform to the value
     * @return The builder
     */
    public ResponseSpecBuilder expectHeader(String headerName, Matcher<String> expectedValueMatcher) {
        spec.header(headerName, expectedValueMatcher);
        return this;
    }

    /**
     * Expect that a response header matches the supplied name and value.
     *
     * @param headerName    The name of the expected header
     * @param expectedValue The value of the expected header
     * @return The builder
     */
    public ResponseSpecBuilder expectHeader(String headerName, String expectedValue) {
        spec.header(headerName, expectedValue);
        return this;
    }

    /**
     * Expect that response cookies matches those specified in a Map.
     *

     * E.g. expect that the response of the GET request to "/something" contains cookies <tt>cookieName1=cookieValue1</tt>
     * and <tt>cookieName2=cookieValue2</tt>:
     *

     * Map expectedCookies = new HashMap();
     * expectedCookies.put("cookieName1", "cookieValue1"));
     * expectedCookies.put("cookieName2", "cookieValue2");
     *

     *

     * You can also use Hamcrest matchers:
     *

     * Map expectedCookies = new HashMap();
     * expectedCookies.put("cookieName1", containsString("Value1"));
     * expectedCookies.put("cookieName2", "cookieValue2");
     *

     *

     *
     * @param expectedCookies A Map of expected response cookies
     * @return The builder
     */
    public ResponseSpecBuilder expectCookies(Map<String, Object> expectedCookies) {
        spec.cookies(expectedCookies);
        return this;
    }

    /**
     * Expect that a response cookie matches the supplied cookie name and hamcrest matcher.
     *

     * E.g. <tt>cookieName1=cookieValue1</tt>
     *

     *
     * @param cookieName           The name of the expected cookie
     * @param expectedValueMatcher The Hamcrest matcher that must conform to the value
     * @return The builder
     */
    public ResponseSpecBuilder expectCookie(String cookieName, Matcher<String> expectedValueMatcher) {
        spec.cookie(cookieName, expectedValueMatcher);
        return this;
    }

    /**
     * Expect that a detailed response cookie matches the supplied cookie name and hamcrest matcher (see {@link DetailedCookieMatcher}.
     *

     * E.g. expect that the response of the GET request to "/something" contain cookie <tt>cookieName1=cookieValue1</tt>
     *

     * expectCookie("cookieName1", detailedCookie().value("cookieValue1").secured(true));
     *

     *

     *

     *

     * You can also expect several cookies:
     *

     * expectCookie("cookieName1", detailedCookie().value("cookieValue1").secured(true))
     *      .expectCookie("cookieName2", detailedCookie().value("cookieValue2").secured(false));
     *

     *

     *
     * @param cookieName            The name of the expected cookie
     * @param detailedCookieMatcher The Hamcrest matcher that must conform to the cookie
     * @return The builder
     */
    public ResponseSpecBuilder expectCookie(String cookieName, DetailedCookieMatcher detailedCookieMatcher) {
        spec.cookie(cookieName, detailedCookieMatcher);
        return this;
    }

    /**
     * Expect that a response cookie matches the supplied name and value.
     *
     * @param cookieName    The name of the expected cookie
     * @param expectedValue The value of the expected cookie
     * @return The builder
     */
    public ResponseSpecBuilder expectCookie(String cookieName, String expectedValue) {
        spec.cookie(cookieName, expectedValue);
        return this;
    }

    /**
     * Expect that a cookie exist in the response, regardless of value (it may have no value at all).
     *
     * @param cookieName the cookie to validate that it exists
     * @return the builder
     */
    public ResponseSpecBuilder expectCookie(String cookieName) {
        spec.cookie(cookieName);
        return this;
    }

    /**
     * Validate that the response time (in milliseconds) matches the supplied matcher.
     *
     * @param matcher The matcher
     * @return the builder
     */
    public ResponseSpecBuilder expectResponseTime(Matcher<Long> matcher) {
        spec.time(matcher);
        return this;
    }

    /**
     * Validate that the response time matches the supplied matcher and time unit.
     *
     * @param matcher  The matcher
     * @param timeUnit The timeout
     * @return the builder
     */
    public ResponseSpecBuilder expectResponseTime(Matcher<Long> matcher, TimeUnit timeUnit) {
        spec.time(matcher, timeUnit);
        return this;
    }

    /**
     * Set the root path of the response body so that you don't need to write the entire path for each expectation.
     * E.g. instead of writing:
     *

     *

     * expect().
     *          body("x.y.firstName", is(..)).
     *          body("x.y.lastName", is(..)).
     *          body("x.y.age", is(..)).
     *          body("x.y.gender", is(..)).
     * when().
     *          get(..);
     *

     *

     * you can use a root path and do:
     *

     * expect().
     *          rootPath("x.y").
     *          body("firstName", is(..)).
     *          body("lastName", is(..)).
     *          body("age", is(..)).
     *          body("gender", is(..)).
     * when().
     *          get(..);
     *

     *
     * @param rootPath The root path to use.
     */
    public ResponseSpecBuilder rootPath(String rootPath) {
        spec.rootPath(rootPath);
        return this;
    }

    /**
     * Set the root path of the response body so that you don't need to write the entire path for each expectation. The same as {@link #rootPath(String)}
     * but also provides a way to defined arguments.
     *
     * @param rootPath  The root path to use.
     * @param arguments The arguments.
     * @see ResponseSpecification#root(String, java.util.List)
     */
    public ResponseSpecBuilder rootPath(String rootPath, List<Argument> arguments) {
        spec.root(rootPath, arguments);
        return this;
    }

    /**
     * Append the given path to the root path of the response body so that you don't need to write the entire path for each expectation.
     * E.g. instead of writing:
     *

     *

     * expect().
     *          root("x.y").
     *          body("age", is(..)).
     *          body("gender", is(..)).
     *          body("name.firstName", is(..)).
     *          body("name.lastName", is(..)).
     * when().
     *          get(..);
     *

     *

     * you can use a append root and do:
     *

     * expect().
     *          root("x.y").
     *          body("age", is(..)).
     *          body("gender", is(..)).
     *          appendRoot("name").
     *          body("firstName", is(..)).
     *          body("lastName", is(..)).
     * when().
     *          get(..);
     *

     *
     * @param pathToAppend The root path to append.
     */
    public ResponseSpecBuilder appendRootPath(String pathToAppend) {
        spec.appendRootPath(pathToAppend);
        return this;
    }

    /**
     * Append the given path to the root path with arguments supplied of the response body so that you don't need to write the entire path for each expectation.
     * This is mainly useful when you have parts of the path defined in variables.
     * E.g. instead of writing:
     *

     *

     * String namePath = "name";
     * expect().
     *          root("x.y").
     *          body("age", is(..)).
     *          body("gender", is(..)).
     *          body(namePath + "first", is(..)).
     *          body(namePath + "last", is(..)).
     * when().
     *          get(..);
     *

     *

     * you can use a append root and do:
     *

     * String namePath = "name";
     * expect().
     *          root("x.y").
     *          body("age", is(..)).
     *          body("gender", is(..)).
     *          appendRoot("%s", withArgs(namePath)).
     *          body("first", is(..)).
     *          body("last", is(..)).
     * when().
     *          get(..);
     *

     *
     * @param pathToAppend The root path to use. The path and arguments follows the standard formatting syntax of Java.
     */
    public ResponseSpecBuilder appendRootPath(String pathToAppend, List<Argument> arguments) {
        spec.appendRootPath(pathToAppend, arguments);
        return this;
    }

    /**
     * Reset the root path of the response body so that you don't need to write the entire path for each expectation.
     * For example:
     *

     *

     * expect().
     *          root("x.y").
     *          body("firstName", is(..)).
     *          body("lastName", is(..)).
     *          noRoot()
     *          body("z.something1", is(..)).
     *          body("w.something2", is(..)).
     * when().
     *          get(..);
     *

     *

     * This is the same as calling rootPath("") but more expressive.
     *
     * @see #rootPath(String)
     */
    public ResponseSpecBuilder noRootPath() {
        spec.noRootPath();
        return this;
    }

    /**
     * Detach the given path from the root path.
     * E.g. instead of writing:
     *

     *

     * when().
     *          get(..);
     * then().
     *          root("x.y").
     *          body("age", is(..)).
     *          body("gender", is(..)).
     *          root("x").
     *          body("firstName", is(..)).
     *          body("lastName", is(..)).
     *

     *

     * you can use a append root and do:
     *

     * when().
     *          get(..);
     * then().
     *          root("x.y").
     *          body("age", is(..)).
     *          body("gender", is(..)).
     *          detachRoot("y").
     *          body("firstName", is(..)).
     *          body("lastName", is(..)).
     *

     *
     * @param pathToDetach The root path to detach.
     */
    public ResponseSpecBuilder detachRootPath(String pathToDetach) {
        spec.detachRootPath(pathToDetach);
        return this;
    }

    /**
     * Set the response content type to be contentType.
     *
Note that this will affect the way the response is decoded.
     * E,g. if you can't use JSON/XML matching (see e.g. {@link #expectBody(String, org.hamcrest.Matcher)}) if you specify a
     * content-type of "text/plain". If you don't specify the response content type REST Assured will automatically try to
     * figure out which content type to use.

     *
     * @param contentType The content type of the response.
     * @return The builder
     */
    public ResponseSpecBuilder expectContentType(ContentType contentType) {
        spec.contentType(contentType);
        return this;
    }

    /**
     * Set the response content type to be contentType.
     *
Note that this will affect the way the response is decoded.
     * E,g. if you can't use JSON/XML matching (see e.g. {@link #expectBody(String, org.hamcrest.Matcher)}) if you specify a
     * content-type of "text/plain". If you don't specify the response content type REST Assured will automatically try to
     * figure out which content type to use.

     *
     * @param contentType The content type of the response.
     * @return The builder
     */
    public ResponseSpecBuilder expectContentType(String contentType) {
        spec.contentType(contentType);
        return this;
    }

    /**
     * Expect that the response body conforms to one or more Hamcrest matchers.
     *
     * @param matcher The hamcrest matcher that must response content must match.
     * @return The builder
     */
    public ResponseSpecBuilder expectBody(Matcher<?> matcher) {
        spec.body(matcher);
        return this;
    }

    /**
     * Expect that the JSON or XML response content conforms to one or more Hamcrest matchers.

     *
JSON example

     *

     * Assume that a GET request to "/lotto" returns a JSON response containing:
     *

     * { "lotto":{
     *   "lottoId":5,
     *   "winning-numbers":[2,45,34,23,7,5,3],
     *   "winners":[{
     *     "winnerId":23,
     *     "numbers":[2,45,34,23,3,5]
     *   },{
     *     "winnerId":54,
     *     "numbers":[52,3,12,11,18,22]
     *   }]
     *  }}
     *

     *

     * You can verify that the lottoId is equal to 5 like this:
     *

     * ResponseSpecBuilder builder = new ResponseSpecBuilder();
     * builder.expectBody("lotto.lottoId", equalTo(5));
     *

     *
     * @param matcher The hamcrest matcher that the response content must match.
     * @return The builder
     */
    public ResponseSpecBuilder expectBody(String path, Matcher<?> matcher) {
        spec.body(path, matcher);
        return this;
    }

    /**
     * Same as {@link #expectBody(String, org.hamcrest.Matcher)} expect that you can pass arguments to the path. This
     * is useful in situations where you have e.g. pre-defined variables that constitutes the path:
     *

     * String someSubPath = "else";
     * int index = 1;
     * expect().body("something.%s[%d]", withArgs(someSubPath, index), equalTo("some value")). ..
     *

     *

     * or if you have complex root paths and don't wish to duplicate the path for small variations:
     *

     * expect().
     *          root("filters.filterConfig[%d].filterConfigGroups.find { it.name == 'Gold' }.includes").
     *          body(withArgs(0), hasItem("first")).
     *          body(withArgs(1), hasItem("second")).
     *          ..
     *

     *

     * The path and arguments follows the standard formatting syntax of Java.
     *

     * Note that withArgs can be statically imported from the io.restassured.RestAssured class.
     *

     *
     * @param path    The body path
     * @param matcher The hamcrest matcher that must response body must match.
     * @return the response specification
     * @see #expectBody(String, org.hamcrest.Matcher)
     */
    public ResponseSpecBuilder expectBody(String path, List<Argument> arguments, Matcher<?> matcher) {
        spec.body(path, arguments, matcher);
        return this;
    }

    /**
     * Merge this builder with settings from another specification. Note that the supplied specification
     * can overwrite data in the current specification. The following settings are overwritten:
     *

     *
Content type

     *
Root path</
     *
Status code

     *
Status line

     *

     * The following settings are merged:
     *

     *
Response body expectations

     *
Cookies

     *
Headers

     *

     *
     * @param specification The specification the add.
     * @return The builder
     */
    public ResponseSpecBuilder addResponseSpecification(ResponseSpecification specification) {
        if (!(specification instanceof ResponseSpecificationImpl)) {
            throw new IllegalArgumentException("specification must be of type " + ResponseSpecificationImpl.class.getClass() + ".");
        }

        ResponseSpecificationImpl rs = (ResponseSpecificationImpl) specification;
        SpecificationMerger.merge((ResponseSpecificationImpl) spec, rs);
        return this;
    }

    /**
     * Enabled logging with the specified log detail. Set a {@link LogConfig} to configure the print stream and pretty printing options.
     *
     * @param logDetail The log detail.
     * @return ResponseSpecBuilder
     */
    public ResponseSpecBuilder log(LogDetail logDetail) {
        notNull(logDetail, LogDetail.class);
        spec.logDetail(logDetail);
        return this;
    }

    /**
     * Register a content-type to be parsed using a predefined parser. E.g. let's say you want parse
     * content-type <tt>application/vnd.uoml+xml</tt> with the XML parser to be able to verify the response using the XML dot notations:
     *

     * expect().body("document.child", equalsTo("something"))..
     *

     * Since <tt>application/vnd.uoml+xml</tt> is not registered to be processed by the XML parser by default you need to explicitly
     * tell REST Assured to use this parser before making the request:
     *

     * expect().parser("application/vnd.uoml+xml", Parser.XML").when(). ..;
     *

     *

     * You can also specify by default by using:
     *

     * RestAssured.registerParser("application/vnd.uoml+xml, Parser.XML");
     *

     *
     * @param contentType The content-type to register
     * @param parser      The parser to use when verifying the response.
     * @return The builder
     */
    public ResponseSpecBuilder registerParser(String contentType, Parser parser) {
        spec.parser(contentType, parser);
        return this;
    }

    /**
     * Register a default predefined parser that will be used if no other parser (registered or pre-defined) matches the response
     * content-type. E.g. let's say that for some reason no content-type is defined in the response but the content is nevertheless
     * JSON encoded. To be able to expect the content in REST Assured you need to set the default parser:
     *

     * expect().defaultParser(Parser.JSON).when(). ..;
     *

     *

     * You can also specify it for every response by using:
     *

     * RestAssured.defaultParser(Parser.JSON);
     *

     *
     * @param parser The parser to use when verifying the response if no other parser is found for the response content-type.
     */
    public ResponseSpecBuilder setDefaultParser(Parser parser) {
        spec.defaultParser(parser);
        return this;
    }

    /**
     * Build the response specification.
     *
     * @return The assembled response specification
     */
    public ResponseSpecification build() {
        return spec;
    }

    private ResponseParserRegistrar getResponseParserRegistrar() {
        ResponseParserRegistrar rpr;
        Field registrarField = null;
        try {
            registrarField = RestAssured.class.getDeclaredField("RESPONSE_PARSER_REGISTRAR");
            try {
                registrarField.setAccessible(true);
                rpr = (ResponseParserRegistrar) registrarField.get(RestAssured.class);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } finally {
            registrarField.setAccessible(false);
        }
        return new ResponseParserRegistrar(rpr);
    }

    private RestAssuredConfig restAssuredConfig() {
        return RestAssured.config == null ? new RestAssuredConfig() : RestAssured.config;
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.module.jsv;

import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jackson.JsonLoader;
import com.github.fge.jsonschema.core.report.ProcessingMessage;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.main.JsonSchema;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import com.google.common.collect.Lists;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.List;

/**
 * A Hamcrest matcher that can be used to validate that a JSON document matches a given JSON schema.
 * Typical use-case in REST Assured:
 *

 * get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json"));
 *

 *

 * The {@link #matchesJsonSchemaInClasspath(String)} is defined in this class and validates that the response body of the request to "<tt>/products</tt>"
 * matches the <tt>products-schema.json</tt> schema located in classpath. It's also possible to supply some settings, for example the {@link com.github.fge.jsonschema.main.JsonSchemaFactory}
 * that the matcher will use when validating the schema:
 *

 * JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.newBuilder().setValidationConfiguration(ValidationConfiguration.newBuilder().setDefaultVersion(DRAFTV4).freeze()).freeze();
 * get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json").using(jsonSchemaFactory));
 *

 * or:
 *

 * get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json").using(settings().with().checkedValidation(false)))
 *

 * where "settings" is found in {@link JsonSchemaValidatorSettings#settings()}.
 *

 * It's also possible to specify static configuration that is reused for all matcher invocations. For example if you never wish to use checked validation you can configure
 * that {@link JsonSchemaValidator} like this:
 *

 *

 * JsonSchemaValidator.settings = settings().with().checkedValidation(false);
 *

 * This means that
 *

 * get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json"));
 *

 * will use unchecked validation (since it was configured statically).
 *

 * To reset the {@link JsonSchemaValidator} to its default state you can call {@link #reset()}:
 *

 *

 * JsonSchemaValidator.reset();
 *

 *

 */
public class JsonSchemaValidator extends TypeSafeMatcher<String> {

    /**
     * Default json schema factory instance
     */
    public static JsonSchemaValidatorSettings settings;

    private final Object schema;
    private final JsonSchemaValidatorSettings instanceSettings;

    private ProcessingReport report;

    private JsonSchemaValidator(Object schema, JsonSchemaValidatorSettings jsonSchemaValidatorSettings) {
        if (jsonSchemaValidatorSettings == null) {
            throw new IllegalArgumentException(JsonSchemaValidatorSettings.class.getSimpleName() + " cannot be null.");
        }
        this.schema = schema;
        this.instanceSettings = jsonSchemaValidatorSettings;
    }

    /**
     * Creates a Hamcrest matcher that validates that a JSON document conforms to the JSON schema provided to this method.
     *
     * @param schema The string defining the JSON schema
     * @return A Hamcrest matcher
     */
    public static JsonSchemaValidator matchesJsonSchema(String schema) {
        return new JsonSchemaValidatorFactory<String>() {
            @Override
            JsonNode createSchemaInstance(String input) throws IOException {
                return JsonLoader.fromString(input);
            }
        }.create(schema);
    }

    /**
     * Creates a Hamcrest matcher that validates that a JSON document conforms to the JSON schema provided to this method.
     *
     * @param pathToSchemaInClasspath The string that points to a JSON schema in classpath.
     * @return A Hamcrest matcher
     */
    public static JsonSchemaValidator matchesJsonSchemaInClasspath(String pathToSchemaInClasspath) {
        return matchesJsonSchema(Thread.currentThread().getContextClassLoader().getResource(pathToSchemaInClasspath));
    }

    /**
     * Creates a Hamcrest matcher that validates that a JSON document conforms to the JSON schema provided to this method.
     *
     * @param schema The input stream that points to a JSON schema
     * @return A Hamcrest matcher
     */
    public static JsonSchemaValidator matchesJsonSchema(InputStream schema) {
        return matchesJsonSchema(new InputStreamReader(schema));
    }

    /**
     * Creates a Hamcrest matcher that validates that a JSON document conforms to the JSON schema provided to this method.
     *
     * @param schema The reader that points to a JSON schema
     * @return A Hamcrest matcher
     */
    public static JsonSchemaValidator matchesJsonSchema(Reader schema) {
        return new JsonSchemaValidatorFactory<Reader>() {
            @Override
            JsonNode createSchemaInstance(Reader input) throws IOException {
                return JsonLoader.fromReader(input);
            }
        }.create(schema);
    }

    /**
     * Creates a Hamcrest matcher that validates that a JSON document conforms to the JSON schema provided to this method.
     *
     * @param file The file that points to a JSON schema
     * @return A Hamcrest matcher
     */
    public static JsonSchemaValidator matchesJsonSchema(File file) {
        return new JsonSchemaValidatorFactory<File>() {
            @Override
            JsonNode createSchemaInstance(File input) throws IOException {
                return JsonLoader.fromFile(input);
            }
        }.create(file);
    }

    public static JsonSchemaValidator matchesJsonSchema(URL url) {
        return new JsonSchemaValidatorFactory<URL>() {
            @Override
            Object createSchemaInstance(URL input) throws IOException {
                return input;
            }
        }.create(url);
    }

    /**
     * Creates a Hamcrest matcher that validates that a JSON document conforms to the JSON schema loaded by the supplied URI.
     *

     * Note: Converts the URI to a URL and loads this URL.
     *

     *
     * @param uri The URI that points to a JSON schema
     * @return A Hamcrest matcher
     */
    public static JsonSchemaValidator matchesJsonSchema(URI uri) {
        return matchesJsonSchema(toURL(uri));
    }

    /**
     * Validate the JSON document using the supplied jsonSchemaFactory instance.
     *
     * @param jsonSchemaFactory The json schema factory instance to use.
     * @return A Hamcrest matcher
     */
    public Matcher<?> using(JsonSchemaFactory jsonSchemaFactory) {
        return new JsonSchemaValidator(schema, instanceSettings.jsonSchemaFactory(jsonSchemaFactory));
    }

    /**
     * Validate the JSON document using the supplied jsonSchemaValidatorSettings instance.
     *
     * @param jsonSchemaValidatorSettings The json schema validator settings instance to use.
     * @return A Hamcrest matcher
     */
    public Matcher<?> using(JsonSchemaValidatorSettings jsonSchemaValidatorSettings) {
        return new JsonSchemaValidator(schema, jsonSchemaValidatorSettings);
    }

    private static URL toURL(URI uri) {
        validateSchemaIsNotNull(uri);
        try {
            return uri.toURL();
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Can't convert the supplied URI to a URL", e);
        }
    }

    @Override
    protected boolean matchesSafely(String content) {
        try {
            JsonNode contentAsJsonNode = JsonLoader.fromString(content);
            JsonSchemaFactory jsonSchemaFactory = instanceSettings.jsonSchemaFactory();
            Schema loadedSchema = loadSchema(schema, instanceSettings);
            final JsonSchema jsonSchema;
            if (loadedSchema.hasType(JsonNode.class)) {
                jsonSchema = jsonSchemaFactory.getJsonSchema(JsonNode.class.cast(loadedSchema.schema));
            } else if (loadedSchema.hasType(String.class)) {
                jsonSchema = jsonSchemaFactory.getJsonSchema(String.class.cast(loadedSchema.schema));
            } else {
                throw new RuntimeException("Internal error when loading schema from factory. Type was " + loadedSchema.schema.getClass().getName());
            }

            if (instanceSettings.shouldUseCheckedValidation()) {
                report = jsonSchema.validate(contentAsJsonNode);
            } else {
                report = jsonSchema.validateUnchecked(contentAsJsonNode);
            }
            return report.isSuccess();
        } catch (Exception e) {
            throw new JsonSchemaValidationException(e);
        }
    }

    public void describeTo(Description description) {
        if (report != null) {
            description.appendText("The content to match the given JSON schema.\n");
            List<ProcessingMessage> messages = Lists.newArrayList(report);
            if (!messages.isEmpty()) {
                for (final ProcessingMessage message : messages)
                    description.appendText(message.toString());
            }
        }
    }

    private Schema loadSchema(Object input, JsonSchemaValidatorSettings instanceSettings) {
        if (input instanceof JsonNode) {
            return new Schema(input);
        } else if (input instanceof URL) {
            final Object loadedSchema;
            if (instanceSettings.shouldParseUriAndUrlsAsJsonNode()) {
                try {
                    loadedSchema = JsonLoader.fromURL((URL) input);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            } else {
                loadedSchema = input.toString();
            }
            return new Schema(loadedSchema);
        } else {
            throw new RuntimeException("Internal error when loading schema: Input was instance of " + input.getClass().getName());
        }
    }

    private static void validateSchemaIsNotNull(Object schema) {
        if (schema == null) {
            throw new IllegalArgumentException("Schema to use cannot be null");
        }
    }

    private static abstract class JsonSchemaValidatorFactory<T> {

        private JsonSchemaValidatorSettings createSettings() {
            return settings == null ? new JsonSchemaValidatorSettings() : settings;
        }

        public JsonSchemaValidator create(T schema) {
            validateSchemaIsNotNull(schema);
            Object loadedSchema;
            try {
                loadedSchema = createSchemaInstance(schema);
            } catch (IOException e) {
                throw new JsonSchemaValidationException(e);
            }

            return new JsonSchemaValidator(loadedSchema, createSettings());
        }

        abstract Object createSchemaInstance(T input) throws IOException;
    }

    private static class Schema {
        private final Object schema;

        private Schema(Object result) {
            this.schema = result;
        }

        public boolean hasType(Class<?> type) {
            return type.isAssignableFrom(schema.getClass());
        }

        public <T> T as(Class<T> type) {
            return type.cast(schema);
        }
    }

    /**
     * Reset the static {@link #settings} to null.
     */
    public static void reset() {
        settings = null;
    }
}