/*
 * 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.test.api;

import org.apache.jackrabbit.test.AbstractJCRTest;
import org.apache.jackrabbit.test.NotExecutableException;
import org.apache.jackrabbit.test.ISO8601;
import org.apache.jackrabbit.test.api.nodetype.NodeTypeUtil;

import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.Node;
import javax.jcr.Value;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Property;
import javax.jcr.ValueFormatException;
import java.util.Calendar;
import java.util.Date;

/**
 * <code>SetPropertyAssumeTypeTest</code> tests if when setting a property
 * of type <code>PropertyType.UNDEFINED</code> the type is assumed correctly.
 * The signatures <code>Node.setProperty(String, Value, int)</code>,
 * <code>Node.setProperty(String, String, int)</code>,
 * <code>Node.setProperty(String, Value[], int)</code> and
 * <code>Node.setProperty(String, Node)</code> are tested.
 *
 */
public class SetPropertyAssumeTypeTest extends AbstractJCRTest {

    private Node testNode;
    private String testPropName;
    private Value binaryValue;
    private Value booleanValue;
    private Value dateValue;
    private Value doubleValue;
    private Value longValue;
    private Value nameValue;
    private Value pathValue;
    private Value stringValue;
    private Value binaryValues[];
    private Value booleanValues[];
    private Value dateValues[];
    private Value doubleValues[];
    private Value longValues[];
    private Value nameValues[];
    private Value pathValues[];
    private Value stringValues[];

