package com.limegroup.gnutella;


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Set;
import java.util.StringTokenizer;

import org.limewire.io.IpPort;
import org.limewire.io.IpPortImpl;
import org.limewire.io.IpPortSet;
import org.limewire.util.Base32;
import org.limewire.util.PrivilegedAccessor;

import junit.framework.Test;

import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PushRequest;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.search.HostData;
import com.limegroup.gnutella.stubs.ActivityCallbackStub;

/**
 * Checks whether (multi)leaves avoid forwarding messages to ultrapeers, do
 * redirects properly, etc.  The test includes a leaf attached to 3 
 * Ultrapeers.
 */
@SuppressWarnings("unchecked")
public class ClientSideBrowseHostTest extends ClientSideTestCase {

    private MyActivityCallback callback;

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

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

    public static void main(String[] args) {
        junit.textui.TestRunner.run(suite());
    }
    
    ///////////////////////// Actual Tests ////////////////////////////
    
    // Tests the following behaviors:
    // ------------------------------
    // 1. that the client makes a correct direct connection if possible
    // 2. that the client makes a correct push proxy connection if necessary
    // 3. if all else fails the client sends a PushRequest

    public static void globalSetUp() throws Exception {
        PrivilegedAccessor.setValue(RouterService.getAcceptor(),"_acceptedIncoming", Boolean.TRUE);
    }

    public void testHTTPRequest() throws Exception {
        callback = (MyActivityCallback) getCallback();

        drain(testUP[0]);
        // some setup
        final byte[] clientGUID = GUID.makeGuid();

        // construct and send a query        
        byte[] guid = GUID.makeGuid();
        RouterService.query(guid, "boalt.org");

        // the testUP[0] should get it
        Message m = null;
        do {
            m = testUP[0].receive(TIMEOUT);
        } while (!(m instanceof QueryRequest)) ;

        // set up a server socket
        ServerSocket ss = new ServerSocket(7000);
        ss.setReuseAddress(true);
        ss.setSoTimeout(TIMEOUT);

        // send a reply with some PushProxy info
        IpPort[] proxies = new IpPortImpl[1];
        proxies[0] = new IpPortImpl("127.0.0.1", 7000);
        Response[] res = new Response[1];
        res[0] = new Response(10, 10, "boalt.org");
        m = new QueryReply(m.getGUID(), (byte) 1, 7000, 
                           InetAddress.getLocalHost().getAddress(), 0, res, 
                           clientGUID, new byte[0], false, false, true,
                           true, false, false, null);
        testUP[0].send(m);
        testUP[0].flush();

        // wait a while for Leaf to process result
        Thread.sleep(1000);
        assertNotNull(callback.getRFD());

        // tell the leaf to browse host the file, should result in direct HTTP
        // request
        RouterService.doAsynchronousBrowseHost(callback.getRFD().getHost(),
                                callback.getRFD().getPort(),
                                new GUID(GUID.makeGuid()), new GUID(clientGUID),
                                null, false);

        // wait for the incoming HTTP request
        Socket httpSock = ss.accept();
        assertNotNull(httpSock);

        // start reading and confirming the HTTP request
        String currLine = null;
        BufferedReader reader = 
            new BufferedReader(new
                               InputStreamReader(httpSock.getInputStream()));

        // confirm a GET/HEAD pushproxy request
        currLine = reader.readLine();
        assertTrue(currLine.startsWith("GET / HTTP/1.1"));
        
        // make sure the node sends the correct Host val
        currLine = reader.readLine();
        assertTrue(currLine.startsWith("Host:"));
        StringTokenizer st = new StringTokenizer(currLine, ":");
        assertEquals(st.nextToken(), "Host");
        InetAddress addr = InetAddress.getByName(st.nextToken().trim());
        Arrays.equals(addr.getAddress(), RouterService.getAddress());
        assertEquals(Integer.parseInt(st.nextToken()), SERVER_PORT);

        // let the other side do its thing
        Thread.sleep(500);

        // send back a 200 and make sure no PushRequest is sent via the normal
        // way
        BufferedWriter writer = 
            new BufferedWriter(new
                               OutputStreamWriter(httpSock.getOutputStream()));

        writer.write("HTTP/1.1 200 OK\r\n");
        writer.flush();
        writer.write("\r\n");
        writer.flush();
        //TODO: should i send some Query Hits?  Might be a good test.
        httpSock.close();

        try {
            do {
                m = testUP[0].receive(TIMEOUT);
                assertTrue(!(m instanceof PushRequest));
            } while (true) ;
        }
        catch (InterruptedIOException expected) {}

        // awesome - everything checks out!
        ss.close();
    }


