/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.core.data;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Properties;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.ValueFactory;

import junit.framework.TestCase;

import org.apache.commons.io.FileUtils;
import org.apache.jackrabbit.api.JackrabbitRepository;
import org.apache.jackrabbit.api.JackrabbitRepositoryFactory;
import org.apache.jackrabbit.api.management.MarkEventListener;
import org.apache.jackrabbit.core.RepositoryFactoryImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test case for the scenario where the GC thread traverses the workspace and at
 * some point, a subtree that the GC thread did not see yet is moved to a location
 * that the thread has already traversed. The GC thread should not ignore binaries 
 * references by this subtree and eventually delete them.
 */
public class GCSubtreeMoveTest extends TestCase {

    private static final Logger logger = LoggerFactory.getLogger(GCSubtreeMoveTest.class);

    private String testDirectory;
    private JackrabbitRepository repository;
    private Session sessionGarbageCollector;
    private Session sessionMover;

    public void setUp() throws IOException {
        testDirectory = "target/" + getClass().getSimpleName()  + "/" + getName();
        FileUtils.deleteDirectory(new File(testDirectory));
    }

    public void tearDown() throws IOException {
        sessionGarbageCollector.logout();
        sessionMover.logout();
        repository.shutdown();

        repository = null;
        sessionGarbageCollector = null;
        sessionMover = null;

        FileUtils.deleteDirectory(new File(testDirectory));
        testDirectory = null;
    }

    public void test() {
        setupRepository();

        GarbageCollector garbageCollector = setupGarbageCollector();
        // To make sure even listener for NODE_ADDED is registered in GC.
        garbageCollector.setPersistenceManagerScan(false);

        assertEquals(0, getBinaryCount(garbageCollector));
        setupNodes();
        assertEquals(1, getBinaryCount(garbageCollector));
        garbageCollector.getDataStore().clearInUse();

        garbageCollector.setMarkEventListener(new MarkEventListener() {

            public void beforeScanning(Node node) throws RepositoryException {
                String path = node.getPath();
                if (path.startsWith("/node")) {
                    log("Traversing: " + node.getPath());
                }

                if ("/node1".equals(node.getPath())) {
                    String from = "/node2/node3";
                    String to = "/node0/node3";
                    log("Moving " + from + " -> " + to);
                    sessionMover.move(from, to);
                    sessionMover.save();
                    sleepForFile();
                }
            }
        });

        try {
            garbageCollector.getDataStore().clearInUse();
            garbageCollector.mark();
            garbageCollector.stopScan();
            sleepForFile();
            int numberOfDeleted = garbageCollector.sweep();
            log("Number of deleted: " + numberOfDeleted);
            // Binary data should still be there.
            assertEquals(1, getBinaryCount(garbageCollector));
        } catch (RepositoryException e) {
            e.printStackTrace();
            failWithException(e);
        } finally {
            garbageCollector.close();
        }
    }

    private void setupNodes() {
        try {
            Node rootNode = sessionMover.getRootNode();
            rootNode.addNode("node0");
            rootNode.addNode("node1");
            Node node2 = rootNode.addNode("node2");
            Node node3 = node2.addNode("node3");
            Node nodeWithBinary = node3.addNode("node-with-binary");
            ValueFactory vf = sessionGarbageCollector.getValueFactory();
            nodeWithBinary.setProperty("prop", vf.createBinary(new RandomInputStream(10, 1000)));
            sessionMover.save();
            sleepForFile();
        } catch (RepositoryException e) {
            failWithException(e);
        }
    }

    private void sleepForFile() {
        // Make sure the file is old (access time resolution is 2 seconds)
        try {
            Thread.sleep(2200);
        } catch (InterruptedException ignore) {
        }
    }

    private void setupRepository() {
        JackrabbitRepositoryFactory repositoryFactory = new RepositoryFactoryImpl();
        createRepository(repositoryFactory);
        login();
    }

    private void createRepository(JackrabbitRepositoryFactory repositoryFactory) {
        Properties prop = new Properties();
        prop.setProperty("org.apache.jackrabbit.repository.home", testDirectory);
        prop.setProperty("org.apache.jackrabbit.repository.conf", testDirectory + "/repository.xml");
        try {
            repository = (JackrabbitRepository)repositoryFactory.getRepository(prop);
        } catch (RepositoryException e) {
            failWithException(e);
        };
    }

    private void login() {
        try {
            sessionGarbageCollector = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
            sessionMover = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
        } catch (Exception e) {
            failWithException(e);
        }
    }

    private GarbageCollector setupGarbageCollector() {
        try {
            return ((SessionImpl) sessionGarbageCollector).createDataStoreGarbageCollector();
        } catch (RepositoryException e) {
            failWithException(e);
        }
        return null;
    }

    private void failWithException(Exception e) {
        fail("Not expected: " + e.getMessage());
    }

    private int getBinaryCount(GarbageCollector garbageCollector) {
        int count = 0;
        Iterator<DataIdentifier> it;
        try {
            it = garbageCollector.getDataStore().getAllIdentifiers();
            while (it.hasNext()) {
                it.next();
                count++;
            }
        } catch (DataStoreException e) {
            failWithException(e);
        }
        log("Binary count: " + count);
        return count;
    }

    private void log(String message) {
        logger.debug(message);
        //System.out.println(message);
    }
}
