/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.sling.testing.tools.http;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.regex.Pattern;

import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

/** Executes a Request and provides convenience methods
 *  to validate the results.
 */
public class RequestExecutor {
    private final DefaultHttpClient httpClient;
    private HttpUriRequest request; 
    private HttpResponse response;
    private HttpEntity entity;
    private String content;
    
    /**
     * HttpRequestInterceptor for preemptive authentication, based on httpclient
     * 4.0 example
     */
    private static class PreemptiveAuthInterceptor implements
            HttpRequestInterceptor {

        public void process(HttpRequest request, HttpContext context)
                throws HttpException, IOException {

            AuthState authState = 
                (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE);
            CredentialsProvider credsProvider = 
                (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER);
            HttpHost targetHost = 
                (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);

            // If not auth scheme has been initialized yet
            if (authState.getAuthScheme() == null) {
                AuthScope authScope = 
                    new AuthScope(targetHost.getHostName(), targetHost.getPort());

                // Obtain credentials matching the target host
                Credentials creds = credsProvider.getCredentials(authScope);

                // If found, generate BasicScheme preemptively
                if(creds != null) {
                    authState.setAuthScheme(new BasicScheme());
                    authState.setCredentials(creds);
                }
            }
        }
    }
    
    public RequestExecutor(DefaultHttpClient client) {
        httpClient = client;
    }
    
    public String toString() {
        if(request == null) {
            return "Request";
        }
        return request.getMethod() + " request to " + request.getURI();
    }
    
    public RequestExecutor execute(Request r) throws ClientProtocolException, IOException {
        clear();
        r.customizeIfNeeded();
        request = r.getRequest();
        
        // Optionally setup for basic authentication
        if(r.getUsername() != null) {
            httpClient.getCredentialsProvider().setCredentials(
                    AuthScope.ANY,
                    new UsernamePasswordCredentials(r.getUsername(), r.getPassword()));

            // And add request interceptor to have preemptive authentication
            httpClient.addRequestInterceptor(new PreemptiveAuthInterceptor(), 0);
        } else {
            // Make sure existing credentials are not reused - but looks like we
            // cannot set null as credentials
            httpClient.getCredentialsProvider().setCredentials(
                    AuthScope.ANY,
                    new UsernamePasswordCredentials(getClass().getName(), getClass().getSimpleName()));
            httpClient.removeRequestInterceptorByClass(PreemptiveAuthInterceptor.class);
        }
        
        // Setup redirects
        httpClient.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, r.getRedirects());
        
        // Execute request
        response = httpClient.execute(request);
        entity = response.getEntity();
        if(entity != null) {
            consumeEntity();
        }
        return this;
    }

    /** Can be overridden to consume in a different way, or not at all */ 
    protected void consumeEntity() throws ParseException, IOException {
        content = EntityUtils.toString(entity);
        entity.consumeContent();
    }
    
    protected void clear() {
        request = null;
        entity = null;
        response = null;
        content = null;
    }

    /** Verify that response matches supplied status */
    public RequestExecutor assertStatus(int expected) {
        assertNotNull(this.toString(), response);
        assertEquals(this + ": expecting status " + expected, expected, response.getStatusLine().getStatusCode());
        return this;
    }
    
    /** Verify that response matches supplied content type */
    public RequestExecutor assertContentType(String expected) {
        assertNotNull(this.toString(), response);
        if(entity == null) {
            fail(this + ": no entity in response, cannot check content type");
        }
        
        // Remove whatever follows semicolon in content-type
        String contentType = entity.getContentType().getValue();
        if(contentType != null) {
            contentType = contentType.split(";")[0].trim();
        }
        
        // And check for match
        assertEquals(this + ": expecting content type " + expected, expected, contentType);
        return this;
    }

    /** For each supplied regexp, fail unless content contains at 
     *  least one line that matches.
     *  Regexps are automatically prefixed/suffixed with .* so as
     *  to have match partial lines.
     */
    public RequestExecutor assertContentRegexp(String... regexp) throws IOException {
        assertNotNull(this.toString(), response);
        nextPattern:
        for(String expr : regexp) {
            final Pattern p = Pattern.compile(".*" + expr + ".*");
            final BufferedReader br = new BufferedReader(new StringReader(content));
            String line = null;
            while( (line = br.readLine()) != null) {
                if(p.matcher(line).matches()) {
                    continue nextPattern;
                }
            }
            fail(this + ": no match for regexp '" + expr + "', content=\n" + content);
        }
        return this;
    }

    /** For each supplied string, fail unless content contains it */
    public RequestExecutor assertContentContains(String... expected) throws ParseException, IOException {
        assertNotNull(this.toString(), response);
        for(String exp : expected) {
            if(!content.contains(exp)) {
                fail(this + ": content does not contain '" + exp + "', content=\n" + content);
            }
        }
        return this;
    }
    
    public void generateDocumentation(RequestDocumentor documentor, String...metadata) throws IOException {
        documentor.generateDocumentation(this, metadata);
    }

    public HttpUriRequest getRequest() {
        return request;
    }

    public HttpResponse getResponse() {
        return response;
    }

    public HttpEntity getEntity() {
        return entity;
    }
    
    public String getContent() {
        return content;
    }
}

@@ -5963,37 +5963,47 @@
  %22: 
-expecting
+%5CnBody: %22 + content + %22%5CnHTTP
  status
+:
  %22
- + expected
 , ex

include response body when asserting an http status code