/*
 *******************************************************************************
 * Copyright (C) 2006-2011, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */

package com.ibm.icu.tests;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;

import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;

import junit.framework.TestCase;

/**
 * Implement boilerplate tests.
 * Currently there is only one method, testEHCS, which tests equals, hashCode, 
 * clone, and serialization.
 */
public abstract class ICUTestCase extends TestCase {
    private static final Object[] EMPTY_ARGS = {};
	private static final Class<?>[] EMPTY_CLASSES = {};
        
    private static final Locale oldLocale = Locale.getDefault();
    private static final ULocale oldULocale = ULocale.getDefault();
    private static final java.util.TimeZone oldJTimeZone = java.util.TimeZone.getDefault();
    private static final TimeZone oldITimeZone = TimeZone.getDefault();
                
    // TODO: what's the best way to check this?
    public static final boolean testingWrapper = true;

    protected void setUp() throws Exception {
        super.setUp();
        Locale.setDefault(Locale.US);
        ULocale.setDefault(ULocale.US);
        java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("PST"));
        TimeZone.setDefault(TimeZone.getTimeZone("PST"));
    }
        
    protected void tearDown() throws Exception {
        ULocale.setDefault(oldULocale);
        Locale.setDefault(oldLocale);
        TimeZone.setDefault(oldITimeZone);
        java.util.TimeZone.setDefault(oldJTimeZone);
        super.tearDown();
    }

    private static final Object test = new Object();
        
    /**
     * Assert that two objects are _not_ equal.  Curiously missing from Assert.
     * @param lhs an object to test, may be null
     * @param rhs an object to test, may be null
     */
    public static void assertNotEqual(Object lhs, Object rhs) {
        if (lhs == null) {
            if (rhs == null) fail("null equals null");
        } else {
            if (lhs.equals(rhs)) {
                fail(lhs.toString() + " equals " + rhs);
            }
        }
    }
        
    public static void assertNotEqual(long lhs, long rhs) {
        if (lhs == rhs) {
            fail("values are equal: " + lhs);
        }
    }
        
    /**
     * Test whether equality, hashCode, clone, and serialization work as expected.  
     * Equals(Object) is assumed to return false (not throw an exception) if passed 
     * null or an object of an incompatible class.
     * Hashcodes must be equal iff the two objects compare equal.  No attempt is made to
     * evaluate the quality of the hashcode distribution, so (in particular) degenerate 
     * hashcode implementations will pass this test.
     * Clone will be tested if the method "clone" is public on the class of obj.  
     * It is assumed to return an object that compares equal to obj.
     * Serialization will be tested if object implements Serializable or Externalizable.
     * It is assumed the serialized/deserialized object compares equal to obj.
     * @param obj the object to test
     * @param eq an object that should compare equal to, but is not the same as, obj.  
     *     it should be assignable to the class of obj.
     * @param neq a non-null object that should not compare equal to obj.  
     *     it should be assignable to the class of obj.
     */
	public static void testEHCS(Object obj, Object eq, Object neq) {
        if (obj == null || eq == null || neq == null) {
            throw new NullPointerException();
        }
        Class<? extends Object> cls = obj.getClass();
        if (!(cls.isAssignableFrom(eq.getClass()) && cls.isAssignableFrom(neq.getClass()))) {
            throw new IllegalArgumentException("unassignable classes");
        }
                
        // reflexive
        assertEquals(obj, obj);
                
        // should return false, not throw exception
        assertNotEqual(obj, test);
        assertNotEqual(obj, null);
                
        // commutative
        assertEquals(obj, eq);
        assertEquals(eq, obj);
                
        assertNotEqual(obj, neq);
        assertNotEqual(neq, obj);
                
        // equal objects MUST have equal hashes, unequal objects MAY have equal hashes
        assertEquals(obj.hashCode(), eq.hashCode());
                
        Object clone = null;
        try {
            // look for public clone method and call it if available
            Method method_clone = cls.getMethod("clone", EMPTY_CLASSES);
            clone = method_clone.invoke(obj, EMPTY_ARGS);
            assertNotNull(clone);
        }
        catch(NoSuchMethodException e) {
            // ok
        }
        catch(InvocationTargetException e) {
            // ok
        }
        catch(IllegalAccessException e) {
            // ok
        }
                
        if (clone != null) {
            assertEquals(obj, clone);
            assertEquals(clone, obj);
        }
                
        if (obj instanceof Serializable || obj instanceof Externalizable) {
            Object ser = null;
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(clone);
                oos.close();
                                
                ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(bis);
                ser = ois.readObject();
                ois.close();
            }
            catch(IOException e) {
                System.err.println(e.getMessage());
                throw new RuntimeException(e);
            }
            catch(ClassNotFoundException e) {
                System.err.println(e.getMessage());
                throw new RuntimeException(e);
            }
                        
            if (ser != null) {
                assertEquals(obj, ser);
                assertEquals(ser, obj);
                assertEquals(obj.hashCode(), ser.hashCode());
            }
        }
    }

    /**
     * Fail if the arrays are not equal.  To be equal, the arrays must
     * be the same length, and each element in the left array must compare
     * equal to the corresponding element of the right array.
     * Also fails if one of the objects is not an array.
     * @param lhs the left array
     * @param rhs the right array
     */
    public static void assertArraysEqual(Object lhs, Object rhs) {
        Class<? extends Object> lcls = lhs.getClass();
        Class<? extends Object> rcls = rhs.getClass();
        if (!(lcls.isArray() && rcls.isArray())) {
            fail("objects are not arrays");
        }
        String result = arraysAreEqual(lhs, rhs);
        if (result != null) {
            fail(result);
        }
    }

    /**
     * Fail if the arrays are equal.  Also fails if one or the other 
     * argument is not an array.
     * @param lhs the left array
     * @param rhs the right array
     */
    public static void assertArraysNotEqual(Object lhs, Object rhs) {
        Class<? extends Object> lcls = lhs.getClass();
        Class<? extends Object> rcls = rhs.getClass();
        if (!(lcls.isArray() && rcls.isArray())) {
            fail("objects are not arrays");
        }
        String result = arraysAreEqual(lhs, rhs);
        if (result == null) {
            fail("arrays are equal");
        }
    }
        
    // slow but general
    private static String arraysAreEqual(Object lhsa, Object rhsa) {
        int lhsl = Array.getLength(lhsa);
        int rhsl = Array.getLength(rhsa);
        if (lhsl != rhsl) {
            return "length " + lhsl + " != " + rhsl;
        }
        boolean lhsaA = lhsa.getClass().getComponentType().isArray();
        boolean rhsaA = rhsa.getClass().getComponentType().isArray();
        if (lhsaA != rhsaA) {
            return (lhsaA ? "" : "non-") + "array != " + (rhsaA ? "" : "non-") + "array";
        }
        for (int i = 0; i < lhsl; ++i) {
            Object lhse = Array.get(lhsa, i);
            Object rhse = Array.get(rhsa, i);
            if (lhse == null) {
                if (rhse != null) {
                    return "null != " + rhse;
                }
            } else {
                if (lhsaA) {
                    String result = arraysAreEqual(lhse, rhse);
                    if (result != null) {
                        if (result.charAt(0) != '[') {
                            result = " " + result;
                        }
                        return "[" + i + "]" + result;
                    }
                } else {
                    if (!lhse.equals(rhse)) {
                        return lhse.toString() + " != " + rhse;
                    }
                }
            }
        }
        return null;
    }
        
    // much more painful and slow than it should be... partly because of the
    // oddness of clone, partly because arrays don't provide a Method for 
    // 'clone' despite the fact that they implement it and make it public.
    public static Object cloneComplex(Object obj) {
        Object result = null;
        if (obj != null) {
            Class<? extends Object> cls = obj.getClass();
            if (cls.isArray()) {
                int len = Array.getLength(obj);
                Class<?> typ = cls.getComponentType();
                result = Array.newInstance(typ, len);
                boolean prim = typ.isPrimitive();
                for (int i = 0; i < len; ++i) {
                    Object elem = Array.get(obj, i);
                    Array.set(result, i, prim ? elem : cloneComplex(elem));
                }
            } else {
                result = obj; // default
                try {
                    Method cloneM = cls.getMethod("clone", EMPTY_CLASSES);
                    result = cloneM.invoke(obj, EMPTY_ARGS);
                }
                catch (NoSuchMethodException e) {
                }
                catch (IllegalAccessException e) {
                }
                catch (InvocationTargetException e) {
                }
            }
        }
        return result;
    }
}
