/* JUG Java Uuid Generator
 * UUIDTimerTest.java
 * Created on July 28, 2003, 10:09 PM
 *
 * Copyright (c) 2003 Eric Bie
 *
 * Licensed under the License specified in the file LICENSE which is
 * included with the source code.
 * You may not use this file except in compliance with the License.
 *
 * 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 com.fasterxml.uuid;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.textui.TestRunner;

import com.fasterxml.uuid.UUIDTimer;

/**
 * JUnit Test class for the com.fasterxml.uuid.UUIDTimer class.
 *
 * @author Eric Bie
 */
public class UUIDTimerTest extends TestCase
{
    // constants for use in the tests
    private static final int UUID_TIMER_ARRAY_LENGTH = 10;
    private static final int SIZE_OF_TEST_ARRAY = 10000;
    
    public UUIDTimerTest(java.lang.String testName)
    {
        super(testName);
    }
    
    public static Test suite()
    {
        TestSuite suite = new TestSuite(UUIDTimerTest.class);
        return suite;
    }
    
    public static void main(String[] args)
    {
        TestRunner.run(suite());
    }
    
    /**************************************************************************
     * Begin constructor tests
     *************************************************************************/
    /**
     * Test of UUIDTimer(SecureRandom) constructor,
     * of class com.fasterxml.uuid.UUIDTimer.
     */
    public void testSecureRandomUUIDTimerConstructor() throws IOException
    {
        // try passing a null SecureRandom argument
        try
        {
            /*UUIDTimer uuid_timer =*/ new UUIDTimer((SecureRandom)null, null);
            // if we reach here we didn't catch what we should have
            fail("Expected exception not caught");
        }
        catch (NullPointerException ex)
        {
            // caught the expected exception, this is good, just go on
        }
        catch (Exception ex)
        {
            fail("Unexpected exception caught");
        }
        
        // now construct a valid case
        SecureRandom secure_random = new SecureRandom();
        UUIDTimer uuid_timer = new UUIDTimer(secure_random, null);
        
        // we'll do a simple run to see that it at least produces output
        byte[] test_array = new byte[UUID_TIMER_ARRAY_LENGTH];
        uuid_timer.getAndSetTimestamp(test_array);
        // check that it's not all null
        assertArrayNotEqual(test_array,
                            new byte[UUID_TIMER_ARRAY_LENGTH],
                            UUID_TIMER_ARRAY_LENGTH);
    }    
    /**************************************************************************
     * End constructor tests
     *************************************************************************/

    /**
     * Test of getAndSetTimestamp method, of class com.fasterxml.uuid.UUIDTimer.
     */
    public void testGetTimestamp() throws IOException
    {
        // constant for use in this test
        final int EXTRA_DATA_LENGTH = 9;
        
        // construct a UUIDTimer
        SecureRandom secure_random = new SecureRandom();
        UUIDTimer uuid_timer = new UUIDTimer(secure_random, null);
        
        // test an array thats too small
        try
        {
            byte[] test_array = new byte[UUID_TIMER_ARRAY_LENGTH - 1];
            uuid_timer.getAndSetTimestamp(test_array);
            // if we get here, we didn't catch the expected exception
            fail("Expected exception not caught");
        }
        catch (ArrayIndexOutOfBoundsException ex)
        {
            // caught the expected exception, this is good, just go on
        }
        catch (Exception ex)
        {
            fail("Unexpected exception caught");
        }
        
        // construct a valid array exactly big enough and see that it works
        byte[] test_array = new byte[UUID_TIMER_ARRAY_LENGTH];
        uuid_timer.getAndSetTimestamp(test_array);
        // check that it's not all null
        assertArrayNotEqual(test_array,
                            new byte[UUID_TIMER_ARRAY_LENGTH],
                            UUID_TIMER_ARRAY_LENGTH);
        
        // construct a valid array bigger then we need
        // and make sure getAndSetTimeStamp only touches the begining part
        test_array = new byte[UUID_TIMER_ARRAY_LENGTH + EXTRA_DATA_LENGTH];
        Arrays.fill(test_array, (byte)'x');
        uuid_timer.getAndSetTimestamp(test_array);
        for (int i = 0; i < EXTRA_DATA_LENGTH; ++i)
        {
            assertEquals("test_array element was corrupted",
                        (byte)'x',
                        test_array[i + UUID_TIMER_ARRAY_LENGTH]);
        }
        // check that the timer portion is not all null
        assertArrayNotEqual(test_array,
                            new byte[UUID_TIMER_ARRAY_LENGTH],
                            UUID_TIMER_ARRAY_LENGTH);
        
        // now make a bunch of timer elements and validate that they are
        // are well behaved timer elements
        byte[][] array_of_uuid_timer_byte_arrays =
            new byte[SIZE_OF_TEST_ARRAY][UUID_TIMER_ARRAY_LENGTH];
        
        // before generating all the uuid timer arrays, get the start time
        long start_time = System.currentTimeMillis();
        
        // now create the array of uuid timer output arrays
        for (int i = 0; i < array_of_uuid_timer_byte_arrays.length; i++)
        {
            uuid_timer.getAndSetTimestamp(array_of_uuid_timer_byte_arrays[i]);
        }
        
        // now capture the end time
        long end_time = System.currentTimeMillis();
        
        // convert the array into array of longs holding the numerical values
        Long[] uuid_timer_array_of_longs =
            convertArrayOfByteArraysToArrayOfLongs(
                array_of_uuid_timer_byte_arrays);
        
        // check that none of the UUID Timer arrays are all null
        checkUUIDTimerLongArrayForNonNullTimes(uuid_timer_array_of_longs);

        // check that all UUID Timers were generated with correct order
        checkUUIDTimerLongArrayForCorrectOrdering(uuid_timer_array_of_longs);
        
        // check that all UUID Timers were unique
        checkUUIDTimerLongArrayForUniqueness(uuid_timer_array_of_longs);
        
        // check that all timestamps are between the start and end time
        checkUUIDTimerLongArrayForCorrectCreationTime(
            uuid_timer_array_of_longs, start_time, end_time);
    }
    
