/*
 * Copyright (C) 2005-2013 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * 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 .
 */
package org.alfresco.repo.forum;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.domain.activities.ActivityPostDAO;
import org.alfresco.repo.domain.activities.ActivityPostEntity;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.PermissionReference;
import org.alfresco.repo.security.permissions.impl.ModelDAO;
import org.alfresco.repo.security.permissions.impl.PermissionServiceImpl;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
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.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.test.junitrules.AlfrescoPerson;
import org.alfresco.util.test.junitrules.ApplicationContextInit;
import org.alfresco.util.test.junitrules.RunAsFullyAuthenticatedRule;
import org.alfresco.util.test.junitrules.TemporaryNodes;
import org.alfresco.util.test.junitrules.TemporarySites;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
/**
 * Test class for some {@link ForumModel forum model}-related functionality, specifically comments.
 * There is no (fully-featured) "CommentService" or "DiscussionService" and the REST API simply creates the appropriate
 * content structure as required by the forum model.
 * 
 * @author Neil McErlean
 * @since 4.0
 */
public class CommentsTest
{
    private static final ApplicationContextInit APP_CONTEXT_INIT = new ApplicationContextInit();
    
    public static final String USER_ONE_NAME = "userone";
    public static final AlfrescoPerson TEST_USER1 = new AlfrescoPerson(APP_CONTEXT_INIT, USER_ONE_NAME);
    
    // Tie them together in a static Rule Chain
    @ClassRule public static RuleChain STATIC_RULE_CHAIN = RuleChain.outerRule(APP_CONTEXT_INIT)
                                                                    .around(TEST_USER1);
    
    // A JUnit Rule to run all tests as user1
    public RunAsFullyAuthenticatedRule runAsRule = new RunAsFullyAuthenticatedRule(TEST_USER1);
    
    // A JUnit Rule to manage test nodes use in each test method
    public TemporaryNodes testNodes = new TemporaryNodes(APP_CONTEXT_INIT);
    public TemporarySites testSites = new TemporarySites(APP_CONTEXT_INIT);
    
    // Tie them together in a non-static rule chain.
    @Rule public RuleChain ruleChain = RuleChain.outerRule(runAsRule)
                                                .around(testSites)
                                                .around(testNodes);
    
    // Services
    private static BehaviourFilter behaviourFilter;
    private static ContentService contentService;
    private static NodeService nodeService;
    private static Repository repositoryHelper;
    private static SiteService siteService;
    private static RetryingTransactionHelper transactionHelper;
    private static AuthenticationComponent authenticationComponent;
    private static CommentService commentService;
    private static MutableAuthenticationService authenticationService;
    private static PersonService personService;
    private static ActivityPostDAO postDAO;
    private static PermissionServiceImpl permissionServiceImpl;
    private static ModelDAO permissionModelDAO;
    // These NodeRefs are used by the test methods.
    private static NodeRef COMPANY_HOME;
    private SiteInfo testSite;
    private NodeRef testFolder;
    private List testDocs;
    
    @BeforeClass public static void initBasicServices() throws Exception
    {
        behaviourFilter = (BehaviourFilter)APP_CONTEXT_INIT.getApplicationContext().getBean("policyBehaviourFilter");
        contentService = (ContentService)APP_CONTEXT_INIT.getApplicationContext().getBean("ContentService");
        nodeService = (NodeService)APP_CONTEXT_INIT.getApplicationContext().getBean("NodeService");
        repositoryHelper = (Repository)APP_CONTEXT_INIT.getApplicationContext().getBean("repositoryHelper");
        siteService = (SiteService)APP_CONTEXT_INIT.getApplicationContext().getBean("SiteService");
        transactionHelper = (RetryingTransactionHelper)APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper");
        
        authenticationComponent = (AuthenticationComponent)APP_CONTEXT_INIT.getApplicationContext().getBean("authenticationComponent");
        commentService = (CommentService)APP_CONTEXT_INIT.getApplicationContext().getBean("commentService");
        authenticationService = (MutableAuthenticationService)APP_CONTEXT_INIT.getApplicationContext().getBean("AuthenticationService");
        personService = (PersonService)APP_CONTEXT_INIT.getApplicationContext().getBean("PersonService");
        postDAO = (ActivityPostDAO)APP_CONTEXT_INIT.getApplicationContext().getBean("postDAO");
        permissionServiceImpl = (PermissionServiceImpl)APP_CONTEXT_INIT.getApplicationContext().getBean("permissionServiceImpl");
        permissionModelDAO = (ModelDAO)APP_CONTEXT_INIT.getApplicationContext().getBean("permissionsModelDAO");
        
        COMPANY_HOME = repositoryHelper.getCompanyHome();
    }
    
