package org.jgroups.tests;

import org.jgroups.*;
import org.jgroups.protocols.Discovery;
import org.jgroups.protocols.MERGE2;
import org.jgroups.protocols.pbcast.CoordGmsImpl;
import org.jgroups.protocols.pbcast.GMS;
import org.jgroups.protocols.pbcast.NAKACK2;
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, d;
    protected MyReceiver ra, rb, rc, rd;
    protected boolean multicast_transport;

    @BeforeMethod
    protected void start() throws Exception {
        a=createChannel(true, 4);
        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");

        Util.waitUntilAllChannelsHaveSameSize(30000, 1000, a,b,c);
        multicast_transport=isMulticastTransport(a);
    }

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

    @SuppressWarnings("unchecked")
    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 use 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
    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);

        for(JChannel ch: new JChannel[]{a,b,c}) {
            GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
            gms.setLevel("trace");
        }

        System.out.println("\n==== Injecting a merge event into A, B and C ====");
        injectMergeEvent(merge_evt,a,b,c);

        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));

        for(JChannel ch: new JChannel[]{a,b,c}) {
            GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
            gms.setLevel("warn");
        }
    }


    /**
     * Test the following scenario (https://issues.jboss.org/browse/JGRP-1451):
     * - A: {A,C,B}
     * - B: {A,C,B}
     * - C: {A,C,B}
     * - D: {B,A,C,D}
     * - Merging should end up with a view where everybody has A, B, C and D in the same view
     */
    public void testMergeWithDifferentPartitions() throws Exception {
        d=createChannel(a);
        d.setName("D");
        rd=new MyReceiver("D", d);
        d.setReceiver(rd);
        modifyConfigs(d);
        d.connect("OverlappingMergeTest");

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

        View view_d=Util.createView(b.getAddress(), 4, b.getAddress(), a.getAddress(), c.getAddress(), d.getAddress());
        System.out.println("\n ==== Injecting view " + view_d + " into D ====\n");
        injectView(view_d, false, d);
        assert !Util.isCoordinator(d);

        for(JChannel ch: Arrays.asList(a,b,c,d))
            System.out.println(ch.getName() + ": " + ch.getView());


        // 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());
        views.put(d.getAddress(),d.getView());
        Event merge_evt=new Event(Event.MERGE, views);

        System.out.println("\n==== Injecting a merge event into A, B, C and D====");

        for(JChannel ch: new JChannel[]{a,b,c,d}) {
            GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
            gms.setLevel("trace");
        }

        injectMergeEvent(merge_evt,a,b,c,d);

        for(int i=0; i < 20; i++) {
            if(a.getView().size() == 4 && b.getView().size() == 4 && c.getView().size() == 4 && d.getView().size() == 4)
                break;
            Util.sleep(500);
        }

        for(JChannel ch: Arrays.asList(a,b,c,d))
            System.out.println(ch.getName() + ": " + ch.getView() + " (coord=" + isCoord(ch) + ")");

        for(JChannel ch: Arrays.asList(a,b,c,d))
            assert ch.getView().size() == 4: ch.getName() + ": view is " + ch.getView();
        System.out.println("\n");
    }


    /**
     * Tests a merge where all members have views whose ViewIds have the same creator, but different IDs, e.g.:
     * A: A|5 {A}
     * B: A|6 {A,B}
     * C: A|7 {A,B,C}
     */
    public void testSameCreatorDifferentIDs() throws Exception {
        MERGE2 merge=(MERGE2)a.getProtocolStack().findProtocol(MERGE2.class);
        if(merge == null) {
            merge=new MERGE2();
            a.getProtocolStack().insertProtocol(merge,ProtocolStack.ABOVE,Discovery.class);
            merge.init();
            merge.down(new Event(Event.SET_LOCAL_ADDRESS, a.getAddress()));
        }
        View view=Util.createView(a.getAddress(), 5, a.getAddress());
        injectView(view, a);

        view=Util.createView(a.getAddress(), 6, a.getAddress(), b.getAddress());
        injectView(view, b);

        view=Util.createView(a.getAddress(), 7, a.getAddress(), b.getAddress(), c.getAddress());
        injectView(view, c);

        System.out.println("\nA's view: " + a.getView());
        System.out.println("B's view: " + b.getView());
        System.out.println("C's view: " + c.getView());


        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);

        for(JChannel ch: new JChannel[]{a,b,c})
            ch.getProtocolStack().findProtocol(GMS.class).setLevel("trace");

        merge.sendMergeSolicitation();

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

        for(JChannel ch: new JChannel[]{a,b,c})
            ch.getProtocolStack().findProtocol(GMS.class).setLevel("warn");

        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());
    }



    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(": ");
            NAKACK2 nakack=(NAKACK2)ch.getProtocolStack().findProtocol(NAKACK2.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, boolean print_receivers, JChannel ... channels) {
        for(JChannel ch: channels) {
            GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
            gms.installView(view);
        }
        if(!print_receivers)
            return;
        for(JChannel ch: channels) {
            MyReceiver receiver=(MyReceiver)ch.getReceiver();
            System.out.println("[" + receiver.name + "] view=" + ch.getView());
        }
    }

    private static void injectView(View view, JChannel ... channels) {
        injectView(view, true, channels);
    }


    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, String.valueOf(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();
    }


    @SuppressWarnings("unchecked")
    protected void checkReceivedMessages(Tuple<MyReceiver,Integer> ... expected_messages) {
        for(int i=0; i < 15; 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(1000);
        }

        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 + ")";
        }
    }

    @SuppressWarnings("unchecked")
    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();
    }


    protected boolean isCoord(JChannel ch) {
        GMS gms=(GMS)ch.getProtocolStack().findProtocol(GMS.class);
        return gms.getImpl() instanceof CoordGmsImpl;
    }


    private static void modifyConfigs(JChannel ... channels) throws Exception {
        for(JChannel ch: channels) {
            ProtocolStack stack=ch.getProtocolStack();
            stack.removeProtocols("MERGE2","MERGE3","FD_SOCK","FD","FD_ALL","FC","MFC","UFC","VERIFY_SUSPECT", "STATE_TRANSFER");
            NAKACK2 nak=(NAKACK2)stack.findProtocol(NAKACK2.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;
        }
    }



}