/*
 * 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.brooklyn.util.http;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;

/**
 * Utility assertions on HTTP.
 * 
 * @author aled
 */
public class HttpAsserts {

    private static final Logger LOG = LoggerFactory.getLogger(HttpAsserts.class);

    /** @return whether the given HTTP status code is a "success" class code (2xx) */
    public static boolean isHealthyStatusCode(int code) {
        return code>=200 && code<=299;
    }
    
    /** Asserts that the given HTTP status code indicates "success", i.e. {@link #isHealthyStatusCode(int)} is true */
    public static void assertHealthyStatusCode(int code) {
        if (isHealthyStatusCode(code)) return;
        Asserts.fail("Expected success status code, got: " + code);
    }

    /** Asserts that the given HTTP status code does not indicate "success", i.e. {@link #isHealthyStatusCode(int)} returns false */
    public static void assertNotHealthyStatusCode(int code) {
        if (!isHealthyStatusCode(code)) return;
        Asserts.fail("Expected non-success status code, got: " + code);
    }

    /** @return whether the given HTTP status code is a "client error" class code (4xx) */
    public static boolean isClientErrorStatusCode(int code) {
        return code>=400 && code<=499;
    }
    
    /** Asserts that the given HTTP status code indicates "client error", i.e. {@link #isClientErrorStatusCode(int)} is true */
    public static void assertClientErrorStatusCode(int code) {
        if (isClientErrorStatusCode(code)) return;
        Asserts.fail("Expected client error status code, got: " + code);        
    }

    /** @return whether the given HTTP status code is a "server error" class code (5xx) */
    public static boolean isServerErrorStatusCode(int code) {
        return code>=500 && code<=599;
    }
    
    /** Asserts that the given HTTP status code indicates "server error", i.e. {@link #isServerErrorStatusCode(int)} is true */
    public static void assertServerErrorStatusCode(int code) {
        if (isServerErrorStatusCode(code)) return;
        Asserts.fail("Expected server error status code, got: " + code);        
    }

    /**
     * Asserts that gets back any "valid" response - i.e. not an exception. This could be an unauthorized,
     * a redirect, a 404, or anything else that implies there is web-server listening on that port.
     *
     * @param url The URL to connect to.
     */
    public static void assertUrlReachable(String url) {
        try {
            HttpTool.getHttpStatusCode(url);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted for "+url+" (in assertion that is reachable)", e);
        } catch (Exception e) {
            throw new IllegalStateException("Server at "+url+" Asserts.failed to respond (in assertion that is reachable): "+e, e);
        }
    }

    /**
     * Asserts that the URL could not be reached, detected as an IOException.
     *
     * @param url The URL to connect to.
     */

    public static void assertUrlUnreachable(String url) {
        try {
            int statusCode = HttpTool.getHttpStatusCode(url);
            Asserts.fail("Expected url " + url + " unreachable, but got status code " + statusCode);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted for "+url+" (in assertion that unreachable)", e);
        } catch (Exception e) {
            IOException cause = Exceptions.getFirstThrowableOfType(e, IOException.class);
            if (cause != null) {
                // success; clean shutdown transitioning from 400 to error
            } else {
                propagateAsAssertionError(e);
            }                        
        }
    }

    /**
     * Asserts that the URL becomes unreachable within a default time period.
     *
     * @param url The URL
     */
    public static void assertUrlUnreachableEventually(final String url) {
        assertUrlUnreachableEventually(Maps.<String,Object>newLinkedHashMap(), url);
    }


    /**
     * Asserts that the URL becomes unreachable within a configurable time period.
     *
     * @param flags The flags controlling the timeout.
     *              For details see {@link org.apache.brooklyn.test.Asserts#succeedsEventually(java.util.Map, java.util.concurrent.Callable)}
     * @param url The URL
     */
    public static void assertUrlUnreachableEventually(Map<String,?> flags, final String url) {
        assertEventually(flags, new Runnable() {
            @Override
            public void run() {
                assertUrlUnreachable(url);
            }
        });
    }

    /**
     * Assert that the status code returned from the URL is in the given codes.
     *
     * @param url The URL to get.
     * @param acceptableReturnCodes The return codes that are expected.
     */
    public static void assertHttpStatusCodeEquals(String url, int... acceptableReturnCodes) {
        List<Integer> acceptableCodes = Lists.newArrayList();
        for (int code : acceptableReturnCodes) {
            acceptableCodes.add(code);
        }
        try {
            int actualCode = HttpTool.getHttpStatusCode(url);
            Asserts.assertTrue(acceptableCodes.contains(actualCode), "code=" + actualCode + "; expected=" + acceptableCodes + "; url=" + url);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted for "+url+" (in assertion that result code is "+acceptableCodes+")", e);
        } catch (Exception e) {
            throw new IllegalStateException("Server at "+url+" Asserts.failed to respond (in assertion that result code is "+acceptableCodes+"): "+e, e);
        }
    }

    public static void assertHttpStatusCodeEventuallyEquals(final String url, final int expectedCode) {
        assertHttpStatusCodeEventuallyEquals(Maps.<String,Object>newLinkedHashMap(), url, expectedCode);
    }

    public static void assertHttpStatusCodeEventuallyEquals(Map<String,?> flags, final String url, final int expectedCode) {
        assertEventually(flags, new Runnable() {
            @Override
            public void run() {
                assertHttpStatusCodeEquals(url, expectedCode);
            }
        });
    }

