/*
 * #%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.audit.access;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.transaction.UserTransaction;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.audit.AuditComponent;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.version.VersionModel;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.ContentData;
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.repository.StoreRef;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.cmr.version.VersionType;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.debug.NodeStoreInspector;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ComparisonFailure;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.context.ApplicationContext;

/**
 * Integration test for AccessAuditor.
 * 
 * @author Alan Davis
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class AccessAuditorTest
{
    // Integration test environment
    private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
    private static ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
    private static NodeService nodeService = serviceRegistry.getNodeService();
    private static TransactionService transactionService = serviceRegistry.getTransactionService(); 
    private static NamespaceService namespaceService = serviceRegistry.getNamespaceService();
    private static PolicyComponent policyComponent = (PolicyComponent) ctx.getBean("policyComponent");
    private static AuthenticationComponent authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent");
    
    // Integration test data store
    private static StoreRef storeRef;
    private static NodeRef homeFolder;
    private static NodeRef folder0;
    private static NodeRef folder1;
    private static NodeRef folder2;
    private static NodeRef folder3;
    private static NodeRef content0;
    private static NodeRef content1;
    private static NodeRef content2;
    private static NodeRef content3;
    
    // Test setup
    private static AccessAuditor auditor;
    private static Properties properties;
    private static NodeRef workingCopyNodeRef;
    private UserTransaction txn;

    // To check results
    private static List<Map<String, Serializable>> auditMapList = new ArrayList<Map<String, Serializable>>();
    
    @SuppressWarnings("unchecked")
    @BeforeClass
    public static void setUpBeforeClass() throws Exception
    {
        AuthenticationUtil.setRunAsUserSystem();
        
        storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis());
        NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
        
        homeFolder = nodeService.createNode(
                rootNodeRef,
                ContentModel.ASSOC_CHILDREN,
                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "homeFolder"),
                ContentModel.TYPE_FOLDER).getChildRef();

        folder0 = newFolder(homeFolder, "folder0");
        folder1 = newFolder(homeFolder, "folder1");
        folder2 = newFolder(homeFolder, "folder2");
        folder3 = newFolder(homeFolder, "folder3");
        
        content0 = newContent(folder0, "content0");
        content1 = newContent(folder1, "content1");
        content2 = newContent(folder2, "content2");
        content3 = newContent(folder3, "content3");

        System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef));

        try
        {
            authenticationComponent.clearCurrentSecurityContext();
        }
        catch (Throwable e)
        {
            // ignore
        }

        // Mock up an auditComponent to see the results of our tests
        AuditComponent auditComponent = mock(AuditComponent.class);
        when(auditComponent.areAuditValuesRequired(anyString())).thenReturn(true);
        when(auditComponent.recordAuditValues(anyString(), anyMap())).thenAnswer(new Answer<Map<String, Serializable>>()
                {
                    public Map<String, Serializable> answer(InvocationOnMock invocation) throws Throwable
                    {
                        Object[] args = invocation.getArguments();
                        Map<String, Serializable> auditMap = (Map<String, Serializable>)args[1];
                        if ("/alfresco-access/transaction".equals(args[0]))
                        {
                            auditMapList.add(auditMap);
                        }
                        return auditMap;
                    }
                });

        // Create our own properties object for use by the auditor
        properties = new Properties();
        properties.put("audit.alfresco-access.sub-actions.enabled", "false");

        // Set properties
        auditor = new AccessAuditor();
        auditor.setTransactionService(transactionService);
        auditor.setNamespaceService(namespaceService);
        auditor.setNodeInfoFactory(new NodeInfoFactory(nodeService, namespaceService));
        auditor.setPolicyComponent(policyComponent);
        auditor.setProperties(properties);
        auditor.setAuditComponent(auditComponent);
        
        // Simulate spring call after properties set
        auditor.afterPropertiesSet();    
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception
    {
        AuthenticationUtil.setRunAsUserSystem();

        System.out.println(NodeStoreInspector.dumpNodeStore(nodeService, storeRef));
        
        nodeService.deleteStore(storeRef);
        
        try
        {
            authenticationComponent.clearCurrentSecurityContext();
        }
        catch (Throwable e)
        {
            // ignore
        }
        
        properties = null;
        auditor = null;
    }

    @Before
    public void setUp() throws Exception
    {
        // authenticate
        authenticationComponent.setSystemUserAsCurrentUser();
        
        // start the transaction
        txn = transactionService.getUserTransaction();
        txn.begin();
    }

    @After
    public void tearDown() throws Exception
    {
        try
        {
            authenticationComponent.clearCurrentSecurityContext();
        }
        catch (Throwable e)
        {
            // ignore
        }

        try
        {
            if (txn != null)
            {
                txn.rollback();
            }
        }
        catch (Throwable e)
        {
            // ignore
        }
        
        auditMapList.clear();
    }

    private static NodeRef newFolder(NodeRef parent, String name)
    {
        return serviceRegistry.getFileFolderService().create(
                parent,
                name,
                ContentModel.TYPE_FOLDER).getNodeRef();
    }

    private static NodeRef newContent(NodeRef parent, String name)
    {
        PropertyMap propertyMap0 = new PropertyMap();
        propertyMap0.put(ContentModel.PROP_CONTENT, new ContentData(null, "text/plain", 0L, "UTF-16", Locale.ENGLISH));
        NodeRef content = nodeService.createNode(
                parent,
                ContentModel.ASSOC_CONTAINS,
                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name),
                ContentModel.TYPE_CONTENT,
                propertyMap0).getChildRef();
        ContentWriter writer = serviceRegistry.getContentService().getWriter(content, ContentModel.TYPE_CONTENT, true);
        writer.putContent("The cat sat on the mat.");
        
        return content;
    }

    private Map<String, Serializable> getVersionProperties()
    {
        Map<String, Serializable> versionProperties = new HashMap<String, Serializable>();
        versionProperties.put(Version.PROP_DESCRIPTION, "This is a test");
        return versionProperties;
    }

    private void assertContains(String expected, Serializable actual)
    {
        String actualString = (String)actual;
        if (actual == null || !actualString.contains(expected))
        {
            throw new ComparisonFailure("Expected not contained in actual.", expected, actualString);
        }
    }

    @Test
    public final void test01OnCreateNodeAndOnUpdateProperties() throws Exception
    {
        newContent(homeFolder, "content4");

        txn.commit();
        txn = null;
        
        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("CREATE", auditMap.get("action"));
        assertContains("createNode", auditMap.get("sub-actions"));
        assertContains("updateNodeProperties", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:content4", auditMap.get("path"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test02OnCopyComplete() throws Exception
    {
      serviceRegistry.getFileFolderService().copy(content2, folder1, null); // keep leaf name

      txn.commit();
      txn = null;
      
      // TODO do we record the parent or the full path? Do we need to?

      assertEquals(1, auditMapList.size());
      Map<String, Serializable> auditMap = auditMapList.get(0);
      assertEquals("COPY", auditMap.get("action"));
      assertContains("createNode", auditMap.get("sub-actions"));
      assertContains("updateNodeProperties", auditMap.get("sub-actions"));
      assertContains("addNodeAspect", auditMap.get("sub-actions"));
      assertContains("copyNode", auditMap.get("sub-actions"));
      assertEquals("/cm:homeFolder/cm:folder1/cm:content2", auditMap.get("path"));
      assertEquals("/cm:homeFolder/cm:folder2/cm:content2", auditMap.get("copy/from/path"));
      assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test03OnCopyCompleteAndNewName() throws Exception
    {
      serviceRegistry.getFileFolderService().copy(content2, folder1, "newName1");

      txn.commit();
      txn = null;
      
      // TODO do we record the parent or the full path? Do we need to?

      assertEquals(1, auditMapList.size());
      Map<String, Serializable> auditMap = auditMapList.get(0);
      assertEquals("COPY", auditMap.get("action"));
      assertContains("createNode", auditMap.get("sub-actions"));
      assertContains("updateNodeProperties", auditMap.get("sub-actions"));
      assertContains("addNodeAspect", auditMap.get("sub-actions"));
      assertContains("copyNode", auditMap.get("sub-actions"));
      assertEquals("/cm:homeFolder/cm:folder1/cm:newName1", auditMap.get("path"));
      assertEquals("/cm:homeFolder/cm:folder2/cm:content2", auditMap.get("copy/from/path"));
      assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test04OnMoveNode() throws Exception
    {
        serviceRegistry.getNodeService().moveNode(content3, folder1, ContentModel.ASSOC_CONTAINS,  null);  // keep leaf name

        txn.commit();
        txn = null;
        
        // TODO do we record the parent or the full path? Do we need to?
        
        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("MOVE", auditMap.get("action"));
        assertContains("moveNode", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content3", auditMap.get("path"));
        assertEquals("/cm:homeFolder/cm:folder3/cm:content3", auditMap.get("move/from/path"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test05OnMoveNodeAndNewName() throws Exception
    {
        serviceRegistry.getNodeService().moveNode(content3, folder1, ContentModel.ASSOC_CONTAINS,  QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "newName2"));

        txn.commit();
        txn = null;
        
        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("MOVE", auditMap.get("action"));
        assertContains("moveNode", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:newName2", auditMap.get("path"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content3", auditMap.get("move/from/path"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test06BeforeDeleteNode() throws Exception
    {
        serviceRegistry.getNodeService().deleteNode(content0);

        txn.commit();
        txn = null;
        
        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("DELETE", auditMap.get("action"));
        assertContains("deleteNode", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder0/cm:content0", auditMap.get("path"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test07OnAddAspect() throws Exception
    {
        serviceRegistry.getNodeService().addAspect(content1, ContentModel.ASPECT_AUTHOR, null);
        serviceRegistry.getNodeService().addAspect(content1, ContentModel.ASPECT_OWNABLE, null);

        txn.commit();
        txn = null;
        
        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap  = auditMapList.get(0);
        assertEquals("addNodeAspect", auditMap.get("action"));
        assertContains("addNodeAspect", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path"));
        assertEquals(2, ((Set<?>)auditMap.get("aspects/add")).size());
        assertTrue("Individual author aspect missing", auditMap.containsKey("aspects/add/cm:author"));
        assertTrue("Individual ownable aspect missing", auditMap.containsKey("aspects/add/cm:ownable"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test08OnRemoveAspect() throws Exception
    {
        serviceRegistry.getNodeService().removeAspect(content1, ContentModel.ASPECT_AUTHOR);

        txn.commit();
        txn = null;
        
        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("deleteNodeAspect", auditMap.get("action"));
        assertContains("deleteNodeAspect", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path"));
        assertEquals(1, ((Set<?>)auditMap.get("aspects/delete")).size());
        assertTrue("Individual author aspect missing", auditMap.containsKey("aspects/delete/cm:author"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test09OnContentUpdate() throws Exception
    {
        ContentWriter writer = serviceRegistry.getContentService().getWriter(content1, ContentModel.TYPE_CONTENT, true);
        writer.putContent("The cow jumped over the moon.");

        txn.commit();
        txn = null;

        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("UPDATE CONTENT", auditMap.get("action")); // TODO Should be UPDATE CONTENT
        assertContains("updateContent", auditMap.get("sub-actions"));
        assertContains("updateNodeProperties", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test10OnContentRead() throws Exception
    {
        serviceRegistry.getContentService().getReader(content1, ContentModel.TYPE_CONTENT);

        txn.commit();
        txn = null;

        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("READ", auditMap.get("action"));
        assertContains("readContent",  auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test11OnCreateVersion() throws Exception
    {
       Map<String, Serializable> versionProperties = getVersionProperties();
       serviceRegistry.getVersionService().createVersion(content1, versionProperties);

        txn.commit();
        txn = null;
        
        assertEquals(1, auditMapList.size());
        Map<String, Serializable> auditMap = auditMapList.get(0);
        assertEquals("CREATE VERSION", auditMap.get("action"));
        assertContains("updateNodeProperties", auditMap.get("sub-actions"));
        assertContains("createVersion", auditMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", auditMap.get("path"));
        assertTrue("cm:versionable should be a value with in the set", ((Set<?>)auditMap.get("aspects/add")).contains(ContentModel.ASPECT_VERSIONABLE));
        assertTrue("Individual versionable aspect should exist", auditMap.containsKey("aspects/add/cm:versionable"));
        assertEquals("cm:content", auditMap.get("type"));
    }

    @Test
    public final void test12OnCheckOut() throws Exception
    {
        workingCopyNodeRef = serviceRegistry.getCheckOutCheckInService().checkout(content1);
        
        txn.commit();
        txn = null;

        assertEquals(2, auditMapList.size());
        boolean origIn0 = ((String)auditMapList.get(0).get("path")).endsWith("cm:content1");
        Map<String, Serializable> origMap = auditMapList.get(origIn0 ? 0 : 1);
        Map<String, Serializable> workMap = auditMapList.get(origIn0 ? 1 : 0);
        
        // original file
        assertEquals("addNodeAspect", origMap.get("action"));
        // createNode createContent readContent updateNodeProperties addNodeAspect copyNode checkOut createVersion
        assertContains("updateNodeProperties", origMap.get("sub-actions"));
        assertEquals("cm:content", origMap.get("type"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", origMap.get("path"));

        // working copy
        assertEquals("CHECK OUT", workMap.get("action"));
        assertContains("createNode", workMap.get("sub-actions"));
        assertContains("createContent", workMap.get("sub-actions"));
        assertContains("updateNodeProperties", workMap.get("sub-actions"));
        assertContains("addNodeAspect", workMap.get("sub-actions"));
        assertContains("copyNode", workMap.get("sub-actions"));
        assertContains("checkOut", workMap.get("sub-actions"));
        assertContains("createVersion", workMap.get("sub-actions"));
        assertTrue("Expected working copy", ((String)workMap.get("path")).endsWith("(Working Copy)") &&
                                            ((String)workMap.get("path")).startsWith("/cm:homeFolder/cm:folder1/"));
        assertEquals("cm:content", workMap.get("type"));
    }

    @Test
    public final void test13OnCheckIn() throws Exception
    {
        Map<String, Serializable> checkinProperties = new HashMap<String, Serializable>();
        checkinProperties.put(Version.PROP_DESCRIPTION, null);
        checkinProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR);
        serviceRegistry.getCheckOutCheckInService().checkin(workingCopyNodeRef, checkinProperties);
        
        txn.commit();
        txn = null;

        assertEquals(2, auditMapList.size());
        boolean origIn0 = ((String)auditMapList.get(0).get("path")).endsWith("cm:content1");
        Map<String, Serializable> origMap = auditMapList.get(origIn0 ? 0 : 1);
        Map<String, Serializable> workMap = auditMapList.get(origIn0 ? 1 : 0);

        // working copy
        assertEquals("DELETE", workMap.get("action"));
        assertContains("deleteNode", workMap.get("sub-actions"));
        assertTrue("Expected working copy", ((String)workMap.get("path")).endsWith("(Working Copy)") &&
                                            ((String)workMap.get("path")).startsWith("/cm:homeFolder/cm:folder1/"));
        assertEquals("cm:content", workMap.get("type"));

        // original file
        assertEquals("CHECK IN", origMap.get("action"));
        assertContains("deleteNodeAspect", origMap.get("sub-actions"));     
        assertContains("copyNode", origMap.get("sub-actions"));
        assertContains("createVersion", origMap.get("sub-actions"));
        assertContains("updateNodeProperties", origMap.get("sub-actions"));
        assertContains("checkIn", origMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", origMap.get("path"));
        assertEquals("cm:content", origMap.get("type"));
    }

    @Test
    public final void test14OnCancelCheckOut() throws Exception
    {
        workingCopyNodeRef = serviceRegistry.getCheckOutCheckInService().checkout(content1);
        txn.commit();
        txn = null;

        tearDown();
        setUp();
        
        serviceRegistry.getCheckOutCheckInService().cancelCheckout(workingCopyNodeRef);
        
        txn.commit();
        txn = null;

        assertEquals(2, auditMapList.size());
        boolean origIn0 = ((String)auditMapList.get(0).get("path")).endsWith("cm:content1");
        Map<String, Serializable> origMap = auditMapList.get(origIn0 ? 0 : 1);
        Map<String, Serializable> workMap = auditMapList.get(origIn0 ? 1 : 0);
        
        // working copy
        assertEquals("DELETE", workMap.get("action"));
        assertContains("deleteNode", workMap.get("sub-actions"));
        assertTrue("Expected working copy", ((String)workMap.get("path")).endsWith("(Working Copy)") &&
                                              ((String)workMap.get("path")).startsWith("/cm:homeFolder/cm:folder1/"));
        assertEquals("cm:content", workMap.get("type"));

        // original file
        assertEquals("CANCEL CHECK OUT", origMap.get("action"));
        assertContains("deleteNodeAspect", origMap.get("sub-actions"));
        assertContains("cancelCheckOut", origMap.get("sub-actions"));
        assertEquals("/cm:homeFolder/cm:folder1/cm:content1", origMap.get("path"));
        assertEquals("cm:content", origMap.get("type"));
    }
}