// Copyright 2013-2019 Michel Kraemer
//
// 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 de.undercouch.gradle.tasks.download;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpRequest;
import org.apache.commons.io.FileUtils;
import org.junit.BeforeClass;
import org.junit.Test;
import org.littleshoot.proxy.HttpFilters;
import org.littleshoot.proxy.HttpFiltersAdapter;
import org.littleshoot.proxy.HttpFiltersSourceAdapter;
import org.littleshoot.proxy.HttpProxyServer;
import org.littleshoot.proxy.HttpProxyServerBootstrap;
import org.littleshoot.proxy.ProxyAuthenticator;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;

import java.io.File;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.junit.Assert.assertEquals;

/**
 * Tests if the download task plugin can download files through a proxy
 * @author Michel Kraemer
 */
public class ProxyTest extends TestBaseWithMockServer {
    private static String PROXY_USERNAME = "testuser123";
    private static String PROXY_PASSWORD = "testpass456";
    
    private HttpProxyServer proxy;
    private int proxyPort;
    private int proxyCounter = 0;

    /**
     * Host name of the local machine
     */
    private static String localHostName;

    /**
     * Find a free socket port
     * @return the number of the free port
     * @throws IOException if an IO error occurred
     */
    private static int findPort() throws IOException {
        try (ServerSocket socket = new ServerSocket(0)) {
            return socket.getLocalPort();
        }
    }

    /**
     * Gets the local host name to use for the tests
     * @throws UnknownHostException if the local host name could not be
     * resolved into an address
     * @throws SocketException if an I/O error occurs
     */
    @BeforeClass
    public static void setUpClass() throws UnknownHostException, SocketException {
        try {
            // noinspection ResultOfMethodCallIgnored
            InetAddress.getByName("localhost.localdomain");
            localHostName = "localhost.localdomain";
        } catch (UnknownHostException e) {
            localHostName = findSiteLocal();
            if (localHostName == null) {
                localHostName = InetAddress.getLocalHost().getCanonicalHostName();
            }
        }
    }

    /**
     * Get a site local IP4 address from the current node's interfaces
     * @return the IP address or {@code null} if the address could not
     * be determined
     * @throws SocketException if an I/O error occurs
     */
    private static String findSiteLocal() throws SocketException {
        Enumeration<NetworkInterface> interfaces =
                NetworkInterface.getNetworkInterfaces();
        while (interfaces.hasMoreElements()) {
            NetworkInterface n = interfaces.nextElement();
            Enumeration<InetAddress> addresses = n.getInetAddresses();
            while (addresses.hasMoreElements()) {
                InetAddress i = addresses.nextElement();
                if (i.isSiteLocalAddress() && i instanceof Inet4Address) {
                    return i.getHostAddress();
                }
            }
        }
        return null;
    }
    
    /**
     * Runs a proxy server counting requests
     * @param authenticating true if the proxy should require authentication
     * @throws Exception if an error occurred
     */
    private void startProxy(boolean authenticating) throws Exception {
        proxyPort = findPort();
        
        HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer.bootstrap()
                .withPort(proxyPort)
                .withFiltersSource(new HttpFiltersSourceAdapter() {
                    public HttpFilters filterRequest(HttpRequest originalRequest,
                            ChannelHandlerContext ctx) {
                       return new HttpFiltersAdapter(originalRequest) {
                          @Override
                          public void proxyToServerRequestSent() {
                              proxyCounter++;
                          }
                       };
                    }
                });
        
        if (authenticating) {
            bootstrap = bootstrap.withProxyAuthenticator(new ProxyAuthenticator() {
                @Override
                public boolean authenticate(String userName, String password) {
                    return PROXY_USERNAME.equals(userName) &&
                            PROXY_PASSWORD.equals(password);
                }

                @Override
                public String getRealm() {
                    return "gradle-download-task";
                }
            });
        }
        
        proxy = bootstrap.start();
    }
    
    /**
     * Stops the proxy server
     */
    private void stopProxy() {
        proxy.stop();
    }
    
    /**
     * Tests if a single file can be downloaded through a proxy server
     * @param authenticating true if the proxy should require authentication
     * @throws Exception if anything goes wrong
     */
    private void testProxy(boolean authenticating) throws Exception {
        testProxy(authenticating, "", 1);
    }
    
    /**
     * Tests if a single file can be downloaded through a proxy server
     * @param authenticating true if the proxy should require authentication
     * @param newNonProxyHosts new value of the "http.nonProxyHosts" system property
     * @param expectedProxyCounter number of times the request is expected to hit the proxy
     * @throws Exception if anything goes wrong
     */
    private void testProxy(boolean authenticating, String newNonProxyHosts,
            int expectedProxyCounter) throws Exception {
        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .willReturn(aResponse()
                        .withHeader("content-length", String.valueOf(CONTENTS.length()))
                        .withBody(CONTENTS)));

        String proxyHost = System.getProperty("http.proxyHost");
        String proxyPort = System.getProperty("http.proxyPort");
        String nonProxyHosts = System.getProperty("http.nonProxyHosts");
        String proxyUser = System.getProperty("http.proxyUser");
        String proxyPassword = System.getProperty("http.proxyPassword");
        
        startProxy(authenticating);
        try {
            System.setProperty("http.proxyHost", "127.0.0.1");
            System.setProperty("http.proxyPort", String.valueOf(this.proxyPort));
            System.setProperty("http.nonProxyHosts", newNonProxyHosts);
            
            if (authenticating) {
                System.setProperty("http.proxyUser", PROXY_USERNAME);
                System.setProperty("http.proxyPassword", PROXY_PASSWORD);
            }
            
            Download t = makeProjectAndTask();
            t.src("http://" + localHostName + ":" + wireMockRule.port() + "/" + TEST_FILE_NAME);
            File dst = folder.newFile();
            t.dest(dst);
            t.execute();
            
            String dstContents = FileUtils.readFileToString(dst);
            assertEquals(CONTENTS, dstContents);
            assertEquals(expectedProxyCounter, proxyCounter);
        } finally {
            stopProxy();
            if (proxyHost == null) {
                System.getProperties().remove("http.proxyHost");
            } else {
                System.setProperty("http.proxyHost", proxyHost);
            }
            if (proxyPort == null) {
                System.getProperties().remove("http.proxyPort");
            } else {
                System.setProperty("http.proxyPort", proxyPort);
            }
            if (nonProxyHosts == null) {
                System.getProperties().remove("http.nonProxyHosts");
            } else {
                System.setProperty("http.nonProxyHosts", nonProxyHosts);
            }
            if (proxyUser == null) {
                System.getProperties().remove("http.proxyUser");
            } else {
                System.setProperty("http.proxyUser", proxyUser);
            }
            if (proxyPassword == null) {
                System.getProperties().remove("http.proxyPassword");
            } else {
                System.setProperty("http.proxyPassword", proxyPassword);
            }
        }
    }
    
    /**
     * Tests if a single file can be downloaded through a proxy server
     * @throws Exception if anything goes wrong
     */
    @Test
    public void normalProxy() throws Exception {
        testProxy(false);
    }
    
    /**
     * Tests if a single file can be downloaded through a proxy server
     * @throws Exception if anything goes wrong
     */
    @Test
    public void authenticationProxy() throws Exception {
        testProxy(true);
    }
    
    /**
     * Tests if the "http.nonProxyHosts" system property is respected 
     * @throws Exception if anything goes wrong
     */
    @Test
    public void nonProxyHosts() throws Exception {
        testProxy(false, localHostName, 0);
        testProxy(false, "example.com", 1);
    }
}