    public void testPushProxyRequest() throws Exception {
        Thread.sleep(6000);
        callback = (MyActivityCallback) getCallback();
        drain(testUP[0]);
        // some setup
        final byte[] clientGUID = GUID.makeGuid();

        // construct and send a query        
        byte[] guid = GUID.makeGuid();
        RouterService.query(guid, "nyu.edu");

        // the testUP[0] should get it
        Message m = null;
        do {
            m = testUP[0].receive(TIMEOUT);
        } while (!(m instanceof QueryRequest)) ;

        // set up a server socket to wait for proxy request
        ServerSocket ss = new ServerSocket(7000);
        ss.setReuseAddress(true);
        ss.setSoTimeout(TIMEOUT*4);

        // send a reply with some PushProxy info
        //final PushProxyInterface[] proxies = 
        //  new QueryReply.PushProxyContainer[1];
        final Set proxies = new IpPortSet();
        proxies.add(new IpPortImpl("127.0.0.1", 7000));
        Response[] res = new Response[1];
        res[0] = new Response(10, 10, "nyu.edu");
        m = new QueryReply(m.getGUID(), (byte) 1, 6999, 
                           InetAddress.getLocalHost().getAddress(), 0, res, 
                           clientGUID, new byte[0], false, false, true,
                           true, false, false, proxies);
        testUP[0].send(m);
        testUP[0].flush();

        // wait a while for Leaf to process result
        Thread.sleep(2000);
        assertTrue(callback.getRFD() != null);

        // tell the leaf to browse host the file, should result in PushProxy
        // request
        RouterService.doAsynchronousBrowseHost(callback.getRFD().getHost(),
                                callback.getRFD().getPort(),
                                new GUID(GUID.makeGuid()), new GUID(clientGUID),
                                proxies, false);

        // wait for the incoming PushProxy request
        // increase the timeout since we send udp pushes first
        ss.setSoTimeout(7000);
        Socket httpSock = ss.accept();
        assertNotNull(httpSock);
        
        BufferedWriter sockWriter  = 
            new BufferedWriter(new
                               OutputStreamWriter(httpSock.getOutputStream()));
        sockWriter.write("HTTP/1.1 202 OK\r\n");
        sockWriter.flush();
        

        // start reading and confirming the HTTP request
        String currLine = null;
        BufferedReader reader = 
            new BufferedReader(new
                               InputStreamReader(httpSock.getInputStream()));

        // confirm a GET/HEAD pushproxy request
        currLine = reader.readLine();
        assertTrue(currLine.startsWith("GET /gnutella/push-proxy") ||
                   currLine.startsWith("HEAD /gnutella/push-proxy"));
        
        // make sure it sends the correct client GUID
        int beginIndex = currLine.indexOf("ID=") + 3;
        String guidString = currLine.substring(beginIndex, beginIndex+26);
        GUID guidFromBackend = new GUID(clientGUID);
        GUID guidFromNetwork = new GUID(Base32.decode(guidString));
        assertEquals(guidFromNetwork, guidFromBackend);

        // make sure the node sends the correct X-Node
        currLine = reader.readLine();
        assertTrue(currLine.startsWith("X-Node:"));
        StringTokenizer st = new StringTokenizer(currLine, ":");
        assertEquals(st.nextToken(), "X-Node");
        InetAddress addr = InetAddress.getByName(st.nextToken().trim());
        Arrays.equals(addr.getAddress(), RouterService.getAddress());
        assertEquals(Integer.parseInt(st.nextToken()), SERVER_PORT);

        // now we need to GIV
        Socket push = new Socket(InetAddress.getLocalHost(), SERVER_PORT);
        BufferedWriter writer = 
            new BufferedWriter(new
                               OutputStreamWriter(push.getOutputStream()));
        writer.write("GIV 0:" + new GUID(clientGUID).toHexString() + "/\r\n");
        writer.write("\r\n");
        writer.flush();

        // confirm a BrowseHost request
        reader = 
            new BufferedReader(new
                               InputStreamReader(push.getInputStream()));
        currLine = reader.readLine();
        assertTrue(currLine.startsWith("GET / HTTP/1.1"));
        
        // make sure the node sends the correct Host val
        currLine = reader.readLine();
        assertTrue(currLine.startsWith("Host:"));
        st = new StringTokenizer(currLine, ":");
        assertEquals(st.nextToken(), "Host");
        addr = InetAddress.getByName(st.nextToken().trim());
        Arrays.equals(addr.getAddress(), RouterService.getAddress());
        assertEquals(Integer.parseInt(st.nextToken()), SERVER_PORT);

        // let the other side do its thing
        Thread.sleep(500);

        // send back a 200 and make sure no PushRequest is sent via the normal
        // way
        writer = 
            new BufferedWriter(new
                               OutputStreamWriter(push.getOutputStream()));

        writer.write("HTTP/1.1 200 OK\r\n");
        writer.flush();
        writer.write("\r\n");
        writer.flush();
        httpSock.close();

        try {
            do {
                m = testUP[0].receive(TIMEOUT);
                assertNotInstanceof(m.toString(), PushRequest.class, m);
            } while (true) ;
        }
        catch (InterruptedIOException expected) {}

        // awesome - everything checks out!
        ss.close();
    }


