// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.searchlib.expression;

import com.yahoo.io.GrowableByteBuffer;
import com.yahoo.text.Utf8;
import com.yahoo.vespa.objects.BufferSerializer;
import com.yahoo.vespa.objects.Identifiable;
import org.junit.Test;

import java.nio.ByteBuffer;
import java.util.Arrays;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * @author baldersheim
 */
public class ExpressionTestCase {

    private static final double delta = 0.00000001;

    @Test
    public void testRangeBucketPreDefFunctionNode() {
        assertMultiArgFunctionNode(new RangeBucketPreDefFunctionNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")), new AttributeNode("foo")));
        assertEquals(new RangeBucketPreDefFunctionNode(), new RangeBucketPreDefFunctionNode());
        assertEquals(new RangeBucketPreDefFunctionNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")), new AttributeNode("foo")),
                     new RangeBucketPreDefFunctionNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")), new AttributeNode("foo")));
        assertNotEquals(new RangeBucketPreDefFunctionNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")), new AttributeNode("foo")),
                        new RangeBucketPreDefFunctionNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "21")), new AttributeNode("foo")));
        assertNotEquals(new RangeBucketPreDefFunctionNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")), new AttributeNode("foo")),
                        new RangeBucketPreDefFunctionNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")), new AttributeNode("bar")));
    }

    @Test
    public void testFixedWidthBucketFunctionNode() {
        assertMultiArgFunctionNode(new FixedWidthBucketFunctionNode());
        assertEquals(new FixedWidthBucketFunctionNode(), new FixedWidthBucketFunctionNode());
        assertEquals(new FixedWidthBucketFunctionNode(new IntegerResultNode(5), new AttributeNode("foo")),
                     new FixedWidthBucketFunctionNode(new IntegerResultNode(5), new AttributeNode("foo")));
        assertNotEquals(new FixedWidthBucketFunctionNode(new IntegerResultNode(5), new AttributeNode("foo")),
                        new FixedWidthBucketFunctionNode(new IntegerResultNode(6), new AttributeNode("foo")));
        assertNotEquals(new FixedWidthBucketFunctionNode(new IntegerResultNode(5), new AttributeNode("foo")),
                        new FixedWidthBucketFunctionNode(new IntegerResultNode(5), new AttributeNode("bar")));
    }

    @Test
    public void testIntegerBucketResultNodeVector() {
        assertResultNode(new IntegerBucketResultNodeVector().add(new IntegerBucketResultNode(10, 20)));
        assertEquals(new IntegerBucketResultNodeVector().add(new IntegerBucketResultNode(10, 20)),
                     new IntegerBucketResultNodeVector().add(new IntegerBucketResultNode(10, 20)));
        assertNotEquals(new IntegerBucketResultNodeVector().add(new IntegerBucketResultNode(10, 20)),
                        new IntegerBucketResultNodeVector());
        assertNotEquals(new IntegerBucketResultNodeVector().add(new IntegerBucketResultNode(10, 20)),
                        new IntegerBucketResultNodeVector().add(new IntegerBucketResultNode(11, 20)));
    }

    @Test
    public void testFloatBucketResultNodeVector() {
        assertResultNode(new FloatBucketResultNodeVector().add(new FloatBucketResultNode(10, 20)));
        assertEquals(new FloatBucketResultNodeVector().add(new FloatBucketResultNode(10, 20)),
                     new FloatBucketResultNodeVector().add(new FloatBucketResultNode(10, 20)));
        assertNotEquals(new FloatBucketResultNodeVector().add(new FloatBucketResultNode(10, 20)),
                        new FloatBucketResultNodeVector());
        assertNotEquals(new FloatBucketResultNodeVector().add(new FloatBucketResultNode(10, 20)),
                        new FloatBucketResultNodeVector().add(new FloatBucketResultNode(11, 20)));
    }

    @Test
    public void testStringBucketResultNodeVector() {
        assertResultNode(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")));
        assertEquals(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")),
                     new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")));
        assertNotEquals(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")),
                        new StringBucketResultNodeVector());
        assertNotEquals(new StringBucketResultNodeVector().add(new StringBucketResultNode("10", "20")),
                        new StringBucketResultNodeVector().add(new StringBucketResultNode("11", "20")));
    }

    @Test
    public void testIntegerBucketResultNode() {
        assertResultNode(new IntegerBucketResultNode(10, 20));
        assertEquals(new IntegerBucketResultNode(10, 20), new IntegerBucketResultNode(10, 20));
        assertNotEquals(new IntegerBucketResultNode(10, 20), new IntegerBucketResultNode(11, 20));
        assertNotEquals(new IntegerBucketResultNode(10, 20), new IntegerBucketResultNode(10, 21));
    }

    @Test
    public void testFloatBucketResultNode() {
        assertResultNode(new FloatBucketResultNode(10.0, 20.0));
        assertEquals(new FloatBucketResultNode(10.0, 20.0), new FloatBucketResultNode(10.0, 20.0));
        assertNotEquals(new FloatBucketResultNode(10.0, 20.0), new FloatBucketResultNode(11.0, 20.0));
        assertNotEquals(new FloatBucketResultNode(10.0, 20.0), new FloatBucketResultNode(10.0, 21.0));
    }

    @Test
    public void testStringBucketResultNode() {
        assertResultNode(new StringBucketResultNode("10.0", "20.0"));
        assertEquals(new StringBucketResultNode("10.0", "20.0"), new StringBucketResultNode("10.0", "20.0"));
        assertNotEquals(new StringBucketResultNode("10.0", "20.0"), new StringBucketResultNode("11.0", "20.0"));
        assertNotEquals(new StringBucketResultNode("10.0", "20.0"), new StringBucketResultNode("10.0", "21.0"));
        compare(new StringBucketResultNode("10.0", "20.0"), new StringBucketResultNode("10.0", "21.0"), new StringBucketResultNode("10.0", "22.0"));
        compare(new StringBucketResultNode("10.0", "20.0"), new StringBucketResultNode("11.0", "19.0"), new StringBucketResultNode("11.0", "20.0"));
        compare(new StringBucketResultNode(StringResultNode.getNegativeInfinity(), new StringResultNode("20.0")),
                new StringBucketResultNode("11.0", "19.0"), new StringBucketResultNode("11.0", "20.0"));
        compare(new StringBucketResultNode(StringResultNode.getNegativeInfinity(), new StringResultNode("20.0")),
                new StringBucketResultNode(StringResultNode.getNegativeInfinity(), new StringResultNode("21.0")),
                new StringBucketResultNode("11.0", "20.0"));
        compare(new StringBucketResultNode("10.0", "20.0"), new StringBucketResultNode("10.0", "21.0"),
                new StringBucketResultNode(new StringResultNode("10.0"), StringResultNode.getPositiveInfinity()));
        compare(new StringBucketResultNode(new StringResultNode("10.0"), StringResultNode.getPositiveInfinity()),
                new StringBucketResultNode("11.0", "19.0"), new StringBucketResultNode("11.0", "20.0"));
    }

    @Test
    public void testPositiveInfinity() {
        PositiveInfinityResultNode inf = new PositiveInfinityResultNode();
        PositiveInfinityResultNode inf2 = new PositiveInfinityResultNode();
        assertResultNode(inf);
        assertEquals(inf, inf2);
    }

    @Test
    public void testAddFunctionNode() {
        assertMultiArgFunctionNode(new AddFunctionNode());
        assertFunctionNode(new AddFunctionNode().addArg(new ConstantNode(new IntegerResultNode(2)))
                                                .addArg(new ConstantNode(new IntegerResultNode(3))),
                           5, 5.0, "5", longAsRaw(5));
        assertFunctionNode(new AddFunctionNode().addArg(new ConstantNode(new FloatResultNode(3.0)))
                                                .addArg(new ConstantNode(new IntegerResultNode(2))),
                           5, 5.0, "5.0", doubleAsRaw(5.0));
        assertFunctionNode(new AddFunctionNode().addArg(new ConstantNode(new IntegerResultNode(3)))
                                                .addArg(new ConstantNode(new FloatResultNode(2.0))),
                           5, 5.0, "5.0", doubleAsRaw(5.0));
    }

    @Test
    public void testAndFunctionNode() {
        assertMultiArgFunctionNode(new AndFunctionNode());
        assertFunctionNode(new AndFunctionNode().addArg(new ConstantNode(new IntegerResultNode(3)))
                                                .addArg(new ConstantNode(new IntegerResultNode(7))),
                           3, 3.0, "3", longAsRaw(3));
    }

    @Test
    public void testZCurveFunctionNode() {
        assertMultiArgFunctionNode(
                new ZCurveFunctionNode(new ConstantNode(new IntegerResultNode(7)), ZCurveFunctionNode.Dimension.Y));
    }

    @Test
    public void testTimeStampFunctionNode() {
        assertMultiArgFunctionNode(new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Hour, true));
        assertEquals(new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Hour, true),
                     new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Hour, true));
        assertNotEquals(
                new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Hour,
                                          true),
                new TimeStampFunctionNode(new AttributeNode("testattributt"), TimeStampFunctionNode.TimePart.Hour,
                                          true));
        assertNotEquals(
                new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Hour,
                                          true),
                new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Year,
                                          true));
        assertNotEquals(
                new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Hour,
                                          true),
                new TimeStampFunctionNode(new AttributeNode("testattribute"), TimeStampFunctionNode.TimePart.Hour,
                                          false));
    }

    @Test
    public void testExpressionRefNode() {
        AggregationRefNode ref = new AggregationRefNode(3);
        assertEquals(3, ref.getIndex());
    }

    @Test
    public void testAttributeNode() {
        try {
            new AttributeNode(null);
            fail("Should not be able to set null attribute name.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new AttributeNode().setAttributeName(null);
            fail("Should not be able to set null attribute name.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new AttributeNode().prepare();
            fail("Should not be possible to prepare or execute attribute node");
        } catch (RuntimeException e) {
            // expected
        }
        try {
            new AttributeNode().execute();
            fail("Should not be possible to prepare or execute attribute node");
        } catch (RuntimeException e) {
            // expected
        }
        AttributeNode a = new AttributeNode("testattribute");
        assertEquals("testattribute", a.getAttributeName());
        AttributeNode b = (AttributeNode)assertSerialize(a);
        assertEquals("testattribute", b.getAttributeName());
        AttributeNode c = new AttributeNode("testattribute");
        assertEquals(b, c);
        c.setAttributeName("fail");
        assertFalse(b.equals(c));
    }

    @Test
    public void testAttributeMapLookupNode() {
        assertEquals(AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "my_key"),
                     AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "my_key"));
        assertNotEquals(AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "my_key"),
                        AttributeMapLookupNode.fromKey("null", "map.key", "map.value", "my_key"));
        assertNotEquals(AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "my_key"),
                        AttributeMapLookupNode.fromKey("map{\"my_key\"}", "null", "map.value", "my_key"));
        assertNotEquals(AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "my_key"),
                        AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "null", "my_key"));
        assertNotEquals(AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "my_key"),
                        AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "null"));

        assertEquals(AttributeMapLookupNode.fromKeySourceAttribute("map{attribute(key_source)}", "map.key", "map.value", "key_source"),
                     AttributeMapLookupNode.fromKeySourceAttribute("map{attribute(key_source)}", "map.key", "map.value", "key_source"));
        assertNotEquals(AttributeMapLookupNode.fromKeySourceAttribute("map{attribute(key_source)}", "map.key", "map.value", "key_source"),
                        AttributeMapLookupNode.fromKeySourceAttribute("map{attribute(key_source)}", "map.key", "map.value", "null"));

        assertAttributeMapLookupNodeSerialize(
                AttributeMapLookupNode.fromKey("map{\"my_key\"}", "map.key", "map.value", "my_key"));
        assertAttributeMapLookupNodeSerialize(
                AttributeMapLookupNode.fromKeySourceAttribute("map{attribute(key_source)}", "map.key", "map.value", "key_source"));
    }

    private static void assertAttributeMapLookupNodeSerialize(AttributeMapLookupNode a) {
        AttributeMapLookupNode b = (AttributeMapLookupNode)assertSerialize(a);
        assertEquals(a, b);
        AttributeMapLookupNode c = (AttributeMapLookupNode)assertSerialize(b);
        assertEquals(a, c);
    }

    @Test
    public void testInterpolatedLookupNode() {
        ExpressionNode argA = new ConstantNode(new FloatResultNode(2.71828182846));
        ExpressionNode argB = new ConstantNode(new FloatResultNode(3.14159265359));
        try {
            new InterpolatedLookupNode(null, argA);
            fail("Should not be able to set null attribute name.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new InterpolatedLookupNode().setAttributeName(null);
            fail("Should not be able to set null attribute name.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new InterpolatedLookupNode().prepare();
            fail("Should not be possible to prepare or execute interpolatedlookup node");
        } catch (RuntimeException e) {
            // expected
        }
        try {
            new InterpolatedLookupNode().execute();
            fail("Should not be possible to prepare or execute interpolatedlookup node");
        } catch (RuntimeException e) {
            // expected
        }
        ExpressionNode a1 = new InterpolatedLookupNode().setAttributeName("foo").addArg(argA);
        InterpolatedLookupNode a2 = new InterpolatedLookupNode("foo", argA);
        assertEquals("foo", ((InterpolatedLookupNode)a1).getAttributeName());
        assertEquals("foo", a2.getAttributeName());
        assertEquals(argA, ((InterpolatedLookupNode)a1).getArg());
        assertEquals(argA, a2.getArg());
        assertEquals(a1, a2);
        InterpolatedLookupNode b1 = new InterpolatedLookupNode("foo", argB);
        InterpolatedLookupNode b2 = new InterpolatedLookupNode("bar", argA);
        assertFalse(a1.equals(b1));
        assertFalse(a1.equals(b2));
        assertFalse(a2.equals(b1));
        assertFalse(a2.equals(b2));
        a2.setAttributeName("fail");
        assertFalse(a1.equals(a2));
    }

    @Test
    public void testCatFunctionNode() {
        assertMultiArgFunctionNode(new CatFunctionNode());
        assertFunctionNode(new CatFunctionNode().addArg(new ConstantNode(new RawResultNode(asRaw('1', '2'))))
                                                .addArg(new ConstantNode(new RawResultNode(asRaw('3', '4')))),
                           0, 0.0, "1234", asRaw('1', '2', '3', '4'));
    }

    @Test
    public void testStrCatFunctionNode() {
        assertMultiArgFunctionNode(new StrCatFunctionNode());
        assertFunctionNode(new StrCatFunctionNode().addArg(new ConstantNode(new StringResultNode("foo")))
                                                   .addArg(new ConstantNode(new StringResultNode("bar"))),
                           0, 0.0, "foobar", stringAsRaw("foobar"));
    }

    @Test
    public void testDivideFunctionNode() {
        assertMultiArgFunctionNode(new DivideFunctionNode());
        assertFunctionNode(new DivideFunctionNode().addArg(new ConstantNode(new IntegerResultNode(10)))
                                                   .addArg(new ConstantNode(new IntegerResultNode(2))),
                           5, 5.0, "5", longAsRaw(5));
        assertFunctionNode(new DivideFunctionNode().addArg(new ConstantNode(new IntegerResultNode(6)))
                                                   .addArg(new ConstantNode(new FloatResultNode(2.0))),
                           3, 3.0, "3.0", doubleAsRaw(3.0));
        assertFunctionNode(new DivideFunctionNode().addArg(new ConstantNode(new IntegerResultNode(6)))
                                                   .addArg(new ConstantNode(new FloatResultNode(12.0))),
                           1, 0.5, "0.5", doubleAsRaw(0.5));
    }

    @Test
    public void testDocumentFieldNode() {
        try {
            new DocumentFieldNode(null);
            fail("Should not be able to set null field name.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new DocumentFieldNode().setDocumentFieldName(null);
            fail("Should not be able to set null field name.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new DocumentFieldNode("foo").prepare();
            fail("Should not be able to prepare documentfieldnode");
        } catch (RuntimeException e) {
            // expected
        }
        try {
            new DocumentFieldNode("foo").execute();
            fail("Should not be able to execute documentfieldnode");
        } catch (RuntimeException e) {
            // expected
        }
        DocumentFieldNode a = new DocumentFieldNode("testdocumentfield");
        assertEquals("testdocumentfield", a.getDocumentFieldName());
        DocumentFieldNode b = (DocumentFieldNode)assertSerialize(a);
        assertEquals("testdocumentfield", b.getDocumentFieldName());
        DocumentFieldNode c = new DocumentFieldNode("testdocumentfield");
        assertEquals(b, c);
        c.setDocumentFieldName("fail");
        assertFalse(b.equals(c));
    }

    @Test
    public void testFloatResultNode() {
        FloatResultNode a = new FloatResultNode(7.3);
        assertEquals(a.getInteger(), 7);
        assertEquals(a.getFloat(), 7.3, delta);
        assertEquals(a.getString(), "7.3");
        assertEquals(a.getNumber(), Double.valueOf(7.3));
        byte[] raw = a.getRaw();
        assertEquals(raw.length, 8);
        assertResultNode(a);
        compare(new FloatResultNode(-1), new FloatResultNode(0), new FloatResultNode(1));
        a.set(new FloatResultNode(4));
        assertResultNode(a);

        FloatResultNode b = new FloatResultNode(7.5);
        assertEquals(b.getInteger(), 8);
        assertEquals(b.getFloat(), 7.5, delta);
        assertEquals(b.getString(), "7.5");
        assertEquals(b.getNumber(), Double.valueOf(7.5));
    }

    @Test
    public void testGetDocIdNamespaceSpecificFunctionNode() {
        GetDocIdNamespaceSpecificFunctionNode a = new GetDocIdNamespaceSpecificFunctionNode(new IntegerResultNode(7));
        assertTrue(a.getResult() instanceof IntegerResultNode);
        GetDocIdNamespaceSpecificFunctionNode b = (GetDocIdNamespaceSpecificFunctionNode)assertSerialize(a);
        assertTrue(b.getResult() instanceof IntegerResultNode);
        assertEquals(7, b.getResult().getInteger());
        GetDocIdNamespaceSpecificFunctionNode c = new GetDocIdNamespaceSpecificFunctionNode(new IntegerResultNode(7));
        assertEquals(b, c);
        try {
            new GetDocIdNamespaceSpecificFunctionNode(new IntegerResultNode(7)).prepare();
            fail("Should not be able to prepare documentfieldnode");
        } catch (RuntimeException e) {
            // expected
        }
        try {
            new GetDocIdNamespaceSpecificFunctionNode(new IntegerResultNode(7)).execute();
            fail("Should not be able to execute documentfieldnode");
        } catch (RuntimeException e) {
            // expected
        }
    }

    @Test
    public void testIntegerResultNode() {
        IntegerResultNode a = new IntegerResultNode(7);
        assertEquals(a.getInteger(), 7);
        assertEquals(a.getFloat(), 7.0, delta);
        assertEquals(a.getString(), "7");
        assertEquals(a.getNumber(), Long.valueOf(7));
        byte[] raw = a.getRaw();
        assertEquals(raw.length, 8);
        assertResultNode(a);
        compare(new IntegerResultNode(-1), new IntegerResultNode(0), new IntegerResultNode(1));
        compare(new IntegerResultNode(-1), new IntegerResultNode(0), new IntegerResultNode(0x80000000L));
    }

    @Test
    public void testMaxFunctionNode() {
        assertMultiArgFunctionNode(new MaxFunctionNode());
        assertFunctionNode(new MaxFunctionNode().addArg(new ConstantNode(new IntegerResultNode(3)))
                                                .addArg(new ConstantNode(new IntegerResultNode(5))),
                           5, 5.0, "5", longAsRaw(5));
        assertFunctionNode(new MaxFunctionNode().addArg(new ConstantNode(new FloatResultNode(4.9999999)))
                                                .addArg(new ConstantNode(new IntegerResultNode(5))),
                           5, 5.0, "5.0", doubleAsRaw(5.0));
    }

    @Test
    public void testMD5BitFunctionNode() {
        try {
            new MD5BitFunctionNode(null, 64);
            fail("Should not be able to set null argument.");
        } catch (NullPointerException e) {
            // expected
        }
        try {
            new MD5BitFunctionNode().prepare();
            fail("Should not be able to run prepare.");
        } catch (RuntimeException e) {
            // expected
        }
        try {
            new MD5BitFunctionNode().execute();
            fail("Should not be able to run execute.");
        } catch (RuntimeException e) {
            // expected
        }
        assertUnaryBitFunctionNode(new MD5BitFunctionNode());
    }

    @Test
    public void testMinFunctionNode() {
        assertMultiArgFunctionNode(new MinFunctionNode());
        assertFunctionNode(new MinFunctionNode().addArg(new ConstantNode(new IntegerResultNode(3)))
                                                .addArg(new ConstantNode(new IntegerResultNode(5))),
                           3, 3.0, "3", longAsRaw(3));
        assertFunctionNode(new MinFunctionNode().addArg(new ConstantNode(new FloatResultNode(4.9999999)))
                                                .addArg(new ConstantNode(new IntegerResultNode(5))),
                           5, 4.9999999, "4.9999999", doubleAsRaw(4.9999999));
    }

    @Test
    public void testModuloFunctionNode() {
        assertMultiArgFunctionNode(new ModuloFunctionNode());
        assertFunctionNode(new ModuloFunctionNode().addArg(new ConstantNode(new IntegerResultNode(13)))
                                                   .addArg(new ConstantNode(new IntegerResultNode(5))),
                           3, 3.0, "3", longAsRaw(3));
        assertFunctionNode(new ModuloFunctionNode().addArg(new ConstantNode(new FloatResultNode(4.9999999)))
                                                   .addArg(new ConstantNode(new IntegerResultNode(5))),
                           5, 4.9999999, "4.9999999", doubleAsRaw(4.9999999));
    }

    @Test
    public void testMultiplyFunctionNode() {
        assertMultiArgFunctionNode(new MultiplyFunctionNode());
        assertFunctionNode(new MultiplyFunctionNode().addArg(new ConstantNode(new IntegerResultNode(3)))
                                                     .addArg(new ConstantNode(new IntegerResultNode(5))),
                           15, 15.0, "15", longAsRaw(15));
        assertFunctionNode(new MultiplyFunctionNode().addArg(new ConstantNode(new FloatResultNode(4.5)))
                                                     .addArg(new ConstantNode(new IntegerResultNode(5))),
                           23, 22.5, "22.5", doubleAsRaw(22.5));
    }

    @Test
    public void testNegateFunctionNode() {
        assertMultiArgFunctionNode(new NegateFunctionNode());
        assertFunctionNode(new NegateFunctionNode().addArg(new ConstantNode(new IntegerResultNode(3))),
                           -3, -3.0, "-3", longAsRaw(-3));
        assertFunctionNode(new NegateFunctionNode().addArg(new ConstantNode(new FloatResultNode(3.0))),
                           -3, -3.0, "-3.0", doubleAsRaw(-3.0));
    }

    @Test
    public void testSortFunctionNode() {
        assertMultiArgFunctionNode(new SortFunctionNode());
    }

    @Test
    public void testReverseFunctionNode() {
        assertMultiArgFunctionNode(new ReverseFunctionNode());
    }

    @Test
    public void testToIntFunctionNode() {
        assertMultiArgFunctionNode(new ToIntFunctionNode());
        assertFunctionNode(new ToIntFunctionNode().addArg(new ConstantNode(new StringResultNode("1337"))),
                           1337, 1337.0, "1337", longAsRaw(1337));
    }

    @Test
    public void testToFloatFunctionNode() {
        assertMultiArgFunctionNode(new ToFloatFunctionNode());
        assertFunctionNode(new ToFloatFunctionNode().addArg(new ConstantNode(new FloatResultNode(3.14))),
                           3, 3.14, "3.14", doubleAsRaw(3.14));
    }

    @Test
    public void testMathFunctionNode() {
        assertMultiArgFunctionNode(new MathFunctionNode(MathFunctionNode.Function.LOG10));
        assertFunctionNode(new MathFunctionNode(MathFunctionNode.Function.LOG10).addArg(new ConstantNode(new IntegerResultNode(100000))),
                           5, 5.0, "5.0", doubleAsRaw(5.0));
    }

    @Test
    public void testStrLenFunctionNode() {
        assertMultiArgFunctionNode(new StrLenFunctionNode());
        assertFunctionNode(new StrLenFunctionNode().addArg(new ConstantNode(new StringResultNode("foo"))),
                           3, 3.0, "3", longAsRaw(3));
    }

    @Test
    public void testNormalizeSubjectFunctionNode() {
        assertMultiArgFunctionNode(new NormalizeSubjectFunctionNode());
        assertFunctionNode(new NormalizeSubjectFunctionNode().addArg(new ConstantNode(new StringResultNode("Re: Your mail"))),
                           0, 0, "Your mail", stringAsRaw("Your mail"));
    }

    @Test
    public void testNormalizeSubjectFunctionNode2() {
        assertMultiArgFunctionNode(new NormalizeSubjectFunctionNode());
        assertFunctionNode(new NormalizeSubjectFunctionNode().addArg(new ConstantNode(new StringResultNode("Your mail"))),
                           0, 0, "Your mail", stringAsRaw("Your mail"));
    }

    @Test
    public void testNumElemFunctionNode() {
        assertMultiArgFunctionNode(new NumElemFunctionNode());
        assertFunctionNode(new NumElemFunctionNode().addArg(new ConstantNode(new IntegerResultNode(1337))),
                           1, 1.0, "1", longAsRaw(1));
    }

    @Test
    public void testToStringFunctionNode() {
        assertMultiArgFunctionNode(new ToStringFunctionNode());
        assertFunctionNode(new ToStringFunctionNode().addArg(new ConstantNode(new IntegerResultNode(1337))),
                           1337, 1337.0, "1337", stringAsRaw("1337"));
    }

    @Test
    public void testToRawFunctionNode() {
        assertMultiArgFunctionNode(new ToRawFunctionNode());
        assertFunctionNode(new ToRawFunctionNode().addArg(new ConstantNode(new IntegerResultNode(1337))),
                           1337, 1337.0, "1337", longAsRaw(1337));
    }

    @Test
    public void testOrFunctionNode() {
        assertMultiArgFunctionNode(new OrFunctionNode());
        assertFunctionNode(new OrFunctionNode().addArg(new ConstantNode(new IntegerResultNode(2)))
                                               .addArg(new ConstantNode(new IntegerResultNode(4))),
                           6, 6.0, "6", longAsRaw(6));
    }

    @Test
    public void testDebugWaitFunctionNode() {
    	assertFunctionNode(
                new DebugWaitFunctionNode(new OrFunctionNode().addArg(new ConstantNode(new IntegerResultNode(2)))
                                                              .addArg(new ConstantNode(new IntegerResultNode(4))),
                                          0.01,
                                          true),
                6, 6.0, "6", longAsRaw(6));
    	DebugWaitFunctionNode n = new DebugWaitFunctionNode(new OrFunctionNode().addArg(new ConstantNode(new IntegerResultNode(2)))
				 																.addArg(new ConstantNode(new IntegerResultNode(4))),
				 											0.3,
				 											false);
    	n.prepare();
    	long start = System.currentTimeMillis();
    	n.execute();
    	long end = System.currentTimeMillis();
    	assertTrue(end - start > 250);
    	
    	DebugWaitFunctionNode n2 = new DebugWaitFunctionNode(new OrFunctionNode().addArg(new ConstantNode(new IntegerResultNode(2)))
																  				 .addArg(new ConstantNode(new IntegerResultNode(4))),
														     0.5,
														     true);
    	n2.prepare();
    	start = System.currentTimeMillis();
    	n2.execute();
    	end = System.currentTimeMillis();
    	assertTrue(end - start > 450);
    }

    @Test
    public void testRawResultNode() {
        try {
            new RawResultNode(null);
            fail("Should not be able to set null value.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new RawResultNode().setValue(null);
            fail("Should not be able to set null value.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        byte[] b = { '7', '.', '4' };
        RawResultNode a = new RawResultNode(b);
        byte[] raw = a.getRaw();
        assertEquals(raw.length, 3);
        assertEquals(raw[0], '7');
        assertEquals(raw[1], '.');
        assertEquals(raw[2], '4');
        assertEquals(a.getInteger(), 0);
        assertEquals(a.getFloat(), 0.0, delta);
        assertEquals(a.getString(), "7.4");
        assertResultNode(a);
        compare(new RawResultNode(), new RawResultNode(new byte [] {'z'}), new RawResultNode(new byte [] {'z', 'z'}));
        compare(new RawResultNode(new byte [] {'z'}), new RawResultNode(new byte [] {'z', 'z'}), new RawResultNode(new byte [] {'z','z','z'}));
        compare(new RawResultNode(new byte [] {'z'}), new RawResultNode(new byte [] {'z','z'}), new PositiveInfinityResultNode());
        byte [] b1 = {0x00};
        byte [] b2 = {0x07};
        byte [] b3 = {0x7f};
        byte [] b4 = {(byte)0x80};
        byte [] b5 = {(byte)0xb1};
        byte [] b6 = {(byte)0xff};

        assertEquals(0x00, b1[0]);
        assertEquals(0x07, b2[0]);
        assertEquals(0x7f, b3[0]);
        assertEquals(0x80, ((int)b4[0]) & 0xff);
        assertEquals(0xb1, ((int)b5[0]) & 0xff);
        assertEquals(0xff, ((int)b6[0]) & 0xff);

        RawResultNode r1 = new RawResultNode(b1);
        RawResultNode r2 = new RawResultNode(b2);
        RawResultNode r3 = new RawResultNode(b3);
        RawResultNode r4 = new RawResultNode(b4);
        RawResultNode r5 = new RawResultNode(b5);
        RawResultNode r6 = new RawResultNode(b6);

        assertTrue(r1.compareTo(r1) == 0);
        assertTrue(r1.compareTo(r2) < 0);
        assertTrue(r1.compareTo(r3) < 0);
        assertTrue(r1.compareTo(r4) < 0);
        assertTrue(r1.compareTo(r5) < 0);
        assertTrue(r1.compareTo(r6) < 0);

        assertTrue(r2.compareTo(r1) > 0);
        assertTrue(r2.compareTo(r2) == 0);
        assertTrue(r2.compareTo(r3) < 0);
        assertTrue(r2.compareTo(r4) < 0);
        assertTrue(r2.compareTo(r5) < 0);
        assertTrue(r2.compareTo(r6) < 0);

        assertTrue(r3.compareTo(r1) > 0);
        assertTrue(r3.compareTo(r2) > 0);
        assertTrue(r3.compareTo(r3) == 0);
        assertTrue(r3.compareTo(r4) < 0);
        assertTrue(r3.compareTo(r5) < 0);
        assertTrue(r3.compareTo(r6) < 0);

        assertTrue(r4.compareTo(r1) > 0);
        assertTrue(r4.compareTo(r2) > 0);
        assertTrue(r4.compareTo(r3) > 0);
        assertTrue(r4.compareTo(r4) == 0);
        assertTrue(r4.compareTo(r5) < 0);
        assertTrue(r4.compareTo(r6) < 0);

        assertTrue(r5.compareTo(r1) > 0);
        assertTrue(r5.compareTo(r2) > 0);
        assertTrue(r5.compareTo(r3) > 0);
        assertTrue(r5.compareTo(r4) > 0);
        assertTrue(r5.compareTo(r5) == 0);
        assertTrue(r5.compareTo(r6) < 0);

        assertTrue(r6.compareTo(r1) > 0);
        assertTrue(r6.compareTo(r2) > 0);
        assertTrue(r6.compareTo(r3) > 0);
        assertTrue(r6.compareTo(r4) > 0);
        assertTrue(r6.compareTo(r5) > 0);
        assertTrue(r6.compareTo(r6) == 0);

    }

    private void compare(ResultNode small, ResultNode medium, ResultNode large) {
        assertTrue(small.compareTo(medium) < 0);
        assertTrue(small.compareTo(large) < 0);
        assertTrue(medium.compareTo(large) < 0);
        assertTrue(medium.compareTo(small) > 0);
        assertTrue(large.compareTo(small) > 0);
        assertTrue(large.compareTo(medium) > 0);
        assertEquals(0, small.compareTo(small));
        assertEquals(0, medium.compareTo(medium));
        assertEquals(0, large.compareTo(large));
    }

    private void compareAllPairwise(ResultNode... orderedNodes) {
        for (int i = 0; i < orderedNodes.length; ++i) {
            for (int j = 0; j < orderedNodes.length; ++j) {
                var ctx = String.format("lhs(i=%d): %s, rhs(j=%d): %s", i, orderedNodes[i], j, orderedNodes[j]);
                if (j < i) {
                    assertTrue(ctx, orderedNodes[i].compareTo(orderedNodes[j]) > 0);
                } else if (j > i) {
                    assertTrue(ctx, orderedNodes[i].compareTo(orderedNodes[j]) < 0);
                } else { // j == i
                    assertEquals(ctx, orderedNodes[i].compareTo(orderedNodes[j]), 0);
                }
            }
        }
    }

    @Test
    public void testStringResultNode() {
        try {
            new StringResultNode(null);
            fail("Should not be able to set null value.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new StringResultNode().setValue(null);
            fail("Should not be able to set null value.");
        } catch (IllegalArgumentException e) {
            // expected
        }
        StringResultNode a = new StringResultNode("42");
        assertEquals(a.getInteger(), 42);
        assertEquals(a.getFloat(), 42, delta);
        assertEquals(a.getString(), "42");

        a = new StringResultNode("7.3");
        assertEquals(a.getInteger(), 0);
        assertEquals(a.getFloat(), 7.3, delta);
        assertEquals(a.getString(), "7.3");

        byte[] raw = a.getRaw();
        assertEquals(raw.length, 3);
        assertEquals(Arrays.compareUnsigned(raw, Utf8.toBytes("7.3")), 0);

        assertResultNode(a);
        compare(new StringResultNode(), new StringResultNode("z"), new StringResultNode("zz"));
        compare(new StringResultNode("z"), new StringResultNode("zz"), new StringResultNode("zzz"));
        compare(new StringResultNode("a"), new StringResultNode("zz"), new PositiveInfinityResultNode());
        assertEquals(new StringResultNode("zz").compareTo(new StringResultNode("zz")), 0);

        // Remove this assertion if we remove support for implicit stringification
        compare(new StringResultNode(), new StringResultNode("12"), new IntegerResultNode(13));

        var node = new StringResultNode("foo");
        node.append(new StringResultNode("bar"));
        assertEquals(node.getString(), "foobar");
        node.add(new StringResultNode("baz"));
        assertEquals(node.getString(), "foobarbaz");

        node.set(new StringResultNode("ABC"));
        assertEquals(node.getString(), "ABC");

        node.max(new StringResultNode("ABA"));
        assertEquals(node.getString(), "ABC");

        node.max(new StringResultNode("ABD"));
        assertEquals(node.getString(), "ABD");

        node.min(new StringResultNode("ABE"));
        assertEquals(node.getString(), "ABD");

        node.min(new StringResultNode("ABC"));
        assertEquals(node.getString(), "ABC");

        node.set(new IntegerResultNode(1234));
        assertEquals(node.getString(), "1234");
    }

    @Test
    public void string_result_node_comparison_is_memcmp_unsigned_byte_ordered() {
        // Comparison is first by shared prefix (if equal), then ties are resolved by checking length.
        // This is not actually valid UTF-8, so we depend on unchecked-ness...
        compareAllPairwise(StringResultNode.ofUncheckedUtf8Array(new byte[] { 0 }),
                           StringResultNode.ofUncheckedUtf8Array(new byte[] { 0, 0 }),
                           StringResultNode.ofUncheckedUtf8Array(new byte[] { 0, 0, 1 }),
                           StringResultNode.ofUncheckedUtf8Array(new byte[] { 1 }),
                           StringResultNode.ofUncheckedUtf8Array(new byte[] { 127 }),  // 0x7F
                           StringResultNode.ofUncheckedUtf8Array(new byte[] { -128 }), // 0x80
                           StringResultNode.ofUncheckedUtf8Array(new byte[] { -1 }),   // 0xFF
                           StringResultNode.ofUncheckedUtf8Array(new byte[] { -1, 0 }));
    }

    // Ensure that the ordering of string nodes on the container matches that of the C++ backend.
    // The backend directly orders on the memcmp order of UTF-8 strings, so the container must do the same.
    // In particular, this means that UTF-16 java.lang.String sorting is a no-go.
    @Test
    public void string_result_node_comparison_is_utf8_byte_ordered() {
        // UTF-8 representation of "fried shrimp" emoji U+1F364 which requires a UTF-16 surrogate pair (2 chars)
        // Its UTF-16 representation is D83C DF64
        var surrogateUtf8 = new byte[] { -16, -97, -115, -92 }; // F0 9F 8D A4
        // UTF-8 representation of U+F0BA, which exists in the Unicode private usage range. Only requires 1 UTF-16 char.
        // Its UTF-16 representation is F0BA
        var nonSurrogateUtf8 = new byte[] { -17, -126, -70 }; // EF 82 BA

        var s1 = new StringResultNode(Utf8.toString(surrogateUtf8));
        var s2 = new StringResultNode(Utf8.toString(nonSurrogateUtf8));
        //   UTF-8:  s2 < s1, since 0xEF < 0xF0
        //   UTF-16: s1 < s2, since 0xD83C < 0xF0BA
        // Assert that ordering is defined by UTF-8, not (surrogated) UTF-16.
        assertTrue(s2.compareTo(s1) < 0);
        assertTrue(s1.compareTo(s2) > 0);
    }

    @Test
    public void testXorBitFunctionNode() {
        try {
            new XorBitFunctionNode(null, 64);
            fail("Should not be able to set null argument.");
        } catch (NullPointerException e) {
            // expected
        }
        try {
            new XorBitFunctionNode().prepare();
            fail("Should not be able to run prepare.");
        } catch (RuntimeException e) {
            // expected
        }
        try {
            new XorBitFunctionNode().execute();
            fail("Should not be able to run execute.");
        } catch (RuntimeException e) {
            // expected
        }
        assertUnaryBitFunctionNode(new XorBitFunctionNode());
    }

    @Test
    public void testUcaFunctionNode() {
        try {
            new UcaFunctionNode(null, "foo");
            fail("Should not be able to set null argument.");
        } catch (NullPointerException e) {
            // expected
        }
        try {
            new UcaFunctionNode().prepare();
            fail("Should not be able to run prepare.");
        } catch (RuntimeException e) {
            // expected
        }
        try {
            new UcaFunctionNode().execute();
            fail("Should not be able to run execute.");
        } catch (RuntimeException e) {
            // expected
        }
        assertUcaFunctionNode(new UcaFunctionNode(new ConstantNode(new IntegerResultNode(1337)), "foo", "bar"));
    }

    @Test
    public void testNestedFunctions() {
        assertFunctionNode(new AddFunctionNode()
                                   .addArg(new MultiplyFunctionNode().addArg(new ConstantNode(new IntegerResultNode(3)))
                                                                     .addArg(new ConstantNode(
                                                                             new StringResultNode("4"))))
                                   .addArg(new ConstantNode(new FloatResultNode(2.0))),
                           14, 14.0, "14.0", doubleAsRaw(14.0));
    }

    @Test
    public void testArithmeticNodes() {
        ExpressionNode i1 = new ConstantNode(new IntegerResultNode(1));
        ExpressionNode i2 = new ConstantNode(new IntegerResultNode(2));
        ExpressionNode f2 = new ConstantNode(new FloatResultNode(9.9));
        ExpressionNode s2 = new ConstantNode(new StringResultNode("2"));
        ExpressionNode r2 = new ConstantNode(new RawResultNode(asRaw(2)));

        AddFunctionNode add1 = new AddFunctionNode();
        add1.addArg(i1).addArg(i2);
        ExpressionNode exp1 = add1;
        exp1.prepare();
        assertTrue(exp1.getResult() instanceof IntegerResultNode);
        assertTrue(exp1.execute());
        assertEquals(exp1.getResult().getInteger(), 3);
        assertTrue(exp1.execute());
        assertEquals(exp1.getResult().getInteger(), 3);

        AddFunctionNode add2 = new AddFunctionNode();
        add2.addArg(i1);
        add2.addArg(f2);
        add2.prepare();
        assertTrue(add2.getResult() instanceof FloatResultNode);

        AddFunctionNode add3 = new AddFunctionNode();
        add3.addArg(i1);
        add3.addArg(s2);
        add3.prepare();
        assertTrue(add3.getResult() instanceof IntegerResultNode);

        AddFunctionNode add4 = new AddFunctionNode();
        add4.addArg(i1);
        add4.addArg(r2);
        add4.prepare();
        assertTrue(add4.getResult() instanceof IntegerResultNode);
    }

    @Test
    public void testArithmeticOperations() {
        ExpressionNode i1 = new ConstantNode(new IntegerResultNode(1793253241));
        ExpressionNode i2 = new ConstantNode(new IntegerResultNode(1676521321));
        ExpressionNode f1 = new ConstantNode(new FloatResultNode(1.1109876));
        ExpressionNode f2 = new ConstantNode(new FloatResultNode(9.767681239));

        assertAdd(i1, i2, 3469774562l, 3469774562l);
        assertAdd(i1, f2, 1793253251l, 1793253250.767681239);
        assertAdd(f1, f2, 11, 10.878668839);
        assertMultiply(i1, i2, 3006427292488851361l, 3006427292488851361l);
        assertMultiply(i1, f2, 17515926039l, 1793253241.0 * 9.767681239);
        assertMultiply(f1, f2, 11, 10.8517727372816364);
    }

    // --------------------------------------------------------------------------------
    //
    // Everything below this point is helper functions.
    //
    // --------------------------------------------------------------------------------
    private static void assertNotEquals(Object lhs, Object rhs) {
        assertFalse(lhs.equals(rhs));
    }

    private static void assertUcaFunctionNode(UcaFunctionNode node) {
        UcaFunctionNode obj = node.clone();
        assertEquals(obj, node);
        assertMultiArgFunctionNode((UcaFunctionNode)Identifiable.createFromId(node.getClassId()));
    }

    public byte[] asRaw(int ... extra) {
        byte[] mybytes = new byte[extra.length];
        for (int i = 0; i < mybytes.length; i++) {
            mybytes[i] = (byte)extra[i];
        }
        return mybytes;
    }

    public byte[] longAsRaw(long value) {
        return ByteBuffer.allocate(8).putLong(value).array();
    }

    public byte[] doubleAsRaw(double value) {
        return ByteBuffer.allocate(8).putDouble(value).array();
    }

    public byte[] stringAsRaw(String value) {
        return Utf8.toBytes(value);
    }

    private static void assertUnaryBitFunctionNode(UnaryBitFunctionNode node) {
        UnaryBitFunctionNode obj = (UnaryBitFunctionNode)node.clone();
        assertEquals(obj, node);

        obj.setNumBits(obj.getNumBits() + 1);
        assertFalse(obj.equals(node));

        assertMultiArgFunctionNode((UnaryBitFunctionNode)Identifiable.createFromId(node.getClassId()));
    }

    private static void assertMultiArgFunctionNode(MultiArgFunctionNode node) {
        try {
            node.addArg(null);
            fail("Should not be able to add a null argument.");
        } catch (NullPointerException e) {
            // expected
        }
        int initialSz = node.getNumArgs();
        node.addArg(new ConstantNode(new IntegerResultNode(69)));
        assertEquals(1+initialSz, node.getNumArgs());
        node.addArg(new ConstantNode(new IntegerResultNode(6699)));
        assertEquals(2+initialSz, node.getNumArgs());
        node.addArg(new ConstantNode(new IntegerResultNode(666999)));
        assertEquals(3+initialSz, node.getNumArgs());

        MultiArgFunctionNode obj = (MultiArgFunctionNode)assertSerialize(node);
        assertEquals(node, obj);
        assertEquals(node.getNumArgs(), obj.getNumArgs());
        for (int i = 0, len = node.getNumArgs(); i < len; i++) {
            assertEquals(node.getArg(i), obj.getArg(i));
        }

        obj.addArg(new ConstantNode(new IntegerResultNode(69)));
        assertFalse(node.equals(obj));
    }

    public void assertAdd(ExpressionNode arg1, ExpressionNode arg2, long lexpected, double dexpected) {
        assertArith(new AddFunctionNode(), arg1, arg2, lexpected, dexpected);
    }

    public void assertMultiply(ExpressionNode arg1, ExpressionNode arg2, long lexpected, double dexpected) {
        assertArith(new MultiplyFunctionNode(), arg1, arg2, lexpected, dexpected);
    }

    public void assertArith(MultiArgFunctionNode node, ExpressionNode arg1, ExpressionNode arg2, long lexpected, double dexpected) {
        node.addArg(arg1);
        node.addArg(arg2);
        node.prepare();
        node.execute();
        assertEquals(lexpected, node.getResult().getInteger());
        assertEquals(dexpected, node.getResult().getFloat(), delta);
    }

    public void assertFunctionNode(FunctionNode node, long lexpected, double dexpected, String sexpected, byte[] rexpected) {
        node.prepare();
        node.execute();
        assertEquals(lexpected, node.getResult().getInteger());
        assertEquals(dexpected, node.getResult().getFloat(), delta);
        assertEquals(sexpected, node.getResult().getString());
        assertTrue(Arrays.equals(rexpected, node.getResult().getRaw()));
    }

    private static void assertResultNode(ResultNode node) {
        BufferSerializer buf = new BufferSerializer(new GrowableByteBuffer());
        long oldInteger = node.getInteger();
        double oldFloat = node.getFloat();
        String oldString = node.getString();
        byte[] oldRaw = node.getRaw();
        node.serialize(buf);
        buf.flip();
        node.deserialize(buf);
        assertEquals(oldInteger, node.getInteger());
        assertEquals(oldFloat, node.getFloat(), delta);
        assertEquals(oldString, node.getString());
        assertEquals(oldRaw.length, node.getRaw().length);

        buf = new BufferSerializer(new GrowableByteBuffer());
        node.serializeWithId(buf);
        buf.flip();
        node.deserializeWithId(buf);
        assertEquals(oldInteger, node.getInteger());
        assertEquals(oldFloat, node.getFloat(), delta);
        assertEquals(oldString, node.getString());
        assertEquals(oldRaw.length, node.getRaw().length);

        buf = new BufferSerializer(new GrowableByteBuffer());
        node.serializeWithId(buf);
        buf.flip();
        ResultNode obj = (ResultNode)Identifiable.create(buf);
        assertEquals(oldInteger, obj.getInteger());
        assertEquals(oldFloat, obj.getFloat(), delta);
        assertEquals(oldString, obj.getString());
        assertEquals(oldRaw.length, obj.getRaw().length);

        assertSerialize(node);
    }

    private static Identifiable assertSerialize(Identifiable node) {
        BufferSerializer buf = new BufferSerializer(new GrowableByteBuffer());
        node.serializeWithId(buf);
        buf.flip();
        Identifiable created = Identifiable.create(buf);
        assertEquals(node, created);
        assertFalse(buf.getBuf().hasRemaining());
        Identifiable cloned = created.clone();
        assertEquals(node, cloned);
        BufferSerializer createdBuffer = new BufferSerializer(new GrowableByteBuffer());
        BufferSerializer clonedBuffer = new BufferSerializer(new GrowableByteBuffer());
        created.serializeWithId(createdBuffer);
        cloned.serializeWithId(clonedBuffer);
        assertEquals(createdBuffer.getBuf().limit(), clonedBuffer.getBuf().limit());
        assertEquals(createdBuffer.position(), clonedBuffer.position());
        createdBuffer.getBuf().flip();
        clonedBuffer.getBuf().flip();
        for (int i = 0; i < createdBuffer.getBuf().limit(); i++) {
            assertEquals(createdBuffer.getBuf().get(), clonedBuffer.getBuf().get());
        }
        return created;
    }

}