    @Before public void createSomeContentForCommentingOn() throws Exception
    {
        // Create some content which we will comment on.
        testSite = testSites.createSite("sitePreset", "testSite", "test site title", "test site description", SiteVisibility.PUBLIC, USER_ONE_NAME);
        final NodeRef doclib = siteService.getContainer(testSite.getShortName(), SiteService.DOCUMENT_LIBRARY);
        testFolder = testNodes.createFolder(doclib, "testFolder", TEST_USER1.getUsername());
        testDocs = new ArrayList(3);
        for (int i = 0; i < 3; i++)
        {
            NodeRef testNode = testNodes.createQuickFile(MimetypeMap.MIMETYPE_TEXT_PLAIN, COMPANY_HOME, "testDocInFolder" + i, TEST_USER1.getUsername());
            testDocs.add(testNode);
        }
    }
    
    // MNT-11667 "createComment" method creates activity for users who are not supposed to see the file
    @Test public void testMNT11667() throws Exception
    {
        final String userTwo = "usertwo";
        try
        {
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public Void execute() throws Throwable
                {
                    authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
                    createUser(userTwo);
                    assertTrue(siteService.hasSite(testSite.getShortName()));
                    authenticationComponent.setCurrentUser(USER_ONE_NAME);
                    // invite user to a site with 'Collaborator' role
                    siteService.setMembership(testSite.getShortName(), userTwo, SiteModel.SITE_COLLABORATOR);
                    assertEquals(SiteModel.SITE_MANAGER, siteService.getMembersRole(testSite.getShortName(), USER_ONE_NAME));
                    assertEquals(SiteModel.SITE_COLLABORATOR, siteService.getMembersRole(testSite.getShortName(), userTwo));
                    // get container of site
                    NodeRef doclib = siteService.getContainer(testSite.getShortName(), SiteService.DOCUMENT_LIBRARY);
                    // create file in container of site
                    NodeRef testNode = testNodes.createQuickFile(MimetypeMap.MIMETYPE_TEXT_PLAIN, doclib, "testDoc", USER_ONE_NAME);
                    assertTrue(nodeService.exists(testNode));
                    // change permissions
                    permissionServiceImpl.setInheritParentPermissions(testNode, false);
                    permissionServiceImpl.setPermission(testNode, USER_ONE_NAME, PermissionService.ALL_PERMISSIONS, true);
                    // create comment
                    NodeRef comment = commentService.createComment(testNode, "This is the comment title", "This is a Web Script comment", true);
                    assertTrue(nodeService.exists(comment));
                    // get post activity
                    ActivityPostEntity params = new ActivityPostEntity();
                    params.setStatus(ActivityPostEntity.STATUS.PENDING.toString());
                    List activityPostList = postDAO.selectPosts(params, -1);
                    String activityNodeRef = null;
                    for (ActivityPostEntity activityPostEntry : activityPostList)
                    {
                        if ("comments".equals(activityPostEntry.getAppTool()))
                        {
                            String activityData = activityPostEntry.getActivityData();
                            JSONObject json = new JSONObject(activityData);
                            activityNodeRef = (String) json.get("nodeRef");
                        }
                    }
                    assertFalse(activityNodeRef == null);
                    NodeRef nodeRef = new NodeRef(activityNodeRef);
                    assertTrue(permissionServiceImpl.hasPermission(nodeRef, getPermission(PermissionService.READ)) == AccessStatus.ALLOWED);
                    assertTrue(permissionServiceImpl.hasPermission(nodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.ALLOWED);
                    assertTrue(permissionServiceImpl.hasPermission(nodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.ALLOWED);
                    authenticationComponent.setCurrentUser(userTwo);
                    assertTrue(permissionServiceImpl.hasPermission(nodeRef, getPermission(PermissionService.READ)) == AccessStatus.DENIED);
                    assertTrue(permissionServiceImpl.hasPermission(nodeRef, getPermission(PermissionService.WRITE)) == AccessStatus.DENIED);
                    assertTrue(permissionServiceImpl.hasPermission(nodeRef, getPermission(PermissionService.DELETE)) == AccessStatus.DENIED);
                    return null;
                }
            });
        }
        finally
        {
            authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
            if (personService.personExists(userTwo))
            {
                personService.deletePerson(userTwo);
            }
            if (authenticationService.authenticationExists(userTwo))
            {
                authenticationService.deleteAuthentication(userTwo);
            }
        }
    }
    
    private PermissionReference getPermission(String permission)
    {
        return permissionModelDAO.getPermissionReference(null, permission);
    }
    
