package org.limewire.http;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;

import junit.framework.Test;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.limewire.concurrent.SimpleTimer;
import org.limewire.http.httpclient.LimeHttpClient;
import org.limewire.io.IOUtils;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.io.SimpleNetworkInstanceUtils;
import org.limewire.net.EmptySocketBindingSettings;
import org.limewire.net.LimeWireNetModule;
import org.limewire.net.ProxySettings;
import org.limewire.net.SocketBindingSettings;
import org.limewire.net.SocketsManager;
import org.limewire.net.ProxySettings.ProxyType;
import org.limewire.util.BaseTestCase;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.name.Names;

public class ProxyTest extends BaseTestCase {

    private static final int PROXY_PORT = 9990;

    private final int DEST_PORT = 9999;

    private FakeProxyServer fps;

    private ProxySettingsStub proxySettings;
    private SocketsManager socketsManager;
    private LimeHttpClient httpClient;

    public ProxyTest(String name) {
        super(name);
    }

    public static Test suite() {
        return buildTestSuite(ProxyTest.class);
    }

    public static void main(String[] args) {
        junit.textui.TestRunner.run(suite());
    }

    @Override
    public void setUp() throws Exception {
        proxySettings = new ProxySettingsStub();
        
        fps = new FakeProxyServer(PROXY_PORT, DEST_PORT);
        fps.setMakeError(false);
        
        Injector injector = Guice.createInjector(new LimeWireNetModule(), new LimeWireHttpModule(), new AbstractModule() {
            @Override
            protected void configure() {
                bind(ProxySettings.class).toInstance(proxySettings);
                bind(SocketBindingSettings.class).to(EmptySocketBindingSettings.class);
                SimpleTimer timer = new SimpleTimer(true);
                bind(ScheduledExecutorService.class).annotatedWith(Names.named("backgroundExecutor")).toInstance(timer);
                bind(ExecutorService.class).annotatedWith(Names.named("backgroundExecutor")).toInstance(timer);
                bind(Executor.class).annotatedWith(Names.named("backgroundExecutor")).toInstance(timer);
                bind(NetworkInstanceUtils.class).to(SimpleNetworkInstanceUtils.class);
            }
        });        
        
        socketsManager = injector.getInstance(SocketsManager.class);
        httpClient = injector.getInstance(LimeHttpClient.class);
        
        
        proxySettings.setProxyForPrivate(true);
        proxySettings.setProxyHost("127.0.0.1");
        proxySettings.setProxyPort(PROXY_PORT);
        proxySettings.setProxyUser("");
        proxySettings.setProxyPass("");
    }

    @Override
    public void tearDown() throws Exception {
        fps.killServers();
        
        LimeTestUtils.waitForNIO();
    }

    /**
     * If Proxy is off we should connect directly
     */
    public void testConnectionsWithProxyOff() throws Exception {
        proxySettings.setProxyType(ProxyType.NONE);
        fps.setProxyOn(false);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.NONE);

