/*
 * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors.
 *
 * 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 org.switchyard.component.test.mixins.http;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import org.junit.Assert;
import org.switchyard.test.SwitchYardTestKit;
import org.switchyard.test.mixins.AbstractTestMixIn;

/**
 * HTTP Test Mix In.
 *
 * @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
 * @author Magesh Kumar B <mageshbk@jboss.com> (C) 2013 Red Hat Inc.
 */
public class HTTPMixIn extends AbstractTestMixIn {
    private static Logger _logger = Logger.getLogger(HTTPMixIn.class);

    /**
     * Constant representing HTTP DELETE method.
     */
    public static final String HTTP_DELETE = "DELETE";

    /**
     * Constant representing HTTP GET method.
     */
    public static final String HTTP_GET = "GET";

    /**
     * Constant representing HTTP HEAD method.
     */
    public static final String HTTP_HEAD = "HEAD";

    /**
     * Constant representing HTTP POST method.
     */
    public static final String HTTP_POST = "POST";

    /**
     * Constant representing HTTP PUT method.
     */
    public static final String HTTP_PUT = "PUT";

    /**
     * Constant representing HTTP OPTIONS method.
     */
    public static final String HTTP_OPTIONS = "OPTIONS";

    private HttpClient _httpClient;
    private String _contentType = "text/xml";
    private HashMap<String,String> _requestHeaders = new HashMap<String,String>();
    private HashMap<String,String> _expectedHeaders = new HashMap<String,String>();
    private boolean _dumpMessages = false;
    private SSLContext _sslContext;

    /**
     * Default constructor.
     */
    public HTTPMixIn() {
        super();
    }

    /**
     * Create a new HTTPMixIn.
     * 
     * @param sslContext SSLContext to use for secure connections.
     */
    public HTTPMixIn(SSLContext sslContext) {
        this();
        _sslContext = sslContext;
    }

    /**
     * Set the content type.
     * <p/>
     * Default content type is "text/xml".
     *
     * @param contentType The content type.
     * @return This HTTPMixIn instance.
     */
    public HTTPMixIn setContentType(String contentType) {
        this._contentType = contentType;
        return this;
    }

    @Override
    public void initialize() {
        _httpClient = HttpClientBuilder.create().setSslcontext(_sslContext).build();
    }

    /**
     * Send the specified request payload to the specified HTTP endpoint using the method specified.
     * @param endpointURL The HTTP endpoint URL.
     * @param request The request payload.
     * @param method The request method.
     * @return The HTTP response code.
     */
    public int sendStringAndGetStatus(String endpointURL, String request, String method) {
        HttpResponse httpResponse = sendStringAndGetMethod(endpointURL, request, method);
        int status = httpResponse.getStatusLine().getStatusCode();
        if ((httpResponse.getEntity() != null) && (httpResponse.getEntity().getContentLength() == -1)) {
            _httpClient.getConnectionManager().closeIdleConnections(0, TimeUnit.SECONDS);
        } else {
            EntityUtils.consumeQuietly(httpResponse.getEntity());
        }
        return status;
    }

    /**
     * Send the specified request payload to the specified HTTP endpoint using the method specified.
     * @param endpointURL The HTTP endpoint URL.
     * @param request The request payload.
     * @param method The request method.
     * @return The HTTP response payload.
     */
    public String sendString(String endpointURL, String request, String method) {
        String response = null;
        try {
            HttpResponse httpResponse = sendStringAndGetMethod(endpointURL, request, method);
            response = EntityUtils.toString(httpResponse.getEntity());
        } catch (IOException ioe) {
            _logger.error("Unable to get response", ioe);
        }
        return response;
    }

    /**
     * Send the specified request payload to the specified HTTP endpoint using the method specified.
     * @param endpointURL The HTTP endpoint URL.
     * @param request The request payload.
     * @param method The request method.
     * @return The HttpResponse object.
     */
    public HttpResponse sendStringAndGetMethod(String endpointURL, String request, String method) {
        if (_dumpMessages) {
            _logger.info("Sending a " + method + " request to [" + endpointURL + "]");
            _logger.info("Request body:[" + request + "]");
        }
        HttpRequestBase httpMethod = null;
        HttpResponse response = null;
        try {
            if (method.equals(HTTP_PUT)) {
                httpMethod = new HttpPut(endpointURL);
                ((HttpPut)httpMethod).setEntity(new StringEntity(request, _contentType, "UTF-8"));
            } else if (method.equals(HTTP_POST)) {
                httpMethod = new HttpPost(endpointURL);
                ((HttpPost)httpMethod).setEntity(new StringEntity(request, _contentType, "UTF-8"));
            } else if (method.equals(HTTP_DELETE)) {
                httpMethod = new HttpDelete(endpointURL);
            } else if (method.equals(HTTP_OPTIONS)) {
                httpMethod = new HttpOptions(endpointURL);
            } else if (method.equals(HTTP_HEAD)) {
                httpMethod = new HttpHead(endpointURL);
            } else {
                httpMethod = new HttpGet(endpointURL);
            }
            response = execute(httpMethod);
        } catch (UnsupportedEncodingException e) {
            _logger.error("Unable to set request entity", e);
        }
        return response;
    }