    private void createUser(String userName)
    {
        // if user with given user name doesn't already exist then create user
        if (authenticationService.authenticationExists(userName) == false)
        {
            // create user
            authenticationService.createAuthentication(userName, "password".toCharArray());
            
            // create person properties
            PropertyMap personProps = new PropertyMap();
            personProps.put(ContentModel.PROP_USERNAME, userName);
            personProps.put(ContentModel.PROP_FIRSTNAME, userName);
            personProps.put(ContentModel.PROP_LASTNAME, userName);
            personProps.put(ContentModel.PROP_EMAIL, "FirstName123.LastName123@email.com");
            personProps.put(ContentModel.PROP_JOBTITLE, "JobTitle123");
            personProps.put(ContentModel.PROP_JOBTITLE, "Organisation123");
            
            // create person node for user
            personService.createPerson(personProps);
        }
    }
    
    /**
     * This test method comments on some nodes asserting that the commentCount rollup property
     * responds correctly to the changing number of comments.
     */
    @Test public void commentOnDocsCheckingCommentCountRollup() throws Exception
    {
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public Void execute() throws Throwable
                {
                    // All test nodes are uncommented initially.
                    for (NodeRef nr : testDocs)
                    {
                        assertCommentCountIs(nr, 0);
                    }
                    
                    // Comment on each node twice
                    Map> mapDiscussableToComments = new HashMap>();
                        
                    for (NodeRef nr : testDocs)
                    {
                        final ArrayList comments = new ArrayList();
                        mapDiscussableToComments.put(nr, comments);
                        
                        comments.add(applyComment(nr, "Test comment 1 " + System.currentTimeMillis()));
                        Thread.sleep(50); // 50 ms sleep so comments aren't simultaneous.
                        
                        comments.add(applyComment(nr, "Test comment 2 " + System.currentTimeMillis()));
                        Thread.sleep(50);
                    }
                    
                    // Check that the rollup comment counts are accurate.
                    for (NodeRef nr : testDocs)
                    {
                        assertCommentCountIs(nr, 2);
                    }
                    
                    // Remove comments
                    for (Map.Entry> entry : mapDiscussableToComments.entrySet())
                    {
                        for (NodeRef commentNode : entry.getValue())
                        {
                            nodeService.deleteNode(commentNode);
                        }
                    }
                    
                    // All test nodes are uncommented again.
                    for (NodeRef nr : testDocs)
                    {
                        assertCommentCountIs(nr, 0);
                    }
                    
                    return null;
                }
            });
    }
    
    /**
     * This test method tests that commented nodes from before Swift have their comment counts correctly rolled up.
     * Nodes that were commented on in prior versions of Alfresco will not have commentCount rollups -
     * neither the aspect nor the property defined within it. Alfresco lazily calculates commentCount rollups for these
     * nodes. So they will appear to have a count of 0 (undefined, really) and will not be given the "(count)" UI decoration.
     * Then when a comment is added (or removed), the comment count should be recalculated from scratch.
     */
    @Test public void testRollupOfPreSwiftNodes() throws Exception
    {
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public Void execute() throws Throwable
                {
                    assertTrue("Not enough test docs for this test case", testDocs.size() >= 2);
                    NodeRef node1 = testDocs.get(0);
                    NodeRef node2 = testDocs.get(1);
                    
                    // We will simulate pre-Swift commenting by temporarily disabling the behaviours that add the aspect & do the rollups.
                    behaviourFilter.disableBehaviour(ForumModel.TYPE_POST);
                    
                    for (NodeRef nr : new NodeRef[]{node1, node2})
                    {
                        // All test nodes initially do not have the commentsRollup aspect.
                        assertFalse("Test node had comments rollup aspect.", nodeService.hasAspect(nr, ForumModel.ASPECT_COMMENTS_ROLLUP));
                    }
                    
                    // Comment on each node - we need to save one comment noderef in order to delete it later.
                    NodeRef commentOnNode1 = applyComment(node1, "Hello", true);
                    applyComment(node1, "Bonjour", true);
                    applyComment(node2, "Hola", true);
                    applyComment(node2, "Bout ye?", true);
                    
                    // Check that the rollup comment counts are still not present. And re-enable the behaviours after we check.
                    for (NodeRef nr : new NodeRef[]{node1, node2})
                    {
                        assertFalse("Test node had comments rollup aspect.", nodeService.hasAspect(nr, ForumModel.ASPECT_COMMENTS_ROLLUP));
                    }
                    behaviourFilter.enableBehaviour(ForumModel.TYPE_POST);
                    
                    // Now the addition or deletion of a comment, should trigger a recalculation of the comment rollup from scratch.
                    applyComment(node2, "hello again");
                    nodeService.deleteNode(commentOnNode1);
                    assertCommentCountIs(node2, 3);
                    assertCommentCountIs(node1, 1);
                    
                    return null;
                }
            });
    }
    
    /**
     * This test method tests that nodes whose commentCount is set to -1 have their commentCounts recalculated.
     * This feature (see ALF-8498) is to allow customers to set their counts to -1 thus triggering a recount for that document.
     */
    @Test public void testTriggerCommentRecount() throws Exception
    {
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public Void execute() throws Throwable
                {
                    NodeRef testDoc = testDocs.get(0);
                    applyComment(testDoc, "Hello 1");
                    applyComment(testDoc, "Hello 2");
                    applyComment(testDoc, "Hello 3");
                    
                    assertCommentCountIs(testDoc, 3);
                    
                    // We'll cheat and just set it to an arbitrary value.
                    nodeService.setProperty(testDoc, ForumModel.PROP_COMMENT_COUNT, 42);
                    
                    // It should have that value - even though it's wrong.
                    assertCommentCountIs(testDoc, 42);
                    
                    // Now we'll set it to the trigger value -1.
                    nodeService.setProperty(testDoc, ForumModel.PROP_COMMENT_COUNT, ForumPostBehaviours.COUNT_TRIGGER_VALUE);
                    
                    // It should have the correct, recalculated value.
                    assertCommentCountIs(testDoc, 3);
                    
                    return null;
                }
            });
    }
    
    /**
     * This method asserts that the commentCount (rollup) is as specified for the given node.
     */
    private void assertCommentCountIs(NodeRef discussableNode, int expectedCount)
    {
        final Serializable commentCount = nodeService.getProperty(discussableNode, ForumModel.PROP_COMMENT_COUNT);
        if (expectedCount == 0)
        {
            assertTrue("Uncommented node should have EITHER no commentsRollup aspect OR commentCount of zero.",
                    !nodeService.hasAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP) ||
                    (commentCount != null && commentCount.equals(new Integer(0))
                            ));
        }
        else
        {
            assertTrue("Commented node should have discussable aspect.", nodeService.hasAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP));
            assertEquals("Wrong comment count", expectedCount, commentCount);
        }
    }
    
    private NodeRef applyComment(NodeRef nr, String comment)
    {
        return applyComment(nr, comment, false);
    }
    /**
     * This method applies the specified comment to the specified node.
     * As there is no CommentService or DiscussionService, we mimic here what the comments REST API does,
     * by manually creating the correct content structure using the nodeService. Behaviours will do some
     * of the work for us. See comments.post.json.js for comparison.
     * @param nr nodeRef to comment on.
     * @param comment the text of the comment.
     * @param suppressRollups if true, commentsRollup aspect will not be added.
     * @return the NodeRef of the fm:post comment node.
     * 
     * @see CommentsTest#testRollupOfPreSwiftNodes() for use of suppressRollups.
     */
    private NodeRef applyComment(NodeRef nr, String comment, boolean suppressRollups)
    {
        // There is no CommentService, so we have to create the node structure by hand.
        // This is what happens within e.g. comment.put.json.js when comments are submitted via the REST API.
        if (!nodeService.hasAspect(nr, ForumModel.ASPECT_DISCUSSABLE))
        {
            nodeService.addAspect(nr, ForumModel.ASPECT_DISCUSSABLE, null);
        }
        if (!nodeService.hasAspect(nr, ForumModel.ASPECT_COMMENTS_ROLLUP) && !suppressRollups)
        {
            nodeService.addAspect(nr, ForumModel.ASPECT_COMMENTS_ROLLUP, null);
        }
        // Forum node is created automatically by DiscussableAspect behaviour.
        NodeRef forumNode = nodeService.getChildAssocs(nr, ForumModel.ASSOC_DISCUSSION, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion")).get(0).getChildRef();
        
        final List existingTopics = nodeService.getChildAssocs(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"));
        NodeRef topicNode = null;
        if (existingTopics.isEmpty())
        {
            topicNode = nodeService.createNode(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"), ForumModel.TYPE_TOPIC).getChildRef();
        }
        else
        {
            topicNode = existingTopics.get(0).getChildRef();
        }
        NodeRef postNode = nodeService.createNode(topicNode, ContentModel.ASSOC_CONTAINS, QName.createQName("comment" + System.currentTimeMillis()), ForumModel.TYPE_POST).getChildRef();
        nodeService.setProperty(postNode, ContentModel.PROP_CONTENT, new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null));
        ContentWriter writer = contentService.getWriter(postNode, ContentModel.PROP_CONTENT, true);
        writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
        writer.setEncoding("UTF-8");
        writer.putContent(comment);
        
        return postNode;
    }
}