/**
 * Copyright (C) 2015-2019 Expedia, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.hotels.heat.core.utils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.http.conn.ConnectTimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;

import com.hotels.heat.core.handlers.TestSuiteHandler;
import com.hotels.heat.core.specificexception.HeatException;
import com.hotels.heat.core.utils.log.LoggingUtils;

import io.restassured.RestAssured;
import io.restassured.filter.log.LogDetail;
import io.restassured.http.Method;
import io.restassured.internal.print.RequestPrinter;
import io.restassured.response.Response;
import io.restassured.specification.FilterableRequestSpecification;
import io.restassured.specification.MultiPartSpecification;
import io.restassured.specification.RequestSpecification;


/**
 * Utility class to make requests and retrieve responses with rest assured.
 */
public class RestAssuredRequestMaker {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestAssuredRequestMaker.class);
    private static final String LOG_LEVEL_PROPERTY      = "setLogLevel";
    private static final String HEADER_CONTENT_TYPE = "Content-Type";

    private RequestSpecification requestSpecification;
    private String defUrl;
    private final LoggingUtils logUtils;
    private String webappPath;

    /**
     * Constructor of the class RestAssuredRequestMaker. This is the class that really executes the request against the service under test.
     */
    public RestAssuredRequestMaker() {
        this.logUtils = TestSuiteHandler.getInstance().getLogUtils();
        this.defUrl = "";
        this.webappPath = "";
    }

    public void setBasePath(String webappPath) {
        this.webappPath = webappPath;
    }

    /**
     * Builds a test request starting from test case parameters.
     *
     * @param httpMethod http request method
     * @param singleRequestParamsMap map containing json attribute related to a single test. It contains exactly the same data as the ones contained
     *                               in the json input file
     * @return TestRequest object with all the info related to the test to run
     */
    public TestRequest buildRequestByParams(Method httpMethod, Map singleRequestParamsMap) {

        Map<String, String> cookiesParam = setRequestParameters(singleRequestParamsMap, TestCaseUtils.JSON_FIELD_COOKIES);
        Map<String, Object> queryParams = setRequestParameters(singleRequestParamsMap, TestCaseUtils.JSON_FIELD_QUERY_PARAMETERS);
        Map<String, String> headersParam = setRequestParameters(singleRequestParamsMap, TestCaseUtils.JSON_FIELD_HEADERS);
        if (!headersParam.containsKey(HEADER_CONTENT_TYPE)) {
            headersParam.put(HEADER_CONTENT_TYPE, "application/json"); // set default Content-Type
        }

        String url = this.webappPath;
        if (singleRequestParamsMap.containsKey(TestCaseUtils.JSON_FIELD_URL)) {
            url += (String) singleRequestParamsMap.get(TestCaseUtils.JSON_FIELD_URL);
        }

        setRequestSpecification(cookiesParam, headersParam, url);

        logUtils.trace("HTTP-METHOD >> {}", httpMethod.name());

        headersParam.forEach((k, v) -> logUtils.trace("HEADER-PARAM >> {}:{}", k, v));

        cookiesParam.forEach((k, v) -> logUtils.trace("COOKIE-PARAM >> {}:{}", k, v));

        logUtils.trace("URL >> '{}'", url);

        TestRequest tcRequest = new TestRequest(url);
        tcRequest.setHeadersParams(headersParam);
        tcRequest.setCookieParams(cookiesParam);
        tcRequest.setHttpMethod(httpMethod);
        tcRequest.setQueryParams(queryParams);

        return tcRequest;
    }

    /**
     * Execute a request and return the related response. If the test is in DEBUG log level modality, it shows also the curl so that it can be
     * more useful to replicate a single request in a separate shell.
     *
     * @param testRequest TestRequest object with all the info related to the test to run
     * @return the Response retrieved from the service under test
     */
    public Response executeTestRequest(TestRequest testRequest) {
        HttpRequestFormatter reqFormatter = new HttpRequestFormatter(testRequest, logUtils);
        logUtils.debug("CURL-HINT >> \n" + reqFormatter.toCURL());

        return executeHttpRequest(testRequest.getHttpMethod(), testRequest.getUrl(), testRequest.getQueryParams());

    }

    private String getPostBodyFromQueryParams(Method httpMethod, Map<String, Object> queryParams) {
        String postBody = (String) queryParams.get(TestCaseUtils.JSON_FIELD_POST_BODY);
        try {
            postBody = HttpRequestFormatter.readPostBodyFromFile(postBody);
        } catch (FileNotFoundException fileException) {
            throw new HeatException(this.logUtils.getTestCaseDetails() + "RestAssuredMessages::executeHttpRequest --> post body file '" + postBody + "' not found!");
        } catch (IOException ioException) {
            throw new HeatException(this.logUtils.getTestCaseDetails() + "RestAssuredMessages::executeHttpRequest --> IOException message: " + ioException.getLocalizedMessage());
        } catch (Exception ex) {
            logUtils.error("({}) -- Exception in buildRequestByParams: {}", httpMethod.name(), ex.getLocalizedMessage());
        }
        return postBody;
    }

    private <K, V> Map<K, V> setRequestParameters(Map singleRequestParamsMap, String paramName) {
        Map<K, V> extractedParams;
        if (singleRequestParamsMap.containsKey(paramName)) {
            extractedParams = (Map<K, V>) singleRequestParamsMap.get(paramName);
        } else {
            extractedParams = new HashMap();
        }
        return extractedParams;
    }

    /**
     * executeHttpRequest is the method that, according to the specific http method (GET, POST, PUT, DELETE) executes the request.
     *
     * @param httpMethod is the HTTP method of the request
     * @param url is the url of the service to call
     * @param queryParams is a Map<String, String> with the query parameters
     * @return the response of the request done.
     */
    public Response executeHttpRequest(Method httpMethod, String url, Map<String, Object> queryParams) {
        Response serviceResponse = null;

        try {
            requestSpecification.redirects().follow(false);

            if (queryParams.containsKey(TestCaseUtils.JSON_FIELD_POST_BODY)) {
                requestSpecification.body(getPostBodyFromQueryParams(httpMethod, queryParams));
            } else if (queryParams.containsKey(TestCaseUtils.JSON_FIELD_MULTIPART_BODY)) {
                final Object parts = queryParams.get(TestCaseUtils.JSON_FIELD_MULTIPART_BODY);
                if (!(parts instanceof List)) {
                    throw new HeatException("'parts' definition should be an array");
                }

                final List<MultiPartSpecification> multipart = MultipartUtils.convertToMultipart((List<Map<String, String>>) parts);

                multipart.forEach(requestSpecification::multiPart);

            } else if (!queryParams.isEmpty()) {
                addQueryParameters(queryParams);
            }

            logUtils.debug("Detailed Request: \n{}", getRequestDetails(httpMethod, url));
            switch (httpMethod) {
            case GET:
                serviceResponse = requestSpecification.when().get(url);
                break;
            case PUT:
                serviceResponse = requestSpecification.when().put(url);
                break;
            case DELETE:
                serviceResponse = requestSpecification.when().delete(url);
                break;
            case POST:
                serviceResponse = requestSpecification.when().post(url);
                break;
            case OPTIONS:
                serviceResponse = requestSpecification.when().options(url);
                break;
            default:
                logUtils.warning("HTTP METHOD '{}' not recognized. GET METHOD used as default", httpMethod.toString());
                serviceResponse = requestSpecification.when().get(url);
                break;
            }
            logUtils.debug("The response is: {}", serviceResponse.asString());
        } catch (Exception oEx) {
            logUtils.error("exception --> {}", oEx.getLocalizedMessage());
            if (oEx.getClass().equals(ConnectTimeoutException.class)) {
                throw new HeatException(this.logUtils.getTestCaseDetails() + "RestAssuredMessages::executeHttpRequest --> Connect Timeout Exception");
            }
            logUtils.error("The response is null");
        }
        return serviceResponse;

    }


    private void addQueryParameters(Map<String, ?> queryParams) {
        queryParams.forEach((key, value) -> {
            if (value instanceof String) {
                requestSpecification.parameter(key, value);
            } else {
                ((ArrayList<String>) value).forEach(singleValue -> {
                    requestSpecification.parameter(key, singleValue);
                });
            }
        });
    }

    /**
     * Method useful to set data to the RequestSpecification (url, cookies and headers).
     *
     * @param url is the url of the service to call
     * @param cookies is a key-value Map with the cookies
     * @param headers is a key-value Map with the headers
     */
    public void setRequestSpecification(Map<String, String> cookies, Map<String, String> headers, String url) {
        requestSpecification = protocolSetting(url);
        if (cookies != null) {
            requestSpecification = requestSpecification.cookies(cookies);
        }
        if (headers != null) {
            requestSpecification = requestSpecification.headers(headers);
        }
    }

    /**
     * This method returns a RequestSpecification object based on a specific
     * protocol.
     *
     * @param url the environment url.
     * @return RequestSpecification RequestSpecification
     */
    public RequestSpecification protocolSetting(String url) {
        RequestSpecification testReqSpecification;
        if (url.startsWith("https://")) {
            testReqSpecification = givenSsl();
        } else {
            testReqSpecification = given();
        }
        return testReqSpecification;
    }

    /**
     * This method returns a RequestSpecification object specific for the http
     * method.
     *
     * @return the request specification
     */
    private RequestSpecification given() {
        RequestSpecification restSpec;
        if (LoggingUtils.LOG_LEVEL_DEBUG.equals(System.getProperty(LOG_LEVEL_PROPERTY, LoggingUtils.LOG_LEVEL_INFO).toUpperCase())) {
            restSpec = RestAssured.given().baseUri(defUrl);
        } else {
            restSpec = RestAssured.given().log().ifValidationFails().baseUri(defUrl);
        }
        return restSpec;
    }

    /**
     * This method returns a RequestSpecification object specific for the https
     * method.
     *
     * @return the request specification for SSL endpoint
     */
    private RequestSpecification givenSsl() {
        RequestSpecification restSpec;
        if (LoggingUtils.LOG_LEVEL_DEBUG.equals(System.getProperty(LOG_LEVEL_PROPERTY, LoggingUtils.LOG_LEVEL_INFO).toUpperCase())) {
            restSpec = RestAssured.given().urlEncodingEnabled(true).relaxedHTTPSValidation().baseUri(defUrl);
        } else {
            restSpec = RestAssured.given().urlEncodingEnabled(true).log().ifValidationFails().relaxedHTTPSValidation().baseUri(defUrl);
        }
        return restSpec;
    }

    private String getRequestDetails(Method httpMethod, String url) {
        Optional<String> requestDetails = Optional.absent();
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            requestDetails = Optional.fromNullable(RequestPrinter.print((FilterableRequestSpecification) requestSpecification, httpMethod.name(), url, LogDetail.ALL,
                    new PrintStream(os), true));
        } catch (IOException e) {
            logUtils.error("Unable to log 'Request Details', error occured during retrieving the information");
        }
        return requestDetails.or("");
    }

}