package org.jgroups.tests;

import org.jgroups.*;
import org.jgroups.protocols.pbcast.GMS;
import org.jgroups.protocols.pbcast.NAKACK;
import org.jgroups.protocols.pbcast.STABLE;
import org.jgroups.stack.ProtocolStack;
import org.jgroups.util.Digest;
import org.jgroups.util.Tuple;
import org.jgroups.util.Util;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.*;

/**
 * Tests overlapping merges, e.g. A: {A,B}, B: {A,B} and C: {A,B,C}. Tests unicast as well as multicast seqno tables.<br/>
 * Related JIRA: https://jira.jboss.org/jira/browse/JGRP-940
 * @author Bela Ban
 */
@Test(groups=Global.STACK_DEPENDENT,sequential=true)
public class OverlappingMergeTest extends ChannelTestBase {
    protected JChannel a, b, c;
    protected MyReceiver ra, rb, rc;
    protected boolean multicast_transport;

    @BeforeMethod
    protected void start() throws Exception {
        a=createChannel(true, 3);
        a.setName("A");
        ra=new MyReceiver("A", a);
        a.setReceiver(ra);

        b=createChannel(a);
        b.setName("B");
        rb=new MyReceiver("B", b);
        b.setReceiver(rb);

        c=createChannel(a);
        c.setName("C");
        rc=new MyReceiver("C", c);
        c.setReceiver(rc);
        modifyConfigs(a, b, c);

        a.connect("OverlappingMergeTest");
        b.connect("OverlappingMergeTest");
        c.connect("OverlappingMergeTest");
        View view=c.getView();
        assert view.size() == 3 : "view is " + view;

        multicast_transport=isMulticastTransport(a);
    }

    @AfterMethod
    protected void stop() throws Exception {
        Util.close(c,b,a);
        ra.clear(); rb.clear(); rc.clear();
    }

    public void testRegularMessageSending() throws Exception {
        sendMessages(5, a, b, c);
        checkReceivedMessages(make(ra, 15), make(rb,15), make(rc,15));
    }

    /**
     * Verifies that unicasts are received correctly by all participants after an overlapping merge. The following steps
     * are executed:
     * <ol>
     * <li/>Group is {A,B,C}, A is the coordinator
     * <li/>MERGE2 is removed from all members
     * <li/>VERIFY_SUSPECT is removed from all members
     * <li/>Everyone sends 5 unicast messages to everyone else
     * <li/>Everyone sends 5 multicasts
     * <li/>A SUSPECT(A) event is injected into B's stack (GMS). This causes a new view {B,C} to be multicast by B
     * <li/>B and C install {B,C}
     * <li/>B and C trash the connection table for A in UNICAST
     * <li/>A ignores the view, it still has view {A,B,C} and all connection tables intact in UNICAST
     * <li/>We now inject a MERGE(A,B) event into A. This should ause A and B as coords to create a new MergeView {A,B,C}
     * <li/>The merge already fails because the unicast between A and B fails due to the reason given below !
     *      Once this is fixed, the next step below should work, too !
     * <li/>A sends a unicast to B and C. This should fail until JGRP-940 has been fixed !
     * <li/>Reason: B and C trashed A's conntables in UNICAST, but A didn't trash its conn tables for B and C, so
     * we have non-matching seqnos !
     * </ol>
     */
    public void testOverlappingMergeWithBC() throws Exception {
        sendMessages(5, a, b, c);
        checkReceivedMessages(make(ra, 15), make(rb,15), make(rc,15));

        // Inject view {B,C} into B and C:
        View new_view=Util.createView(b.getAddress(), 10, b.getAddress(), c.getAddress());
        System.out.println("\n ==== Injecting view " + new_view + " into B and C ====");
        injectView(new_view, b, c);
        makeCoordinator(b);
        assert Util.isCoordinator(a);
        assert Util.isCoordinator(b);
        assert !Util.isCoordinator(c);

        System.out.println("A's view: " + a.getView());
        System.out.println("B's view: " + b.getView());
        System.out.println("C's view: " + c.getView());
        assert a.getView().size() == 3 : "A's view is " + a.getView();
        assert b.getView().size() == 2 : "B's view is " + b.getView();
        assert c.getView().size() == 2 : "C's view is " + c.getView();

        System.out.println("\n==== Sending messages while the cluster is partitioned ====");
        sendMessages(5, a, b, c);
        if(multicast_transport) {
            // B and C drop A's multicasts, but A will receive B's and C's multicasts
            checkReceivedMessages(make(ra, 15), make(rb,10), make(rc,10));
        }
        else {
            // B and C drop A's multicasts, and won't send their multicasts to A (A only receives its owm multicasts)
            checkReceivedMessages(make(ra, 5), make(rb,10), make(rc,10));
        }
        
        System.out.println("\n ==== Digests are:\n" + dumpDigests(a,b,c));

        // start merging
        Map<Address,View> views=new HashMap<Address,View>();
        views.put(a.getAddress(), a.getView());
        views.put(b.getAddress(), b.getView());
        views.put(c.getAddress(), c.getView());
        Event merge_evt=new Event(Event.MERGE, views);
        JChannel merge_leader=determineMergeLeader(a, b);
        System.out.println("\n==== Injecting a merge event (leader=" + merge_leader.getAddress() + ") ====");
        injectMergeEvent(merge_evt, merge_leader);

        System.out.println("\n==== checking views after merge ====:");
        for(int i=0; i < 10; i++) {
            if(a.getView().size() == 3 && b.getView().size() == 3 && c.getView().size() == 3) {
                System.out.println("views are correct: all views have a size of 3");
                break;
            }
            System.out.print(".");
            runStableProtocol(a); runStableProtocol(b); runStableProtocol(c);
            Util.sleep(1000);
        }

        System.out.println("\n ==== Digests after the merge:\n" + dumpDigests(a,b,c));

        View va=a.getView(), vb=b.getView(), vc=c.getView();
        System.out.println("\nA's view: " + va);
        System.out.println("B's view: " + vb);
        System.out.println("C's view: " + vc);
        assert va.size() == 3 : "A's view is " + va;
        assert vb.size() == 3 : "B's view is " + vb;
        assert vc.size() == 3 : "C's view is " + vc;

        System.out.println("\n==== Sending messages after merge ====");
        sendMessages(5, a, b, c);
        checkReceivedMessages(make(ra, 15), make(rb,15), make(rc,15));
    }


