package org.jgroups.tests;

import org.jgroups.*;
import org.jgroups.conf.ConfiguratorFactory;
import org.jgroups.conf.ProtocolConfiguration;
import org.jgroups.conf.ProtocolStackConfigurator;
import org.jgroups.protocols.BasicTCP;
import org.jgroups.protocols.TCPPING;
import org.jgroups.protocols.TP;
import org.jgroups.protocols.UDP;
import org.jgroups.stack.IpAddress;
import org.jgroups.stack.Protocol;
import org.jgroups.stack.ProtocolStack;
import org.jgroups.util.ResourceManager;
import org.jgroups.util.TimeScheduler;
import org.jgroups.util.Util;
import org.testng.AssertJUnit;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import java.net.InetAddress;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;


/**
 * Tests which test the shared transport
 * @author Bela Ban
 */
@Test(groups=Global.STACK_DEPENDENT,sequential=true)
public class SharedTransportTest extends ChannelTestBase {
    private JChannel a, b, c;
    private MyReceiver r1, r2, r3;
    static final String SINGLETON_1="singleton-1", SINGLETON_2="singleton-2";


    @AfterMethod
    protected void tearDown() throws Exception {
        Util.close(c,b,a);
        r1=r2=r3=null;
    }


    public void testCreationNonSharedTransport() throws Exception {
        a=createChannel(true, 1, "A");
        a.connect("SharedTransportTest.testCreationNonSharedTransport");
        View view=a.getView();
        System.out.println("view = " + view);
        assert view.size() == 1;
    }


