/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr;

import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import org.junit.After;
import org.junit.Before;
import org.modeshape.jcr.api.JcrTools;
import org.modeshape.schematic.document.Changes;
import org.modeshape.schematic.document.Document;
import org.modeshape.schematic.document.EditableArray;
import org.modeshape.schematic.document.EditableDocument;
import org.modeshape.schematic.document.Editor;
import org.modeshape.schematic.document.Json;
import junit.framework.AssertionFailedError;

/**
 * A base class for tests that require a new JcrSession and JcrRepository for each test method.
 */
public abstract class SingleUseAbstractTest extends AbstractJcrRepositoryTest {

    protected static final String REPO_NAME = "testRepo";

    /**
     * Flag that will signal to {@link #beforeEach()} whether to automatically start the repository using the
     * {@link #createRepositoryConfiguration(String) default configuration}.
     * <p>
     * There are two ways to run tests with this class:
     * <ol>
     * <li>All tests runs against a fresh repository created from the same configuration. In this case, the
     * {@link #startRepositoryAutomatically} variable should be set to true, and the
     * {@link #createRepositoryConfiguration(String)} should be overridden if a non-default configuration is to be
     * used for all the tests.</li>
     * <li>Each test requires a fresh repository with a different configuration. In this case, the
     * {@link #startRepositoryAutomatically} variable should be set to <code>false</code>, and each test should then call one of
     * the {@link #startRepositoryWithConfiguration(RepositoryConfiguration)} methods before using the repository.</li>
     * </ol>
     */
    private boolean startRepositoryAutomatically = true;

    protected JcrRepository repository;
    protected JcrSession session;
    protected JcrTools tools;
    
    protected void startRepository() throws Exception {
        RepositoryConfiguration repositoryConfiguration = createRepositoryConfiguration(REPO_NAME);
        repositoryConfiguration = repositoryConfiguration.with(new TestingEnvironment());
        repository = new JcrRepository(repositoryConfiguration);
        repository.start();
        session = repository.login();
    }

    protected void stopRepository() throws Exception {
        try {
            try {
                if (session != null && session.isLive()) session.logout();
            } finally {
                TestingUtil.killRepositories(repository);
            }
        } finally {
            repository = null;
        }
    }

    @Override
    @Before
    public void beforeEach() throws Exception {
        super.beforeEach();
        if (startRepositoryAutomatically()) {
            startRepository();
        }
        tools = new JcrTools();
    }

    @After
    public void afterEach() throws Exception {
        stopRepository();
    }

    @Override
    protected JcrSession session() {
        return session;
    }

    protected Session newSession() throws RepositoryException {
        return repository.login();
    }

    protected Session jcrSession() {
        return session;
    }

    @Override
    protected JcrRepository repository() {
        return repository;
    }

    /**
     * Subclasses can override this method to define the RepositoryConfiguration that will be used for the given repository name
     * By default, this method simply returns an empty configuration:
     *
     * @param repositoryName the name of the repository to create; never null
     * @return the repository configuration
     * @throws Exception if there is a problem creating the configuration
     */
    protected RepositoryConfiguration createRepositoryConfiguration(String repositoryName) throws Exception {
        return new RepositoryConfiguration(repositoryName);
    }

    /**
     * Subclasses can call this method at the beginning of each test to shutdown any currently-running repository and to start up
     * a new repository with the given JSON configuration content.
     * 
     * @param configContent the JSON string containing the configuration for the repository (note that single quotes can be used
     *        in place of double quote, making it easier for to specify a JSON content as a Java string)
     * @throws Exception if there was a problem starting the repository
     * @see #startRepositoryWithConfiguration(Document)
     * @see #startRepositoryWithConfiguration(InputStream)
     * @see #startRepositoryWithConfiguration(RepositoryConfiguration)
     * @see #startRepositoryAutomatically
     */
    protected void startRepositoryWithConfiguration( String configContent ) throws Exception {
        assertThat(configContent, is(notNullValue()));
        Document doc = Json.read(configContent);
        startRepositoryWithConfiguration(doc);
    }

    /**
     * Subclasses can call this method at the beginning of each test to shutdown any currently-running repository and to start up
     * a new repository with the given JSON configuration document.
     * 
     * @param doc the JSON document containing the configuration for the repository
     * @throws Exception if there was a problem starting the repository
     * @see #startRepositoryWithConfiguration(String)
     * @see #startRepositoryWithConfiguration(InputStream)
     * @see #startRepositoryWithConfiguration(RepositoryConfiguration)
     * @see #startRepositoryAutomatically
     */
    protected void startRepositoryWithConfiguration( Document doc ) throws Exception {
        assertThat(doc, is(notNullValue()));
        RepositoryConfiguration config = new RepositoryConfiguration(doc, REPO_NAME);
        startRepositoryWithConfiguration(config);
    }

    /**
     * Subclasses can call this method at the beginning of each test to shutdown any currently-running repository and to start up
     * a new repository with the given JSON configuration content.
     * 
     * @param configInputStream the input stream containing the JSON content defining the configuration for the repository
     * @throws Exception if there was a problem starting the repository
     * @see #startRepositoryWithConfiguration(String)
     * @see #startRepositoryWithConfiguration(Document)
     * @see #startRepositoryWithConfiguration(RepositoryConfiguration)
     * @see #startRepositoryAutomatically
     */
    protected void startRepositoryWithConfiguration( InputStream configInputStream ) throws Exception {
        assertThat(configInputStream, is(notNullValue()));
        RepositoryConfiguration config = RepositoryConfiguration.read(configInputStream, REPO_NAME);
        startRepositoryWithConfiguration(config);
    }
 