    /**
     * Verifies that unicasts are received correctly by all participants after an overlapping merge. The following steps
     * are executed:
     * <ol>
     * <li/>Group is {A,B,C}
     * <li/>Install view {A,C} in A and {A,B,C} in B and C
     * <li/>Try to initiate a merge. This should FAIL until https://jira.jboss.org/jira/browse/JGRP-937 has
     *      been implemented: B and C's MERGE2 protocols will never send out merge requests as they see A as coord 
     * </ol>
     */
    @Test(enabled=true)
    public void testOverlappingMergeWithABC() throws Exception {
        sendMessages(5, a, b, c);
        checkReceivedMessages(make(ra, 15), make(rb,15), make(rc,15));

        // Inject view {A,C} into A:
        View new_view=Util.createView(a.getAddress(), 4, a.getAddress(), c.getAddress());
        System.out.println("\n ==== Injecting view " + new_view + " into A ====");
        injectView(new_view, a);
        assertTrue(Util.isCoordinator(a));
        assertFalse(Util.isCoordinator(b));
        assertFalse(Util.isCoordinator(c));

        System.out.println("A's view: " + a.getView());
        System.out.println("B's view: " + b.getView());
        System.out.println("C's view: " + c.getView());
        assertEquals("A's view is " + a.getView(), 2, a.getView().size());
        assertEquals("B's view is " + b.getView(), 3, b.getView().size());
        assertEquals("C's view is " + c.getView(), 3, c.getView().size());


        // start merging
        Map<Address,View> views=new HashMap<Address,View>();
        views.put(a.getAddress(), a.getView());
        views.put(b.getAddress(), b.getView());
        views.put(c.getAddress(), c.getView());
        Event merge_evt=new Event(Event.MERGE, views);
        System.out.println("\n==== Injecting a merge event (leader=" + a.getAddress() + ") ====");
        injectMergeEvent(merge_evt, a);

        System.out.println("\n==== checking views after merge ====:");
        for(int i=0; i < 10; i++) {
            if(a.getView().size() == 3 && b.getView().size() == 3 && c.getView().size() == 3) {
                System.out.println("views are correct: all views have a size of 3");
                break;
            }
            System.out.print(".");
            for(JChannel ch: new JChannel[]{a,b,c})
                runStableProtocol(ch);
            Util.sleep(1000);
        }

        System.out.println("\n ==== Digests after the merge:\n" + dumpDigests(a,b,c));

        View va=a.getView(), vb=b.getView(), vc=c.getView();
        System.out.println("\nA's view: " + va);
        System.out.println("B's view: " + vb);
        System.out.println("C's view: " + vc);
        assertEquals("A's view is " + va, 3, va.size());
        assertEquals("B's view is " + vb, 3, vb.size());
        assertEquals("C's view is " + vc, 3, vc.size());

        System.out.println("\n==== Sending messages after merge ====");
        sendMessages(5, a, b, c);
        checkReceivedMessages(make(ra, 15), make(rb,15), make(rc,15));
    }


    private static void makeCoordinator(JChannel ch) {
        GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
        gms.becomeCoordinator();
    }