    /**************************************************************************
     * Begin private helper functions for use in tests
     *************************************************************************/
    private Long[] convertArrayOfByteArraysToArrayOfLongs(
        byte[][] uuidTimerArrayOfByteArrays)
    {
        Long[] array_of_longs = new Long[uuidTimerArrayOfByteArrays.length];
        for (int i = 0; i < uuidTimerArrayOfByteArrays.length; i++)
        {
            // collect the UUID time stamp which is
            // the number of 100-nanosecond intervals since
            // 00:00:00.00 15 October 1582
            long uuid_timer = 0L;
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][3] & 0xFFL) <<  0);
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][2] & 0xFFL) <<  8);
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][1] & 0xFFL) << 16);
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][0] & 0xFFL) << 24);
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][5] & 0xFFL) << 32);
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][4] & 0xFFL) << 40);
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][7] & 0xFFL) << 48);
            uuid_timer |= ((uuidTimerArrayOfByteArrays[i][6] & 0xFFL) << 56);
            
            array_of_longs[i] = new Long(uuid_timer);
        }
        
        return array_of_longs;
    }

    private class ReverseOrderUUIDTimerLongComparator implements Comparator<Long>
    {
        // this Comparator class has a compare which orders reverse of the
        // compare method in UUIDTimerArrayComparator (so we can be sure our
        // arrays below are 'not ordered in sorted order'
        // before we sort them).
        public int compare(Long uuid_timer_long1, Long uuid_timer_long2) {
            return -uuid_timer_long1.compareTo(uuid_timer_long2);
        }
        
        // we are only implementing equals because it's needed, super should do
        public boolean equals(Object o)
        {
            return super.equals(o);
        }
    }
    
    private void checkUUIDTimerLongArrayForCorrectOrdering(
        Long[] uuidTimerLongArray)
    {
        // now we'll clone the array and reverse it
        Long[] uuid_timer_sorted_arrays = (Long[])uuidTimerLongArray.clone();
        assertEquals("Cloned array length did not match",
                    uuidTimerLongArray.length,
                    uuid_timer_sorted_arrays.length);
        
        ReverseOrderUUIDTimerLongComparator rev_order_uuid_timer_comp =
            new ReverseOrderUUIDTimerLongComparator();
        Arrays.sort(uuid_timer_sorted_arrays, rev_order_uuid_timer_comp);
        
        // let's check that the array is actually reversed
        int sorted_arrays_length = uuid_timer_sorted_arrays.length;
        for (int i = 0; i < sorted_arrays_length; i++)
        {
            assertTrue(
                "Reverse order check on uuid timer arrays failed" +
                    " on element " + i + ": " +
                    uuidTimerLongArray[i].longValue() + " does not equal " +
                    uuid_timer_sorted_arrays[
                        sorted_arrays_length - (1 + i)].longValue(),
                uuidTimerLongArray[i].equals(
                    uuid_timer_sorted_arrays[sorted_arrays_length - (1 + i)]));
        }
        
        // now let's sort the reversed array and check that it sorted to
        // the same order as the original
        Arrays.sort(uuid_timer_sorted_arrays);
        for (int i = 0; i < sorted_arrays_length; i++)
        {
            assertTrue(
                "Same order check on uuid timer arrays failed on element " +
                    i + ": " + uuidTimerLongArray[i].longValue() +
                    " does not equal " +
                    uuid_timer_sorted_arrays[i].longValue(),
                uuidTimerLongArray[i].equals(uuid_timer_sorted_arrays[i]));
        }
    }

    private void checkUUIDTimerLongArrayForUniqueness(Long[] uuidTimerLongArray)
    {
        // here we'll assert that all elements in the list are not equal to
        // each other (aka, there should be no duplicates) we'll do this by
        // inserting all elements into a Set and making sure none of them
        // were already present (add will return false if it was already there)
        Set<Long> set = new HashSet<Long>();
        for (int i = 0; i < uuidTimerLongArray.length; i++)
        {
            assertTrue("Uniqueness test failed on insert into HashSet",
                set.add(uuidTimerLongArray[i]));
            assertFalse(
                "Paranoia Uniqueness test failed (second insert into HashSet)",
                set.add(uuidTimerLongArray[i]));
        }
    }
    
    private void checkUUIDTimerLongArrayForCorrectCreationTime(
        Long[] uuidTimerLongArray,
        long startTime,
        long endTime)
    {
        // we need to convert from 100-naonsecond units (as used in UUIDs)
        // to millisecond units as used in UTC based time
        final long MILLI_CONVERSION_FACTOR = 10000L;
        // Since System.currentTimeMillis() returns time epoc time
        // (from 1-Jan-1970), and UUIDs use time from the beginning of
        // Gregorian calendar (15-Oct-1582) we have a offset for correction
        final long GREGORIAN_CALENDAR_START_TO_UTC_START_OFFSET =
            122192928000000000L;
        
        assertTrue("Start time was not before the end time",
                startTime < endTime);
        
        // let's check that all the uuid timer longs in the array have a
        // timestamp which lands between the start and end time
        for (int i = 0; i < uuidTimerLongArray.length; i++)
        {
            long uuid_time = uuidTimerLongArray[i].longValue();
            
            // first we'll remove the gregorian offset
            uuid_time -= GREGORIAN_CALENDAR_START_TO_UTC_START_OFFSET;

            // and convert to milliseconds as the system clock is in millis
            uuid_time /= MILLI_CONVERSION_FACTOR;

            // now check that the times are correct
            assertTrue(
                "Start time: " + startTime +
                    " was not before UUID timestamp: " + uuid_time,
                startTime  <= uuid_time);
            assertTrue(
                "UUID timestamp: " + uuid_time +
                    " was not before the end time: " + endTime,
                uuid_time <= endTime);
        }
    }

    private void checkUUIDTimerLongArrayForNonNullTimes(
        Long[] uuidTimerLongArray)
    {
        for (int i = 0; i < uuidTimerLongArray.length; i++)
        {
            assertFalse("Timer Long was null",
                0 == uuidTimerLongArray[i].longValue());
        }
    }

    private void assertArrayNotEqual(byte[] array1, byte[] array2, int length)
    {
        assertTrue("array1 was not equal or longer then length",
                    array1.length >= length);
        assertTrue("array2 was not equal or longer then length",
                    array2.length >= length);
        
        for (int i = 0; i < length; ++i)
        {
            // we know the arrays aren't equal the first time we
            // fine an array element that isn't equal.
            // in that case just return
            if (array1[i] != array2[i])
            {
                return;
            }
        }
        // if we get out of the loop, both arrays were identical, so fail
        fail("All elements of Array1 were equal to all elements of Array2");
    }
    /**************************************************************************
     * End private helper functions for use in tests
     *************************************************************************/
}
