/**************************************************************
 * 
 * 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 lib;

import com.sun.star.beans.Property;
import com.sun.star.beans.PropertyAttribute;
import com.sun.star.beans.PropertyVetoException;
import com.sun.star.beans.XPropertySet;
import com.sun.star.beans.XPropertySetInfo;
import com.sun.star.beans.UnknownPropertyException;
import com.sun.star.lang.XServiceInfo;
import com.sun.star.lang.IllegalArgumentException;
import com.sun.star.lang.WrappedTargetException;
import com.sun.star.uno.UnoRuntime;

import java.lang.reflect.Method;

import util.ValueChanger;
import util.ValueComparer;
import util.utils;

import com.sun.star.uno.Any;
import com.sun.star.uno.AnyConverter;
import com.sun.star.uno.Type;

/**
 * MultiPropertyTest extends the functionality of MultiMethodTest to support
 * services testing. Since, in most cases, service tests has one method testing
 * most of its properties, the MultiPropertyTest provides unified version of
 * the method: testProperty().
 *
 * <p>The testProperty() is called, when the MultiMethodTest's testing method
 * is not found in the subclass. So, by defining such methods for properties
 * the standard testing behavioutr can be changed.
 *
 * <p>The testing behaviour also can be changed by overriding compare(),
 * getNewVAlue() or toString(Object) methods, or by extending PropertyTester
 * class.
 *
 * @see MultiMethodTest
 * @see #testProperty(String)
 * @see #testProperty(String, Propertytester)
 * @see #getNewValue
 * @see #compare
 * @see #toString(Object)
 */
public class MultiPropertyTest extends MultiMethodTest
{

    /**
     * Contains a XPropertySet interface of the tested object. Is initialized
     * in MultiMethodTest code.
     */
    public XPropertySet oObj;
    protected boolean optionalService = false;

    /**
     * Overrides super.before() to check the service is supported by the object.
     */
    protected void before()
    {
        XServiceInfo xInfo = (XServiceInfo) UnoRuntime.queryInterface(
                XServiceInfo.class, oObj);

        optionalService = entry.isOptional;

        String theService = getTestedClassName();
        if (xInfo != null && !xInfo.supportsService(theService))
        {
            log.println("Service " + theService + " not available");
            if (optionalService)
            {
                log.println("This is OK since it is optional");
            }
            else
            {
                Status.failed(theService + " is not supported");
            }
        }
    }

    /**
     * Overrides MultiMethodTest.invokeTestMethod(). If the test for the
     * <code>meth</code> is not available (<code>meth</code> == <tt>null</tt>)
     * calls testProperty method for the method. Otherwise calls
     * super.invokeTestMethod().
     *
     * @see #MultiMethodTest.invokeTestMethod()
     */
    protected void invokeTestMethod(Method meth, String methName)
    {
        if (meth != null)
        {
            super.invokeTestMethod(meth, methName);
        }
        else
        {
            testProperty(methName);
        }
    }

    /**
     * PropertyTester class defines how to test a property and defined
     * to allow subclasses of MultiPropertyTest to change the testing
     * behaviour more flexible, since the behaviour can be customized for
     * each property separately, by providing subclass of PropertyTester
     * and passing it to testProperty(String, PropertyTester method).
     */
    public class PropertyTester
    {

        /**
         * The method defines the whole process of testing propName
         * property.
         *
         * <p>First, it checks if the property exists(it maybe optional).
         * Then, a value to set the property with is calculated with
         * getNewValue method. Normally, the new value is calculated
         * based on old value, but subclasses can override the behaviour
         * (for example, if old value is null) and specify their own value.
         * Then the property is set with that new value and the result(
         * it maybe an exception too, for example a PropertyVetoException)
         * is checked with checkResult method.
         *
         * @param propName - the property to test.
         * @result - adds the result of testing propName property to
         *           MultiMethodTest.tRes.
         */
        protected void testProperty(String propName)
        {
            XPropertySetInfo info = oObj.getPropertySetInfo();

            if (info != null)
            {
                final boolean bHasProperty = info.hasPropertyByName(propName);
                if (!bHasProperty)
                {
                    if (isOptional(propName) || optionalService)
                    {
                        // skipping optional property test
                        log.println("Property '" + propName + "' is optional and not supported");
                        tRes.tested(propName, true);
                        return;
                    }
                    else
                    {
                        // cannot test the property
                        log.println("Tested XPropertySet does not contain'" + propName + "' property");
                        tRes.tested(propName, false);
                        return;
                    }
                }
            }

            try
            {
                Object oldValue = oObj.getPropertyValue(propName);
                
                if( (oldValue==null) || utils.isVoid(oldValue) )
                {
                    // #i111560# method getNewValue() does not work with an empty oldValue
                    Property prop = info.getPropertyByName(propName);
                    if( (prop.Attributes & PropertyAttribute.MAYBEVOID) != 0 )
                    {
                        // todo: implement a new test independent from method getNewValue()
                        log.println("changing initially empty MAYBEVOID properties is not supported by the test framework so far - skip test of property: " + propName);  
                        tRes.tested(propName, true);
                        return;
                    }
                    else
                    {
                        log.println( "property '"+propName+"' is not set but is not MAYBEVOID");  
                        tRes.tested(propName, false);
                        return;
                    }
                }
                
                Object newValue;

                // trying to create new value
                try
                {
                    newValue = getNewValue(propName, oldValue);
                }
                catch (java.lang.IllegalArgumentException e)
                {
                    // skipping test since new value is not available
                    Status.failed("Cannot create new value for '" + propName + " : " + e.getMessage());
                    return;
                }

                // for an exception thrown during setting new value
                // to pass it to checkResult method
                Exception exception = null;

                try
                {
                    log.println("try to set:");
                    log.println("old = " + toString(oldValue));
                    log.println("new = " + toString(newValue));
                    oObj.setPropertyValue(propName, newValue);
                }
                catch (IllegalArgumentException e)
                {
                    exception = e;
                }
                catch (PropertyVetoException e)
                {
                    exception = e;
                }
                catch (WrappedTargetException e)
                {
                    exception = e;
                }
                catch (UnknownPropertyException e)
                {
                    exception = e;
                }
                catch (RuntimeException e)
                {
                    exception = e;
                }

                // getting result value
                Object resValue = oObj.getPropertyValue(propName);

                // checking results
                checkResult(propName, oldValue, newValue, resValue, exception);
            }
            catch (Exception e)
            {
                log.println("Exception occured while testing property '" + propName + "'");
                e.printStackTrace(log);
                tRes.tested(propName, false);
            }
        }

        /**
         * The method checks result of setting a new value to the
         * property based o the following arguments:
         *   @propName - the property to test
         *   @oldValue - the old value of the property, before changing it.
         *   @newValue - the new value the property has been set with
         *   @resValue - the value of the property after having changed it
         *   @exception - if not null - the exception thrown by
         *                 XPropertySet.setPropertyValue, else indicates
         *                 normal method completion.
         *
         * <p>If the property is READ_ONLY, than either PropertyVetoException
         * should be thrown or the value of property should not have changed
         * (resValue is compared with oldValue with compare method).
         *
         * <p>If the property is not READ_ONLY, checks that the new value has
         * been successfully set(resValue is compared with newValue with
         * compare method).
         *
         * <p>If the exception is not null then(except the case of read-only
         * property and PropertyVetoException above) it is rethrown to allow
         * further catching it if needed.
         *
         * <p>Subclasses can override to change this behaviour.
         */
        protected void checkResult(String propName, Object oldValue,
                Object newValue, Object resValue, Exception exception)
                throws Exception
        {
            XPropertySetInfo info = oObj.getPropertySetInfo();
            if (info == null)
            {
                log.println("Can't get XPropertySetInfo for property " + propName);
                tRes.tested(propName, false);
                return;
            }
            Property prop = info.getPropertyByName(propName);

            short attr = prop.Attributes;
            boolean readOnly = (prop.Attributes & PropertyAttribute.READONLY) != 0;
            boolean maybeVoid = (prop.Attributes & PropertyAttribute.MAYBEVOID) != 0;
            //check get-set methods
            if (maybeVoid)
            {
                log.println("Property " + propName + " is void");
            }
            if (readOnly)
            {
                log.println("Property " + propName + " is readOnly");
            }
            if (util.utils.isVoid(oldValue) && !maybeVoid)
            {
                log.println(propName + " is void, but it's not MAYBEVOID");
                tRes.tested(propName, false);
            }
            else if (oldValue == null)
            {
                log.println(propName + " has null value, and therefore can't be changed");
                tRes.tested(propName, true);
            }
            else if (readOnly)
            {
                // check if exception was thrown
                if (exception != null)
                {
                    if (exception instanceof PropertyVetoException)
                    {
                        // the change of read only prohibited - OK
                        log.println("Property is ReadOnly and wasn't changed");
                        log.println("Property '" + propName + "' OK");
                        tRes.tested(propName, true);
                    }
                    else if (exception instanceof IllegalArgumentException)
                    {
                        // the change of read only prohibited - OK
                        log.println("Property is ReadOnly and wasn't changed");
                        log.println("Property '" + propName + "' OK");
                        tRes.tested(propName, true);
                    }
                    else if (exception instanceof UnknownPropertyException)
                    {
                        // the change of read only prohibited - OK
                        log.println("Property is ReadOnly and wasn't changed");
                        log.println("Property '" + propName + "' OK");
                        tRes.tested(propName, true);
                    }
                    else if (exception instanceof RuntimeException)
                    {
                        // the change of read only prohibited - OK
                        log.println("Property is ReadOnly and wasn't changed");
                        log.println("Property '" + propName + "' OK");
                        tRes.tested(propName, true);
                    }
                    else
                    {
                        throw exception;
                    }
                }
                else
                {
                    // if no exception - check that value
                    // has not changed
                    if (!compare(resValue, oldValue))
                    {
                        log.println("Read only property '" + propName + "' has changed");
                        try
                        {
                            if (!util.utils.isVoid(oldValue) && oldValue instanceof Any)
                            {
                                oldValue = AnyConverter.toObject(new Type(((Any) oldValue).getClass()), oldValue);
                            }
//                            log.println("old = " + toString(oldValue));
//                            log.println("new = " + toString(newValue));
                            log.println("result = " + toString(resValue));
                        }
                        catch (com.sun.star.lang.IllegalArgumentException iae)
                        {
                            log.println("NOTIFY: this property needs further investigations.");
                            log.println("\t The type seems to be an Any with value of NULL.");
                            log.println("\t Maybe the property should get it's own test method.");
                        }

                        tRes.tested(propName, false);
                    }
                    else
                    {
                        log.println("Read only property '" + propName + "' hasn't changed");
                        log.println("Property '" + propName + "' OK");
                        tRes.tested(propName, true);
                    }
                }
            }
            else
            {
                if (exception == null)
                {
                    // if no exception thrown
                    // check that the new value is set
                    if ((!compare(resValue, newValue)) || (compare(resValue, oldValue)))
                    {
                        log.println("Value for '" + propName + "' hasn't changed as expected");
                        try
                        {
                            if (!util.utils.isVoid(oldValue) && oldValue instanceof Any)
                            {
                                oldValue = AnyConverter.toObject(new Type(((Any) oldValue).getClass()), oldValue);
                            }
//                            log.println("old = " + toString(oldValue));
//                            log.println("new = " + toString(newValue));
                            log.println("result = " + toString(resValue));
                        }
                        catch (com.sun.star.lang.IllegalArgumentException iae)
                        {
                            log.println("NOTIFY: this property needs further investigations.");
                            log.println("\t The type seems to be an Any with value of NULL.");
                            log.println("\t Maybe the property should get it's own test method.");
                        }
                        if (resValue != null)
                        {
                            if ((!compare(resValue, oldValue)) || (!resValue.equals(oldValue)))
                            {
                                log.println("But it has changed.");
                                tRes.tested(propName, true);
                            }
                            else
                            {
                                tRes.tested(propName, false);
                            }
                        }
                        else
                        {
                            tRes.tested(propName, false);
                        }
                        //tRes.tested(propName, false);
                    }
                    else
                    {
                        log.println("Property '" + propName + "' OK");
                        try
                        {
                            if (!util.utils.isVoid(oldValue) && oldValue instanceof Any)
                            {
                                oldValue = AnyConverter.toObject(new Type(((Any) oldValue).getClass()), oldValue);
                            }
//                            log.println("old = " + toString(oldValue));
//                            log.println("new = " + toString(newValue));
                            log.println("result = " + toString(resValue));
                        }
                        catch (com.sun.star.lang.IllegalArgumentException iae)
                        {
                        }
                        tRes.tested(propName, true);
                    }
                }
                else
                {
                    throw exception;
                }
            }
        }

