/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.activities;

import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.List;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.activities.feed.FeedGenerator;
import org.alfresco.repo.activities.feed.local.LocalFeedTaskProcessor;
import org.alfresco.repo.activities.post.lookup.PostLookup;
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
import org.alfresco.repo.node.archive.NodeArchiveService;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.service.cmr.activities.ActivityService;
import org.alfresco.service.cmr.activities.FeedControl;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.cmr.site.SiteVisibility;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.PropertyMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.quartz.Scheduler;
import org.springframework.context.ApplicationContext;

/**
 * Simple Activity Service unit test using site (membership) activities
 * 
 * @author janv
 */
public abstract class AbstractSiteActivityTest
{
    private static Log logger = LogFactory.getLog(AbstractSiteActivityTest.class);
    
    private ApplicationContext applicationContext;
    
    private SiteService siteService;
    private ActivityService activityService;
    private MutableAuthenticationService authenticationService;
    private PersonService personService;
    private PostLookup postLookup;
    private FeedGenerator feedGenerator;
    private NodeArchiveService nodeArchiveService;
    
    //
    // Test config & data
    //
    
    // Location of activity type templates (for site activities)
    private static final String TEST_TEMPLATES_LOCATION = "activities"; // assumes test-resources is on classpath
    
    // Test users
    private static final String ADMIN_USER = "admin";
    private static final String ADMIN_PW = "admin";
    
    private static String user1 = null;
    private static String user2 = null;
    private static String user3 = null;
    private static String user4 = null;
    
    private static final String USER_PW = "password";
    
    // Test sites
    private static String site1 = null;
    private static String site2 = null;
    private static String site3 = null;
    
    // AppToolId for site membership activities
    private static String appToolId = "siteService"; // refer to SiteService
    
    private static boolean membersAddedUpdated = false;
    private static boolean membersRemoved = false;
    private static boolean controlsCreated = false;
    
    public AbstractSiteActivityTest()
    {
    }
    
    @Before
    public void setUp() throws Exception
    {
        applicationContext = ApplicationContextHelper.getApplicationContext();
        String testid = ""+System.currentTimeMillis();
        
        // Let's shut down the scheduler so that we aren't competing with the scheduled versions of the post lookup and
        // feed generator jobs
        Scheduler scheduler = (Scheduler) applicationContext.getBean("schedulerFactory");
        scheduler.shutdown();
        
        // Get the required services
        this.activityService = (ActivityService)applicationContext.getBean("activityService");
        this.siteService = (SiteService)applicationContext.getBean("SiteService");
        this.authenticationService = (MutableAuthenticationService)applicationContext.getBean("AuthenticationService");
        this.personService = (PersonService)applicationContext.getBean("PersonService");
        this.nodeArchiveService = (NodeArchiveService)applicationContext.getBean("nodeArchiveService");
        
        LocalFeedTaskProcessor feedProcessor = null;
        
        // alternative: would need to add subsystem context to config location (see above)
        //this.postLookup = (PostLookup)applicationContext.getBean("postLookup");
        //this.feedGenerator = (FeedGenerator)applicationContext.getBean("feedGenerator");
        //feedProcessor = (LocalFeedTaskProcessor)applicationContext.getBean("feedTaskProcessor");
        
        ChildApplicationContextFactory activitiesFeed = (ChildApplicationContextFactory)applicationContext.getBean("ActivitiesFeed");
        ApplicationContext activitiesFeedCtx = activitiesFeed.getApplicationContext();
        this.postLookup = (PostLookup)activitiesFeedCtx.getBean("postLookup");
        this.feedGenerator = (FeedGenerator)activitiesFeedCtx.getBean("feedGenerator");
        feedProcessor = (LocalFeedTaskProcessor)activitiesFeedCtx.getBean("feedTaskProcessor");
        
        
        List<String> templateSearchPaths = new ArrayList<String>(1);
        templateSearchPaths.add(TEST_TEMPLATES_LOCATION);
        feedProcessor.setTemplateSearchPaths(templateSearchPaths);
        feedProcessor.setUseRemoteCallbacks(false);
        
        site1 = "test_site1_" + testid;
        site2 = "test_site2_" + testid;
        site3 = "test_site3_" + testid;
        
        user1 = "Test_User1_" + testid;
        user2 = "Test_User2_" + testid;
        user3 = "Test_User3_" + testid;
        user4 = "Test_User4_" + testid;
        
        
        // create users
        
        login(ADMIN_USER, ADMIN_PW);
        
        createUser(user1, USER_PW);
        createUser(user2, USER_PW);
        createUser(user3, USER_PW);
        createUser(user4, USER_PW);
        
        // create sites
        
        // create public site
        createSite(site1, true);
        
        // create private sites
        createSite(site2, false);
        createSite(site3, false);
    }
    