    public void setUp() throws Exception {
        super.setUp();
        testNode = testRootNode.addNode(nodeName1, testNodeType);

        binaryValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BINARY);
        booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN);
        dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE);
        doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE);
        longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG);
        nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME);
        pathValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.PATH);
        stringValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.STRING);

        binaryValues = new Value[] {binaryValue};
        booleanValues = new Value[] {booleanValue};
        dateValues = new Value[] {dateValue};
        doubleValues = new Value[] {doubleValue};
        longValues = new Value[] {longValue};
        nameValues = new Value[] {nameValue};
        pathValues = new Value[] {pathValue};
        stringValues = new Value[] {stringValue};
    }

    protected void tearDown() throws Exception {
        testNode = null;
        testPropName = null;
        binaryValue = null;
        booleanValue = null;
        dateValue = null;
        doubleValue = null;
        longValue = null;
        nameValue = null;
        pathValue = null;
        stringValue = null;
        binaryValues = null;
        booleanValues = null;
        dateValues = null;
        doubleValues = null;
        longValues = null;
        nameValues = null;
        pathValues = null;
        stringValues = null;
        super.tearDown();
    }

    /**
     * Tests if <code>Node.setProperty(String, Value, int)</code> if the node
     * type of this node does not indicate a specific property type, then the
     * type parameter is used.
     */
    public void testValue() throws NotExecutableException, RepositoryException {

        setUpNodeWithUndefinedProperty(false);

        Property prop;

        prop = testNode.setProperty(testPropName, binaryValue, PropertyType.BINARY);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.BINARY,
                     prop.getType());

        prop = testNode.setProperty(testPropName, stringValue, PropertyType.BOOLEAN);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.BOOLEAN,
                     prop.getType());

        prop = testNode.setProperty(testPropName, doubleValue, PropertyType.DATE);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.DATE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValue, PropertyType.DOUBLE);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.DOUBLE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValue, PropertyType.LONG);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.LONG,
                     prop.getType());

        // create a PathValue that is convertible to the value of name property
        Value valueConvertibleToName = superuser.getValueFactory().createValue(nameValue.getString(), PropertyType.PATH);
        prop = testNode.setProperty(testPropName, valueConvertibleToName, PropertyType.NAME);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.NAME,
                     prop.getType());

        prop = testNode.setProperty(testPropName, nameValue, PropertyType.PATH);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.PATH,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValue, PropertyType.STRING);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.STRING,
                     prop.getType());
    }

    /**
     * Tests if <code>Node.setProperty(String, Value[], int)</code> if the node
     * type of this node does not indicate a specific property type, then the
     * type parameter is used.
     */
    public void testValues() throws NotExecutableException, RepositoryException {

        setUpNodeWithUndefinedProperty(true);

        Property prop;

        prop = testNode.setProperty(testPropName, binaryValues, PropertyType.BINARY);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.BINARY,
                     prop.getType());

        prop = testNode.setProperty(testPropName, stringValues, PropertyType.BOOLEAN);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.BOOLEAN,
                     prop.getType());

        prop = testNode.setProperty(testPropName, doubleValues, PropertyType.DATE);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.DATE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValues, PropertyType.DOUBLE);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.DOUBLE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValues, PropertyType.LONG);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.LONG,
                     prop.getType());

        // create a PathValue that is convertible to the value of name property
        Value valuesConvertibleToName[] =
            new Value[] {superuser.getValueFactory().createValue(nameValue.getString(), PropertyType.PATH)};
        prop = testNode.setProperty(testPropName, valuesConvertibleToName, PropertyType.NAME);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.NAME,
                     prop.getType());

        prop = testNode.setProperty(testPropName, nameValues, PropertyType.PATH);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.PATH,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValues, PropertyType.STRING);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.STRING,
                     prop.getType());
    }

    /**
     * Tests if <code>Node.setProperty(String, String, int)</code> if the node
     * type of this node does not indicate a specific property type, then the
     * type parameter is used.
     */
    public void testString() throws NotExecutableException, RepositoryException {

        setUpNodeWithUndefinedProperty(false);

        Property prop;

        prop = testNode.setProperty(testPropName, binaryValue.getString(), PropertyType.BINARY);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.BINARY,
                     prop.getType());

        prop = testNode.setProperty(testPropName, booleanValue.getString(), PropertyType.BOOLEAN);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.BOOLEAN,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValue.getString(), PropertyType.DATE);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.DATE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, doubleValue.getString(), PropertyType.DOUBLE);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.DOUBLE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, longValue.getString(), PropertyType.LONG);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.LONG,
                     prop.getType());

        prop = testNode.setProperty(testPropName, nameValue.getString(), PropertyType.NAME);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.NAME,
                     prop.getType());

        prop = testNode.setProperty(testPropName, pathValue.getString(), PropertyType.PATH);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.PATH,
                     prop.getType());

        prop = testNode.setProperty(testPropName, stringValue.getString(), PropertyType.STRING);
        assertEquals("setProperty(String, Value, int) of a property of type undefined " +
                     "must assume the property type of the type parameter.",
                     PropertyType.STRING,
                     prop.getType());
    }

    /**
     * Tests if <code>Node.setProperty(String, Value)</code> if the node type of
     * this node does not indicate a specific property type, then the property
     * type of the supplied Value object is used and if the property already
     * exists (has previously been set) it assumes the new property type.
     */
    public void testValueAssumeTypeOfValue() throws NotExecutableException, RepositoryException {

        setUpNodeWithUndefinedProperty(false);

        Property prop;

        prop = testNode.setProperty(testPropName, binaryValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.BINARY,
                     prop.getType());

        prop = testNode.setProperty(testPropName, booleanValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.BOOLEAN,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.DATE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, doubleValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.DOUBLE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, longValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.LONG,
                     prop.getType());

        prop = testNode.setProperty(testPropName, nameValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.NAME,
                     prop.getType());

        prop = testNode.setProperty(testPropName, pathValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.PATH,
                     prop.getType());

        prop = testNode.setProperty(testPropName, stringValue);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.STRING,
                     prop.getType());
    }

    /**
     * Tests if <code>Node.setProperty(String, Node)</code> if the node type of
     * this node does not indicate a specific property type, then the property
     * type of the supplied Value object is used and if the property already
     * exists (has previously been set) it assumes the new property type.
     */
    public void testNodeAssumeTypeOfValue()
        throws NotExecutableException, RepositoryException {

        setUpNodeWithUndefinedProperty(false);

        Node referenceableNode = testRootNode.addNode(nodeName2);
        ensureMixinType(referenceableNode, mixReferenceable);

        // some implementations may require a save after addMixin()
        testRootNode.getSession().save();

        Property prop = testNode.setProperty(testPropName, referenceableNode);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.REFERENCE,
                     prop.getType());
    }

    /**
     * Tests if <code>Node.setProperty(String, Value[])</code> if the node type of
     * this node does not indicate a specific property type, then the property
     * type of the supplied Value object is used and if the property already
     * exists (has previously been set) it assumes the new property type.
     */
    public void testValuesAssumeTypeOfValue() throws NotExecutableException, RepositoryException {

        setUpNodeWithUndefinedProperty(true);

        Property prop;

        prop = testNode.setProperty(testPropName, binaryValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.BINARY,
                     prop.getType());

        prop = testNode.setProperty(testPropName, booleanValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.BOOLEAN,
                     prop.getType());

        prop = testNode.setProperty(testPropName, dateValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.DATE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, doubleValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.DOUBLE,
                     prop.getType());

        prop = testNode.setProperty(testPropName, longValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.LONG,
                     prop.getType());

        prop = testNode.setProperty(testPropName, nameValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.NAME,
                     prop.getType());

        prop = testNode.setProperty(testPropName, pathValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.PATH,
                     prop.getType());

        prop = testNode.setProperty(testPropName, stringValues);
        assertEquals("setProperty(String, Value) of a property of type undefined " +
                     "must assume the property type of the supplied value object.",
                     PropertyType.STRING,
                     prop.getType());
    }

    /**
     * Tests if <code>Node.setProperty(String, Value, int)</code> throws a
     * ConstraintViolationException if the type parameter and the type of the
     * property do not match. The exception has to be thrown either immediately
     * (by this method) or on save.
     */
    public void testValueConstraintViolationExceptionBecauseOfInvalidTypeParameter()
        throws NotExecutableException, RepositoryException {

        try {
            Calendar cal = Calendar.getInstance();
            cal.setTime(new Date(0));
            Value v = superuser.getValueFactory().createValue(ISO8601.format(cal));
            testNode.setProperty(propertyName1, v, PropertyType.DATE);
            testRootNode.getSession().save();
            fail("Node.setProperty(String, Value, int) must throw a " +
                 "ConstraintViolationExcpetion if the type parameter and the " +
                 "type of the property do not match." );
        }
        catch (ConstraintViolationException e) {
            // success
        }
    }

    /**
     * Tests if <code>Node.setProperty(String, String, int)</code> throws a
     * ConstraintViolationException if the type parameter and the type of the
     * property do not match. The exception has to be thrown either immediately
     * (by this method) or on save.
     */
    public void testStringConstraintViolationExceptionBecauseOfInvalidTypeParameter()
        throws NotExecutableException, RepositoryException {

        try {
            Calendar cal = Calendar.getInstance();
            cal.setTime(new Date(0));
            testNode.setProperty(propertyName1, ISO8601.format(cal), PropertyType.DATE);
            testRootNode.getSession().save();
            fail("Node.setProperty(String, Value, int) must throw a " +
                 "ConstraintViolationExcpetion if the type parameter and the " +
                 "type of the property do not match." );
        }
        catch (ConstraintViolationException e) {
            // success
        }
    }

    /**
     * Tests if <code>Node.setProperty(String, Value[], int)</code> throws a
     * ConstraintViolationException or ValueFormatException if the type
     * parameter and the type of the property do not match. The exception has to
     * be thrown either immediately (by this method) or on save.
     */
    public void testValuesConstraintViolationExceptionBecauseOfInvalidTypeParameter()
        throws NotExecutableException, RepositoryException {

        try {
            testNode.setProperty(propertyName1, stringValues, PropertyType.DATE);
            testRootNode.getSession().save();
            fail("Node.setProperty(String, Value, int) must throw a " +
                    "ConstraintViolationExcpetion or a ValueFormatException if " +
                    "the type parameter and the type of the property do not match.");
        } catch (ConstraintViolationException e) {
            // success
        } catch (ValueFormatException e) {
            // success
        }
    }

    //--------------------------< internal >------------------------------------

    private void setUpNodeWithUndefinedProperty(boolean multiple)
        throws NotExecutableException {

        try {
            // locate a property definition of type undefined
            PropertyDefinition propDef =
                    NodeTypeUtil.locatePropertyDef(superuser, PropertyType.UNDEFINED, multiple, false, false, false);

            if (propDef == null) {
                throw new NotExecutableException("No testable property of type " +
                                                 "UNDEFINED has been found.");
            }

            // create a node of type propDef.getDeclaringNodeType()
            String nodeType = propDef.getDeclaringNodeType().getName();
            testNode = testRootNode.addNode(nodeName1, nodeType);
            testPropName = propDef.getName();
        }
        catch (RepositoryException e) {
            throw new NotExecutableException("Not able to set up test items.");
        }
    }

}