    public static void assertContentContainsText(final String url, final String phrase, final String ...additionalPhrases) {
        try {
            String contents = HttpTool.getContent(url);
            Asserts.assertTrue(contents != null && contents.length() > 0);
            for (String text: Lists.asList(phrase, additionalPhrases)) {
                if (!contents.contains(text)) {
                    LOG.warn("CONTENTS OF URL "+url+" MISSING TEXT: "+text+"\n"+contents);
                    Asserts.fail("URL "+url+" does not contain text: "+text);
                }
            }
        } catch (Exception e) {
            throw propagateAsAssertionError(e);
        }
    }

    public static void assertContentNotContainsText(final String url, final String phrase, final String ...additionalPhrases) {
        try {
            String contents = HttpTool.getContent(url);
            Asserts.assertTrue(contents != null);
            for (String text: Lists.asList(phrase, additionalPhrases)) {
                if (contents.contains(text)) {
                    LOG.warn("CONTENTS OF URL "+url+" HAS TEXT: "+text+"\n"+contents);
                    Asserts.fail("URL "+url+" contain text: "+text);
                }
            }
        } catch (Exception e) {
            throw propagateAsAssertionError(e);
        }
    }

    public static void assertErrorContentContainsText(final String url, final String phrase, final String ...additionalPhrases) {
        try {
            String contents = HttpTool.getErrorContent(url);
            Asserts.assertTrue(contents != null && contents.length() > 0);
            for (String text: Lists.asList(phrase, additionalPhrases)) {
                if (!contents.contains(text)) {
                    LOG.warn("CONTENTS OF URL "+url+" MISSING TEXT: "+text+"\n"+contents);
                    Asserts.fail("URL "+url+" does not contain text: "+text);
                }
            }
        } catch (Exception e) {
            throw propagateAsAssertionError(e);
        }
    }


    public static void assertErrorContentNotContainsText(final String url, final String phrase, final String ...additionalPhrases) {
        try {
            String err = HttpTool.getErrorContent(url);
            Asserts.assertTrue(err != null);
            for (String text: Lists.asList(phrase, additionalPhrases)) {
                if (err.contains(text)) {
                    LOG.warn("CONTENTS OF URL "+url+" HAS TEXT: "+text+"\n"+err);
                    Asserts.fail("URL "+url+" contain text: "+text);
                }
            }
        } catch (Exception e) {
            throw propagateAsAssertionError(e);
        }
    }

    private static AssertionError propagateAsAssertionError(Exception e) {
        final AssertionError assertionError = new AssertionError("Assertion failed", e);
        return assertionError;
    }

    public static void assertContentEventuallyContainsText(final String url, final String phrase, final String ...additionalPhrases) {
        assertContentEventuallyContainsText(MutableMap.<String,Object>of(), url, phrase, additionalPhrases);
    }
    
    public static void assertContentEventuallyContainsText(Map<String,?> flags, final String url, final String phrase, final String ...additionalPhrases) {
        assertEventually(flags, new Runnable() {
            @Override
            public void run() {
                assertContentContainsText(url, phrase, additionalPhrases);
            }
        });
    }

    private static void assertEventually(Map<String,?> flags, Runnable r) {
        try {
            Asserts.succeedsEventually(flags, r);
        } catch (Exception e) {
            throw propagateAsAssertionError(e);
        }
    }

    private static void assertEventually(Runnable r) {
        try {
            Asserts.succeedsEventually(r);
        } catch (Exception e) {
            throw propagateAsAssertionError(e);
        }
    }

    public static void assertContentMatches(String url, String regex) {
        String contents = HttpTool.getContent(url);
        Asserts.assertNotNull(contents);
        Asserts.assertTrue(contents.matches(regex), "Contents does not match expected regex ("+regex+"): "+contents);
    }

    public static void assertContentEventuallyMatches(final String url, final String regex) {
        assertEventually(new Runnable() {
            @Override
            public void run() {
                assertContentMatches(url, regex);
            }
        });
    }

    public static void assertContentEventuallyMatches(Map<String,?> flags, final String url, final String regex) {
        assertEventually(flags, new Runnable() {
            @Override
            public void run() {
                assertContentMatches(url, regex);
            }
        });
    }



    /**
     * Schedules (with the given executor) a poller that repeatedly accesses the given url, to confirm it always gives
     * back the expected status code.
     * 
     * Expected usage is to query the future, such as:
     * 
     * <pre>
     * {@code
     * Future<?> future = assertAsyncHttpStatusCodeContinuallyEquals(executor, url, 200);
     * // do other stuff...
     * if (future.isDone()) future.get(); // get exception if its Asserts.failed
     * }
     * </pre>
     *
     * NOTE that the exception thrown by future.get() is a java.util.concurrent.ExecutionException,
     * not an AssertionError.
     * 
     * For stopping it, you can either do future.cancel(true), or you can just do executor.shutdownNow().
     * 
     * TODO Look at difference between this and WebAppMonitor, to decide if this should be kept.
     */
    public static ListenableFuture<?> assertAsyncHttpStatusCodeContinuallyEquals(ListeningExecutorService executor, final String url, final int expectedStatusCode) {
        return executor.submit(new Runnable() {
            @Override public void run() {
                // TODO Need to drop logging; remove sleep when that's done.
                while (!Thread.currentThread().isInterrupted()) {
                    assertHttpStatusCodeEquals(url, expectedStatusCode);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        return; // graceful return
                    }
                }
            }
        });
    }

}
