/**
 * 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.openejb.junit.context;

import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Hashtable;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class UtilTest {
    /*
    * No setter method
    */
    private String noSetter;

    /**
     * 0 parameter setter
     */
    private String noParamSetter;

    public void setNoParamSetter() {
    }

    /**
     * Setter with multiple parameter
     */
    private String multiParamSetter;

    public void setMultiParamSetter(final String s1, final String s2) {
    }

    /**
     * Setter isn't void
     */
    private String notVoidSetter;

    public String setNotVoidSetter(final String s) {
        return null;
    }

    /**
     * Setter has a primitive argument, but is supplied a null argument
     */
    private int primitiveParamSetter;

    public void setPrimitiveParamSetter(final int i) {
    }

    /**
     * Setter has a different type of parameter
     */
    private String wrongInstanceSetter;

    public void setWrongInstanceSetter(final Integer i) {
    }

    /**
     * A static setter with an instance field, which can't be a valid setter.
     */
    private String staticSetterInstanceField;

    private static void setStaticSetterInstanceField(final String s) {
    }

    private static abstract class AbstractClass {
        private String abstractSetter;

        abstract void setAbstractSetter(String s);
    }

    /*
    * Setter is private, so it should be setAccessible
    */
    private String privateSetter;

    private void setPrivateSetter(final String s) {
    }

    /*
    * Static setter and field
    */
    private static String staticSetterStaticField;

    public static void setStaticSetterStaticField(final String s) {
    }

    /**
     * Instance setter for a static field
     */
    private static String instanceSetterStaticField;

    public void setInstanceSetterStaticField(final String s) {
    }

    /**
     * Instance field
     */
    private String instanceField;

    public void setInstanceField(final String s) {
    }

    /**
     * Primitive instance field and setter
     */
    private int primitiveInstanceField;

    public void setPrimitiveInstanceField(final int i) {
    }

    /*
    * Two name matching setters, of which only one should be found
    */
    private String twoSetter;

    public void setTwoSetter(final String s, final String s1) {
    }

    public void setTwoSetter(final String s) {
    }

    /**
     * 2 matching setters, should pick the most specific, ie. String
     */
    private String twoAccessibleSetter;

    public void setTwoAccessibleSetter(final String s) {
    }

    public void setTwoAccessibleSetter(final Object s) {
    }

    /**
     * 2 matching setters, should pick the most specific, ie. IllegalArgumentException
     */
    private IllegalArgumentException threeAccessibleNoExactMatchSetter;

    public void setThreeAccessibleNoExactMatchSetter(final Object s) {
    }

    public void setThreeAccessibleNoExactMatchSetter(final Throwable s) {
    }

    public void setThreeAccessibleNoExactMatchSetter(final Exception s) {
    }


    /**
     * Used to test failure of finding a setter, where a method with a name equal
     * to the setter name exists. This would be cases where the method doesn't match
     * a method
     */
    private void findInvalidSetter(final String fieldName, final Object setValue) throws Exception {
        // check the field exists
        final Field field = getClass().getDeclaredField(fieldName);

        // check a method with the correct name exists
        final String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        boolean foundMethod = false;
        for (final Method method : getClass().getDeclaredMethods()) {
            if (setterName.equals(method.getName())) {
                foundMethod = true;
                break;
            }
        }
        assertTrue("No Method exists with name: " + setterName, foundMethod);

        // now check that the findSetter method returns null
        final Method ret = Util.findSetter(getClass(), field, setValue);
        assertNull("Invalid setter returned.", ret);
    }

    /**
     * Used to test success of finding a setter, where a method with a name equal
     * to the setter name exists and findSetter returns that method.
     *
     * @return found setter so further checks can be performed
     */
    private Method findValidSetter(final String fieldName, final Object setValue) throws Exception {
        // check the field exists
        final Field field = getClass().getDeclaredField(fieldName);

        // check a method with the correct name exists
        final String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        Method foundMethod = null;
        for (final Method method : getClass().getDeclaredMethods()) {
            if (setterName.equals(method.getName())) {
                foundMethod = method;
                break;
            }
        }
        assertNotNull("No Method exists with name: " + setterName, foundMethod);

        // now check that the findSetter method returns the desired valid method
        final Method ret = Util.findSetter(getClass(), field, setValue);
        assertNotNull("Couldn't find setter.", ret);
        assertEquals(foundMethod, ret);

        return ret;
    }

    @Test
    public void testFindSetterInvalidSetters() throws Exception {
        findInvalidSetter("noParamSetter", "ValueString");
        findInvalidSetter("multiParamSetter", "ValueString");
        findInvalidSetter("notVoidSetter", "ValueString");
        findInvalidSetter("primitiveParamSetter", null);
        findInvalidSetter("wrongInstanceSetter", "ValueString");
        findInvalidSetter("staticSetterInstanceField", "ValueString");
    }

    @Test
    public void testFindSetterNoSetters() throws Exception {
        final String fieldName = "noSetter";
        final String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);

        final Field field = getClass().getDeclaredField(fieldName);

        try {
            getClass().getDeclaredMethod(setterName, String.class);
        } catch (final NoSuchMethodException e) {
        }

        final Method method = Util.findSetter(getClass(), field, "ValueString");
        assertNull("Setter found.", method);
    }

    @Test
    public void testFindSetterAbstractSetter() throws Exception {
        final String fieldName = "abstractSetter";
        final String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);

        final Field field = AbstractClass.class.getDeclaredField(fieldName);

        // will fail if the method doesn't exist
        AbstractClass.class.getDeclaredMethod(setterName, String.class);

        final Method method = Util.findSetter(AbstractClass.class, field, "ValueString");
        assertNull("Abstract setter returned.", method);
    }

    @Test
    public void testFindSetterValidSetters() throws Exception {
        findValidSetter("staticSetterStaticField", "ValueString");
        findValidSetter("instanceSetterStaticField", "ValueString");
        findValidSetter("instanceField", "ValueString");
        findValidSetter("primitiveInstanceField", 1);

        final Method privateMethod = findValidSetter("privateSetter", "ValueString");
        assertTrue(privateMethod.isAccessible());
    }

    @Test
    public void testFindSetterDualValidSetters() throws Exception {
        final String fieldName = "twoSetter";

        // check the field exists
        final Field field = getClass().getDeclaredField(fieldName);

        // check a method with the correct name exists
        final String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        getClass().getDeclaredMethod(setterName, String.class, String.class);
        final Method setterMethod = getClass().getDeclaredMethod(setterName, String.class);

        // now check that the findSetter method returns null
        final Method ret = Util.findSetter(getClass(), field, "ValueString");
        assertNotNull("Couldn't find setter.", ret);
        assertEquals(setterMethod, ret);
    }

    @Test
    public void testFindSetterBestMatchSetters() throws Exception {
        final String fieldName = "twoAccessibleSetter";

        // check the field exists
        final Field field = getClass().getDeclaredField(fieldName);

        // check a method with the correct name exists
        final String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        getClass().getDeclaredMethod(setterName, Object.class);
        final Method setterMethod = getClass().getDeclaredMethod(setterName, String.class);

        // now check that the findSetter method returns null
        final Method ret = Util.findSetter(getClass(), field, "ValueString");
        assertNotNull("Couldn't find setter.", ret);
        assertEquals(setterMethod, ret);
    }

    @Test
    public void testFindSetterThreeAccessibleNoExactMatchSetter() throws Exception {
        final String fieldName = "threeAccessibleNoExactMatchSetter";

        // check the field exists
        final Field field = getClass().getDeclaredField(fieldName);

        // check a method with the correct name exists
        final String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        getClass().getDeclaredMethod(setterName, Object.class);
        getClass().getDeclaredMethod(setterName, Throwable.class);
        final Method setterMethod = getClass().getDeclaredMethod(setterName, Exception.class);

        // now check that the findSetter method returns null
        final Method ret = Util.findSetter(getClass(), field, new NumberFormatException());
        assertNotNull("Couldn't find setter.", ret);
        assertEquals(setterMethod, ret);
    }

    @Test
    public void testAddProperty() {
        checkAddProperty("name=value", "name", "value");
        checkAddProperty("name=", "name", "");
        checkAddProperty("name", "name", "");
        checkAddProperty(" name=value ", "name", "value");
        checkAddProperty(" name = value ", "name", "value");
        checkAddProperty("name = value", "name", "value");
        checkAddProperty(" name= ", "name", "");
        checkAddProperty(" name = ", "name", "");
        checkAddProperty("name =", "name", "");
        checkAddProperty("name", "name", "");
        checkAddProperty(" name", "name", "");
        checkAddProperty(" name ", "name", "");
        failAddProperty("=", IllegalArgumentException.class);
        failAddProperty("=value", IllegalArgumentException.class);
        failAddProperty(null, IllegalArgumentException.class);
        failAddProperty("", IllegalArgumentException.class);
    }

    /**
     * Checks if the addProperty method works, adding a property with the specified
     * key/value
     *
     * @param property
     * @param key
     * @param value
     */
    private void checkAddProperty(final String property, final String key, final String value) {
        final Hashtable<String, String> hashtable = new Hashtable<String, String>();
        Util.addProperty(hashtable, property);

        assertEquals(1, hashtable.size());
        assertNotNull(hashtable.get(key));
        assertEquals(value, hashtable.get(key));
    }

    /**
     * Runs the addProperty and fails if it succeeds
     *
     * @param property
     * @param exceptionType the type of failure that should occur
     */
    private void failAddProperty(final String property, final Class<?> exceptionType) {
        try {
            final Hashtable<String, String> hashtable = new Hashtable<String, String>();
            Util.addProperty(hashtable, property);
            fail("Add property succeeded where it shouldn't have.");
        } catch (final Exception e) {
            assertEquals(exceptionType, e.getClass());
        }
    }

    /**
     * Test for isInstance()
     */
    @Test
    public void testIsInstance() {
        // check non primitives first
        checkIsInstance(true, Object.class, null);
        checkIsInstance(true, Object.class, "String");
        checkIsInstance(true, String.class, "String");
        checkIsInstance(false, Integer.class, "String");

        // null primitive
        checkIsInstance(false, int.class, null);

        // then primitives.
        // We check each for a match, and then both a non-match against a primitive and object
        checkIsInstancePrimitive(boolean.class, true);
        checkIsInstancePrimitive(char.class, 'a');
        checkIsInstancePrimitive(byte.class, (byte) 1);
        checkIsInstancePrimitive(short.class, (short) 1);
        checkIsInstancePrimitive(int.class, 1);
        checkIsInstancePrimitive(long.class, 1L);
        checkIsInstancePrimitive(float.class, 1.0f);
        checkIsInstancePrimitive(double.class, 1.0);
    }

    /**
     * Runs a check against isInstance for a primitive type. It does 3 checks
     * <ol>
     * <li>A valid instance
     * <li>An invalid primitive instance
     * <li>An invalid non-primitive instance
     * <ol>
     *
     * @param type
     * @param validInstance
     */
    private void checkIsInstancePrimitive(final Class<?> type, final Object validInstance) {
        // the invalidPrimitive is an integer in all cases, except against class int.class
        // where we use a long
        final Object invalidPrimitive;
        if (type == int.class) {
            invalidPrimitive = 1L;
        } else {
            invalidPrimitive = 1;
        }

        checkIsInstance(true, type, validInstance);
        checkIsInstance(false, type, invalidPrimitive);
        checkIsInstance(false, type, "Object");
    }

    /**
     * Runs a check against isInstance, checking if it returns the expected value
     *
     * @param type
     * @param instance
     * @param expected Expected return value
     */
    private void checkIsInstance(final boolean expected, final Class<?> type, final Object instance) {
        assertEquals(expected, Util.isInstance(type, instance));
    }

    /**
     * Tests the most specific method test
     */
    @Test
    public void testGetMostSpecificMethod() throws Exception {
        final Method mObject = getClass().getDeclaredMethod("setThreeAccessibleNoExactMatchSetter", Object.class);
        final Method mThrowable = getClass().getDeclaredMethod("setThreeAccessibleNoExactMatchSetter", Throwable.class);
        final Method mException = getClass().getDeclaredMethod("setThreeAccessibleNoExactMatchSetter", Exception.class);

        assertEquals(mThrowable, Util.getMostSpecificMethod(mObject, mThrowable));
        assertEquals(mException, Util.getMostSpecificMethod(mException, mThrowable));
        assertEquals(mException, Util.getMostSpecificMethod(mException, mObject));
    }
}