    private static String dumpDigests(JChannel ... channels) {
        StringBuilder sb=new StringBuilder();
        for(JChannel ch: channels) {
            sb.append(ch.getAddress()).append(": ");
            NAKACK nakack=(NAKACK)ch.getProtocolStack().findProtocol(NAKACK.class);
            Digest digest=nakack.getDigest();
            sb.append(digest).append("\n");
        }
        return sb.toString();
    }

    private static JChannel determineMergeLeader(JChannel ... coords) {
        Membership tmp=new Membership();
        for(JChannel ch: coords) {
            tmp.add(ch.getAddress());
        }
        tmp.sort();
        Address  merge_leader=tmp.elementAt(0);
        for(JChannel ch: coords) {
            if(ch.getAddress().equals(merge_leader))
                return ch;
        }
        return null;
    }

    private static void injectView(View view, JChannel ... channels) {
        for(JChannel ch: channels) {
            GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
            gms.installView(view);
        }
        for(JChannel ch: channels) {
            MyReceiver receiver=(MyReceiver)ch.getReceiver();
            System.out.println("[" + receiver.name + "] view=" + ch.getView());
        }
    }


    private static void injectMergeEvent(Event evt, JChannel ... channels) {
        for(JChannel ch: channels) {
            GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
            gms.up(evt);
        }
    }


    private void sendMessages(int num_msgs, JChannel... channels) throws Exception {
        ra.clear(); rb.clear(); rc.clear();
        for(JChannel ch: channels) {
            for(int i=1; i <= num_msgs; i++)
                ch.send(null, null, "#" + i);
        }
    }

    private static void runStableProtocol(JChannel ch) {
        STABLE stable=(STABLE)ch.getProtocolStack().findProtocol(STABLE.class);
        if(stable != null)
            stable.runMessageGarbageCollection();
    }

    protected boolean isMulticastTransport(JChannel ch) {
        return ch.getProtocolStack().getTransport().supportsMulticasting();
    }



    protected void checkReceivedMessages(Tuple<MyReceiver,Integer> ... expected_messages) {
        for(int i=0; i < 30; i++) {
            boolean all_received=true;
            for(Tuple<MyReceiver,Integer> tuple: expected_messages) {
                MyReceiver receiver=tuple.getVal1();
                List<Message> mcasts=receiver.getMulticasts();
                int mcasts_received=mcasts.size();
                int expected_mcasts=tuple.getVal2();
                if(mcasts_received != expected_mcasts) {
                    all_received=false;
                    break;
                }
                runStableProtocol(receiver.ch);
            }
            if(all_received)
                break;
            Util.sleep(500);
        }

        for(Tuple<MyReceiver,Integer> tuple: expected_messages) {
            MyReceiver receiver=tuple.getVal1();
            List<Message> mcasts=receiver.getMulticasts();
            int mcasts_received=mcasts.size();
            System.out.println("receiver " + receiver + ": mcasts=" + mcasts_received);
        }

        for(Tuple<MyReceiver,Integer> tuple: expected_messages) {
            MyReceiver receiver=tuple.getVal1();
            List<Message> mcasts=receiver.getMulticasts();
            int mcasts_received=mcasts.size();
            int expected_mcasts=tuple.getVal2();
            assert mcasts_received == expected_mcasts : "(" + receiver.name + ") num_mcasts=" + print(mcasts) +
              " expected: " + expected_mcasts + ")";
        }
    }

    protected Tuple<MyReceiver,Integer> make(MyReceiver r, int expected_msgs) {
        return new Tuple<MyReceiver,Integer>(r, expected_msgs);
    }


    private static String print(List<Message> msgs) {
        StringBuilder sb=new StringBuilder();
        for(Message msg: msgs) {
            sb.append(msg.getSrc()).append(": ").append(msg.getObject()).append(" ");
        }
        return sb.toString();
    }


    private static void modifyConfigs(JChannel ... channels) throws Exception {
        for(JChannel ch: channels) {
            ProtocolStack stack=ch.getProtocolStack();
            stack.removeProtocol("MERGE2");
            stack.removeProtocol("FC");
            stack.removeProtocol("VERIFY_SUSPECT");
            NAKACK nak=(NAKACK)stack.findProtocol(NAKACK.class);
            if(nak != null)
                nak.setLogDiscardMessages(false);
        }
    }



    protected static class MyReceiver extends ReceiverAdapter {
        final String name;
        View view=null;
        final JChannel ch;
        final List<Message> mcasts=new ArrayList<Message>(20);

        public MyReceiver(String name, JChannel ch) {
            this.name=name;
            this.ch=ch;
        }

        public void receive(Message msg) {
            Address dest=msg.getDest();
            if(dest == null)
                mcasts.add(msg);
        }

        public void viewAccepted(View new_view) {
            view=new_view;
        }

        public List<Message> getMulticasts() { return mcasts; }
        public void clear() {mcasts.clear();}
        public Address getAddress() {return ch != null? ch.getAddress() : null;}

        public String toString() {
            return name;
        }
    }



}