package org.jgroups.tests;

import org.jgroups.*;
import org.jgroups.util.Util;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Tests concurrent and partial flushes
 * @author Manik Surtani
 * @author Bela Ban
 */
@Test(groups=Global.FLUSH, sequential=true)
public class ConcurrentFlushTest extends ChannelTestBase {

    JChannel c1, c2, c3;

    @AfterMethod
    void tearDown() throws Exception {
        Util.close(c3, c2, c1);
    }

    protected boolean useBlocking() {
        return true;
    }



    /**
     * Tests A.startFlush(), followed by another A.startFlush()
     */
    @Test
    public void testTwoStartFlushesOnSameMemberWithTotalFlush() throws Exception {
        c1=createChannel(true, 3);
        c1.connect("testTwoStartFlushes");
        c2=createChannel(c1);
        c2.connect("testTwoStartFlushes");
        assertViewsReceived(c1, c2);

        boolean rc=startFlush(c1, true);
        assert rc;

        rc=startFlush(c1, false);
        assert rc;

        rc=startFlush(c1, 1, 500, false);
        assert !rc;
        stopFlush(c1);

        rc=startFlush(c1, true);
        assert rc;
    }

    /**
     * Tests A.startFlush(), followed by another A.startFlush()
     */
    public void testTwoStartFlushesOnDifferentMembersWithTotalFlush() throws Exception {
        c1=createChannel(true, 3);
        c1.connect("testTwoStartFlushesOnDifferentMembersWithTotalFlush");
        c2=createChannel(c1);
        c2.connect("testTwoStartFlushesOnDifferentMembersWithTotalFlush");
        assertViewsReceived(c1, c2);

        boolean rc=startFlush(c1, false);
        assert rc;

        rc=startFlush(c2, 1, 500, false);
        assert !rc;

        stopFlush(c1);

        rc=startFlush(c2, false);
        assert rc;
        stopFlush(c2);

        rc=startFlush(c1, false);
        assert rc;
        stopFlush(c2); // c2 can actually stop a flush started by c1

        rc=startFlush(c2, true);
        assert rc;
    }

    /**
     * Tests 2 channels calling FLUSH simultaneously
     */
    @Test
    public void testConcurrentFlush() throws Exception {
        c1=createChannel(true, 2);
        c1.connect("testConcurrentFlush");
        c2=createChannel(c1);
        c2.connect("testConcurrentFlush");

        assertViewsReceived(c1, c2);

        final CountDownLatch startFlushLatch=new CountDownLatch(1);
        final CountDownLatch stopFlushLatch=new CountDownLatch(1);
        final CountDownLatch flushStartReceived=new CountDownLatch(2);
        final CountDownLatch flushStopReceived=new CountDownLatch(2);

        Thread t1=new Thread() {
            public void run() {
                try {
                    startFlushLatch.await();
                    boolean rc=Util.startFlush(c1);
                    System.out.println("t1: rc=" + rc);
                }
                catch(InterruptedException e) {
                    interrupt();
                }

                try {
                    stopFlushLatch.await();
                }
                catch(InterruptedException e) {
                    interrupt();
                }
                finally {
                    c1.stopFlush();
                }
            }
        };

        Thread t2=new Thread() {
            public void run() {
                try {
                    startFlushLatch.await();
                    boolean rc=Util.startFlush(c2);
                    System.out.println("t2: rc=" + rc);
                }
                catch(InterruptedException e) {
                    interrupt();
                }

                try {
                    stopFlushLatch.await();
                }
                catch(InterruptedException e) {
                    interrupt();
                }
                finally {
                    c2.stopFlush();
                }
            }
        };

        Listener l1=new Listener("c1", c1, flushStartReceived, flushStopReceived);
        Listener l2=new Listener("c2", c2, flushStartReceived, flushStopReceived);
        t1.start();
        t2.start();

        startFlushLatch.countDown();

        assertTrue(flushStartReceived.await(60, TimeUnit.SECONDS));

        // at this stage both channels should have started a flush
        stopFlushLatch.countDown();

        t1.join();
        t2.join();

        assertTrue(flushStopReceived.await(60, TimeUnit.SECONDS));


        assert l1.blockReceived;
        assert l1.unblockReceived;
        assert l2.blockReceived;
        assert l2.unblockReceived;
    }

