package com.limegroup.gnutella;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import junit.framework.Test;

import com.limegroup.gnutella.handshaking.LeafHeaders;
import com.limegroup.gnutella.handshaking.UltrapeerHeaders;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.routing.QueryRouteTable;
import com.limegroup.gnutella.routing.RouteTableMessage;
import com.limegroup.gnutella.settings.ApplicationSettings;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.FilterSettings;
import com.limegroup.gnutella.settings.PingPongSettings;
import com.limegroup.gnutella.settings.UltrapeerSettings;
import com.limegroup.gnutella.stubs.ActivityCallbackStub;
import com.limegroup.gnutella.util.LimeTestCase;
import com.limegroup.gnutella.util.EmptyResponder;

/**
 * This test makes sure that pong caching is working correctly between
 * Ultrapeers.
 *
 *  ULTRAPEER_1  ----  CENTRAL TEST ULTRAPEER  ----  ULTRAPEER_2
 *                              |
 *                              |
 *                              |
 *                             LEAF
 */
@SuppressWarnings("unchecked")
public final class PongCachingTest extends LimeTestCase {

	/**
	 * The port that the central Ultrapeer listens on, and that the other nodes
	 * connect to it on.
	 */
    private static final int SERVER_PORT = 6667;

	/**
	 * The timeout value for sockets -- how much time we wait to accept 
	 * individual messages before giving up.
	 */
    private static final int TIMEOUT = 1800;


    /**
     * Leaf connection to the Ultrapeer.
     */
    private Connection LEAF;

    /**
     * Ultrapeer connection.
     */
    private Connection ULTRAPEER_1;

    /**
	 * Second Ultrapeer connection
     */
    private Connection ULTRAPEER_2;

    /**
     * Third Ultrapeer connection
     */
    private Connection ULTRAPEER_3;
    
    /**
     * Fourth Ultrapeer connection
     */
    private Connection ULTRAPEER_4;

	/**
	 * The central Ultrapeer used in the test.
	 */
	private static final RouterService ROUTER_SERVICE = 
		new RouterService(new ActivityCallbackStub());

    public PongCachingTest(String name) {
        super(name);
    }
    
    public static Test suite() {
        return buildTestSuite(PongCachingTest.class);
    }    
   
	public static void main(String[] args) {
		junit.textui.TestRunner.run(suite());
	}
	
	private void buildConnections() {
	    LEAF = new Connection("localhost", SERVER_PORT);
        ULTRAPEER_1 = new Connection("localhost", SERVER_PORT);
        ULTRAPEER_2 = new Connection("localhost", SERVER_PORT);
        ULTRAPEER_3 = new Connection("localhost", SERVER_PORT);
        ULTRAPEER_4 = new Connection("localhost", SERVER_PORT);
    }

	public void setUp() throws Exception {
        //Setup LimeWire backend.  For testing other vendors, you can skip all
        //this and manually configure a client to listen on port 6667, with
        //incoming slots and no connections.
        //To keep LimeWire from connecting to the outside network, we filter out
        //all addresses but localhost and 18.239.0.*.  The latter is used in
        //pongs for testing.  TODO: it would be nice to have a way to prevent
        //BootstrapServerManager from adding defaults and connecting.
        FilterSettings.BLACK_LISTED_IP_ADDRESSES.setValue(
            new String[] {"*.*.*.*"});
        FilterSettings.WHITE_LISTED_IP_ADDRESSES.setValue(
            new String[] {"127.*.*.*", "18.239.0.*"});
        ConnectionSettings.PORT.setValue(SERVER_PORT);
        setSharedDirectories(new File[0]);
		ConnectionSettings.CONNECT_ON_STARTUP.setValue(false);
		UltrapeerSettings.EVER_ULTRAPEER_CAPABLE.setValue(true);
		UltrapeerSettings.DISABLE_ULTRAPEER_MODE.setValue(false);
		UltrapeerSettings.FORCE_ULTRAPEER_MODE.setValue(true);
		UltrapeerSettings.MAX_LEAVES.setValue(4);
		ConnectionSettings.NUM_CONNECTIONS.setValue(3);
		ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false);	
		ConnectionSettings.USE_GWEBCACHE.setValue(false);
		ConnectionSettings.WATCHDOG_ACTIVE.setValue(false);
		ConnectionSettings.SEND_QRP.setValue(false);
		
        UltrapeerSettings.NEED_MIN_CONNECT_TIME.setValue(false);
        
        assertEquals("unexpected port", SERVER_PORT, 
					 ConnectionSettings.PORT.getValue());

		ROUTER_SERVICE.start();
		RouterService.clearHostCatcher();
		RouterService.connect();
        
