/*
 * #%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 .
 * #L%
 */
package org.alfresco.repo.blog;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.service.cmr.blog.BlogPostInfo;
import org.alfresco.service.cmr.blog.BlogService;
import org.alfresco.service.cmr.blog.BlogService.RangedDateProperty;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
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.service.cmr.tagging.TaggingService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyMap;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
/**
 * Test cases for {@link BlogServiceImpl}.
 * 
 * @author Neil Mc Erlean
 * @since 4.0
 */
public class BlogServiceImplTest
{
    private static final ApplicationContext testContext = ApplicationContextHelper.getApplicationContext();
    
    // injected services
    private static MutableAuthenticationService AUTHENTICATION_SERVICE;
    private static BehaviourFilter              BEHAVIOUR_FILTER;
    private static BlogService                  BLOG_SERVICE;
    private static DictionaryService            DICTIONARY_SERVICE;
    private static NodeService                  NODE_SERVICE;
    private static PersonService                PERSON_SERVICE;
    private static RetryingTransactionHelper    TRANSACTION_HELPER;
    private static SiteService                  SITE_SERVICE;
    private static TaggingService               TAGGING_SERVICE;
    
    private static final String TEST_USER = BlogServiceImplTest.class.getSimpleName() + "_testuser";
    private static final String ADMIN_USER = AuthenticationUtil.getAdminUserName();
    
    /**
     * Temporary test nodes (created during a test method) that need deletion after the test method.
     */
    private List testNodesToTidy = new ArrayList();
    /**
     * Temporary test nodes (created BeforeClass) that need deletion after this test class.
     */
    private static List CLASS_TEST_NODES_TO_TIDY = new ArrayList();
    private static SiteInfo BLOG_SITE;
    private static NodeRef BLOG_CONTAINER_NODE;
    
    @BeforeClass public static void initTestsContext() throws Exception
    {
        AUTHENTICATION_SERVICE = (MutableAuthenticationService)testContext.getBean("authenticationService");
        BEHAVIOUR_FILTER       = (BehaviourFilter)testContext.getBean("policyBehaviourFilter");
        BLOG_SERVICE           = (BlogService)testContext.getBean("blogService");
        DICTIONARY_SERVICE     = (DictionaryService)testContext.getBean("dictionaryService");
        NODE_SERVICE           = (NodeService)testContext.getBean("nodeService");
        PERSON_SERVICE         = (PersonService)testContext.getBean("personService");
        TRANSACTION_HELPER     = (RetryingTransactionHelper)testContext.getBean("retryingTransactionHelper");
        SITE_SERVICE           = (SiteService)testContext.getBean("siteService");
        TAGGING_SERVICE        = (TaggingService)testContext.getBean("TaggingService");
        
        AuthenticationUtil.setFullyAuthenticatedUser(ADMIN_USER);
        createUser(TEST_USER);
        
        // We need to create the test site as the test user so that they can contribute content to it in tests below.
        AuthenticationUtil.setFullyAuthenticatedUser(TEST_USER);
        createTestSiteWithBlogContainer();
    }
    
    private static void createTestSiteWithBlogContainer() throws Exception
    {
        BLOG_SITE = TRANSACTION_HELPER.doInTransaction(
            new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public SiteInfo execute() throws Throwable
                {
                    SiteInfo site = SITE_SERVICE.createSite("BlogSitePreset", BlogServiceImplTest.class.getSimpleName() + "_testSite" + GUID.generate(),
                                            "test site title", "test site description", SiteVisibility.PUBLIC);
                    CLASS_TEST_NODES_TO_TIDY.add(site.getNodeRef());
                    return site;
                }
            });
                    
