/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.slick.protocol.gibberish;

import java.util.*;

import junit.framework.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;

/**
 * Tests in this class verify whether a precreated contact list is still there
 * and whether it creating contact groups works as expected.
 *
 * @author Emil Ivov
 */
public class TestOperationSetPersistentPresence
    extends TestCase
{
    private static final Logger logger =
        Logger.getLogger(TestOperationSetPersistentPresence.class);

    private GibberishSlickFixture fixture = new GibberishSlickFixture();
    private OperationSetPersistentPresence opSetPersPresence1 = null;
    private OperationSetPersistentPresence opSetPersPresence2 = null;
    private static final String testGroupName = "NewGroup";
    private static final String testGroupName2 = "Renamed";

    public TestOperationSetPersistentPresence(String name)
    {
        super(name);
    }

    /**
     * Creates a test suite containing all tests of this class followed by
     * test methods that we want executed in a specified order.
     * @return the Test suite to run
     */
    public static Test suite()
    {
        TestSuite suite =
            new TestSuite();

        //the following 2 need to be run in the specified order.
        //(postTestRemoveGroup() needs the group created from
        //postTestCreateGroup() )
        suite.addTest(
            new TestOperationSetPersistentPresence("postTestCreateGroup"));

        //rename
        suite.addTest( new TestOperationSetPersistentPresence(
            "postTestRenameGroup"));

        suite.addTest(
            new TestOperationSetPersistentPresence("postTestRemoveGroup"));

        // create the contact list
        suite.addTest(
            new TestOperationSetPersistentPresence("prepareContactList"));

        suite.addTestSuite(TestOperationSetPersistentPresence.class);

        return suite;
    }

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        fixture.setUp();

        Map<String, OperationSet> supportedOperationSets1 =
            fixture.provider1.getSupportedOperationSets();

        if ( supportedOperationSets1 == null
            || supportedOperationSets1.size() < 1)
            throw new NullPointerException(
                "No OperationSet implementations are supported by "
                +"this Gibberish implementation. ");

        //get the operation set presence here.
        opSetPersPresence1 =
            (OperationSetPersistentPresence)supportedOperationSets1.get(
                OperationSetPersistentPresence.class.getName());

        //if still null then the implementation doesn't offer a presence
        //operation set which is unacceptable for gibberish.
        if (opSetPersPresence1 == null)
            throw new NullPointerException(
                "An implementation of the gibberish service must provide an "
                + "implementation of at least the one of the Presence "
                + "Operation Sets");

        // lets do it once again for the second provider
        Map<String, OperationSet> supportedOperationSets2 =
            fixture.provider2.getSupportedOperationSets();

        if (supportedOperationSets2 == null
            || supportedOperationSets2.size() < 1)
            throw new NullPointerException(
                "No OperationSet implementations are supported by "
                + "this Gibberish implementation. ");

        //get the operation set presence here.
        opSetPersPresence2 =
            (OperationSetPersistentPresence) supportedOperationSets2.get(
                OperationSetPersistentPresence.class.getName());

        //if still null then the implementation doesn't offer a presence
        //operation set which is unacceptable for Gibberish.
        if (opSetPersPresence2 == null)
            throw new NullPointerException(
                "An implementation of the Gibberish service must provide an "
                + "implementation of at least the one of the Presence "
                + "Operation Sets");
    }

    @Override
    protected void tearDown() throws Exception
    {
        fixture.tearDown();
        super.tearDown();
    }

    /**
     * Retrieves a server stored contact list and checks whether it contains
     * all contacts that have been added there during the initialization
     * phase by the testerAgent.
     */
    public void testRetrievingServerStoredContactList()
    {
        ContactGroup rootGroup
            = opSetPersPresence1.getServerStoredContactListRoot();

        logger.debug("=========== Server Stored Contact List =================");

        logger.debug("rootGroup="+rootGroup.getGroupName()
                     +" rootGroup.childContacts="+rootGroup.countContacts()
                     + "rootGroup.childGroups="+rootGroup.countSubgroups()
                     + "Printing rootGroupContents=\n"+rootGroup.toString());

        Hashtable<String, List<String>> expectedContactList
            = GibberishSlickFixture.preInstalledBuddyList;

        logger.debug("============== Expected Contact List ===================");
        logger.debug(expectedContactList);

        //Go through the contact list retrieved by the persistence presence set
        //and remove the name of every contact and group that we find there from
        //the expected contct list hashtable.
        Iterator<ContactGroup> groups = rootGroup.subgroups();
        while (groups.hasNext() )
        {
            ContactGroup group = groups.next();

            List<String> expectedContactsInGroup
                = expectedContactList.get(group.getGroupName());

            // When sending the offline message
            // the sever creates a group NotInContactList,
            // because the buddy we are sending message to is not in
            // the contactlist. So this group must be ignored
            if(!group.getGroupName().equals("NotInContactList"))
            {
                assertNotNull("Group " + group.getGroupName() +
                              " was returned by "
                              +
                    "the server but was not in the expected contact list."
                              , expectedContactsInGroup);

                Iterator<Contact> contactsIter = group.contacts();
                while(contactsIter.hasNext())
                {
                    String contactID = contactsIter.next().getAddress();
                    expectedContactsInGroup.remove(contactID);
                }

                //If we've removed all the sub contacts, remove the group too.
                if(expectedContactsInGroup.size() == 0)
                    expectedContactList.remove(group.getGroupName());
            }
        }

        //whatever we now have in the expected contact list snapshot are groups,
        //that have been added by the testerAgent but that were not retrieved
        //by the persistent presence operation set.
        assertTrue("The following contacts were on the server sidec contact "
                   +"list, but were not returned by the pers. pres. op. set"
                   + expectedContactList.toString()
                   , expectedContactList.isEmpty());
    }

    /**
     * Creates a group in the server stored contact list, makes sure that the
     * corresponding event has been generated and verifies that the group is
     * in the list.
     *
     * @throws java.lang.Exception
     */
    public void postTestCreateGroup()
        throws Exception
    {
        // first clear the list
        fixture.clearProvidersLists();

        Object o = new Object();
        synchronized(o)
        {
            o.wait(3000);
        }

        logger.trace("testing creation of server stored groups");
        //first add a listener
        GroupChangeCollector groupChangeCollector = new GroupChangeCollector();
        opSetPersPresence1
            .addServerStoredGroupChangeListener(groupChangeCollector);

        //create the group
        opSetPersPresence1.createServerStoredContactGroup(
            opSetPersPresence1.getServerStoredContactListRoot(), testGroupName);

        groupChangeCollector.waitForEvent(10000);

        opSetPersPresence1
            .removeServerStoredGroupChangeListener(groupChangeCollector);

        // check whether we got group created event
        assertEquals("Collected Group Change events: ",
                     1, groupChangeCollector.collectedEvents.size());

        assertEquals("Group name.",  testGroupName,
            ((ServerStoredGroupEvent)groupChangeCollector.collectedEvents
                .get(0)).getSourceGroup().getGroupName());

        // check whether the group is retrievable
        ContactGroup group = opSetPersPresence1.getServerStoredContactListRoot()
            .getGroup(testGroupName);

        assertNotNull("A newly created group was not in the contact list.",
                      group);

        assertEquals("New group name", testGroupName, group.getGroupName());

        // when opearting with groups . the group must have entries
        // so changes to take effect. Otherwise group will be lost after loggingout
        try
        {
            opSetPersPresence1.subscribe(group, fixture.userID2);

            synchronized(o)
            {
                o.wait(1500);
            }
        }
        catch (Exception ex)
        {
            fail("error adding entry to group : " +
                 group.getGroupName() + " " +
                 ex.getMessage());
        }
    }


    /**
     * Removes the group created in the server stored contact list by the create
     * group test, makes sure that the corresponding event has been generated
     * and verifies that the group is not in the list any more.
     */
    public void postTestRemoveGroup()
    {
        logger.trace("testing removal of server stored groups");

        //first add a listener
        GroupChangeCollector groupChangeCollector = new GroupChangeCollector();
        opSetPersPresence1
            .addServerStoredGroupChangeListener(groupChangeCollector);

        try
        {
            // remove the group
            opSetPersPresence1.removeServerStoredContactGroup(
                opSetPersPresence1.getServerStoredContactListRoot()
                    .getGroup(testGroupName2));
        }
        catch(OperationFailedException ex)
        {
            logger.error("error removing group", ex);
        }

        groupChangeCollector.waitForEvent(10000);

        opSetPersPresence1
            .removeServerStoredGroupChangeListener(groupChangeCollector);

        // check whether we got group created event
        assertEquals("Collected Group Change event",
                     1, groupChangeCollector.collectedEvents.size());

        assertEquals("Group name.",  testGroupName2,
            ((ServerStoredGroupEvent)groupChangeCollector.collectedEvents
                .get(0)).getSourceGroup().getGroupName());

        // check whether the group is still on the contact list
        ContactGroup group = opSetPersPresence1.getServerStoredContactListRoot()
            .getGroup(testGroupName2);

        assertNull("A freshly removed group was still on the contact list.",
                      group);
    }

    /**
     * Renames our test group and checks whether corresponding events are
     * triggered. Verifies whether the group has really changed its name and
     * whether it is findable by its new name. Also makes sure that it does
     * not exist under its previous name any more.
     */
    public void postTestRenameGroup()
    {
        logger.trace("Testing renaming groups.");

        ContactGroup group = opSetPersPresence1.getServerStoredContactListRoot()
                                .getGroup(testGroupName);

        //first add a listener
        GroupChangeCollector groupChangeCollector = new GroupChangeCollector();
        opSetPersPresence1
            .addServerStoredGroupChangeListener(groupChangeCollector);

        //change the name and wait for a confirmation event
        opSetPersPresence1.renameServerStoredContactGroup(group, testGroupName2);

        groupChangeCollector.waitForEvent(10000);

        opSetPersPresence1
            .removeServerStoredGroupChangeListener(groupChangeCollector);

        //examine the event
        assertEquals("Collected Group Change event",
                     1, groupChangeCollector.collectedEvents.size());

        assertEquals("Group name.",  testGroupName2,
            ((ServerStoredGroupEvent)groupChangeCollector.collectedEvents
                .get(0)).getSourceGroup().getGroupName());

        // check whether the group is still on the contact list
        ContactGroup oldGroup = opSetPersPresence1.getServerStoredContactListRoot()
            .getGroup(testGroupName);

        assertNull("A group was still findable by its old name after renaming.",
                      oldGroup);

        //make sure that we could find the group by its new name.
        ContactGroup newGroup = opSetPersPresence1.getServerStoredContactListRoot()
            .getGroup(testGroupName2);

        assertNotNull("Could not find a renamed group by its new name.",
                      newGroup);
    }

    /**
     * Create the contact list. Later will be test to be sure that creating is ok
     * @throws Exception
     */
    public void prepareContactList()
        throws Exception
    {
        fixture.clearProvidersLists();

        Object o = new Object();
        synchronized(o)
        {
            o.wait(3000);
        }

        String contactList = System.getProperty(
            GibberishProtocolProviderServiceLick.CONTACT_LIST_PROPERTY_NAME
            , null);

        logger.debug("The "
                     + GibberishProtocolProviderServiceLick
                        .CONTACT_LIST_PROPERTY_NAME
                     + " property is set to=" + contactList);

        if(    contactList == null
            || contactList.trim().length() < 6)//at least 4 for a UIN, 1 for the
                                               // dot and 1 for the grp name
            throw new IllegalArgumentException(
                "The " +
                GibberishProtocolProviderServiceLick.CONTACT_LIST_PROPERTY_NAME
                + " property did not contain a contact list.");
        StringTokenizer tokenizer = new StringTokenizer(contactList, " \n\t");

        logger.debug("tokens contained by the CL tokenized="
            +tokenizer.countTokens());

        Hashtable<String, List<String>> contactListToCreate
            = new Hashtable<String, List<String>>();

        //go over all group.uin tokens
        while (tokenizer.hasMoreTokens())
        {
            String groupUinToken = tokenizer.nextToken();
            int dotIndex = groupUinToken.indexOf(".");

            if ( dotIndex == -1 )
            {
                throw new IllegalArgumentException(groupUinToken
                    + " is not a valid Group.UIN token");
            }

            String groupName = groupUinToken.substring(0, dotIndex);
            String uin = groupUinToken.substring(dotIndex + 1);

            if(    groupName.trim().length() < 1
                || uin.trim().length() < 4 )
            {
                throw new IllegalArgumentException(
                    groupName + " or " + uin +
                    " are not a valid group name or Gibberish user id.");
            }

            //check if we've already seen this group and if not - add it
            List<String> uinInThisGroup = contactListToCreate.get(groupName);
            if (uinInThisGroup == null)
            {
                uinInThisGroup = new ArrayList<String>();
                contactListToCreate.put(groupName, uinInThisGroup);
            }

            uinInThisGroup.add(uin);
        }

        // now init the list
        Enumeration<String> newGroupsEnum = contactListToCreate.keys();

        //go over all groups in the contactsToAdd table
        while (newGroupsEnum.hasMoreElements())
        {
            String groupName = newGroupsEnum.nextElement();
            logger.debug("Will add group " + groupName);

            opSetPersPresence1.createServerStoredContactGroup(
                opSetPersPresence1.getServerStoredContactListRoot(), groupName);

            ContactGroup newlyCreatedGroup =
                opSetPersPresence1.getServerStoredContactListRoot().getGroup(groupName);

            Iterator<String> contactsToAddToThisGroup
                = contactListToCreate.get(groupName).iterator();
            while (contactsToAddToThisGroup.hasNext())
            {
                String id = contactsToAddToThisGroup.next();

                logger.debug("Will add buddy " + id);
                opSetPersPresence1.subscribe(newlyCreatedGroup, id);
            }
        }

        //store the created contact list for later reference
        GibberishSlickFixture.preInstalledBuddyList = contactListToCreate;
    }

    /**
     * The class would listen for and store received events delivered to
     * <tt>ServerStoredGroupListener</tt>s.
     */
    private class GroupChangeCollector implements ServerStoredGroupListener
    {
        public ArrayList<EventObject> collectedEvents = new ArrayList<EventObject>();

        /**
         * Blocks until at least one event is received or until waitFor
         * miliseconds pass (whicever happens first).
         *
         * @param waitFor the number of miliseconds that we should be waiting
         * for an event before simply bailing out.
         */
        public void waitForEvent(long waitFor)
        {
            synchronized(this)
            {
                if(collectedEvents.size() > 0)
                    return;

                try{
                    wait(waitFor);
                }
                catch (InterruptedException ex)
                {
                    logger.debug(
                        "Interrupted while waiting for a subscription evt", ex);
                }
            }
        }

        /**
         * Called whnever an indication is received that a new server stored
         * group is created.
         * @param evt a ServerStoredGroupChangeEvent containing a reference to
         * the newly created group.
         */
        public void groupCreated(ServerStoredGroupEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }

        /**
         * Called when an indication is received that the name of a server stored
         * contact group has changed.
         * @param evt a ServerStoredGroupChangeEvent containing the details of the
         * name change.
         */
        public void groupNameChanged(ServerStoredGroupEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }

        /**
         * Called whnever an indication is received that an existing server stored
         * group has been removed.
         * @param evt a ServerStoredGroupChangeEvent containing a reference to the
         * newly created group.
         */
        public void groupRemoved(ServerStoredGroupEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }

        /**
         * Called whnever an indication is received that an existing server
         * stored group has been resolved.
         * @param evt a ServerStoredGroupChangeEvent containing a reference to
         * the resolved group.
         */
        public void groupResolved(ServerStoredGroupEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }
    }

    /**
     * The class would listen for and store received subscription modification
     * events.
     */
    private class SubscriptionEventCollector implements SubscriptionListener
    {
        public ArrayList<EventObject> collectedEvents = new ArrayList<EventObject>();

        /**
         * Blocks until at least one event is received or until waitFor
         * milliseconds pass (whichever happens first).
         *
         * @param waitFor the number of milliseconds that we should be waiting
         * for an event before simply bailing out.
         */
        public void waitForEvent(long waitFor)
        {
            logger.trace("Waiting for a persistent subscription event");

            synchronized(this)
            {
                if(collectedEvents.size() > 0)
                {
                    logger.trace("SubEvt already received. " + collectedEvents);
                    return;
                }

                try{
                    wait(waitFor);
                    if(collectedEvents.size() > 0)
                        logger.trace("Received a SubEvt in provider status.");
                    else
                        logger.trace("No SubEvt received for "+waitFor+"ms.");
                }
                catch (InterruptedException ex)
                {
                    logger.debug(
                        "Interrupted while waiting for a subscription evt", ex);
                }
            }
        }

        /**
         * Stores the received subsctiption and notifies all waiting on this
         * object
         * @param evt the SubscriptionEvent containing the corresponding contact
         */
        public void subscriptionCreated(SubscriptionEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }

        /**
         * Stores the received subsctiption and notifies all waiting on this
         * object
         * @param evt the SubscriptionEvent containing the corresponding contact
         */
        public void subscriptionRemoved(SubscriptionEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }

        /**
         * Stores the received subsctiption and notifies all waiting on this
         * object
         * @param evt the SubscriptionEvent containing the corresponding contact
         */
        public void subscriptionFailed(SubscriptionEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }

        /**
         * Stores the received subsctiption and notifies all waiting on this
         * object
         * @param evt the SubscriptionEvent containing the corresponding contact
         */
        public void subscriptionResolved(SubscriptionEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }


        /**
         * Stores the received subsctiption and notifies all waiting on this
         * object
         * @param evt the SubscriptionEvent containing the corresponding contact
         */
        public void subscriptionMoved(SubscriptionMovedEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }

        /**
         * Stores the received subsctiption and notifies all waiting on this
         * object
         * @param evt the SubscriptionEvent containing the corresponding contact
         */
        public void contactModified(ContactPropertyChangeEvent evt)
        {
            synchronized(this)
            {
                logger.debug("Collected evt("+collectedEvents.size()+")= "+evt);
                collectedEvents.add(evt);
                notifyAll();
            }
        }
    }
}