    @After
    public void tearDown() throws Exception
    {
        login(ADMIN_USER, ADMIN_PW);
        
        deleteUser(user1);
        deleteUser(user2);
        deleteUser(user3);
        deleteUser(user4);
        
        deleteSite(site1); 
        deleteSite(site2);
        deleteSite(site3);
        
        membersAddedUpdated  = false;
        membersRemoved = false;
        controlsCreated = false;
    }
    
    protected void createSite(String siteId, boolean isPublic) throws Exception
    {
        siteService.createSite("myPreset", siteId, "myTitle", "myDescription", (isPublic ? SiteVisibility.PUBLIC : SiteVisibility.PRIVATE));
        
        if (logger.isDebugEnabled())
        {
            logger.debug("createdSite: " + siteId);
        }
    }
    
    protected void deleteSite(String siteId) throws Exception
    {
        SiteInfo siteInfo = siteService.getSite(siteId);
        if (siteInfo != null)
        {
            // delete site (and site's associated groups)
            siteService.deleteSite(siteId);
            nodeArchiveService.purgeArchivedNode(nodeArchiveService.getArchivedNode(siteInfo.getNodeRef()));
        }
    }
    
    @Test
    //MNT-9104 If username contains uppercase letters the action of joining a site will not be displayed in "My activities" 
    public void testUserActivitiesOnSite() throws Exception
    {
        login(ADMIN_USER, ADMIN_PW);
        
        addMembership(site1, user4, SiteModel.SITE_CONSUMER);
        
        generateFeed();
        
        login(user4, USER_PW);
        
        getUserFeed(user4, site1, false, false, true, 1);
    }
    
    @Test
    public void testGetSiteFeedsBefore() throws Exception
    {
        login(ADMIN_USER, ADMIN_PW);
        
        getSiteFeed(site1, 0);
        getSiteFeed(site2, 0); // site 2 is private, but accessible to admins
        getSiteFeed(site3, 0); // site 3 is private, but accessible to admins
        
        login(user4, USER_PW);
        
        getSiteFeed(site1, 0); // site 1 is public, hence site feed is accessible to any user of the system
        
        try
        {
            getSiteFeed(site2, 0); // site 2 is private, hence only accessible to members or admins
            
            fail("Site feed for private site should not be accessible to non-admin / non-member");
        }
        catch (AccessDeniedException ade)
        {
            // ignore
        }
        
        try
        {
            getSiteFeed(site3, 0); // site 3 is private, hence only accessible to members or admins
            
            fail("Site feed for private site should not be accessible to non-admin / non-member");
        }
        catch (AccessDeniedException ade)
        {
            // ignore
        }
    }
    
    protected void getSiteFeed(String siteId, int expectedCount) throws Exception
    {
        assertEquals(expectedCount, activityService.getSiteFeedEntries(siteId).size());
    }
    
    @Test
    public void testGetUserFeedsBefore() throws Exception
    {
        // as admin
        
        login(ADMIN_USER, ADMIN_PW);
        
        getUserFeed(user1, true, 0);
        getUserFeed(user2, true, 0);
        getUserFeed(user3, true, 0);
        getUserFeed(user4, true, 0);
        
        // as user1
        
        login(user1, USER_PW);
        
        getUserFeed(user1, false, 0);
        
        // as user2
        
        login(user2, USER_PW);
        
        try
        {
            getUserFeed(user1, true, 0);
            
            fail("User feed should only be accessible to user or an admin");
        }
        catch (AccessDeniedException ade)
        {
            // ignore
        }
        
        
        // as user1 - with filter args ...
        
        login(user1, USER_PW);
        
        getUserFeed(null, site1, false, false, false, 0);
        getUserFeed(null, site2, false, false, false, 0);
        getUserFeed(null, site3, false, false, false, 0);
        
        getUserFeed(null, null, false, true, false, 0);
        getUserFeed(null, null, false, false, true, 0);
        getUserFeed(null, null, false, true, true, 0);
    }
    
    protected void getUserFeed(String userId, boolean isAdmin, int expectedCount) throws Exception
    {
        getUserFeed(userId, null, isAdmin, false, false, expectedCount);
    }
    