    public void testCreationOfDuplicateCluster() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        // makeUnique(a, 2);
        b=createSharedChannel(SINGLETON_1, "B");
        a.connect("x");
        try {
            b.connect("x");
            assert false : "b should not be able to join cluster 'x' as a has already joined it";
        }
        catch(Exception ex) {
            System.out.println("b was not able to join the same cluster (\"x\") as expected");
        }
    }


    public void testView() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        b=createSharedChannel(SINGLETON_2, "B");
        a.setReceiver(new MyReceiver(SINGLETON_1));
        b.setReceiver(new MyReceiver(SINGLETON_2));

        a.connect("x");
        b.connect("x");
        Util.waitUntilAllChannelsHaveSameSize(30000, 1000, a,b);
    }


    public void testView2() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        b=createSharedChannel(SINGLETON_1, "B");
        a.setReceiver(new MyReceiver("first-channel"));
        b.setReceiver(new MyReceiver("second-channel"));

        a.connect("x");
        b.connect("y");

        View view=a.getView();
        assert view.size() == 1;
        view=b.getView();
        assert view.size() == 1;
    }

    /**
     * Tests http://jira.jboss.com/jira/browse/JGRP-689: with TCP or UDP.ip_mcast=false, the transport iterates through
     * the 'members' instance variable to send a group message. However, the 'members' var is the value of the last
     * view change received. If we receive multiple view changes, this leads to incorrect membership.
     * @throws Exception
     */

    public void testView3() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        b=createSharedChannel(SINGLETON_1, "B");
        c=createSharedChannel(SINGLETON_2, "C");
        r1=new MyReceiver("A::" + SINGLETON_1);
        r2=new MyReceiver("B::" + SINGLETON_1);
        r3=new MyReceiver("C::" + SINGLETON_2);
        a.setReceiver(r1);
        b.setReceiver(r2);
        c.setReceiver(r3);

        a.connect("cluster-1");
        c.connect("cluster-1");
        Util.waitUntilAllChannelsHaveSameSize(30000, 1000, a,c);

        a.send(new Message(null, null, "msg-1"));
        c.send(new Message(null, null, "msg-2"));

        // async sending - wait until the message has been received by A and C
        for(int i=0; i < 20; i++) {
            if(r1.getList().size() == 2 && r3.getList().size() == 3)
                break;
            Util.sleep(500);
        }

        List<Message> list=r1.getList();
        assert list.size() == 2;
        list=r3.getList();
        assert list.size() == 2;

        r1.clear();
        r2.clear();
        r3.clear();
        b.connect("cluster-2");

        a.send(new Message(null, null, "msg-3"));
        b.send(new Message(null, null, "msg-4"));
        c.send(new Message(null, null, "msg-5"));
        Util.sleep(1000); // async sending - wait a little

        // printLists(r1, r2, r3);
        list=r1.getList();
        assert list.size() == 2;
        list=r2.getList();
        assert list.size() == 1;
        list=r3.getList();
        assert list.size() == 2;
    }


    public void testView4() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        r1=new MyReceiver("A::" + SINGLETON_1);
        a.setReceiver(r1);

        a.connect("cluster-X");
        a.send(new Message(null, null, "msg-1"));

        Util.sleep(1000); // async sending - wait a little
        List<Message> list=r1.getList();
        assert list.size() == 1;

        a.send(new Message(null, null, "msg-2"));
        a.send(new Message(null, null, "msg-3"));
        a.send(new Message(null, null, "msg-4"));
        Util.sleep(1000); // async sending - wait a little

        list=r1.getList();
        assert list.size() == 4;
    }


    public void testSharedTransportAndNonsharedTransport() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        b=createChannel();
        a.setReceiver(new MyReceiver("first-channel"));
        b.setReceiver(new MyReceiver("second-channel"));

        a.connect("x");
        b.connect("x");
        Util.waitUntilAllChannelsHaveSameSize(30000, 1000, a,b);
    }


    public void testCreationOfDifferentCluster() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        b=createSharedChannel(SINGLETON_2, "B");
        a.connect("x");
        b.connect("x");
        Util.waitUntilAllChannelsHaveSameSize(30000, 1000, a,b);
    }



    public void testReferenceCounting() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        r1=new MyReceiver("a");
        a.setReceiver(r1);

        b=createSharedChannel(SINGLETON_1, "B");
        r2=new MyReceiver("b");
        b.setReceiver(r2);

        c=createSharedChannel(SINGLETON_1, "C");
        r3=new MyReceiver("c");
        c.setReceiver(r3);

        a.connect("A");
        b.connect("B");
        c.connect("C");

        a.send(null, "message from a");
        b.send(null, "message from b");
        c.send(null, "message from c");
        Util.sleep(500);
        assert r1.size() == 1;
        assert r2.size() == 1;
        assert r3.size() == 1;
        r1.clear();
        r2.clear();
        r3.clear();

        b.disconnect();
        System.out.println("\n");
        a.send(null, "message from a");
        c.send(null, "message from c");
        Util.sleep(500);
        assert r1.size() == 1 : "size should be 1 but is " + r1.size();
        assert r3.size() == 1 : "size should be 1 but is " + r3.size();
        r1.clear();
        r3.clear();

        c.disconnect();
        System.out.println("\n");
        a.send(null, "message from a");
        Util.sleep(500);
        assert r1.size() == 1;
    }

    /**
     * Tests that a second channel with the same group name can be
     * created and connected once the first channel is disconnected.
     * @throws Exception
     */

    public void testSimpleReCreation() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        a.setReceiver(new MyReceiver("A"));
        a.connect("A");
        a.disconnect();
        b=createSharedChannel(SINGLETON_1, "B");
        b.setReceiver(new MyReceiver("A'"));
        b.connect("A");
    }

    /**
     * Tests that a second channel with the same group name can be
     * created and connected once the first channel is disconnected even
     * if 3rd channel with a different group name is still using the shared
     * transport.
     * @throws Exception
     */

    public void testCreationFollowedByDeletion() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        a.setReceiver(new MyReceiver("A"));
        a.connect("A");

        b=createSharedChannel(SINGLETON_1, "B");
        b.setReceiver(new MyReceiver("B"));
        b.connect("B");

        b.close();
        a.close();
    }



    public void test2ChannelsCreationFollowedByDeletion() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        a.setReceiver(new MyReceiver("A"));
        a.connect("A");

        b=createSharedChannel(SINGLETON_2, "B");
        b.setReceiver(new MyReceiver("B"));
        b.connect("A");

        c=createSharedChannel(SINGLETON_2, "C");
        c.setReceiver(new MyReceiver("C"));
        c.connect("B");

        c.send(null, "hello world from C");
    }


    /**
     * Test case for https://issues.jboss.org/browse/JGRP-1356
     * @throws Exception
     */
    public void testFailedFirstChannel() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");

        TP transport=a.getProtocolStack().getTransport();
        transport.setBindPort(128); // set the bind_port to an incorrect value (< 1024), this will fail on connect()
        a.setReceiver(new MyReceiver("A"));
        try {
            a.connect("A");
        }
        catch(Exception ex) {
            System.out.println("caught exception - as expected: " + ex);
        }

        b=createSharedChannel(SINGLETON_1, "B");
        b.setReceiver(new MyReceiver("B"));

        try {
            b.connect("B");
        }
        catch(Exception ex) {
            System.out.println("caught exception - as expected: " + ex);
        }

        transport.setBindPort(0); // fix the problem by picking an ephemeral port > 1024
        b.connect("B");
    }


    
    public void testReCreationWithSurvivingChannel() throws Exception {

        // Create 2 channels sharing a transport
        System.out.println("-- creating A");
        a=createSharedChannel(SINGLETON_1, "A");
        a.setReceiver(new MyReceiver("A"));
        a.connect("A");

        System.out.println("-- creating B");
        b=createSharedChannel(SINGLETON_1, "B");
        b.setReceiver(new MyReceiver("B"));
        b.connect("B");

        System.out.println("-- disconnecting A");
        a.disconnect();

        // a is disconnected so we should be able to create a new channel with group "A"
        System.out.println("-- creating A'");
        c=createSharedChannel(SINGLETON_1, "C");
        c.setReceiver(new MyReceiver("A'"));
        c.connect("A");
    }

    /**
     * Tests http://jira.jboss.com/jira/browse/JGRP-737
     * @throws Exception
     */
    public void testShutdownOfTimer() throws Exception {
        a=createSharedChannel(SINGLETON_1, "A");
        b=createSharedChannel(SINGLETON_1, "B");
        a.connect("x");
        b.connect("y");
        TimeScheduler timer1=a.getProtocolStack().getTransport().getTimer();
        TimeScheduler timer2=b.getProtocolStack().getTransport().getTimer();

        assert timer1 == timer2;

        assert !timer1.isShutdown();
        assert !timer2.isShutdown();

        Util.sleep(500);
        b.close();

        assert !timer2.isShutdown();
        assert !timer1.isShutdown();

        a.close(); // now, reference counting reaches 0, so the timer thread pool is stopped
        assert timer2.isShutdown();
        assert timer1.isShutdown();
    }


    /** Create channels A, B and C. Close A. This will close the timer and transports threads (!), so B will
     * not be able to send messages anymore, so C will not receive any messages
     * Tests http://jira.jboss.com/jira/browse/JGRP-737 */
    public void testSendingOfMessagesAfterChannelClose() throws Exception {
        MyReceiver rec_a=new MyReceiver("A"), rec_b=new MyReceiver("B"), rec_c=new MyReceiver("C");
        a=createSharedChannel(SINGLETON_1, "A");
        a.setName("A");
        a.setReceiver(rec_a);
        a.connect("one");

        b=createSharedChannel(SINGLETON_1, "B");
        b.setName("B");
        b.setReceiver(rec_b);
        b.connect("two");

        c=createSharedChannel(SINGLETON_2, "C");
        c.setName("C");
        c.setReceiver(rec_c);
        c.connect("two");

        b.send(null, "first");

        // msg delivery is asynchronous, so give members some time to receive the msg (incl retransmission)
        for(int i=0; i < 20; i++) {
            if(rec_b.size() == 1 && rec_c.size() == 1 && rec_a.size() == 0)
                break;
            Util.sleep(500);
        }

        assertSize(1, rec_b, rec_c);
        assertSize(0, rec_a);
        a.close();

        b.send(null, "second");
        Util.sleep(500);
        assertSize(0, rec_a);
        assertSize(2, rec_b, rec_c);
    }
    
    /**
     * Use a CountDownLatch to concurrently connect 3 channels; confirms
     * the channels connect
     * 
     * @throws Exception
     */
    public void testConcurrentCreation() throws Exception
    {
       a=createSharedChannel(SINGLETON_1, "A");
       r1=new MyReceiver("a");
       a.setReceiver(r1);

       b=createSharedChannel(SINGLETON_1, "B");
       r2=new MyReceiver("b");
       b.setReceiver(r2);

       c=createSharedChannel(SINGLETON_1, "C");
       r3=new MyReceiver("c");
       c.setReceiver(r3);
       
       CountDownLatch startLatch = new CountDownLatch(1);
       CountDownLatch finishLatch = new CountDownLatch(3);
       
       ConnectTask connectA = new ConnectTask(a, "a", startLatch, finishLatch);
       Thread threadA = new Thread(connectA);
       threadA.setDaemon(true);
       threadA.start();
       
       ConnectTask connectB = new ConnectTask(b, "b", startLatch, finishLatch);
       Thread threadB = new Thread(connectB);
       threadB.setDaemon(true);
       threadB.start();
       
       ConnectTask connectC = new ConnectTask(c, "c", startLatch, finishLatch);
       Thread threadC = new Thread(connectC);
       threadC.setDaemon(true);
       threadC.start();
       
       startLatch.countDown();
       
       try
       {
          boolean finished = finishLatch.await(20, TimeUnit.SECONDS);
          
          if (connectA.exception != null)
          {
             AssertJUnit.fail("connectA threw exception " + connectA.exception);
          }
          if (connectB.exception != null)
          {
             AssertJUnit.fail("connectB threw exception " + connectB.exception);
          }
          if (connectC.exception != null)
          {
             AssertJUnit.fail("connectC threw exception " + connectC.exception);
          }
          
          if (!finished) {
             if (threadA.isAlive())
                AssertJUnit.fail("threadA did not finish");
             if (threadB.isAlive())
                AssertJUnit.fail("threadB did not finish");
             if (threadC.isAlive())
                AssertJUnit.fail("threadC did not finish");
          }
       }
       finally
       {
          if (threadA.isAlive())
             threadA.interrupt();
          if (threadB.isAlive())
             threadB.interrupt();
          if (threadC.isAlive())
             threadC.interrupt();
       }
    }

    private static void assertSize(int expected, MyReceiver... receivers) {
        for(MyReceiver recv: receivers) {
            assertEquals(expected, recv.size());
        }
    }

    private JChannel createSharedChannel(String singleton_name, String name) throws Exception {
        ProtocolStackConfigurator config=ConfiguratorFactory.getStackConfigurator(channel_conf);
        List<ProtocolConfiguration> protocols=config.getProtocolStack();
        ProtocolConfiguration transport=protocols.get(0);
        transport.getProperties().put(Global.SINGLETON_NAME, singleton_name);
        JChannel ch=new JChannel(config);
        if(name != null)
            ch.setName(name);
        return ch;
    }


    protected static void makeUnique(Channel channel, int num) throws Exception {
        ProtocolStack stack=channel.getProtocolStack();
        TP transport=stack.getTransport();
        InetAddress bind_addr=transport.getBindAddress();

        if(transport instanceof UDP) {
            String mcast_addr=ResourceManager.getNextMulticastAddress();
            short mcast_port=ResourceManager.getNextMulticastPort(bind_addr);
            ((UDP)transport).setMulticastAddress(InetAddress.getByName(mcast_addr));
            ((UDP)transport).setMulticastPort(mcast_port);
        }
        else if(transport instanceof BasicTCP) {
            List<Short> ports=ResourceManager.getNextTcpPorts(bind_addr, num);
            transport.setBindPort(ports.get(0));
            transport.setPortRange(num);

            Protocol ping=stack.findProtocol(TCPPING.class);
            if(ping == null)
                throw new IllegalStateException("TCP stack must consist of TCP:TCPPING - other config are not supported");

            List<String> initial_hosts=new LinkedList<String>();
            for(short port: ports) {
                initial_hosts.add(bind_addr + "[" + port + "]");
            }
            String tmp=Util.printListWithDelimiter(initial_hosts, ",");
            List<IpAddress> init_hosts = Util.parseCommaDelimitedHosts(tmp, 1) ;
            ((TCPPING)ping).setInitialHosts(init_hosts) ;
        }
        else {
            throw new IllegalStateException("Only UDP and TCP are supported as transport protocols");
        }
    }



    private static class MyReceiver extends ReceiverAdapter {
        final List<Message> list=new LinkedList<Message>();
        final String name;

        private MyReceiver(String name) {
            this.name=name;
        }

        public List<Message> getList() {
            return list;
        }

        public int size() {
            return list.size();
        }

        public void clear() {
            list.clear();
        }

        public void receive(Message msg) {
            System.out.println("[" + name + "]: received message from " + msg.getSrc() + ": " + msg.getObject());
            list.add(msg);
        }

        public void viewAccepted(View new_view) {
            StringBuilder sb=new StringBuilder();
            sb.append("[" + name + "]: view = " + new_view);
            System.out.println(sb);
        }

        public String toString() {
            return super.toString() + " (size=" + list.size() + ")";
        }
    }
    
    private static class ConnectTask implements Runnable
    {
       private final Channel channel;
       private final String clusterName;
       private final CountDownLatch startLatch;
       private final CountDownLatch finishLatch;
       private Exception exception;
       
       ConnectTask(Channel channel, String clusterName, CountDownLatch startLatch, CountDownLatch finishLatch)
       {
          this.channel = channel;
          this.clusterName = clusterName;
          this.startLatch = startLatch;
          this.finishLatch = finishLatch;
       }
       
       public void run()
       {          
          try
          {
             startLatch.await();
             channel.connect(clusterName);
          }
          catch (Exception e)
          {
             e.printStackTrace(System.out);
             this.exception = e;
          }
          finally
          {
             finishLatch.countDown();
          }
         
       }
       
    }

}
