/*
 * #%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.content.caching;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;

import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.content.caching.ContentCacheImpl.NumericFileNameComparator;
import org.alfresco.repo.content.filestore.FileContentReader;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.GUID;
import org.alfresco.util.TempFileProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

/**
 * Tests for the ContentCacheImpl class.
 * 
 * @author Matt Ward
 */
@RunWith(MockitoJUnitRunner.class)
@Category(OwnJVMTestsCategory.class)
public class ContentCacheImplTest
{
    private ContentCacheImpl contentCache;
    private @Mock SimpleCache<Key, String> lookupTable;
    

    @Before
    public void setUp() throws Exception
    {
        contentCache = new ContentCacheImpl();
        contentCache.setMemoryStore(lookupTable);
        contentCache.setCacheRoot(TempFileProvider.getTempDir());
    }
    
    
    @Test(expected=IllegalArgumentException.class)
    public void cannotSetNullCacheRoot()
    {
        contentCache.setCacheRoot(null);
    }
    
    
    @Test
    public void willCreateNonExistentCacheRoot()
    {
        File cacheRoot = new File(TempFileProvider.getTempDir(), GUID.generate());
        cacheRoot.deleteOnExit();
        assertFalse("Pre-condition of test is that cacheRoot does not exist", cacheRoot.exists());
        
        contentCache.setCacheRoot(cacheRoot);
        
        assertTrue("cacheRoot should have been created", cacheRoot.exists());
    }
    
    
    @Test
    public void canGetReaderForItemInCacheHavingLiveFile()
    {
        final String url = "store://content/url.bin";
        Mockito.when(lookupTable.contains(Key.forUrl(url))).thenReturn(true);
        final String path = tempfile().getAbsolutePath();
        Mockito.when(lookupTable.get(Key.forUrl(url))).thenReturn(path);
        
        FileContentReader reader = (FileContentReader) contentCache.getReader(url);
        
        assertEquals("Reader should have correct URL", url, reader.getContentUrl());
        assertEquals("Reader should be for correct cached content file", path, reader.getFile().getAbsolutePath());
        // Important the get(path) was called, so that the timeToIdle is reset
        // for the 'reverse lookup' as well as the URL to path mapping.
        Mockito.verify(lookupTable).get(Key.forCacheFile(path));
    }

    
    @Test(expected=CacheMissException.class)
    public void getReaderForItemInCacheButMissingContentFile()
    {
        final String url = "store://content/url.bin";
        Mockito.when(lookupTable.contains(Key.forUrl(url))).thenReturn(true);
        final String path = "/no/content/file/at/this/path.bin";
        Mockito.when(lookupTable.get(Key.forUrl(url))).thenReturn(path);
          
        try
        {
            contentCache.getReader(url);
        }
        finally
        {
            // Important the get(path) was called, so that the timeToIdle is reset
            // for the 'reverse lookup' as well as the URL to path mapping.
            Mockito.verify(lookupTable).get(Key.forCacheFile(path));
        }
    }


    @Test(expected=CacheMissException.class)
    public void getReaderWhenItemNotInCache()
    {
        final String url = "store://content/url.bin";
        Mockito.when(lookupTable.contains(Key.forUrl(url))).thenReturn(false);
        
        contentCache.getReader(url);
    }
    
    
    @Test
    public void contains()
    {
        final String url = "store://content/url.bin";
        
        Mockito.when(lookupTable.contains(Key.forUrl(url))).thenReturn(true);
        assertTrue(contentCache.contains(Key.forUrl(url)));
        assertTrue(contentCache.contains(url));
        
        Mockito.when(lookupTable.contains(Key.forUrl(url))).thenReturn(false);
        assertFalse(contentCache.contains(Key.forUrl(url)));
        assertFalse(contentCache.contains(url));
    }
    
    
    @Test
    public void putIntoLookup()
    {
        final Key key = Key.forUrl("store://some/url");
        final String value = "/some/path";
        
        contentCache.putIntoLookup(key, value);
        
        Mockito.verify(lookupTable).put(key, value);
    }
    
    
    @Test
    public void getCacheFilePath()
    {
        final String url = "store://some/url.bin";
        final String expectedPath = "/some/cache/file/path";
        Mockito.when(lookupTable.get(Key.forUrl(url))).thenReturn(expectedPath);
        
        String path = contentCache.getCacheFilePath(url);
        
        assertEquals("Paths must match", expectedPath, path);
    }
    
    
    @Test
    public void getContentUrl()
    {
        final File cacheFile = new File("/some/path");
        final String expectedUrl = "store://some/url";
        Mockito.when(lookupTable.get(Key.forCacheFile(cacheFile))).thenReturn(expectedUrl);
        
        String url = contentCache.getContentUrl(cacheFile);
        
        assertEquals("Content URLs should match", expectedUrl, url);
    }
    
    
    @Test
    public void putForZeroLengthFile()
    {
        ContentReader contentReader = Mockito.mock(ContentReader.class);
        Mockito.when(contentReader.getSize()).thenReturn(0L);
        
        boolean putResult = contentCache.put("", contentReader);
        
        assertFalse("Zero length files should not be cached", putResult);
    }
    
    
    @Test
    public void putForNonEmptyFile()
    {
        ContentReader contentReader = Mockito.mock(ContentReader.class);
        Mockito.when(contentReader.getSize()).thenReturn(999000L);
        
        ArgumentCaptor<File> cacheFileArg = ArgumentCaptor.forClass(File.class);
        
        Mockito.doAnswer(new Answer() {
            public Object answer(InvocationOnMock invocation) {
                Object[] args = invocation.getArguments();
                File f = (File) args[0];
                // The file must be present for the rename to work
                // in ContentCacheImpl.put()
                try
                {
                    f.createNewFile();
                }
                catch (IOException error)
                {
                    error.printStackTrace();
                }
                return null;
            }})
        .when(contentReader).getContent(cacheFileArg.capture());
        
        final String url = "store://some/url.bin";
        boolean putResult = contentCache.put(url, contentReader);
        
        assertTrue("Non-empty files should be cached", putResult);

        // The rename will have taken effect
        String cacheFilePath = cacheFileArg.getValue().getAbsolutePath().replace(".tmp", ".bin");
        
        // Check cached item is recorded properly in cache
        Mockito.verify(lookupTable).put(Key.forUrl(url), cacheFilePath);
        Mockito.verify(lookupTable).put(Key.forCacheFile(cacheFilePath), url);
    }
    
    
    @Test
    public void remove()
    {
        final String url = "store://some/url.bin";
        final String path = "/some/path";
        Mockito.when(lookupTable.get(Key.forUrl(url))).thenReturn(path);
        
        contentCache.remove(url);
        
        Mockito.verify(lookupTable).remove(Key.forUrl(url));
        Mockito.verify(lookupTable).remove(Key.forCacheFile(path));
    }
    