    protected void getUserFeed(String userId, String siteId, boolean isAdmin, boolean excludeThisUser, boolean excludeOtherUsers, int expectedCount) throws Exception
    {
        if (userId == null)
        {
            userId = AuthenticationUtil.getFullyAuthenticatedUser();
        }
        assertEquals(expectedCount, activityService.getUserFeedEntries(userId, siteId, excludeThisUser, excludeOtherUsers, null, null).size());
    }
    
    @Test
    public void testUserFeedControls() throws Exception
    {
        if (! controlsCreated)
        {
            // user 1 opts out of all activities for site 1
            login(user1, USER_PW);
            addFeedControl(site1, null);
            
            // user 2 opts out of site membership activities (across all sites)
            login(user2, USER_PW);
            addFeedControl(null, appToolId);
            
            // user 3 opts out of site membership activities for site 1 only
            login(user3, USER_PW);
            addFeedControl(site1, appToolId);
                 
            // TODO add more here, once we have more appToolIds
            
            controlsCreated = true;
        }
    }
    
    @Test
    public void testAddAndUpdateMemberships() throws Exception
    {
        if (! membersAddedUpdated)
        {
            login(ADMIN_USER, ADMIN_PW);
           
            addAndUpdateMemberships(site1, true);  // public site, include all users
            addAndUpdateMemberships(site2, true);  // private site, include all users
            addAndUpdateMemberships(site3, false); // private site, do not include user 4
            
            generateFeed();
            
            membersAddedUpdated = true;
        }
    }
    
    @Test
    public void testGetSiteFeedsAfterAddAndUpdateMemberships() throws Exception
    {
        testAddAndUpdateMemberships();
        
        login(ADMIN_USER, ADMIN_PW);
        
        getSiteFeed(site1, 8); // 8 = 4 users, each with 1 join, 1 role change
        getSiteFeed(site2, 8); // 8 = 4 users, each with 1 join, 1 role change
        getSiteFeed(site3, 6); // 6 = 3 users, each with 1 join, 1 role change (not user 4)
        
        login(user4, USER_PW);
        
        getSiteFeed(site1, 8);
        getSiteFeed(site2, 8); // site 2 is private, user 4 is a member
        
        try
        {
            getSiteFeed(site3, 0); // site 3 is private, user 4 is not a member
            
            fail("Site feed for private site should not be accessible to non-admin / non-member");
        }
        catch (AccessDeniedException ade)
        {
            // ignore
        }
    }
    
    @Test
    public void testRemoveMemberships() throws Exception
    {
        if (! membersRemoved)
        {
            testAddAndUpdateMemberships();
            
            login(ADMIN_USER, ADMIN_PW);
            
            removeMemberships(site1, true);
            removeMemberships(site2, true);
            removeMemberships(site3, false);
            
            generateFeed();
            
            membersRemoved = true;
        }
    }
    
    protected void addAndUpdateMemberships(String siteId, boolean includeUser4) throws Exception
    {
        // add member -> join site
        addMembership(siteId, user1, SiteModel.SITE_CONSUMER);
        addMembership(siteId, user2, SiteModel.SITE_MANAGER);
        addMembership(siteId, user3, SiteModel.SITE_COLLABORATOR);
        
        if (includeUser4) { addMembership(siteId, user4, SiteModel.SITE_CONSUMER); }
        
        // update member -> change role
        updateMembership(siteId, user1, SiteModel.SITE_MANAGER);
        updateMembership(siteId, user2, SiteModel.SITE_COLLABORATOR);
        updateMembership(siteId, user3, SiteModel.SITE_CONSUMER);
        
        if (includeUser4) { updateMembership(siteId, user4, SiteModel.SITE_COLLABORATOR); }
    }
    
    protected void removeMemberships(String siteId, boolean includeUser4) throws Exception
    {
        // remove member -> leave site
        removeMembership(siteId, user1);
        removeMembership(siteId, user2);
        removeMembership(siteId, user3);
        
        if (includeUser4) { removeMembership(siteId, user4); }
    }
    
    private void addFeedControl(String siteId, String appToolId) throws Exception
    {
        // set feed control for current user
        activityService.setFeedControl(new FeedControl(siteId, appToolId));
    }
    