    /**
     * POST the specified request payload to the specified HTTP endpoint.
     * @param endpointURL The HTTP endpoint URL.
     * @param request The request payload.
     * @return The HTTP response payload.
     */
    public String postString(String endpointURL, String request) {
        if (_dumpMessages) {
            _logger.info("Sending a POST request to [" + endpointURL + "]");
            _logger.info("Request body:[" + request + "]");
        }
        
        HttpPost postMethod = new HttpPost(endpointURL);
        try {
            postMethod.setEntity(new StringEntity(request, _contentType, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            _logger.error("Unable to set request entity", e);
        }
        HttpResponse httpResponse = execute(postMethod);
        try {
            return EntityUtils.toString(httpResponse.getEntity());
        } catch (IOException ioe) {
            _logger.error("Unable to get response entity", ioe);
        }
        return null;
    }

    /**
     * POST the specified request payload to the specified HTTP endpoint.
     * @param endpointURL The HTTP endpoint URL.
     * @param request The file resource containing the request payload.
     * @return The HTTP response payload.
     */
    public String postFile(String endpointURL, String request) {

        FileEntity requestEntity =
            new FileEntity(new File(request), "text/xml; charset=utf-8");

        if (_dumpMessages) {
            _logger.info("Sending a POST request to [" + endpointURL + "]");
            ByteArrayOutputStream target = new ByteArrayOutputStream();
            try {
                requestEntity.writeTo(target);
                _logger.info("Request body:[" + target.toString() + "]");
            } catch (IOException e) {
                _logger.error("Unable to write FileEntity to stream", e);
            }
        }

        HttpPost postMethod = new HttpPost(endpointURL);
        postMethod.setEntity(requestEntity);
        HttpResponse httpResponse = execute(postMethod);
        try {
            return EntityUtils.toString(httpResponse.getEntity());
        } catch (IOException ioe) {
            _logger.error("Unable to get response entity", ioe);
        }
        return null;
    }

    /**
     * POST the specified String request to the specified HTTP endpoint and perform an XML compare
     * between the HTTP response and the specified expected response String.
     * @param endpointURL The HTTP endpoint URL.
     * @param request The classpath resource to be posted to the endpoint.
     * @param expectedResponse The String to use to perform the XML test on the response.
     * @return The HTTP response payload.
     */
    public String postStringAndTestXML(String endpointURL, String request, String expectedResponse) {
        String response = postString(endpointURL, request);
        SwitchYardTestKit.compareXMLToString(response, expectedResponse);
        return response;
    }

    /**
     * POST the specified classpath resource to the specified HTTP endpoint.
     * @param endpointURL The HTTP endpoint URL.
     * @param requestResource The classpath resource to be posted to the endpoint.
     * @return The HTTP method.
     */
    public HttpResponse postResourceAndGetMethod(String endpointURL, String requestResource) {
        if (_dumpMessages) {
            _logger.info("Sending a POST request to [" + endpointURL + "]");
            InputStream input = getTestKit().getResourceAsStream(requestResource);
            ByteArrayOutputStream tmp = new ByteArrayOutputStream();
            byte[] data = new byte[4096];
            int l = 0;
            try {
                while ((l = input.read(data)) >= 0) {
                    tmp.write(data, 0, l);
                }
                _logger.info("Request body:[" + new String(tmp.toByteArray()) + "]");
            } catch (IOException e) {
                _logger.error("Unexpected Exception while reading request resource", e);
            }
        }
        
        HttpPost postMethod = new HttpPost(endpointURL);
        InputStream requestStream = getTestKit().getResourceAsStream(requestResource);
        HttpResponse response = null;
        try {
            if (_contentType != null) {
                ContentType contentType = null;
                if (_contentType.contains("charset")) {
                    contentType = ContentType.create(_contentType);
                } else {
                    contentType = ContentType.create(_contentType, "utf-8");
                }
                postMethod.setEntity(new InputStreamEntity(requestStream, requestStream.available(), contentType));
            } else {
                postMethod.setEntity(new InputStreamEntity(requestStream, requestStream.available()));
            }
            response = execute(postMethod);
        } catch (IOException ioe) {
            _logger.error("Unable to get response entity", ioe);
        } finally {
            try {
                requestStream.close();
            } catch (IOException e) {
                Assert.fail("Unexpected exception closing HTTP request resource stream.");
            }
        }
        return response;
    }

    /**
     * POST the specified classpath resource to the specified HTTP endpoint.
     * @param endpointURL The HTTP endpoint URL.
     * @param requestResource The classpath resource to be posted to the endpoint.
     * @return The HTTP response payload.
     */
    public String postResource(String endpointURL, String requestResource) {
        String response = null;
        try {
            HttpResponse httpResponse = postResourceAndGetMethod(endpointURL, requestResource);
            int status = httpResponse.getStatusLine().getStatusCode();
            if (_logger.isDebugEnabled()) {
                _logger.debug("Response returned status code " + status);
            }
            response = EntityUtils.toString(httpResponse.getEntity());
        } catch (IOException ioe) {
            _logger.error("Unable to get response", ioe);
        }
        return response;
    }

    /**
     * POST the specified classpath resource to the specified HTTP endpoint and perform an XML compare
     * between the HTTP response and the specified expected classpath response resource.
     * @param endpointURL The HTTP endpoint URL.
     * @param requestResource The classpath resource to be posted to the endpoint.
     * @param expectedResponseResource The classpath resource to use to perform the XML test on the response.
     * @return The HTTP response payload.
     */
    public String postResourceAndTestXML(String endpointURL, String requestResource, String expectedResponseResource) {
        String response = postResource(endpointURL, requestResource);
        getTestKit().compareXMLToResource(response, expectedResponseResource);
        return response;
    }
    /**
     * POST the specified classpath resource to the specified HTTP endpoint.
     * @param endpointURL The HTTP endpoint URL.
     * @param requestResource The classpath resource to be posted to the endpoint.
     * @return The HTTP status code.
     */
    public int postResourceAndGetStatus(String endpointURL, String requestResource) {
        HttpResponse httpResponse = postResourceAndGetMethod(endpointURL, requestResource);
        int status = httpResponse.getStatusLine().getStatusCode();
        if ((httpResponse.getEntity() != null) && (httpResponse.getEntity().getContentLength() == -1)) {
            _httpClient.getConnectionManager().closeIdleConnections(0, TimeUnit.SECONDS);
        } else {
            EntityUtils.consumeQuietly(httpResponse.getEntity());
        }
        return status;
    }

    /**
     * Execute the supplied HTTP Method.
     *
     * @param method The HTTP Method.
     * @return The HTTP Response.
     */
    public HttpResponse execute(HttpRequestBase method) {
        if (_httpClient == null) {
            Assert.fail("HTTPMixIn not initialized.  You must call the initialize() method before using this MixIn");
        }

        for (String key : _requestHeaders.keySet()) {
            method.setHeader(new BasicHeader(key, _requestHeaders.get(key)));
        }

        if (_dumpMessages) {
            for (Header header : method.getAllHeaders()) {
                _logger.info("Request header:[" + header.getName() + "=" + header.getValue() + "]");
            }
        }

        HttpResponse response = null;
        try {
            response = _httpClient.execute(method);
        } catch (Exception e) {
            Assert.fail("Exception invoking HTTP endpoint '" + method.getURI() + "': " + e.getMessage());
        }
            
        if (_dumpMessages) {
            for (Header header : response.getAllHeaders()) {
                _logger.info("Received response header:[" + header.getName() + "=" + header.getValue() + "]");
            }
            _logger.info("Received response body:[" + response + "]");
        }

        for (String key : _expectedHeaders.keySet()) {
            Header actual = response.getFirstHeader(key);
            Assert.assertNotNull("Checking response header:[" + key + "]", actual);
            Assert.assertEquals("Checking response header:[" + key + "]", _expectedHeaders.get(key), actual.getValue());
        }
            
        return response;
    }

    /**
     * Set a request header.
     * @param name header name
     * @param value header value
     * @return this instance
     */
    public HTTPMixIn setRequestHeader(String name, String value) {
        _requestHeaders.put(name, value);
        return this;
    }
    
    /**
     * Set the list of request headers.
     * @param headers request headers in HashMap
     * @return this instance
     */
    public HTTPMixIn setRequestHeaders(Map<String,String> headers) {
        _requestHeaders.clear();
        _requestHeaders.putAll(headers);
        return this;
    }
    
    /**
     * Set a expected response header.
     * @param name header name
     * @param value header value
     * @return this instance
     */
    public HTTPMixIn setExpectedHeader(String name, String value) {
        _expectedHeaders.put(name, value);
        return this;
    }
    
    /**
     * Set the list of expected response headers.
     * @param headers expected response headers in HashMap
     * @return this instance
     */
    public HTTPMixIn setExpectedHeaders(Map<String,String> headers) {
        _expectedHeaders.clear();
        _expectedHeaders.putAll(headers);
        return this;
    }
    
    /**
     * Whether to dump the request/response message into log or not.
     * @param dumpMessages Whether to dump the request/response message into log or not
     * @return this instance
     */
    public HTTPMixIn setDumpMessages(boolean dumpMessages) {
        _dumpMessages = dumpMessages;
        return this;
    }
    
    @Override
    public void uninitialize() {
        if (_httpClient != null) {
            _httpClient.getConnectionManager().closeIdleConnections(0, TimeUnit.SECONDS);
            _httpClient.getConnectionManager().shutdown();
        }
    }
}