        Socket s = socketsManager.connect(new InetSocketAddress("localhost", DEST_PORT), 0);
        // we should be connected to something, NPE is an error
        s.close();
    }

    /**
     * connect with socks 5
     */
    public void testSOCKS5Connection() throws Exception {
        runSOCKS5Connection(false);
    }

    public void testSOCKS5ConnectionNB() throws Exception {
        runSOCKS5Connection(true);
    }

    private void runSOCKS5Connection(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS5);
        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.SOCKS5);
        connect(nb, true);
    }

    /**
     * connect with socks 5 connection fails with bad code.
     */
    public void testSOCKS5ConnectionWithError() throws Exception {
        runSOCKS5ConnectionWithError(false);
    }

    public void testSOCKS5ConnectionWithErrorNB() throws Exception {
        runSOCKS5ConnectionWithError(true);
    }

    private void runSOCKS5ConnectionWithError(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS5);
        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.SOCKS5);
        fps.setMakeError(true);
        connect(nb, false);
        fps.setMakeError(false);
    }

    /**
     * connect with socks5 with auth
     */
    public void testSOCKS5WithAuth() throws Exception {
        runSOCKS5WithAuth(false);
    }

    public void testSOCKS5WithAuthNB() throws Exception {
        runSOCKS5WithAuth(true);
    }

    private void runSOCKS5WithAuth(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS5);
        proxySettings.setProxyAuthRequired(true);
        proxySettings.setProxyUser(FakeProxyServer.USER);
        proxySettings.setProxyPass(FakeProxyServer.PASS);

        fps.setProxyOn(true);
        fps.setAuthentication(true);
        fps.setProxyVersion(ProxyType.SOCKS5);
        connect(nb, true);
    }

    public void testSOCKS4Connection() throws Exception {
        runSOCKS4Connection(false);
    }

    public void testSOCKS4ConnectionNB() throws Exception {
        runSOCKS4Connection(true);
    }

    private void runSOCKS4Connection(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS4);
        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.SOCKS4);
        connect(nb, true);
    }

    public void testSOCKS4ConnectionWithError() throws Exception {
        runSOCKS4ConnectionWithError(false);
    }

    public void testSOCKS4ConnectionWithErrorNB() throws Exception {
        runSOCKS4ConnectionWithError(true);
    }

    private void runSOCKS4ConnectionWithError(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS4);
        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.SOCKS4);
        fps.setMakeError(true);
        connect(nb, false);
        fps.setMakeError(false);
    }

    public void testSOCKS4WithAuth() throws Exception {
        runSOCKS4WithAuth(false);
    }

    public void testSOCKS4WithAuthNB() throws Exception {
        runSOCKS4WithAuth(true);
    }

    private void runSOCKS4WithAuth(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS4);
        proxySettings.setProxyAuthRequired(true);
        proxySettings.setProxyUser(FakeProxyServer.USER);
        proxySettings.setProxyPass(FakeProxyServer.PASS);

        fps.setProxyOn(true);
        fps.setAuthentication(true);
        fps.setProxyVersion(ProxyType.SOCKS4);
        connect(nb, true);
    }

    /**
     * Tests that HTTP proxies are used correctly
     */
    public void testHTTPProxy() throws Exception {
        runHTTPProxy(false);
    }

    public void testHTTPProxyNB() throws Exception {
        runHTTPProxy(true);
    }

    private void runHTTPProxy(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.HTTP);

        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.HTTP);
        connect(nb, true);
    }

    public void testHTTPProxyWithError() throws Exception {
        runHTTPProxyWithError(false);
    }

    public void testHTTPProxyWithErrorNB() throws Exception {
        runHTTPProxyWithError(true);
    }

    private void runHTTPProxyWithError(boolean nb) throws Exception {
        proxySettings.setProxyType(ProxyType.HTTP);

        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.HTTP);
        fps.setMakeError(true);
        connect(nb, false);
    }

    private void connect(boolean nb, boolean success) throws Exception {
        if (success) {
            Socket s;
            if (!nb) {
                s = socketsManager.connect(new InetSocketAddress("localhost", DEST_PORT), 0);
            } else {
                ConnectObserverStub o = new ConnectObserverStub();
                s = socketsManager.connect(new InetSocketAddress("localhost", DEST_PORT), 0, o);
                o.waitForResponse(5000);
                assertEquals(s, o.getSocket());
                assertNull(o.getIoException());
                assertFalse(o.isShutdown());
            }
            // Must connect somewhere, NPE is an error
            s.close();
        } else {
            if (!nb) {
                try {
                    socketsManager.connect(new InetSocketAddress("localhost", DEST_PORT), 0);
                    fail("acceptedConnection from a bad proxy server");
                } catch (IOException iox) {
                    // Good -- expected behaviour
                }
            } else {
                ConnectObserverStub o = new ConnectObserverStub();
                socketsManager.connect(new InetSocketAddress("localhost", DEST_PORT), 0, o);
                o.waitForResponse(5000);
                assertNull(o.getSocket());
                assertNull(o.getIoException());
                assertTrue(o.isShutdown());
            }
        }
    }
    
    public void testHTTPClientWithProxyOff() throws Exception {
        proxySettings.setProxyType(ProxyType.NONE);
        fps.setProxyOn(false);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.NONE);
        fps.setHttpRequest(true);

        String connectTo = "http://" + "localhost:" + DEST_PORT + "/myFile.txt";
        HttpGet get = new HttpGet(connectTo);
        get.addHeader("connection", "close");
        HttpResponse response = httpClient.execute(get);
        byte[] resp = IOUtils.readFully(response.getEntity().getContent());
        assertEquals("invalid response from server", "hello", new String(resp));
        get.abort();
    }

    public void testHTTPClientProxiesWithHttpProxy() throws Exception {
        proxySettings.setProxyType(ProxyType.HTTP);
        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.HTTP);
        fps.setHttpRequest(true);

        String connectTo = "http://" + "localhost:" + DEST_PORT + "/myFile2.txt";
        HttpGet get = new HttpGet(connectTo);
        get.addHeader("connection", "close");
        // release the connections.
        // TODO client.setHttpConnectionManager(new SimpleHttpConnectionManager());
        HttpResponse response = httpClient.execute(get);
        String resp = new String(IOUtils.readFully(response.getEntity().getContent()));
        assertEquals("invalid response from server", "hello", resp);
        get.abort();
    }

    public void testHTTPClientProxiesWithSocks4() throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS4);
        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.SOCKS4);
        fps.setHttpRequest(true);

        String connectTo = "http://" + "localhost:" + DEST_PORT + "/myFile3.txt";
        HttpGet get = new HttpGet(connectTo);
        get.addHeader("connection", "close");
        // release the connections.
        // TODO client.setHttpConnectionManager(new SimpleHttpConnectionManager());
        HttpResponse response = httpClient.execute(get);
        String resp = new String(IOUtils.readFully(response.getEntity().getContent()));
        assertEquals("invalid response from server", "hello", resp);
        get.abort();
    }

    public void testHTTPClientProxiesWithSocks5() throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS5);
        fps.setProxyOn(true);
        fps.setAuthentication(false);
        fps.setProxyVersion(ProxyType.SOCKS5);
        fps.setHttpRequest(true);

        String connectTo = "http://" + "localhost:" + DEST_PORT + "/myFile4.txt";
        HttpGet get = new HttpGet(connectTo);
        get.addHeader("connection", "close");
        // release the connections.
        // TODO client.setHttpConnectionManager(new SimpleHttpConnectionManager());
        HttpResponse response = httpClient.execute(get);
        String resp = new String(IOUtils.readFully(response.getEntity().getContent()));
        assertEquals("invalid response from server", "hello", resp);
        get.abort();
    }

    public void testHTTPClientProxiesWithAuthSocks4() throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS4);
        proxySettings.setProxyAuthRequired(true);
        proxySettings.setProxyUser(FakeProxyServer.USER);
        proxySettings.setProxyPass(FakeProxyServer.PASS);

        fps.setProxyOn(true);
        fps.setAuthentication(true);
        fps.setProxyVersion(ProxyType.SOCKS4);
        fps.setHttpRequest(true);

        String connectTo = "http://" + "localhost:" + DEST_PORT + "/myFile3.txt";
        HttpGet get = new HttpGet(connectTo);
        get.addHeader("connection", "close");
        // release the connections.
        // TODO client.setHttpConnectionManager(new SimpleHttpConnectionManager());
        HttpResponse response = httpClient.execute(get);
        String resp = new String(IOUtils.readFully(response.getEntity().getContent()));
        assertEquals("invalid response from server", "hello", resp);
        get.abort();
    }

    public void testHTTPClientProxiesWithAuthSocks5() throws Exception {
        proxySettings.setProxyType(ProxyType.SOCKS5);
        proxySettings.setProxyAuthRequired(true);
        proxySettings.setProxyUser(FakeProxyServer.USER);
        proxySettings.setProxyPass(FakeProxyServer.PASS);

        fps.setProxyOn(true);
        fps.setAuthentication(true);
        fps.setProxyVersion(ProxyType.SOCKS5);
        fps.setHttpRequest(true);

        String connectTo = "http://" + "localhost:" + DEST_PORT + "/myFile4.txt";
        HttpGet get = new HttpGet(connectTo);
        get.addHeader("connection", "close");
        // release the connections.
        // TODO client.setHttpConnectionManager(new SimpleHttpConnectionManager());
        HttpResponse response = httpClient.execute(get);
        String resp = new String(IOUtils.readFully(response.getEntity().getContent()));
        assertEquals("invalid response from server", "hello", resp);
        get.abort();
    }

}