    @Test
    public void testGetSiteFeedsAfterRemoveMemberships() throws Exception
    {
        testAddAndUpdateMemberships();
        testRemoveMemberships();
        
        login(ADMIN_USER, ADMIN_PW);
        
        getSiteFeed(site1, 12); // 12 = 4 users, each with 1 join, 1 role change, 1 leave
        getSiteFeed(site2, 12); // 12 = 4 users, each with 1 join, 1 role change, 1 leave
        getSiteFeed(site3, 9);  //  9 = 3 users, each with 1 join, 1 role change, 1 leave (not user 4)
        
        login(user4, USER_PW);
        
        getSiteFeed(site1, 12);
        
        try
        {
            getSiteFeed(site2, 0); // site 2 is private, user 4 is no longer a member
            
            fail("Site feed for private site should not be accessible to non-admin / non-member");
        }
        catch (AccessDeniedException ade)
        {
            // ignore
        }
        
        try
        {
            getSiteFeed(site3, 0); // site 3 is private, user 4 was never a member
            
            fail("Site feed for private site should not be accessible to non-admin / non-member");
        }
        catch (AccessDeniedException ade)
        {
            // ignore
        }
    }
    
    @Test
    public void testGetUserFeedsAfter() throws Exception
    {
        testUserFeedControls();
        testAddAndUpdateMemberships();
        testRemoveMemberships();
        
        // as admin
        
        login(ADMIN_USER, ADMIN_PW);
        
        // site 1, with 4 users, each with 1 join, 1 role change = 4x2 = 8
        // site 2, with 4 users, each with 1 join, 1 role change = 4x2 = 8
        // site 3, with 3 users, each with 1 join, 1 role change = 3x2 = 6
        
        // user 1 belongs to 3 sites = (2x8)+(1x6) = 22
        // user 2 belongs to 3 sites = (2x8)+(1x6) = 22
        // user 3 belongs to 3 sites = (2x8)+(1x6) = 22
        // user 4 belongs to 2 sites = (2x8) = 16
        
        getUserFeed(user1, true, 14);  // 14 = (22 - 8) due to feed control - exclude site 1
        getUserFeed(user2, true, 0);   // 0 = due to feed control - exclude site membership activities (across all sites)
        getUserFeed(user3, true, 14);  // 14 = (22 - 8) due to feed control - exclude site membership activities for site 1
        getUserFeed(user4, true, 16);  // 16 = no feed control
        
        // as user1
        
        login(user1, USER_PW);
        
        getUserFeed(user1, false, 14);
        
        // as user2
        
        login(user2, USER_PW);
        
        try
        {
            getUserFeed(user1, true, 14);
            
            fail("User feed should only be accessible to user or an admin");
        }
        catch (AccessDeniedException ade)
        {
            // ignore
        }
        
        // as user1 - with filter args ...
        
        login(user1, USER_PW);
        
        getUserFeed(null, site1, false, false, false, 0);
        getUserFeed(null, site2, false, false, false, 8);
        getUserFeed(null, site3, false, false, false, 6);
        
        getUserFeed(null, null, false, false, false, 14); // no filter
        getUserFeed(null, null, false, true, false, 12);  // exclude any from user1
        getUserFeed(null, null, false, false, true, 2);   // exclude all except user1
        getUserFeed(null, null, false, true, true, 0);    // exclude all (NOOP)
        
        // TODO - add more (eg. other non-admin user activities)
    }
    
    private void addMembership(String siteId, String userName, String role) throws Exception
    {  
        updateMembership(siteId, userName, role);
    }
    
    private void updateMembership(String siteId, String userName, String role) throws Exception
    {  
        siteService.setMembership(siteId, userName, role);
    }
    
    private void removeMembership(String siteId, String userName) throws Exception
    {
        siteService.removeMembership(siteId, userName);
    }
    
    protected void createUser(String userName, String password)
    {
        if (authenticationService.authenticationExists(userName) == false)
        {
            authenticationService.createAuthentication(userName, password.toCharArray());
            
            PropertyMap ppOne = new PropertyMap(4);
            ppOne.put(ContentModel.PROP_USERNAME, userName);
            ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName");
            ppOne.put(ContentModel.PROP_LASTNAME, "lastName");
            ppOne.put(ContentModel.PROP_EMAIL, "email@email.com");
            ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle");
            
            personService.createPerson(ppOne);
        }
    }
    
    protected void deleteUser(String userName)
    {
        personService.deletePerson(userName);
    }
    
    private void login(String username, String password)
    {
        AuthenticationUtil.setFullyAuthenticatedUser(username);
    }
    
    private void generateFeed() throws Exception
    {
        postLookup.execute();
        feedGenerator.execute();
    }
    
    @AfterClass
    // remove system "user.name.caseSensitive" property
    public static void afterClass()
    {
        System.clearProperty("user.name.caseSensitive");
    }
}
