/*
 * 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.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.InputStream;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.modeshape.common.FixFor;
import org.modeshape.jcr.cache.SiblingCounter;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.basic.BasicName;

public class RepositoryNodeTypeManagerTest {

    private RepositoryConfiguration config;
    private JcrRepository repository;
    private ExecutionContext context;
    private RepositoryNodeTypeManager repoTypeManager;
    private JcrSession session;

    @Before
    public void beforeEach() throws Exception {
        config = new RepositoryConfiguration("repoName").with(new TestingEnvironment());
        repository = new JcrRepository(config);
        repository.start();
        context = repository.runningState().context();
        repoTypeManager = repository.nodeTypeManager();
        session = repository.login();
    }

    @After
    public void afterEach() throws Exception {
        try {
            repository.shutdown().get(3L, TimeUnit.SECONDS);
        } finally {
            repository = null;
            config = null;
        }
    }

    @Test
    public void shouldOnlyHaveOneNamespacesNode() throws Exception {
        NamespaceRegistry registry = context.getNamespaceRegistry();

        Node rootNode = session.getRootNode();
        assertThat(rootNode, is(notNullValue()));

        Node systemNode = rootNode.getNode(JcrLexicon.SYSTEM.getString(registry));
        assertThat(systemNode, is(notNullValue()));

        NodeIterator namespacesNodes = systemNode.getNodes(ModeShapeLexicon.NAMESPACES.getString(registry));
        assertThat(namespacesNodes.getSize(), is(1L));
    }

    @Test
    public void shouldOnlyHaveOneNodeTypesNode() throws Exception {
        NamespaceRegistry registry = context.getNamespaceRegistry();

        Node rootNode = session.getRootNode();
        assertThat(rootNode, is(notNullValue()));

        Node systemNode = rootNode.getNode(JcrLexicon.SYSTEM.getString(registry));
        assertThat(systemNode, is(notNullValue()));

        NodeIterator nodeTypesNodes = systemNode.getNodes(JcrLexicon.NODE_TYPES.getString(registry));
        assertThat(nodeTypesNodes.getSize(), is(1L));
    }

    @Test
    public void shouldAllowMultipleSiblingsDefinitionIfOneSibling() throws Exception {
        NamespaceRegistry registry = context.getNamespaceRegistry();

        // There's no definition for this node or for a * child node that does not allow SNS
        JcrNodeDefinition def = repoTypeManager.getNodeTypes()
                                               .findChildNodeDefinitions(JcrNtLexicon.NODE_TYPE, null)
                                               .findBestDefinitionForChild(JcrLexicon.PROPERTY_DEFINITION,
                                                                           JcrNtLexicon.PROPERTY_DEFINITION, false,
                                                                           SiblingCounter.oneSibling());

        assertThat(def, is(notNullValue()));
        assertThat(def.getName(), is(JcrLexicon.PROPERTY_DEFINITION.getString(registry)));
    }

    @Test
    @FixFor( "MODE-1807" )
    public void shouldAllowOverridingChildDefinitionWithSubtypeOfOriginalDefinition() throws Exception {
        InputStream cndStream = getClass().getResourceAsStream("/cnd/orc.cnd");
        assertThat(cndStream, is(notNullValue()));
        nodeTypeManager().registerNodeTypes(cndStream, true);
    }

    @Test
    @FixFor( "MODE-1857" )
    public void shouldAllowOverridingOfPropertyDefinitions() throws Exception {
        InputStream cnd = getClass().getClassLoader().getResourceAsStream("cnd/overridingPropertyDefinition.cnd");
        assertThat(cnd, is(notNullValue()));
        session.getWorkspace().getNodeTypeManager().registerNodeTypes(cnd, true);

        Node car = session.getRootNode().addNode("car", "car");
        car.setProperty("engine", "4CYL");
        Node cycle = session.getRootNode().addNode("cycle", "motorcycle");
        cycle.setProperty("engine", "2CYL");
        session.save();

        try {
            car.setProperty("engine", "2CYL");
            fail("Should not have allowed setting the 'engine' property on a node of type 'car' to \"2CYL\"");
        } catch (ConstraintViolationException e) {
            // expected ...
        }
        try {
            cycle.setProperty("engine", "4CYL");
            fail("Should not have allowed setting the 'engine' property on a node of type 'car' to \"2CYL\"");
        } catch (ConstraintViolationException e) {
            // expected ...
        }
    }

    @Test
    @FixFor( "MODE-1857" )
    public void shouldAllowOverridingOfPropertyDefinitionsWithResidualDefinitions() throws Exception {
        InputStream cnd = getClass().getClassLoader().getResourceAsStream("cnd/overridingPropertyDefinitionWithResidual.cnd");
        assertThat(cnd, is(notNullValue()));
        session.getWorkspace().getNodeTypeManager().registerNodeTypes(cnd, true);

        Node car = session.getRootNode().addNode("car", "car");
        car.setProperty("engine", "4CYL");
        Node cycle = session.getRootNode().addNode("cycle", "motorcycle");
        cycle.setProperty("engine", "2CYL");
        session.save();

        try {
            car.setProperty("engine", "2CYL");
            fail("Should not have allowed setting the 'engine' property on a node of type 'car' to \"2CYL\"");
        } catch (ConstraintViolationException e) {
            // expected ...
        }
        try {
            cycle.setProperty("engine", "4CYL");
            fail("Should not have allowed setting the 'engine' property on a node of type 'car' to \"2CYL\"");
        } catch (ConstraintViolationException e) {
            // expected ...
        }
    }

    @Test
    @FixFor( "MODE-1916" )
    public void shouldFindPublicChildNodeDefinitionsWhenBothPublicAndProtectedAreDefined() throws Exception {
        InputStream cndStream = getClass().getResourceAsStream("/cnd/protectedDefinitions.cnd");
        assertThat(cndStream, is(notNullValue()));
        nodeTypeManager().registerNodeTypes(cndStream, true);

        Name parking = new BasicName(null, "parking");
        Name level = new BasicName(null, "level");
        Set<Name> garage = Collections.<Name>singleton(new BasicName(null, "garage"));

        JcrNodeDefinition def = repoTypeManager.getNodeTypes().findChildNodeDefinitions(parking, garage)
                                               .findBestDefinitionForChild(level, level, true, SiblingCounter.noSiblings());

        assertNotNull(def);
        Name car = new BasicName(null, "car");
        def = repoTypeManager.getNodeTypes().findChildNodeDefinitions(parking, garage)
                             .findBestDefinitionForChild(car, car, true, SiblingCounter.noSiblings());
        assertNotNull(def);
    }

    private JcrNodeTypeManager nodeTypeManager() throws RepositoryException {
        return session.getWorkspace().getNodeTypeManager();
    }
}
