package org.jgroups.tests;

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

import java.util.Arrays;
import java.util.List;
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,Global.EAP_EXCLUDED}, singleThreaded=true)
public class ConcurrentFlushTest {
    protected JChannel a, b, c;

    @AfterMethod void tearDown() throws Exception {Util.close(c,b,a);}


    /** Tests A.startFlush(), followed by another A.startFlush() */
    public void testTwoStartFlushesOnSameMemberWithTotalFlush() throws Exception {
        a=createChannel("A");
        a.connect("testTwoStartFlushes");
        b=createChannel("B");
        b.connect("testTwoStartFlushes");
        Util.waitUntilAllChannelsHaveSameView(10000, 500, a, b);

        assert startFlush(a, true);
        assert startFlush(a, false);
        assert !startFlush(a, 1, 500, false);
        a.stopFlush();
        assert startFlush(a, true);
    }

    /** Tests A.startFlush(), followed by another A.startFlush() */
    public void testTwoStartFlushesOnDifferentMembersWithTotalFlush() throws Exception {
        a=createChannel("A");
        a.connect("testTwoStartFlushesOnDifferentMembersWithTotalFlush");
        b=createChannel("B");
        b.connect("testTwoStartFlushesOnDifferentMembersWithTotalFlush");
        Util.waitUntilAllChannelsHaveSameView(10000, 500, a, b);

        assert startFlush(a, false);
        assert !startFlush(b, 1, 500, false);
        a.stopFlush();
        assert startFlush(b, false);
        b.stopFlush();
        assert startFlush(a, false);
        b.stopFlush(); // c2 can actually stop a flush started by c1
        assert startFlush(b, true);
    }

    /** Tests 2 channels calling FLUSH simultaneously */
    public void testConcurrentFlush() throws Exception {
        a=createChannel("A");
        a.connect("testConcurrentFlush");
        b=createChannel("B");
        b.connect("testConcurrentFlush");
        Util.waitUntilAllChannelsHaveSameView(10000, 500, a, b);

        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 Flusher(startFlushLatch, stopFlushLatch, a, null);
        Thread t2=new Flusher(startFlushLatch, stopFlushLatch, b, null);
        Listener l1=new Listener("c1",a, flushStartReceived, flushStopReceived);
        Listener l2=new Listener("c2",b, flushStartReceived, flushStopReceived);
        t1.start();
        t2.start();

        startFlushLatch.countDown();

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

        // at this stage both channels should have started a flush
        stopFlushLatch.countDown();
        t1.join();
        t2.join();
        assert 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 */
    public void testConcurrentFlushAndPartialFlush() throws Exception {
        a=createChannel("A");
        a.connect("testConcurrentFlushAndPartialFlush");

        b=createChannel("B");
        b.connect("testConcurrentFlushAndPartialFlush");

        c=createChannel("C");
        c.connect("testConcurrentFlushAndPartialFlush");
        Util.waitUntilAllChannelsHaveSameView(10000, 500, a, b, c);

        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 Flusher(startFlushLatch, stopFlushLatch, a, null);
        Thread t2=new Flusher(startFlushLatch, stopFlushLatch, b, Arrays.asList(b.getAddress(),c.getAddress()));
        Listener l1=new Listener("c1",a, flushStartReceived, flushStopReceived);
        Listener l2=new Listener("c2",b, flushStartReceived, flushStopReceived);
        Listener l3=new Listener("c3",c, flushStartReceived, flushStopReceived);

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

        startFlushLatch.countDown();

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

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

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

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

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

    protected static boolean startFlush(Channel ch, boolean automatic_resume) {
        boolean result=Util.startFlush(ch);
        if(automatic_resume)
            ch.stopFlush();
        return result;
    }

    protected boolean startFlush(Channel ch, int num_attempts, long timeout, boolean automatic_resume) {
        boolean result=Util.startFlush(ch, num_attempts, 10, timeout);
        if(automatic_resume)
            ch.stopFlush();
        return result;
    }


    protected JChannel createChannel(String name) throws Exception {
        return new JChannel(Util.getTestStack(new FLUSH())).name(name);
    }

    protected interface EventSequence {
        /** Return an event string. Events are translated as follows: get state='g', set state='s',
         *  block='b', unlock='u', view='v' */
        String getEventSequence();
        String getName();
    }

    protected static class Flusher extends Thread {
        protected final CountDownLatch startFlushLatch, stopFlushLatch;
        protected final JChannel       ch;
        protected final List<Address>  flushParticipants;

        public Flusher(CountDownLatch startFlushLatch, CountDownLatch stopFlushLatch, JChannel ch, List<Address> flushParticipants) {
            this.startFlushLatch=startFlushLatch;
            this.stopFlushLatch=stopFlushLatch;
            this.ch=ch;
            this.flushParticipants=flushParticipants;
        }

        public void run() {
            try {
                startFlushLatch.await();
                boolean rc=flushParticipants != null && !flushParticipants.isEmpty()?
                  Util.startFlush(ch, flushParticipants) : Util.startFlush(ch);
                System.out.println("Flusher " + Thread.currentThread().getId() + ": rc=" + rc);
            }
            catch(InterruptedException e) {
                interrupt();
            }

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

    protected 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;}

    }

    
}