        BLOG_CONTAINER_NODE = TRANSACTION_HELPER.doInTransaction(
            new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public NodeRef execute() throws Throwable
                {
                    SiteInfo site = BLOG_SITE;
                    NodeRef result = SITE_SERVICE.getContainer(site.getShortName(), BlogServiceImpl.BLOG_COMPONENT);
                      
                    if (result == null)
                    {
                       result = SITE_SERVICE.createContainer(site.getShortName(), BlogServiceImpl.BLOG_COMPONENT,
                             ContentModel.TYPE_FOLDER, null);
                       CLASS_TEST_NODES_TO_TIDY.add(result);
                    }
                      
                    return result;
                }
            });
    }
    /**
     * By default, all tests are run as the admin user.
     */
    @Before public void setAdminUser()
    {
        AuthenticationUtil.setFullyAuthenticatedUser(ADMIN_USER);
    }
    
    @After public void deleteTestNodes() throws Exception
    {
        performDeletionOfNodes(testNodesToTidy);
    }
    
    @AfterClass public static void deleteClassTestNodesAndUsers() throws Exception
    {
        performDeletionOfNodes(CLASS_TEST_NODES_TO_TIDY);
        deleteUser(TEST_USER);
    }
    /**
     * Deletes the specified NodeRefs, if they exist.
     * @param nodesToDelete List
     */
    private static void performDeletionOfNodes(final List nodesToDelete)
    {
        TRANSACTION_HELPER.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public Void execute() throws Throwable
                {
                    AuthenticationUtil.setFullyAuthenticatedUser(ADMIN_USER);
                    
                    for (NodeRef node : nodesToDelete)
                    {
                        if (NODE_SERVICE.exists(node))
                        {
                            // st:site nodes can only be deleted via the SiteService
                            if (NODE_SERVICE.getType(node).equals(SiteModel.TYPE_SITE))
                            {
                                
                                SiteInfo siteInfo = SITE_SERVICE.getSite(node);
                                SITE_SERVICE.deleteSite(siteInfo.getShortName());
                            }
                            else
                            {
                                NODE_SERVICE.deleteNode(node);
                            }
                        }
                    }
                    
                    return null;
                }
            });
    }
    
    @Test public void createDraftBlogPostsAndGetPagedResults() throws Exception
    {
        final int arbitraryNumberGreaterThanPageSize = 42;
        final List submittedBlogPosts = TRANSACTION_HELPER.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback>()
            {
                @Override
                public List execute() throws Throwable
                {
                    List results = new ArrayList();
                    
                    for (int i = 0; i < arbitraryNumberGreaterThanPageSize; i++)
                    {
                        BlogPostInfo newBlogPost;
                        if(i % 2 == 0)
                        {
                           // By container ref
                           newBlogPost = BLOG_SERVICE.createBlogPost(BLOG_CONTAINER_NODE, "title_" + i, "Hello world", true);
                        }
                        else
                        {
                           // By site name
                           newBlogPost = BLOG_SERVICE.createBlogPost(BLOG_SITE.getShortName(), "title_" + i, "Hello world", true);
                        }
                        
                        results.add(newBlogPost.getNodeRef());
                        testNodesToTidy.add(newBlogPost.getNodeRef());
                    }
                    
                    return results;
                }
            });
        
        TRANSACTION_HELPER.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public Void execute() throws Throwable
                {
                    List recoveredBlogPosts = new ArrayList(arbitraryNumberGreaterThanPageSize);
                    
                    final int pageSize = 10;
                    PagingRequest pagingReq = new PagingRequest(0, pageSize, null);
                    pagingReq.setRequestTotalCountMax(arbitraryNumberGreaterThanPageSize); // must be set if calling getTotalResultCount() later
                    
                    PagingResults pagedResults = BLOG_SERVICE.getDrafts(BLOG_CONTAINER_NODE, ADMIN_USER, pagingReq);
                    assertEquals("Wrong total result count.", arbitraryNumberGreaterThanPageSize, (int)pagedResults.getTotalResultCount().getFirst());
                    
                    while (pagedResults.hasMoreItems())
                    {
                        recoveredBlogPosts.addAll(pagedResults.getPage());
                        pagingReq = new PagingRequest(pagingReq.getSkipCount() + pageSize, pageSize, null);
                        pagedResults = BLOG_SERVICE.getDrafts(BLOG_CONTAINER_NODE, ADMIN_USER, pagingReq);
                    }
                    // and the last page, which only has 2 items in it.
                    recoveredBlogPosts.addAll(pagedResults.getPage());
                    
                    assertEquals("Wrong number of blog posts.", submittedBlogPosts.size(), recoveredBlogPosts.size());
                    
                    // Check the list is sorted by cm:created, descending order.
                    assertNodeRefsAreSortedBy(recoveredBlogPosts, ContentModel.PROP_CREATED, false);
                    
                    return null;
                }
            });
    }
    
    /**
     * This method asserts that the given List has NodeRefs in order of the specified date property.
     * 
     * @param blogPosts List
     * @param property a Date property
     * @param ascendingOrder true if ascending order, false for descending.
     */
    private void assertNodeRefsAreSortedBy(List blogPosts, QName property, boolean ascendingOrder)
    {
        final PropertyDefinition propertyDef = DICTIONARY_SERVICE.getProperty(property);
        assertNotNull("Property not recognised.", propertyDef);
        assertEquals("Property was not a Date", DataTypeDefinition.DATETIME, propertyDef.getDataType().getName());
        
        if (blogPosts.size() > 1)
        {
            for (int i = 0; i < blogPosts.size() - 1; i++)
            {
                NodeRef nodeRef1 = blogPosts.get(i).getNodeRef();
                NodeRef nodeRef2 = blogPosts.get(i + 1).getNodeRef();
                Date date1 = (Date) NODE_SERVICE.getProperty(nodeRef1, property);
                Date date2 = (Date) NODE_SERVICE.getProperty(nodeRef2, property);
                
                // Equal dates are applicable to either sort order
                if (date1.equals(date2))
                {
                    continue;
                }
                
                if (ascendingOrder)
                {
                    assertTrue("BlogPosts not asc-sorted by " + property + ". Error at index " + i, date1.before(date2));
                }
                else
                {
                    assertTrue("BlogPosts not desc-sorted by " + property + ". Error at index " + i, date1.after(date2));
                }
            }
        }
    }
    
    @Test public void createTaggedDraftBlogPost() throws Exception
    {
        // Our tags, which are a mixture of English, Accented European and Chinese
        final List tags = Arrays.asList(new String[]{
              "alpha", "beta", "gamma", "fran\u00e7ais", "chinese_\u535a\u5ba2"});
        
        // Create a list of Blog Posts, all drafts, each with one of the tags above.
        TRANSACTION_HELPER.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback>()
            {
                
                @Override
                public List execute() throws Throwable
                {
                    List results = new ArrayList();
                    
                    for (String tag : tags)
                    {
                        final String blogTitle = "draftWithTag" + tag;
                        BlogPostInfo newBlogPost = BLOG_SERVICE.createBlogPost(BLOG_CONTAINER_NODE, blogTitle, "Hello world", true);
                        TAGGING_SERVICE.addTags(newBlogPost.getNodeRef(), Arrays.asList(new String[]{tag}));
                        testNodesToTidy.add(newBlogPost.getNodeRef());
                        results.add(newBlogPost.getNodeRef());
                    }
                    
                    return results;
                }
            });
        
        // Check we get the correct tags back
        TRANSACTION_HELPER.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
            {
                @Override
                public Void execute() throws Throwable
                {
                    // Now we'll recover these blogposts & we should expect to find the same tags.
                    Set expectedTags = new HashSet();
                    expectedTags.addAll(tags);
                    
                    PagingRequest pagingReq = new PagingRequest(0, 10, null);
                    
                    PagingResults pagedResults = BLOG_SERVICE.getDrafts(BLOG_CONTAINER_NODE, ADMIN_USER, pagingReq);
                    assertEquals("Wrong number of blog posts", tags.size(), pagedResults.getPage().size());
                    
                    for (BlogPostInfo bpi : pagedResults.getPage())
                    {
                        NodeRef blogNode = bpi.getNodeRef();
                        List recoveredTags = TAGGING_SERVICE.getTags(blogNode);
                        assertEquals("Wrong number of tags", 1, recoveredTags.size());
                        
                        String tag = recoveredTags.get(0);
                        assertTrue("Tag found on node but not expected: " + tag, expectedTags.remove(tag));
                    }
                    assertTrue("Not all tags were recovered from a blogpost", expectedTags.isEmpty());
                    
                    return null;
                }
            });
        
        // Check we can find the posts by their tags
        TRANSACTION_HELPER.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback()
        {
            @Override
            public Void execute() throws Throwable
            {
                PagingRequest pagingReq = new PagingRequest(0, 10, null);
                RangedDateProperty dates = new RangedDateProperty(null, null, ContentModel.PROP_CREATED); 
                for (String tag : tags)
                {
                   PagingResults pagedResults = 
                      BLOG_SERVICE.findBlogPosts(BLOG_CONTAINER_NODE, dates, tag, pagingReq);
                   
                   // Check we found our post
                   assertEquals("Wrong number of blog posts for " + tag, 1, pagedResults.getPage().size());
                }
                return null;
            }
        });
    }
    
    /**
     * This test method uses the eventually consistent find*() method and so may fail if Lucene is disabled.
     */
    @Test public void findBlogPostsByPublishedDate() throws Exception
    {
        final List tags = Arrays.asList(new String[]{"hello", "goodbye"});
        
        // Going to set some specific published dates on these blog posts & query by date.
        final Calendar cal = Calendar.getInstance();
        cal.set(1971, 6, 15);
        final Date _1971 = cal.getTime();
        cal.set(1975, 0, 1);
        final Date _1975 = cal.getTime();
        cal.set(1980, 0, 1);
        final Date _1980 = cal.getTime();
        cal.set(1981, 0, 1);
        final Date _1981 = cal.getTime();
        cal.set(1985, 6, 15);
        final Date _1985 = cal.getTime();
        cal.set(1991, 6, 15);
        final Date _1991 = cal.getTime();
        
        final Map blogPosts = TRANSACTION_HELPER.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback