/*
* Copyright (c) 2012 The Broad Institute
* 
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
* 
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package htsjdk.variant.bcf2;

// the imports for unit testing.
import htsjdk.variant.VariantBaseTest;
import htsjdk.variant.variantcontext.writer.BCF2Encoder;
import org.testng.Assert;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;


public class BCF2EncoderDecoderUnitTest extends VariantBaseTest {
    private final double FLOAT_TOLERANCE = 1e-6;
    final List<BCF2TypedValue> primitives = new ArrayList<BCF2TypedValue>();
    final List<BCF2TypedValue> basicTypes = new ArrayList<BCF2TypedValue>();
    final List<BCF2TypedValue> forCombinations = new ArrayList<BCF2TypedValue>();

    @BeforeSuite
    public void before() {
        basicTypes.add(new BCF2TypedValue(1, BCF2Type.INT8));
        basicTypes.add(new BCF2TypedValue(1000, BCF2Type.INT16));
        basicTypes.add(new BCF2TypedValue(1000000, BCF2Type.INT32));
        basicTypes.add(new BCF2TypedValue(1.2345e6, BCF2Type.FLOAT));
        basicTypes.add(new BCF2TypedValue("A", BCF2Type.CHAR));

        // small ints
        primitives.add(new BCF2TypedValue(0, BCF2Type.INT8));
        primitives.add(new BCF2TypedValue(10, BCF2Type.INT8));
        primitives.add(new BCF2TypedValue(-1, BCF2Type.INT8));
        primitives.add(new BCF2TypedValue(100, BCF2Type.INT8));
        primitives.add(new BCF2TypedValue(-100, BCF2Type.INT8));
        primitives.add(new BCF2TypedValue(-127, BCF2Type.INT8));    // last value in range
        primitives.add(new BCF2TypedValue( 127, BCF2Type.INT8));    // last value in range

        // medium ints
        primitives.add(new BCF2TypedValue(-1000, BCF2Type.INT16));
        primitives.add(new BCF2TypedValue(1000, BCF2Type.INT16));
        primitives.add(new BCF2TypedValue(-128, BCF2Type.INT16));    // first value in range
        primitives.add(new BCF2TypedValue( 128, BCF2Type.INT16));    // first value in range
        primitives.add(new BCF2TypedValue(-32767, BCF2Type.INT16)); // last value in range
        primitives.add(new BCF2TypedValue( 32767, BCF2Type.INT16)); // last value in range

        // larger ints
        primitives.add(new BCF2TypedValue(-32768, BCF2Type.INT32)); // first value in range
        primitives.add(new BCF2TypedValue( 32768, BCF2Type.INT32)); // first value in range
        primitives.add(new BCF2TypedValue(-100000, BCF2Type.INT32));
        primitives.add(new BCF2TypedValue(100000, BCF2Type.INT32));
        primitives.add(new BCF2TypedValue(-2147483647, BCF2Type.INT32));
        primitives.add(new BCF2TypedValue(2147483647, BCF2Type.INT32));

        // floats
        primitives.add(new BCF2TypedValue(0.0, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-0.0, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(1.0, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-1.0, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(1.1, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-1.1, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(5.0 / 3.0, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-5.0 / 3.0, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(1.23e3, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(1.23e6, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(1.23e9, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(1.23e12, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(1.23e15, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-1.23e3, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-1.23e6, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-1.23e9, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-1.23e12, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(-1.23e15, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(Float.MIN_VALUE, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(Float.MAX_VALUE, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(Double.NEGATIVE_INFINITY, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(Double.POSITIVE_INFINITY, BCF2Type.FLOAT));
        primitives.add(new BCF2TypedValue(Double.NaN, BCF2Type.FLOAT));

        // strings
        //primitives.add(new BCF2TypedValue("", BCFType.CHAR)); <- will be null (which is right)
        primitives.add(new BCF2TypedValue("S", BCF2Type.CHAR));
        primitives.add(new BCF2TypedValue("S2", BCF2Type.CHAR));
        primitives.add(new BCF2TypedValue("12345678910", BCF2Type.CHAR));
        primitives.add(new BCF2TypedValue("ABCDEFGHIJKLMNOPQRSTUVWXYZ", BCF2Type.CHAR));
        primitives.add(new BCF2TypedValue("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ", BCF2Type.CHAR));

        // missing values
        for ( BCF2Type type : BCF2Type.values() ) {
            primitives.add(new BCF2TypedValue(null, type));
        }

        forCombinations.add(new BCF2TypedValue(10, BCF2Type.INT8));
        forCombinations.add(new BCF2TypedValue(100, BCF2Type.INT8));
        forCombinations.add(new BCF2TypedValue(-100, BCF2Type.INT8));
        forCombinations.add(new BCF2TypedValue(-128, BCF2Type.INT16));    // first value in range
        forCombinations.add(new BCF2TypedValue( 128, BCF2Type.INT16));    // first value in range
        forCombinations.add(new BCF2TypedValue(-100000, BCF2Type.INT32));
        forCombinations.add(new BCF2TypedValue(100000, BCF2Type.INT32));
        forCombinations.add(new BCF2TypedValue(0.0, BCF2Type.FLOAT));
        forCombinations.add(new BCF2TypedValue(1.23e6, BCF2Type.FLOAT));
        forCombinations.add(new BCF2TypedValue(-1.23e6, BCF2Type.FLOAT));
        forCombinations.add(new BCF2TypedValue("S", BCF2Type.CHAR));
        forCombinations.add(new BCF2TypedValue("ABCDEFGHIJKLMNOPQRSTUVWXYZ", BCF2Type.CHAR));
        forCombinations.add(new BCF2TypedValue("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ", BCF2Type.CHAR));

        // missing values
        for ( BCF2Type type : BCF2Type.values() ) {
            forCombinations.add(new BCF2TypedValue(null, type));
        }
    }

    // --------------------------------------------------------------------------------
    //
    // merge case Provider
    //
    // --------------------------------------------------------------------------------

    private class BCF2TypedValue {
        final BCF2Type type;
        final Object value;

        private BCF2TypedValue(final int value, final BCF2Type type) {
            this(new Integer(value), type);
        }

        private BCF2TypedValue(final double value, final BCF2Type type) {
            this(new Double(value), type);
        }

        private BCF2TypedValue(final Object value, final BCF2Type type) {
            this.type = type;
            this.value = value;
        }

        public boolean isMissing() { return value == null; }

        @Override
        public String toString() {
            return String.format("%s of %s", value, type);
        }
    }

    // -----------------------------------------------------------------
    //
    // Test encoding of basic types
    //
    // -----------------------------------------------------------------

    @DataProvider(name = "BCF2EncodingTestProviderBasicTypes")
    public Object[][] BCF2EncodingTestProviderBasicTypes() {
        List<Object[]> tests = new ArrayList<Object[]>();
        for ( BCF2TypedValue tv : basicTypes )
            tests.add(new Object[]{Arrays.asList(tv)});
        return tests.toArray(new Object[][]{});
    }

    private interface EncodeMe {
        public void encode(final BCF2Encoder encoder, final BCF2TypedValue tv) throws IOException;
    }


    @Test(dataProvider = "BCF2EncodingTestProviderBasicTypes")
    public void testBCF2BasicTypesWithStaticCalls(final List<BCF2TypedValue> toEncode) throws IOException {
        testBCF2BasicTypesWithEncodeMe(toEncode,
                new EncodeMe() {
                    @Override
                    public void encode(final BCF2Encoder encoder, final BCF2TypedValue tv) throws IOException {
                        switch ( tv.type ) {
                            case INT8:
                            case INT16:
                            case INT32:
                                encoder.encodeTypedInt((Integer)tv.value, tv.type);
                                break;
                            case FLOAT:
                                encoder.encodeTypedFloat((Double)tv.value);
                                break;
                            case CHAR:
                                encoder.encodeTypedString((String)tv.value);
                                break;
                        }
                    }
                });
    }

    @Test(dataProvider = "BCF2EncodingTestProviderBasicTypes")
    public void testBCF2BasicTypesWithObjectType(final List<BCF2TypedValue> toEncode) throws IOException {
        testBCF2BasicTypesWithEncodeMe(toEncode,
                new EncodeMe() {
                    @Override
                    public void encode(final BCF2Encoder encoder, final BCF2TypedValue tv) throws IOException {
                        encoder.encodeTyped(tv.value, tv.type);
                    }
                });
    }

    @Test(dataProvider = "BCF2EncodingTestProviderBasicTypes")
    public void testBCF2BasicTypesWithObjectNoType(final List<BCF2TypedValue> toEncode) throws IOException {
        testBCF2BasicTypesWithEncodeMe(toEncode,
                new EncodeMe() {
                    @Override
                    public void encode(final BCF2Encoder encoder, final BCF2TypedValue tv) throws IOException {
                        encoder.encode(tv.value);
                    }
                });
    }

    public void testBCF2BasicTypesWithEncodeMe(final List<BCF2TypedValue> toEncode, final EncodeMe func) throws IOException {
        for ( final BCF2TypedValue tv : toEncode ) {
            BCF2Encoder encoder = new BCF2Encoder();
            func.encode(encoder, tv);

            BCF2Decoder decoder = new BCF2Decoder(encoder.getRecordBytes());
            final Object decoded = decoder.decodeTypedValue();

            Assert.assertNotNull(decoded);
            Assert.assertFalse(decoded instanceof List);
            myAssertEquals(tv, decoded);
        }
    }

    @Test(dataProvider = "BCF2EncodingTestProviderBasicTypes")
    public void testBCF2EncodingVectors(final List<BCF2TypedValue> toEncode) throws IOException {
        for ( final BCF2TypedValue tv : toEncode ) {
            for ( final int length : Arrays.asList(2, 5, 10, 15, 20, 25) ) {
                BCF2Encoder encoder = new BCF2Encoder();
                List<Object> expected = Collections.nCopies(length, tv.value);
                encoder.encodeTyped(expected, tv.type);

                BCF2Decoder decoder = new BCF2Decoder(encoder.getRecordBytes());
                final Object decoded = decoder.decodeTypedValue();

                Assert.assertTrue(decoded instanceof List);
                final List<Object> decodedList = (List<Object>)decoded;
                Assert.assertEquals(decodedList.size(), expected.size());
                for ( Object decodedValue : decodedList )
                    myAssertEquals(tv, decodedValue);
            }
        }
    }

    @DataProvider(name = "BCF2EncodingTestProviderSingletons")
    public Object[][] BCF2EncodingTestProviderSingletons() {
        List<Object[]> tests = new ArrayList<Object[]>();
        for ( BCF2TypedValue tv : primitives )
            tests.add(new Object[]{Arrays.asList(tv)});
        return tests.toArray(new Object[][]{});
    }

    @Test(dataProvider = "BCF2EncodingTestProviderSingletons")
    public void testBCF2EncodingSingletons(final List<BCF2TypedValue> toEncode) throws IOException {
        final byte[] record = encodeRecord(toEncode);
        decodeRecord(toEncode, record);
    }

    // -----------------------------------------------------------------
    //
    // Test encoding of vectors
    //
    // -----------------------------------------------------------------

    @DataProvider(name = "BCF2EncodingTestProviderSequences")
    public Object[][] BCF2EncodingTestProviderSequences() {
        List<Object[]> tests = new ArrayList<Object[]>();
        for ( BCF2TypedValue tv1 : forCombinations )
            for ( BCF2TypedValue tv2 : forCombinations )
                for ( BCF2TypedValue tv3 : forCombinations )
                    tests.add(new Object[]{Arrays.asList(tv1, tv2, tv3)});
        return tests.toArray(new Object[][]{});
    }

    @Test(dataProvider = "BCF2EncodingTestProviderBasicTypes")
    public void testBCF2EncodingVectorsWithMissing(final List<BCF2TypedValue> toEncode) throws IOException {
        for ( final BCF2TypedValue tv : toEncode ) {
            if ( tv.type != BCF2Type.CHAR ) {
                for ( final int length : Arrays.asList(2, 5, 10, 15, 20, 25) ) {
                    final byte td = BCF2Utils.encodeTypeDescriptor(1, tv.type);

                    final BCF2Encoder encoder = new BCF2Encoder();
                    for ( int i = 0; i < length; i++ ) {
                        encoder.encodeRawValue(i % 2 == 0 ? null : tv.value, tv.type);
                    }

                    final BCF2Decoder decoder = new BCF2Decoder(encoder.getRecordBytes());

                    for ( int i = 0; i < length; i++ ) {
                        final Object decoded = decoder.decodeTypedValue(td);
                        myAssertEquals(i % 2 == 0 ? new BCF2TypedValue(null, tv.type) : tv, decoded);
                    }
                }
            }
        }
    }

    @Test(dataProvider = "BCF2EncodingTestProviderSequences", dependsOnMethods = "testBCF2EncodingSingletons")
    public void testBCF2EncodingTestProviderSequences(final List<BCF2TypedValue> toEncode) throws IOException {
        final byte[] record = encodeRecord(toEncode);
        decodeRecord(toEncode, record);
    }

    // -----------------------------------------------------------------
    //
    // Test strings and lists of strings
    //
    // -----------------------------------------------------------------

    @DataProvider(name = "ListOfStrings")
    public Object[][] listOfStringsProvider() {
        List<Object[]> tests = new ArrayList<Object[]>();
        tests.add(new Object[]{Arrays.asList("s1", "s2"), ",s1,s2"});
        tests.add(new Object[]{Arrays.asList("s1", "s2", "s3"), ",s1,s2,s3"});
        tests.add(new Object[]{Arrays.asList("s1", "s2", "s3", "s4"), ",s1,s2,s3,s4"});
        return tests.toArray(new Object[][]{});
    }

    @Test(dataProvider = "ListOfStrings")
    public void testEncodingListOfString(List<String> strings, String expected) throws IOException {
        final String collapsed = BCF2Utils.collapseStringList(strings);
        Assert.assertEquals(collapsed, expected);
        Assert.assertEquals(BCF2Utils.explodeStringList(collapsed), strings);
    }

    // -----------------------------------------------------------------
    //
    // Tests to determine the best type of arrays of integers
    //
    // -----------------------------------------------------------------

    @DataProvider(name = "BestIntTypeTests")
    public Object[][] BestIntTypeTests() {
        List<Object[]> tests = new ArrayList<Object[]>();
        tests.add(new Object[]{Arrays.asList(1), BCF2Type.INT8});
        tests.add(new Object[]{Arrays.asList(1, 10), BCF2Type.INT8});
        tests.add(new Object[]{Arrays.asList(1, 10, 100), BCF2Type.INT8});
        tests.add(new Object[]{Arrays.asList(1, -1), BCF2Type.INT8});
        tests.add(new Object[]{Arrays.asList(1, 1000), BCF2Type.INT16});
        tests.add(new Object[]{Arrays.asList(1, 1000, 10), BCF2Type.INT16});
        tests.add(new Object[]{Arrays.asList(1, 1000, 100), BCF2Type.INT16});
        tests.add(new Object[]{Arrays.asList(1000), BCF2Type.INT16});
        tests.add(new Object[]{Arrays.asList(100000), BCF2Type.INT32});
        tests.add(new Object[]{Arrays.asList(100000, 10), BCF2Type.INT32});
        tests.add(new Object[]{Arrays.asList(100000, 100), BCF2Type.INT32});
        tests.add(new Object[]{Arrays.asList(100000, 1, -10), BCF2Type.INT32});
        tests.add(new Object[]{Arrays.asList(-100000, 1, -10), BCF2Type.INT32});
        return tests.toArray(new Object[][]{});
    }

    @Test(dataProvider = "BestIntTypeTests")
    public void determineBestEncoding(final List<Integer> ints, final BCF2Type expectedType) throws IOException {
        BCF2Encoder encoder = new BCF2Encoder();
        Assert.assertEquals(BCF2Utils.determineIntegerType(ints), expectedType);
        Assert.assertEquals(BCF2Utils.determineIntegerType(toPrimitive(ints.toArray(new Integer[0]))), expectedType);
    }

    private static int[] toPrimitive ( final Integer[] array ) {
        if ( array == null ) {
            return null;
        }
        else if ( array.length == 0 ) {
            return new int[0];
        }

        final int[] result = new int[array.length];
        for (int i = 0; i < array.length; i++) {
            result[i] = array[i].intValue();
        }
        return result;
    }

    // -----------------------------------------------------------------
    //
    // Tests managing and skipping multiple blocks
    //
    // -----------------------------------------------------------------

    @Test(dataProvider = "BCF2EncodingTestProviderSequences", dependsOnMethods = "testBCF2EncodingTestProviderSequences")
    public void testReadAndSkipWithMultipleBlocks(final List<BCF2TypedValue> block) throws IOException {
        testReadAndSkipWithMultipleBlocks(block, forCombinations);
        testReadAndSkipWithMultipleBlocks(forCombinations, block);
    }

    public void testReadAndSkipWithMultipleBlocks(final List<BCF2TypedValue> block1, final List<BCF2TypedValue> block2) throws IOException {
        final byte[] record1 = encodeRecord(block1);
        final byte[] record2 = encodeRecord(block2);

        // each record is individually good
        decodeRecord(block1, record1);
        decodeRecord(block2, record2);

        BCF2Decoder decoder = new BCF2Decoder();

        // test setting
        decoder.setRecordBytes(record1);
        decodeRecord(block1, decoder);
        decoder.setRecordBytes(record2);
        decodeRecord(block2, decoder);

        // test combining the streams
        final byte[] combined = combineRecords(record1, record2);
        final List<BCF2TypedValue> combinedObjects = new ArrayList<BCF2TypedValue>(block1);
        combinedObjects.addAll(block2);

        // the combined bytes is the same as the combined objects
        InputStream stream = new ByteArrayInputStream(combined);
        decoder.readNextBlock(record1.length, stream);
        decodeRecord(block1, decoder);
        decoder.readNextBlock(record2.length, stream);
        decodeRecord(block2, decoder);

        // skipping the first block allows us to read the second block directly
        stream = new ByteArrayInputStream(combined);
        decoder.skipNextBlock(record1.length, stream);
        decoder.readNextBlock(record2.length, stream);
        decodeRecord(block2, decoder);
    }

    // -----------------------------------------------------------------
    //
    // Test encoding / decoding arrays of ints
    //
    // This checks that we can encode and decode correctly with the
    // low-level decodeIntArray function arrays of values.  This
    // has to be pretty comprehensive as decodeIntArray is a highly optimized
    // piece of code with lots of edge cases.  The values we are encoding
    // don't really matter -- just that the values come back as expected.
    //
    // -----------------------------------------------------------------

    @DataProvider(name = "IntArrays")
    public Object[][] makeIntArrays() {
        List<Object[]> tests = new ArrayList<Object[]>();

        for ( int nValues : Arrays.asList(0, 1, 2, 5, 10, 100) ) {
            for ( int nPad : Arrays.asList(0, 1, 2, 5, 10, 100) ) {
                int nElements = nValues + nPad;

                List<Integer> values = new ArrayList<Integer>(nElements);

                // add nValues from 0 to nValues - 1
                for ( int i = 0; i < nValues; i++ )
                    values.add(i);

                // add nPad nulls
                for ( int i = 0; i < nPad; i++ )
                    values.add(null);

                tests.add(new Object[]{values});
            }
        }

        return tests.toArray(new Object[][]{});
    }

    @Test(dataProvider = "IntArrays")
    public void testIntArrays(final List<Integer> ints) throws IOException {
        final BCF2Encoder encoder = new BCF2Encoder();
        encoder.encodeTyped(ints, BCF2Type.INT16);

        final BCF2Decoder decoder = new BCF2Decoder(encoder.getRecordBytes());

        final byte typeDescriptor = decoder.readTypeDescriptor();

        // read the int[] with the low-level version
        final int size = decoder.decodeNumberOfElements(typeDescriptor);
        final int[] decoded = decoder.decodeIntArray(typeDescriptor, size);

        if ( isMissing(ints) ) {
            // we expect that the result is null in this case
            Assert.assertNull(decoded, "Encoded all missing values -- expected null");
        } else {
            // we expect at least some values to come back
            Assert.assertTrue(decoded.length > 0, "Must have at least 1 element for non-null encoded data");

            // check corresponding values
            for ( int i = 0; i < ints.size(); i++ ) {
                final Integer expected = ints.get(i);

                if ( expected == null ) {
                    Assert.assertTrue(decoded.length <= i, "we expect decoded to be truncated for missing values");
                } else {
                    Assert.assertTrue(decoded.length > i, "we expected at least " + i + " values in decoded array");
                    Assert.assertEquals(decoded[i], (int)expected);
                }
            }
        }
    }

    // -----------------------------------------------------------------
    //
    // Helper routines
    //
    // -----------------------------------------------------------------

    private final byte[] combineRecords(final byte[] record1, final byte[] record2) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(record1);
        baos.write(record2);
        return baos.toByteArray();
    }

    private final byte[] encodeRecord(final List<BCF2TypedValue> toEncode) throws IOException {
        BCF2Encoder encoder = new BCF2Encoder();

        for ( final BCF2TypedValue tv : toEncode ) {
            if ( tv.isMissing() )
                encoder.encodeTypedMissing(tv.type);
            else {
                final BCF2Type encodedType = encoder.encode(tv.value);
                if ( tv.type != null ) // only if we have an expectation
                    Assert.assertEquals(encodedType, tv.type);
            }
        }

        // check output
        final byte[] record = encoder.getRecordBytes();
        Assert.assertNotNull(record);
        Assert.assertTrue(record.length > 0);
        return record;
    }

    private final void decodeRecord(final List<BCF2TypedValue> toEncode, final byte[] record) throws IOException {
        decodeRecord(toEncode, new BCF2Decoder(record));
    }

    private final void decodeRecord(final List<BCF2TypedValue> toEncode, final BCF2Decoder decoder) throws IOException {
        for ( final BCF2TypedValue tv : toEncode ) {
            Assert.assertFalse(decoder.blockIsFullyDecoded());
            final Object decoded = decoder.decodeTypedValue();

            myAssertEquals(tv, decoded);
        }

        Assert.assertTrue(decoder.blockIsFullyDecoded());
    }

    private final void myAssertEquals(final BCF2TypedValue tv, final Object decoded) {
        if ( tv.value == null ) { // special needs for instanceof double
            Assert.assertEquals(decoded, tv.value);
        } else if ( tv.type == BCF2Type.FLOAT ) { // need tolerance for floats, and they aren't null
            Assert.assertTrue(decoded instanceof Double);

            final double valueFloat = (Double)tv.value;
            final double decodedFloat = (Double)decoded;

            VariantBaseTest.assertEqualsDoubleSmart(decodedFloat, valueFloat, FLOAT_TOLERANCE);
        } else
            Assert.assertEquals(decoded, tv.value);
    }

    private final boolean isMissing(final List<Integer> values) {
        if ( values != null )
            for ( Integer value : values )
                if ( value != null )
                    return false;
        return true;
    }
}