        /**
         * The method produces new value of the property from the oldValue.
         * It returns the result of ValueChanger.changePValue method.
         * Subclasses can override the method to return their own value,
         * when the changePValue beahviour is not enough, for example,
         * when oldValue is null.
         */
        protected Object getNewValue(String propName, Object oldValue)
                throws java.lang.IllegalArgumentException
        {
            return ValueChanger.changePValue(oldValue);
        }

        /**
         * The method compares obj1 and obj2. It calls
         * MultiPropertyTest.compare, but subclasses can override to change
         * the behaviour, since normally compare calls Object.equals method
         * which is not apropriate in some cases(e.g., structs with equals
         * not overridden).
         */
        protected boolean compare(Object obj1, Object obj2)
        {
            return callCompare(obj1, obj2);
        }

        /**
         * The method returns a String representation of the obj. It calls
         * MultipropertyTest.toString(Object), but subclasses can override
         * to change the behaviour.
         */
        protected String toString(Object obj)
        {
            return callToString(obj);
        }
    }

    /**
     * Extension for <code>PropertyTester</code> which switches two
     * different values. <code>getNewValue()</code> method of this
     * class returns one of these two values depending on the
     * old value, so new value is not equal to old value.
     */
    public class PropertyValueSwitcher extends PropertyTester
    {

        Object val1 = null;
        Object val2 = null;

        /**
         * Constructs a property tester with two different values
         * specified as parameters.
         *
         * @param val1 Not <code>null</code> value for the property
         * tested.
         * @param val1 Not <code>null</code> value for the property
         * tested which differs from the first value.
         */
        public PropertyValueSwitcher(Object val1, Object val2)
        {
            this.val1 = val1;
            this.val2 = val2;
        }

        /**
         * Overriden method of <code>PropertyTester</code> which
         * retruns new value from two values specified.
         *
         * @return The second value if old value is equal to the first
         * one, the first value otherwise.
         */
        protected Object getNewValue(String propName, Object old)
        {
            if (ValueComparer.equalValue(val1, old))
            {
                return val2;
            }
            else
            {
                return val1;
            }
        }
    }

    /**
     * The method performs testing of propName property using propTester.
     */
    protected void testProperty(String propName, PropertyTester propTester)
    {
        propTester.testProperty(propName);
    }

    /**
     * The method performs testing of propName property. It uses PropertyTester
     * instance for testing.
     */
    protected void testProperty(String propName)
    {
        testProperty(propName, new PropertyTester());
    }

    /**
     * Tests the property using <code>PropertyValueSwitcher</code>
     * tester and two values for this property.
     *
     * @see #PropertyValueSwitcher
     */
    protected void testProperty(String propName, Object val1, Object val2)
    {
        testProperty(propName, new PropertyValueSwitcher(val1, val2));
    }

    /**
     * The method just calls compare. This is a workaround to CodeWarrior's
     * compiler bug.
     */
    private boolean callCompare(Object obj1, Object obj2)
    {
        return compare(obj1, obj2);
    }

    /**
     * Compares two object. In the implementation calls obj1.equals(obj2).
     */
    protected boolean compare(Object obj1, Object obj2)
    {
        return ValueComparer.equalValue(obj1, obj2);
    }

    /**
     * The method just calls toString. This is a workaround to
     * CodeWarrior's compiler bug.
     */
    private String callToString(Object obj)
    {
        return toString(obj);
    }

    /**
     * Gets string representation of the obj. In the implementation
     * returns obj.toString().
     */
    protected String toString(Object obj)
    {
        return obj == null ? "null" : obj.toString();
    }
}