    /**
     * Tests 2 channels calling partial FLUSHes and one calling FLUSH simultaneously
     */
    @Test
    public void testConcurrentFlushAndPartialFlush() throws Exception {
        c1=createChannel(true, 3);
        c1.connect("testConcurrentFlushAndPartialFlush");

        c2=createChannel(c1);
        c2.connect("testConcurrentFlushAndPartialFlush");

        c3=createChannel(c1);
        c3.connect("testConcurrentFlushAndPartialFlush");
        assertViewsReceived(c1, c2, c3);

        final CountDownLatch startFlushLatch=new CountDownLatch(1);
        final CountDownLatch stopFlushLatch=new CountDownLatch(1);

        // 2 because either total or partial has to finish first
        final CountDownLatch flushStartReceived=new CountDownLatch(2);

        // 5 because we have total and partial flush
        final CountDownLatch flushStopReceived=new CountDownLatch(5);

        Thread t1=new Thread() {
            public void run() {
                try {
                    startFlushLatch.await();
                    boolean rc=Util.startFlush(c1);
                    System.out.println("t1: rc=" + rc);
                }
                catch(InterruptedException e) {
                    interrupt();
                }

                try {
                    stopFlushLatch.await();
                }
                catch(InterruptedException e) {
                    interrupt();
                }
                finally {
                    c1.stopFlush();
                }
            }
        };

        Thread t2=new Thread() {
            public void run() {
                try {
                    startFlushLatch.await();
                    // partial, only between c2 and c3
                    boolean rc=Util.startFlush(c2, (Arrays.asList(c2.getAddress(), c3.getAddress())));
                    System.out.println("t2: partial flush rc=" + rc);
                }
                catch(InterruptedException e) {
                    interrupt();
                }

                try {
                    stopFlushLatch.await();
                }
                catch(InterruptedException e) {
                    interrupt();
                }
                finally {
                    c2.stopFlush(Arrays.asList(c2.getAddress(), c3.getAddress()));
                }
            }
        };

        Listener l1=new Listener("c1", c1, flushStartReceived, flushStopReceived);
        Listener l2=new Listener("c2", c2, flushStartReceived, flushStopReceived);
        Listener l3=new Listener("c3", c3, flushStartReceived, flushStopReceived);

        t1.start();
        t2.start();

        startFlushLatch.countDown();

        assertTrue(flushStartReceived.await(60, TimeUnit.SECONDS));

        // at this stage both channels should have started a flush?
        stopFlushLatch.countDown();

        t1.join();
        t2.join();

        assertTrue(flushStopReceived.await(60, TimeUnit.SECONDS));

        assert l1.blockReceived;
        assert l1.unblockReceived;
        assert l2.blockReceived;
        assert l2.unblockReceived;
        assert l3.blockReceived;
        assert l3.unblockReceived;
    }

    private boolean startFlush(Channel ch, boolean automatic_resume) {
        log.debug("starting flush on " + ch.getAddress() + " with automatic resume=" + automatic_resume);
        boolean result=Util.startFlush(ch);
        if(automatic_resume) {
            ch.stopFlush();
        }
        return result;
    }

    private boolean startFlush(Channel ch, int num_attempts, long timeout, boolean automatic_resume) {
        log.debug("starting flush on " + ch.getAddress() + " with automatic resume=" + automatic_resume);
        boolean result=Util.startFlush(ch, num_attempts, 10, timeout);
        if(automatic_resume) {
            ch.stopFlush();
        }
        return result;
    }

    private void stopFlush(Channel ch) {
        log.debug("calling stopFlush()");
        ch.stopFlush();
    }

    private static void assertViewsReceived(JChannel... channels) {
        for(JChannel c : channels)
            assertEquals(c.getView().getMembers().size(), channels.length);
    }

    private static class Listener extends ReceiverAdapter implements EventSequence {
        final String name;
        boolean  blockReceived, unblockReceived;
        JChannel channel;
        CountDownLatch flushStartReceived, flushStopReceived;
        final StringBuilder events=new StringBuilder();


        Listener(String name, JChannel channel, CountDownLatch flushStartReceived, CountDownLatch flushStopReceived) {
            this.name=name;
            this.channel=channel;
            this.flushStartReceived=flushStartReceived;
            this.flushStopReceived=flushStopReceived;
            this.channel.setReceiver(this);
        }

        public void unblock() {
            unblockReceived=true;
            if(flushStopReceived != null)
                flushStopReceived.countDown();
            events.append('u');
        }

        public void block() {
            blockReceived=true;
            if(flushStartReceived != null)
                flushStartReceived.countDown();
            events.append('b');
        }

        public String getEventSequence() {
            return events.toString();
        }

        public void viewAccepted(View new_view) {
            events.append('v');
        }

        public String getName() {
            return name;
        }

    }

    
}
