/*
 * Copyright 2006-2010 Amazon Technologies, Inc. or its affiliates.
 * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 *
 * Licensed 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 com.amazon.carbonado.raw;
import java.lang.reflect.*;
import java.util.*;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.cojen.classfile.*;
import org.cojen.util.*;
import com.amazon.carbonado.*;
import com.amazon.carbonado.info.*;
import com.amazon.carbonado.spi.*;
/**
 * Test case for {@link GenericEncodingStrategy}.
 * 
 * It generates random selections of properties, encodes with random values,
 * and checks that the decoding produces the original results. In addition, the
 * proper ordering of encoded keys is checked.
 *
 * @author Brian S O'Neill
 */
public class TestEncodingStrategy extends TestCase {
    private static final int SHORT_TEST = 100;
    private static final int MEDIUM_TEST = 500;
    private static final int LONG_TEST = 1000;
    private static final int ENCODE_OBJECT_ARRAY = 0;
    private static final int DECODE_OBJECT_ARRAY = 1;
    private static final int ENCODE_OBJECT_ARRAY_PARTIAL = 2;
    private static final int BOGUS_GENERATION = 99;
    // Make sure BOGUS_GENERATION is not included.
    private static final int[] GENERATIONS = {-1, 0, 1, 2, 127, 128, 129, Integer.MAX_VALUE};
    public static void main(String[] args) {
        junit.textui.TestRunner.run(suite());
    }
    public static TestSuite suite() {
        return new TestSuite(TestEncodingStrategy.class);
    }
    private final long mSeed;
    private final StorableProperty[] mProperties;
    private Random mRandom;
    public TestEncodingStrategy(String name) {
        super(name);
        mSeed = 986184829029842L;
        Collection extends StorableProperty> properties =
            StorableIntrospector.examine(TestStorable.class).getAllProperties().values();
        mProperties = properties.toArray(new StorableProperty[0]);
    }
    protected void setUp() {
        mRandom = new Random(mSeed);
    }
    protected void tearDown() {
    }
    public void test_dataEncoding_noProperties() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_noProperties(0, 0, generation);
        }
    }
    public void test_dataEncoding_noProperties_prefix() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_noProperties(5, 0, generation);
        }
    }
    public void test_dataEncoding_noProperties_suffix() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_noProperties(0, 7, generation);
        }
    }
    public void test_dataEncoding_noProperties_prefixAndSuffix() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_noProperties(5, 7, generation);
        }
    }
    private void test_dataEncoding_noProperties(int prefix, int suffix, int generation)
        throws Exception
    {
        GenericEncodingStrategy strategy = new GenericEncodingStrategy
            (TestStorable.class, null, 0, 0, prefix, suffix);
        Method[] methods = generateCodecMethods
            (strategy, new StorableProperty[0], null, generation);
        byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
            .invoke(null, new Object[] {new Object[0]});
        int generationPrefix;
        if (generation < 0) {
            generationPrefix = 0;
        } else if (generation < 128) {
            generationPrefix = 1;
        } else {
            generationPrefix = 4;
        }
        assertEquals(encoded.length, prefix + generationPrefix + suffix);
        if (generation >= 0) {
            if (generationPrefix == 1) {
                assertEquals(generation, encoded[prefix]);
            } else {
                int actualGeneration = DataDecoder.decodeInt(encoded, prefix);
                assertEquals(generation, actualGeneration);
            }
        }
        // Decode should not throw an exception.
        methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
        // Generation mismatch should throw an exception.
        if (generation >= 0) {
            encoded[prefix] = BOGUS_GENERATION;
            try {
                methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
                fail();
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();
                if (cause instanceof CorruptEncodingException) {
                    CorruptEncodingException cee = (CorruptEncodingException) cause;
                    // Make sure error message includes actual generation.
                    assertTrue(cee.getMessage().indexOf(String.valueOf(BOGUS_GENERATION)) >= 0);
                } else {
                    throw e;
                }
            }
        }
    }
    public void test_dataEncoding_multipleProperties() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_multipleProperties(0, 0, generation);
        }
    }
    public void test_dataEncoding_multipleProperties_prefix() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_multipleProperties(5, 0, generation);
        }
    }
    public void test_dataEncoding_multipleProperties_suffix() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_multipleProperties(0, 7, generation);
        }
    }
    public void test_dataEncoding_multipleProperties_prefixAndSuffix() throws Exception {
        for (int generation : GENERATIONS) {
            test_dataEncoding_multipleProperties(5, 7, generation);
        }
    }
    /**
     * @param generation when non-negative, encode a storable layout generation
     * value in one or four bytes.
     */
    private void test_dataEncoding_multipleProperties(int prefix, int suffix, int generation)
        throws Exception
    {
        GenericEncodingStrategy strategy = new GenericEncodingStrategy
            (TestStorable.class, null, 0, 0, prefix, suffix);
        for (int i=0; i[] properties = selectProperties(1, 50);
            Method[] methods = generateCodecMethods(strategy, properties, null, generation);
            Object[] values = selectPropertyValues(properties);
            byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
                .invoke(null, new Object[] {values});
            int generationPrefix;
            if (generation < 0) {
                generationPrefix = 0;
            } else if (generation < 128) {
                generationPrefix = 1;
            } else {
                generationPrefix = 4;
            }
            assertTrue(encoded.length > (prefix + generationPrefix + suffix));
            if (generation >= 0) {
                if (generationPrefix == 1) {
                    assertEquals(generation, encoded[prefix]);
                } else {
                    int actualGeneration = DataDecoder.decodeInt(encoded, prefix);
                    assertEquals(generation, actualGeneration);
                }
            }
            if (prefix > 0) {
                // Fill in with data which should be ignored by decoder.
                for (int p=0; p 0) {
                // Fill in with data which should be ignored by decoder.
                for (int p=0; p= 0) {
                encoded[prefix] = BOGUS_GENERATION;
                try {
                    methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
                    fail();
                } catch (InvocationTargetException e) {
                    Throwable cause = e.getCause();
                    if (cause instanceof CorruptEncodingException) {
                        CorruptEncodingException cee = (CorruptEncodingException) cause;
                        // Make sure error message includes actual generation.
                        assertTrue
                            (cee.getMessage().indexOf(String.valueOf(BOGUS_GENERATION)) >= 0);
                    } else {
                        throw e;
                    }
                }
            }
        }
    }
    public void test_keyEncoding_noProperties() throws Exception {
        test_keyEncoding_noProperties(0, 0);
    }
    public void test_keyEncoding_noProperties_prefix() throws Exception {
        test_keyEncoding_noProperties(5, 0);
    }
    public void test_keyEncoding_noProperties_suffix() throws Exception {
        test_keyEncoding_noProperties(0, 7);
    }
    public void test_keyEncoding_noProperties_prefixAndSuffix() throws Exception {
        test_keyEncoding_noProperties(5, 7);
    }
    private void test_keyEncoding_noProperties(int prefix, int suffix) throws Exception {
        GenericEncodingStrategy strategy = new GenericEncodingStrategy
            (TestStorable.class, null, prefix, suffix, 0, 0);
        Method[] methods = generateCodecMethods
            (strategy, new StorableProperty[0], new Direction[0], -1);
        // Encode should return an empty array.
        byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
            .invoke(null, new Object[] {new Object[0]});
        // This test previously verified that length == prefix+suffix, but for
        // GenericStorableCodec encodeSearchKeyPrefix to work it should not
        // encode the suffix, and besides a key without properties is invalid.
        assertEquals(encoded.length, prefix);
        // Decode should not throw an exception.
        methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
    }
    public void test_keyEncoding_multipleProperties() throws Exception {
        test_keyEncoding_multipleProperties(0, 0);
    }
    public void test_keyEncoding_multipleProperties_prefix() throws Exception {
        test_keyEncoding_multipleProperties(5, 0);
    }
    public void test_keyEncoding_multipleProperties_suffix() throws Exception {
        test_keyEncoding_multipleProperties(0, 7);
    }
    public void test_keyEncoding_multipleProperties_prefixAndSuffix() throws Exception {
        test_keyEncoding_multipleProperties(5, 7);
    }
    private void test_keyEncoding_multipleProperties(int prefix, int suffix) throws Exception {
        GenericEncodingStrategy strategy = new GenericEncodingStrategy
            (TestStorable.class, null, prefix, suffix, 0, 0);
        for (int i=0; i[] properties = selectProperties(1, 50);
            Direction[] directions = selectDirections(properties.length);
            Method[] methods = generateCodecMethods(strategy, properties, directions, -1);
            Object[] values = selectPropertyValues(properties);
            byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
                .invoke(null, new Object[] {values});
            assertTrue(encoded.length > (prefix + suffix));
            // Encode using partial encoding method, but do all
            // properties. Ensure that the encoding is exactly the same.
            byte[] encoded2 = (byte[]) methods[ENCODE_OBJECT_ARRAY_PARTIAL]
                .invoke(null, new Object[] {values, 0, properties.length});
            assertTrue(Arrays.equals(encoded, encoded2));
            if (prefix > 0) {
                // Fill in with data which should be ignored by decoder.
                for (int p=0; p 0) {
                // Fill in with data which should be ignored by decoder.
                for (int p=0; p 0) {
                for (int p=0; p 0) {
                for (int p=0; p 0);
                }
                if (properties.length == 1) {
                    break;
                }
            }
        }
    }
    public void test_keyEncoding_ordering() throws Exception {
        GenericEncodingStrategy strategy =
            new GenericEncodingStrategy(TestStorable.class, null, 0, 0, 0, 0);
        for (int i=0; i[] properties = selectProperties(1, 50);
            Direction[] directions = selectDirections(properties.length);
            Method[] methods = generateCodecMethods(strategy, properties, directions, -1);
            Object[] values_1 = selectPropertyValues(properties);
            byte[] encoded_1 = (byte[]) methods[ENCODE_OBJECT_ARRAY]
                .invoke(null, new Object[] {values_1});
            Object[] values_2 = selectPropertyValues(properties);
            byte[] encoded_2 = (byte[]) methods[ENCODE_OBJECT_ARRAY]
                .invoke(null, new Object[] {values_2});
            int byteOrder = TestDataEncoding.byteArrayCompare(encoded_1, encoded_2);
            int valueOrder = compareValues(properties, directions, values_1, values_2);
            assertEquals(valueOrder, byteOrder);
        }
    }
    private int compareValues(StorableProperty>[] properties, Direction[] directions,
                              Object[] values_1, Object[] values_2) {
        int length = directions.length;
        for (int i=0; i property = properties[i];
            Direction direction = directions[i];
            Object value_1 = values_1[i];
            Object value_2 = values_2[i];
            int result;
            if (property.getType() == byte[].class) {
                result = TestDataEncoding.byteArrayOrNullCompare
                    ((byte[]) value_1, (byte[]) value_2);
            } else {
                if (value_1 == null) {
                    if (value_2 == null) {
                        result = 0;
                    } else {
                        result = 1;
                    }
                } else if (value_2 == null) {
                    result = -1;
                } else {
                    result = Integer.signum(((Comparable) value_1).compareTo(value_2));
                }
            }
            if (result != 0) {
                if (direction == Direction.DESCENDING) {
                    result = -result;
                }
                return result;
            }
        }
        return 0;
    }
    // Method taken from String class and modified a bit.
    private static int indexOf(byte[] source, int sourceOffset, int sourceCount,
                               byte[] target, int targetOffset, int targetCount,
                               int fromIndex) {
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }
        byte first  = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);
        
        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            // Look for first byte.
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }
            
            // Found first byte, now look at the rest of v2
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++);
                
                if (j == end) {
                    // Found whole string.
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }
    /**
     * First method is the encoder, second is the decoder. Both methods are
     * static. Encoder accepts an object array of property values, and it
     * returns a byte array. Decoder accepts an object array to receive
     * property values, an encoded byte array, and it returns void.
     *
     * If generating key encoding and the property count is more than zero,
     * then third element in array is a key encoder that supports partial
     * encoding. In addition to the object array argument, it also accepts an
     * int start and an int end argument. The start of the range is inclusive,
     * and the end is exclusive.
     *
     * @param directions when supplied, build key encoding/decoding. Otherwise,
     * build data encoding/decoding.
     * @param generation when non-negative, encode a storable layout generation
     * value in one or four bytes.
     */
    private Method[] generateCodecMethods(GenericEncodingStrategy strategy,
                                          StorableProperty[] properties,
                                          Direction[] directions,
                                          int generation)
        throws Exception
    {
        ClassInjector ci = ClassInjector.create(TestStorable.class.getName(), null);
        ClassFile cf = new ClassFile(ci.getClassName());
        cf.markSynthetic();
        cf.setTarget("1.5");
        cf.addDefaultConstructor();
        TypeDesc byteArrayType = TypeDesc.forClass(byte[].class);
        TypeDesc objectArrayType = TypeDesc.forClass(Object[].class);
        // Build encode method.
        MethodInfo mi = cf.addMethod(Modifiers.PUBLIC_STATIC, "encode", byteArrayType,
                                     new TypeDesc[] {objectArrayType});
        CodeBuilder b = new CodeBuilder(mi);
        LocalVariable encodedVar;
        if (directions != null) {
            OrderedProperty[] ordered =
                makeOrderedProperties(properties, directions);
            encodedVar =
                strategy.buildKeyEncoding(b, ordered, b.getParameter(0), null, false, null, null);
        } else {
            encodedVar = strategy.buildDataEncoding
                (b, properties, b.getParameter(0), null, false, generation);
        }
        b.loadLocal(encodedVar);
        b.returnValue(byteArrayType);
        // Build decode method.
        mi = cf.addMethod(Modifiers.PUBLIC_STATIC, "decode", null,
                          new TypeDesc[] {objectArrayType, byteArrayType});
        b = new CodeBuilder(mi);
        if (directions != null) {
            OrderedProperty[] ordered =
                makeOrderedProperties(properties, directions);
            strategy.buildKeyDecoding
                (b, ordered, b.getParameter(0), null, false, b.getParameter(1));
        } else {
            strategy.buildDataDecoding
                (b, properties, b.getParameter(0), null, false,
                 generation, null, b.getParameter(1));
        }
        b.returnVoid();
        if (directions != null && properties.length > 0) {
            // Build encode partial key method.
            mi = cf.addMethod
                (Modifiers.PUBLIC_STATIC, "encodePartial", byteArrayType,
                 new TypeDesc[] {objectArrayType, TypeDesc.INT, TypeDesc.INT});
            b = new CodeBuilder(mi);
            OrderedProperty[] ordered =
                makeOrderedProperties(properties, directions);
            encodedVar =
                strategy.buildKeyEncoding(b, ordered, b.getParameter(0), null, false,
                                          b.getParameter(1), b.getParameter(2));
            b.loadLocal(encodedVar);
            b.returnValue(byteArrayType);
        }
        Class> clazz = ci.defineClass(cf);
        Method encode = clazz.getMethod("encode", Object[].class);
        Method decode = clazz.getMethod("decode", Object[].class, byte[].class);
        Method encodePartial = null;
        if (directions != null && properties.length > 0) {
            encodePartial = clazz.getMethod("encodePartial", Object[].class, int.class, int.class);
        }
        Method[] methods = new Method[3];
        methods[ENCODE_OBJECT_ARRAY] = encode;
        methods[DECODE_OBJECT_ARRAY] = decode;
        methods[ENCODE_OBJECT_ARRAY_PARTIAL] = encodePartial;
        return methods;
    }
    private StorableProperty[] selectProperties(int minCount, int maxCount) {
        int length = (minCount == maxCount) ? minCount
            : (mRandom.nextInt(maxCount - minCount + 1) + minCount);
        StorableProperty[] selection = new StorableProperty[length];
        for (int i=length; --i>=0; ) {
            selection[i] = mProperties[mRandom.nextInt(mProperties.length)];
        }
        return selection;
    }
    private Direction[] selectDirections(int count) {
        Direction[] directions = new Direction[count];
        for (int i=count; --i>=0; ) {
            Direction dir;
            switch (mRandom.nextInt(3)) {
            default:
                dir = Direction.UNSPECIFIED;
                break;
            case 1:
                dir = Direction.ASCENDING;
                break;
            case 2:
                dir = Direction.DESCENDING;
                break;
            }
            directions[i] = dir;
        }
        return directions;
    }
    private OrderedProperty[] makeOrderedProperties
        (StorableProperty[] properties, Direction[] directions) {
        int length = properties.length;
        OrderedProperty[] ordered = new OrderedProperty[length];
        for (int i=length; --i>=0; ) {
            ordered[i] = OrderedProperty.get(properties[i], directions[i]);
        }
        return ordered;
    }
    /**
     * Returns an array of the same size with randomly selected values filled
     * in that match the property type.
     */
    private Object[] selectPropertyValues(StorableProperty>[] properties) {
        int length = properties.length;
        Object[] values = new Object[length];
        for (int i=length; --i>=0; ) {
            StorableProperty> property = properties[i];
            TypeDesc type = TypeDesc.forClass(property.getType());
            Object value;
            if (property.isNullable() && mRandom.nextInt(100) == 0) {
                value = null;
            } else {
                TypeDesc prim = type.toPrimitiveType();
                if (prim != null) {
                    switch (prim.getTypeCode()) {
                    case TypeDesc.BOOLEAN_CODE: default:
                        value = mRandom.nextBoolean();
                        break;
                    case TypeDesc.CHAR_CODE:
                        value = (char) mRandom.nextInt();
                        break;
                    case TypeDesc.FLOAT_CODE:
                        value = Float.intBitsToFloat(mRandom.nextInt());
                        break;
                    case TypeDesc.DOUBLE_CODE:
                        value = Double.longBitsToDouble(mRandom.nextLong());
                        break;
                    case TypeDesc.BYTE_CODE:
                        value = (byte) mRandom.nextInt();
                        break;
                    case TypeDesc.SHORT_CODE:
                        value = (short) mRandom.nextInt();
                        break;
                    case TypeDesc.INT_CODE:
                        value = mRandom.nextInt();
                        break;
                    case TypeDesc.LONG_CODE:
                        value = mRandom.nextLong();
                        break;
                    }
                } else if (type == TypeDesc.STRING) {
                    int len = mRandom.nextInt(100);
                    StringBuilder sb = new StringBuilder(len);
                    for (int j=0; j