    protected void startRepositoryWithConfigurationFrom(String pathToConfiguration) throws Exception {
        startRepositoryWithConfiguration(getClass().getClassLoader().getResourceAsStream(pathToConfiguration));
    }

    /**
     * Subclasses can call this method at the beginning of each test to shutdown any currently-running repository and to start up
     * a new repository with the given repository configuration
     * 
     * @param configuration the repository configuration object; may not be null can be used in place of double quote, making it
     *        easier for to specify a JSON content as a Java string)
     * @throws Exception if there was a problem starting the repository
     * @see #startRepositoryWithConfiguration(String)
     * @see #startRepositoryWithConfiguration(Document)
     * @see #startRepositoryWithConfiguration(InputStream)
     * @see #startRepositoryAutomatically
     */
    protected void startRepositoryWithConfiguration( RepositoryConfiguration configuration ) throws Exception {
        // always use the test environment to provide some persistence defaults...
        configuration = configuration.with(new TestingEnvironment());
        assertThat(configuration, is(notNullValue()));
        if (repository != null) {
            try {
                repository.shutdown().get(10, TimeUnit.SECONDS);
            } finally {
                repository = null;
            }
        }
        repository = new JcrRepository(configuration);
        repository.start();
        session = repository.login();
    }

    /**
     * Make sure that a workspace with the supplied name exists.
     * 
     * @param workspaceName the name of the workspace; may not be null
     */
    protected void predefineWorkspace( RepositoryConfiguration configuration, String workspaceName ) {
        assertThat(workspaceName, is(notNullValue()));
        // Edit the configuration ...
        Editor editor = configuration.edit();
        EditableDocument workspaces = editor.getOrCreateDocument("workspaces");
        EditableArray predefined = workspaces.getOrCreateArray("predefined");
        predefined.addStringIfAbsent(workspaceName);

        // And apply the changes ...
        Changes changes = editor.getChanges();
        if (changes.isEmpty()) return;
        try {
            repository.apply(changes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AssertionFailedError("Unexpected error while predefining the \"" + workspaceName + "\" workspace:"
                                           + e.getMessage());
        }
    }

    /**
     * Utility method to get the resource on the classpath given by the supplied name
     * 
     * @param name the name (or path) of the classpath resource
     * @return the input stream to the content; may be null if the resource does not exist
     */
    protected InputStream resourceStream( String name ) {
        return getClass().getClassLoader().getResourceAsStream(name);
    }

    /**
     * Register the node types in the CND file at the given location on the classpath.
     * 
     * @param resourceName the name of the CND file on the classpath
     * @throws RepositoryException if there is a problem registering the node types
     * @throws IOException if the CND file could not be read
     */
    protected void registerNodeTypes( String resourceName ) throws RepositoryException, IOException {
        InputStream stream = resourceStream(resourceName);
        assertThat(stream, is(notNullValue()));
        Workspace workspace = session().getWorkspace();
        org.modeshape.jcr.api.nodetype.NodeTypeManager ntMgr = (org.modeshape.jcr.api.nodetype.NodeTypeManager)workspace.getNodeTypeManager();
        ntMgr.registerNodeTypes(stream, true);
    }

    /**
     * Import under the supplied parent node the repository content in the XML file at the given location on the classpath.
     * 
     * @param parent the node under which the content should be imported; may not be null
     * @param resourceName the name of the XML file on the classpath
     * @param uuidBehavior the UUID behavior; see {@link ImportUUIDBehavior} for values
     * @throws RepositoryException if there is a problem importing the content
     * @throws IOException if the XML file could not be read
     */
    protected void importContent( Node parent,
                                  String resourceName,
                                  int uuidBehavior ) throws RepositoryException, IOException {
        InputStream stream = resourceStream(resourceName);
        assertThat(stream, is(notNullValue()));
        parent.getSession().getWorkspace().importXML(parent.getPath(), stream, uuidBehavior);
    }

    /**
     * Import under the supplied parent node the repository content in the XML file at the given location on the classpath.
     * 
     * @param parentPath the path to the node under which the content should be imported; may not be null
     * @param resourceName the name of the XML file on the classpath
     * @param uuidBehavior the UUID behavior; see {@link ImportUUIDBehavior} for values
     * @throws RepositoryException if there is a problem importing the content
     * @throws IOException if the XML file could not be read
     */
    protected void importContent( String parentPath,
                                  String resourceName,
                                  int uuidBehavior ) throws RepositoryException, IOException {
        InputStream stream = resourceStream(resourceName);
        assertThat(stream, is(notNullValue()));
        session().getWorkspace().importXML(parentPath, stream, uuidBehavior);
    }

    protected boolean startRepositoryAutomatically() {
        return startRepositoryAutomatically;
    }

    protected InputStream resource( String path ) {
        InputStream stream = getClass().getClassLoader().getResourceAsStream(path);
        assertThat(stream, is(notNullValue()));
        return stream;
    }
    
    protected <R> R runInTransaction(Callable<R> operation, String...keysToLock) {
        return repository.runningState().documentStore().localStore().runInTransaction(operation, 0, keysToLock);
    }
}