		connect();
		assertEquals("unexpected port", SERVER_PORT, 
					 ConnectionSettings.PORT.getValue());
	}
	
	public void tearDown() throws Exception {
        drainAll();
		sleep();
		LEAF.close();
		ULTRAPEER_1.close();
		ULTRAPEER_2.close();
        ULTRAPEER_3.close();
        ULTRAPEER_4.close();        
        ConnectionSettings.SEND_QRP.setValue(true);
		RouterService.disconnect();
		sleep();
	}

	private void sleep() {
		try {Thread.sleep(300);}catch(InterruptedException e) {}
	}

	/**
	 * Drains all messages 
	 */
 	private void drainAll() throws Exception {
 		if(ULTRAPEER_1.isOpen()) {
 			drain(ULTRAPEER_1, TIMEOUT);
 		}
 		if(ULTRAPEER_2.isOpen()) {
 			drain(ULTRAPEER_2, TIMEOUT);
 		}
        if(ULTRAPEER_3.isOpen()) {
            drain(ULTRAPEER_3, TIMEOUT);
        }
        if(ULTRAPEER_4.isOpen()) {
            drain(ULTRAPEER_4, TIMEOUT);
        }
 		if(LEAF.isOpen()) {
 			drain(LEAF, TIMEOUT);
 		}
 	}

	/**
	 * Connects all of the nodes to the central test Ultrapeer.
	 */
    private void connect() throws Exception {
		buildConnections();
        //1. first Ultrapeer connection 
        ULTRAPEER_2.initialize(new UltrapeerHeaders("localhost"), new EmptyResponder(), 1000);
        assertTrue("should be open", ULTRAPEER_2.isOpen());
        assertTrue("should be up", ULTRAPEER_2.isSupernodeSupernodeConnection());
        ULTRAPEER_3.initialize(new UltrapeerHeaders("localhost"), new EmptyResponder(), 1000);
        assertTrue("should be open", ULTRAPEER_3.isOpen());
        assertTrue("should be up", ULTRAPEER_3.isSupernodeSupernodeConnection());

        ULTRAPEER_4.initialize(new UltrapeerHeaders("localhost"), new EmptyResponder(), 1000);
        assertTrue("should be open", ULTRAPEER_4.isOpen());
        assertTrue("should be up", ULTRAPEER_4.isSupernodeSupernodeConnection());

        //2. second Ultrapeer connection
        ULTRAPEER_1.initialize(new UltrapeerHeaders("localhost"), new EmptyResponder(), 1000);
        assertTrue("should be open", ULTRAPEER_1.isOpen());
        assertTrue("should be up", ULTRAPEER_1.isSupernodeSupernodeConnection());        
        //3. routed leaf, with route table for "test"
        LEAF.initialize(new LeafHeaders("localhost"), new EmptyResponder(), 1000);
        assertTrue("should be open", LEAF.isOpen());
        assertTrue("should be up", LEAF.isClientSupernodeConnection());        
        QueryRouteTable qrt = new QueryRouteTable();
        qrt.add("test");
        qrt.add("susheel");
        qrt.addIndivisible(HugeTestUtils.UNIQUE_SHA1.toString());
        for (Iterator iter=qrt.encode(null).iterator(); iter.hasNext(); ) {
            LEAF.send((RouteTableMessage)iter.next());
			LEAF.flush();
        }

        // for Ultrapeer 1
        qrt = new QueryRouteTable();
        qrt.add("leehsus");
        qrt.add("awesome");
        for (Iterator iter=qrt.encode(null).iterator(); iter.hasNext(); ) {
            ULTRAPEER_1.send((RouteTableMessage)iter.next());
			ULTRAPEER_1.flush();
        }

		// make sure we get rid of any initial ping pong traffic exchanges
		sleep();
		drainAll();
    }

    /**
     * Tests to make sure that pongs are received properly via
     * pong caching.
     */
    public void testPongsReceivedFromPing() throws Exception {
        PingPongSettings.PINGS_ACTIVE.setValue(false);


        byte[] ip = { (byte)1, (byte)2, (byte)3, (byte)4 };

        for(int i=0; i<PongCacher.NUM_HOPS+4; i++) {
            PingReply curPong = 
                PingReply.create(new GUID().bytes(), (byte)3, 13232, ip, 0, 0, 
                    true, -1, false);
            for(int j=0; j<i; j++) {
                if(j < PongCacher.NUM_HOPS) {
                    curPong.hop();
                }
            }
            PongCacher.instance().addPong(curPong);            
        }
        
        List pongs = PongCacher.instance()
            .getBestPongs(ApplicationSettings.LANGUAGE.getValue());
        assertEquals( PongCacher.NUM_HOPS, pongs.size() );

        Message m = new PingRequest((byte)7);
        ULTRAPEER_1.send(m);
        ULTRAPEER_1.flush();        
        
        Message received;   
        for(int i=0; i<PongCacher.NUM_HOPS; i++) {
            received = getFirstMessageOfType(ULTRAPEER_1, PingReply.class, 10000);
            assertNotNull("should have gotten pong. hop: " + i, received);
        }
        PingPongSettings.PINGS_ACTIVE.setValue(true);
    }


    /**
     * Tests to make sure that pongs are received properly via
     * pong caching when the locale is specified in the ping
     */
    public void testPongsReceivedFromPingWithLocale() throws Exception {
        PingPongSettings.PINGS_ACTIVE.setValue(false);
        byte[] ip = { (byte)1, (byte)2, (byte)3, (byte)3 };
              
        //add english locale pongs
        for(int i=0; i<PongCacher.NUM_HOPS+4; i++) {
            PingReply curPong = 
                PingReply.create(new GUID().bytes(), (byte)3, 13232, ip, 0, 0, 
                    true, -1, false, "en", 1);
            for(int j=0; j<i; j++) {
                if(j < PongCacher.NUM_HOPS) {
                    curPong.hop();
                }
            }
            PongCacher.instance().addPong(curPong);            
        }
        
        byte[] ip2 = { (byte)1, (byte)3, (byte)3, (byte)3 };
        //add ja locale pongs
        for(int i=0; i<PongCacher.NUM_HOPS+4; i++) {
            PingReply curPong = 
                PingReply.create(new GUID().bytes(), (byte)3, 13232, ip2, 
                                 0, 0, true, -1, false, "ja", 1);
            for(int j=0; j<i; j++) {
                if(j < PongCacher.NUM_HOPS) {
                    curPong.hop();
                }
            }
            PongCacher.instance().addPong(curPong);            
        }

        //check that all the pongs are in the PongCacher
        List pongs = PongCacher.instance().getBestPongs("ja");
        assertEquals( PongCacher.NUM_HOPS, pongs.size() );

        pongs = PongCacher.instance().getBestPongs("en");
        assertEquals( PongCacher.NUM_HOPS, pongs.size() );

        //create a ja locale PingRequest
        ApplicationSettings.LANGUAGE.setValue("ja");
        Message m = new PingRequest((byte)7);
        assertEquals("locale of ping should be ja",
                     "ja", ((PingRequest)m).getLocale());

        //send a ja ping using ULTRAPEER_3
        ULTRAPEER_3.send(m);
        ULTRAPEER_3.flush();        

        Thread.sleep(100);

        ApplicationSettings.LANGUAGE.setValue("en");        

        //check for ja pongs
        Message received;   
        for(int i=0; i< PongCacher.NUM_HOPS; i++) {
            received = getFirstMessageOfType(ULTRAPEER_3, 
                                             PingReply.class, 
                                             5000);

            assertNotNull("should have gotten pong. hop: " + i, received);
            PingReply pr = (PingReply)received;
            assertEquals("should be a ja locale pong ",
                         "ja", pr.getClientLocale());
        }


        byte[] ip3 = { (byte)3, (byte)3, (byte)3, (byte)3 };
        //add sv locale pongs
        for(int i=0; i< 2; i++) {
            PingReply curPong = 
                PingReply.create(new GUID().bytes(), (byte)3, 13232, ip3, 
                                 0, 0, true, -1, false, "sv", 1);
            for(int j=0; j<i; j++) {
                if(j < PongCacher.NUM_HOPS) {
                    curPong.hop();
                }
            }
            PongCacher.instance().addPong(curPong);            
        }
        pongs = PongCacher.instance().getBestPongs("sv");
        assertEquals( PongCacher.NUM_HOPS, pongs.size() );        

        //create a sv locale PingRequest
        ApplicationSettings.LANGUAGE.setValue("sv"); 
        Message m2 = new PingRequest((byte)7);
        assertEquals("locale of ping should be sv",
                     "sv", ((PingRequest)m2).getLocale());

        ULTRAPEER_4.send(m2);
        ULTRAPEER_4.flush();

        //check ofr sv pongs
        List returnedPongs = new ArrayList();
        for(int i = 0; i < PongCacher.NUM_HOPS; i++) {
            received = getFirstMessageOfType(ULTRAPEER_4, 
                                             PingReply.class, 
                                             5000);
            assertNotNull("should have gotten pong. hop: " + i, received);
            PingReply pr = (PingReply)received;
            returnedPongs.add(pr);
        }

        //check that there are two "sv" pongs and the rest are 
        //"en" - the default locale
        int numSVPongs = 0;
        Iterator iter = returnedPongs.iterator();
        while(iter.hasNext()) {
            PingReply pr = (PingReply)iter.next();
            if(pr.getClientLocale().equals("sv"))
                numSVPongs++;
            else
                assertEquals("the locale of pong should be en : ",
                             "en", pr.getClientLocale());
        }
        
        assertTrue("should be 2 || 3 pongs with sv, but was: " + numSVPongs, 
                   numSVPongs == 2 || numSVPongs == 3);
        
        PingPongSettings.PINGS_ACTIVE.setValue(true);
    }
}