    @Test
    public void deleteFile()
    {
        File cacheFile = tempfile();
        assertTrue("Temp file should have been written", cacheFile.exists());
        Mockito.when(contentCache.getCacheFilePath("url")).thenReturn(cacheFile.getAbsolutePath());
        
        contentCache.deleteFile("url");
        
        assertFalse("File should have been deleted", cacheFile.exists());
    }
    
    @Test
    public void getWriter()
    {
        final String url = "store://some/url.bin";
        
        FileContentWriter writer = (FileContentWriter) contentCache.getWriter(url);
        writer.putContent("Some test content for " + getClass().getName());
        
        assertEquals(url, writer.getContentUrl());
        // Check cached item is recorded properly in cache
        Mockito.verify(lookupTable).put(Key.forUrl(url), writer.getFile().getAbsolutePath());
        Mockito.verify(lookupTable).put(Key.forCacheFile(writer.getFile().getAbsolutePath()), url);
    }
    
    @Test
    public void compareNumericFileNames()
    {
        NumericFileNameComparator comparator = new NumericFileNameComparator();
        assertEquals(-1, comparator.compare(new File("1"), new File("2")));
        assertEquals(0, comparator.compare(new File("2"), new File("2")));
        assertEquals(1, comparator.compare(new File("2"), new File("1")));
        
        // Make sure that ordering is numeric and not by string value
        assertEquals(-1, comparator.compare(new File("3"), new File("20")));
        assertEquals(1, comparator.compare(new File("20"), new File("3")));
        
        assertEquals(-1, comparator.compare(new File("3"), new File("non-numeric")));
        assertEquals(1, comparator.compare(new File("non-numeric"), new File("3")));
    }
    
    @Test
    public void canVisitOldestDirsFirst()
    {
        File cacheRoot = new File(TempFileProvider.getTempDir(), GUID.generate());
        cacheRoot.deleteOnExit();
        contentCache.setCacheRoot(cacheRoot);
        
        File f1 = tempfile(createDirs("2000/3/30/17/45/31"), "files-are-unsorted.bin");
        File f2 = tempfile(createDirs("2000/3/4/17/45/31"), "another-file.bin");
        File f3 = tempfile(createDirs("2010/12/24/23/59/58"), "a-second-before.bin");
        File f4 = tempfile(createDirs("2010/12/24/23/59/59"), "last-one.bin");
        File f5 = tempfile(createDirs("2000/1/7/2/7/12"), "first-one.bin");
        
        // Check that directories and files are visited in correct order
        FileHandler handler = Mockito.mock(FileHandler.class);
        contentCache.processFiles(handler);
        
        InOrder inOrder = Mockito.inOrder(handler);
        inOrder.verify(handler).handle(f5);
        inOrder.verify(handler).handle(f2);
        inOrder.verify(handler).handle(f1);
        inOrder.verify(handler).handle(f3);
        inOrder.verify(handler).handle(f4);
    }
    


    private File tempfile()
    {
        return tempfile("cached-content", ".bin");
    }
    
    private File tempfile(String name, String suffix)
    {
        File file = TempFileProvider.createTempFile(name, suffix);
        file.deleteOnExit();
        return file;
    }
    
    private File tempfile(File dir, String name)
    {
        File f = new File(dir, name);
        try
        {
            f.createNewFile();
        }
        catch (IOException error)
        {
            throw new RuntimeException(error);
        }
        f.deleteOnExit();
        return f;
    }

    private File createDirs(String path)
    {
        File f = new File(contentCache.getCacheRoot(), path);
        f.mkdirs();
        return f;
    }
}