    public void testSendsPushRequest() throws Exception {
        callback = (MyActivityCallback) getCallback();
        drain(testUP[0]);
        // some setup
        final byte[] clientGUID = GUID.makeGuid();

        // construct and send a query        
        byte[] guid = GUID.makeGuid();
        RouterService.query(guid, "anita");

        // the testUP[0] should get it
        Message m = null;
        do {
            m = testUP[0].receive(TIMEOUT);
        } while (!(m instanceof QueryRequest)) ;

        // send a reply with some BAD PushProxy info
        //final PushProxyInterface[] proxies = 
        //  new QueryReply.PushProxyContainer[1];
        final Set proxies = new IpPortSet();
        proxies.add(new IpPortImpl("127.0.0.1", 7001));
        Response[] res = new Response[1];
        res[0] = new Response(10, 10, "anita");
        m = new QueryReply(m.getGUID(), (byte) 1, 7000, 
                           InetAddress.getLocalHost().getAddress(), 0, res, 
                           clientGUID, new byte[0], false, false, true,
                           true, false, false, proxies);
        testUP[0].send(m);
        testUP[0].flush();

        // wait a while for Leaf to process result
        Thread.sleep(1000);
        assertTrue(callback.getRFD() != null);

        // tell the leaf to browse host the file,
        RouterService.doAsynchronousBrowseHost(callback.getRFD().getHost(),
                                callback.getRFD().getPort(),
                                new GUID(GUID.makeGuid()), new GUID(clientGUID),
                                proxies, false);

        // nothing works for the guy, we should get a PushRequest
        do {
            m = testUP[0].receive(TIMEOUT*30);
        } while (!(m instanceof PushRequest));

        // awesome - everything checks out!
    }




    //////////////////////////////////////////////////////////////////
    public static Integer numUPs() {
        return new Integer(1);
    }

    public static ActivityCallback getActivityCallback() {
        return new MyActivityCallback();
    }

    private static class MyActivityCallback extends ActivityCallbackStub {
        private RemoteFileDesc _rfd = null;
        public RemoteFileDesc getRFD() {
            return _rfd;
        }
        
        public void handleQueryResult(RemoteFileDesc rfd,
                                      HostData data,
                                      Set locs) {
            _rfd = rfd;
        }
    }
}

