package org.jgroups.protocols;

import org.jgroups.*;
import org.jgroups.protocols.pbcast.GMS;
import org.jgroups.protocols.pbcast.NAKACK2;
import org.jgroups.protocols.pbcast.STABLE;
import org.jgroups.stack.Protocol;
import org.jgroups.stack.ProtocolStack;
import org.jgroups.util.Util;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import javax.security.auth.callback.*;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static org.testng.AssertJUnit.assertTrue;

@Test(groups = Global.FUNCTIONAL, singleThreaded = true)
public class SASLTest {
    private static final String REALM = "MyRealm";
    private JChannel a;
    private JChannel b;

    private static JChannel createChannel(String channelName, String mech, String username) throws Exception {
        SASL sasl = new SASL();
        sasl.setMech(mech);
        sasl.setClientCallbackHandler(new MyCallbackHandler(username));
        sasl.setServerCallbackHandler(new MyCallbackHandler(username));
        sasl.setTimeout(5000);
        sasl.sasl_props.put("com.sun.security.sasl.digest.realm", REALM);
        sasl.setLevel("trace");
        GMS gms = new GMS();
        gms.setJoinTimeout(3000);
        return new JChannel(new Protocol[] { new SHARED_LOOPBACK(), new PING(), new MERGE3(), new NAKACK2(),
                new UNICAST3(), new STABLE(), sasl, gms }).name(channelName);
    }

    public void testSASLDigestMD5() throws Exception {
        a = createChannel("A", "DIGEST-MD5", "jack");
        b = createChannel("B", "DIGEST-MD5", "jack");
        a.connect("SaslTest");
        b.connect("SaslTest");
        assertTrue(b.isConnected());
    }

    @Test(expectedExceptions = SecurityException.class)
    public void testSASLDigestMD5Failure() throws Throwable {
        a = createChannel("A", "DIGEST-MD5", "jack");
        b = createChannel("B", "DIGEST-MD5", "jill");
        a.connect("SaslTest");
        try {
            b.connect("SaslTest");
        } catch (Exception e) {
            if (e.getCause() != null)
                throw e.getCause();
        }
    }

    public void testSASLDigestMD5Merge() throws Exception {
        a = createChannel("A", "DIGEST-MD5", "jack");
        b = createChannel("B", "DIGEST-MD5", "jack");
        a.connect("SaslTest");
        b.connect("SaslTest");
        assertTrue(b.isConnected());
        print(a, b);
        createPartitions(a, b);
        print(a, b);
        assertTrue(checkViewSize(1, a, b));
        dropDiscard(a, b);
        mergePartitions(a, b);
        for(int i = 0; i < 10 && !checkViewSize(2, a, b); i++) {
            Util.sleep(500);
        }
        assertTrue(viewContains(a.getView(), a, b));
        assertTrue(viewContains(b.getView(), a, b));
    }

    private boolean viewContains(View view, JChannel... channels) {
        boolean b = true;
        for (JChannel ch : channels) {
            b = b && view.containsMember(ch.getAddress());
        }
        return b;
    }

    private void dropDiscard(JChannel... channels) {
        for (JChannel ch : channels) {
            ch.getProtocolStack().removeProtocol(DISCARD.class);
        }
    }

    private boolean checkViewSize(int expectedSize, JChannel... channels) {
        boolean b = true;
        for (JChannel ch : channels) {
            b = b && ch.getView().size() == expectedSize;
        }
        return b;
    }

    @AfterMethod
    public void cleanup() {
        a.close();
        b.close();
    }

    private static void createPartitions(JChannel... channels) throws Exception {
        for (JChannel ch : channels) {
            DISCARD discard = new DISCARD();
            discard.setDiscardAll(true);
            ch.getProtocolStack().insertProtocol(discard, ProtocolStack.Position.ABOVE, TP.class);
        }

        for (JChannel ch : channels) {
            View view = View.create(ch.getAddress(), 10, ch.getAddress());
            GMS gms = (GMS) ch.getProtocolStack().findProtocol(GMS.class);
            gms.installView(view);
        }
    }

    private static void mergePartitions(JChannel... channels) throws Exception {
        Membership membership = new Membership();
        for (JChannel ch : channels) {
            membership.add(ch.getAddress());
        }
        membership.sort();
        Address leaderAddress = membership.elementAt(0);
        JChannel leader = findChannelByAddress(leaderAddress, channels);
        GMS gms = (GMS) leader.getProtocolStack().findProtocol(GMS.class);
        gms.setLevel("trace");
        Map<Address, View> views =new HashMap<>();
        for (JChannel ch : channels) {
            views.put(ch.getAddress(), ch.getView());
        }
        gms.up(new Event(Event.MERGE, views));
    }

    private static JChannel findChannelByAddress(Address address, JChannel... channels) {
        for (JChannel ch : channels) {
            if (ch.getAddress().equals(address)) {
                return ch;
            }
        }
        return null;
    }

    private static void print(JChannel... channels) {
        for (JChannel ch : channels) {
            System.out.println(ch.getName() + ": " + ch.getView());
        }
    }

    public static class MyCallbackHandler implements CallbackHandler {
        final private String password;

        public MyCallbackHandler(String password) {
            this.password = password;
        }

        @Override
        public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
            for (Callback callback : callbacks) {
                if (callback instanceof NameCallback) {
                    NameCallback nameCallback = (NameCallback) callback;
                    nameCallback.setName("user");
                } else if (callback instanceof PasswordCallback) {
                    PasswordCallback passwordCallback = (PasswordCallback) callback;
                    passwordCallback.setPassword(password.toCharArray());
                } else if (callback instanceof AuthorizeCallback) {
                    AuthorizeCallback authorizeCallback = (AuthorizeCallback) callback;
                    authorizeCallback.setAuthorized(
                            authorizeCallback.getAuthenticationID().equals(authorizeCallback.getAuthorizationID()));
                } else if (callback instanceof RealmCallback) {
                    RealmCallback realmCallback = (RealmCallback) callback;
                    realmCallback.setText(REALM);
                } else {
                    throw new UnsupportedCallbackException(callback);
                }
            }
        }

    }
}
