/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.cassandra.io.tries;

import java.io.IOException;
import java.util.Arrays;

import org.junit.After;
import org.junit.Test;
import org.mockito.Mockito;
import org.quicktheories.api.Pair;
import org.quicktheories.generators.Generate;

import org.apache.cassandra.io.util.DataOutputBuffer;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import static org.quicktheories.QuickTheory.qt;
import static org.quicktheories.generators.SourceDSL.longs;

@SuppressWarnings("unchecked")
public class TrieNodeTest
{
    private final SerializationNode<Integer> sn = Mockito.mock(SerializationNode.class);
    private final DataOutputBuffer out = new DataOutputBuffer();

    @After
    public void after()
    {
        reset(sn);
        out.clear();
    }

    @Test
    public void testTypeFor0Children()
    {
        when(sn.childCount()).thenReturn(0);
        assertSame(TrieNode.Types.PAYLOAD_ONLY, TrieNode.typeFor(sn, 0));
    }

    @Test
    public void testTypeFor1ChildNoPayload()
    {
        when(sn.childCount()).thenReturn(1);
        when(sn.payload()).thenReturn(null);

        qt().forAll(Generate.pick(Arrays.asList(
                Pair.of(Ranges.BITS_4, TrieNode.Types.SINGLE_NOPAYLOAD_4),
                Pair.of(Ranges.BITS_8, TrieNode.Types.SINGLE_8),
                Pair.of(Ranges.BITS_12, TrieNode.Types.SINGLE_NOPAYLOAD_12),
                Pair.of(Ranges.BITS_16, TrieNode.Types.SINGLE_16),
                Pair.of(Ranges.BITS_24, TrieNode.Types.DENSE_24),
                Pair.of(Ranges.BITS_32, TrieNode.Types.DENSE_32),
                Pair.of(Ranges.BITS_40, TrieNode.Types.DENSE_40),
                Pair.of(Ranges.BITS_GT40, TrieNode.Types.LONG_DENSE)
        )).flatMap(p -> longs().between(p._1.min, p._1.max).map(v -> Pair.of(v, p._2)))).check(p -> {
            when(sn.maxPositionDelta(0)).thenReturn(-p._1);
            return p._2 == TrieNode.typeFor(sn, 0);
        });
    }

    @Test
    public void testTypeFor1ChildAndPayload()
    {
        when(sn.childCount()).thenReturn(1);
        when(sn.payload()).thenReturn(1);

        qt().forAll(Generate.pick(Arrays.asList(
                Pair.of(Ranges.BITS_4, TrieNode.Types.SINGLE_8),
                Pair.of(Ranges.BITS_8, TrieNode.Types.SINGLE_8),
                Pair.of(Ranges.BITS_12, TrieNode.Types.SINGLE_16),
                Pair.of(Ranges.BITS_16, TrieNode.Types.SINGLE_16),
                Pair.of(Ranges.BITS_24, TrieNode.Types.DENSE_24),
                Pair.of(Ranges.BITS_32, TrieNode.Types.DENSE_32),
                Pair.of(Ranges.BITS_40, TrieNode.Types.DENSE_40),
                Pair.of(Ranges.BITS_GT40, TrieNode.Types.LONG_DENSE)
        )).flatMap(p -> longs().between(p._1.min, p._1.max).map(v -> Pair.of(v, p._2)))).check(p -> {
            when(sn.maxPositionDelta(0)).thenReturn(-p._1);
            return p._2 == TrieNode.typeFor(sn, 0);
        });
    }

    @Test
    public void testTypeForMoreChildrenAndNoPayload()
    {
        when(sn.childCount()).thenReturn(2);
        when(sn.payload()).thenReturn(null);

        qt().forAll(Generate.pick(Arrays.asList(
                Pair.of(Ranges.BITS_4, TrieNode.Types.DENSE_12),
                Pair.of(Ranges.BITS_8, TrieNode.Types.DENSE_12),
                Pair.of(Ranges.BITS_12, TrieNode.Types.DENSE_12),
                Pair.of(Ranges.BITS_16, TrieNode.Types.DENSE_16),
                Pair.of(Ranges.BITS_24, TrieNode.Types.DENSE_24),
                Pair.of(Ranges.BITS_32, TrieNode.Types.DENSE_32),
                Pair.of(Ranges.BITS_40, TrieNode.Types.DENSE_40),
                Pair.of(Ranges.BITS_GT40, TrieNode.Types.LONG_DENSE)
        )).flatMap(p -> longs().between(p._1.min, p._1.max).map(v -> Pair.of(v, p._2)))).check(p -> {
            when(sn.maxPositionDelta(0)).thenReturn(-p._1);
            return p._2 == TrieNode.typeFor(sn, 0);
        });
    }

    @Test
    public void testPayloadOnlyNode() throws IOException
    {
        TrieNode.Types.PAYLOAD_ONLY.serialize(out, null, 1 | 4, 0);
        out.flush();

        TrieNode node = TrieNode.at(out.asNewBuffer(), 0);
        assertEquals(TrieNode.Types.PAYLOAD_ONLY, node);
        assertEquals(1 | 4, node.payloadFlags(out.asNewBuffer(), 0));
        assertEquals(1, node.payloadPosition(out.asNewBuffer(), 0));
        assertEquals(1, node.sizeofNode(null));
        assertEquals(-1, node.search(null, 0, 0));
        assertEquals(Integer.MAX_VALUE, node.transitionByte(null, 0, 0));
        assertEquals(123, node.lesserTransition(null, 0, 0, 0, 123));
        assertEquals(123, node.greaterTransition(null, 0, 0, 0, 123));
        assertEquals(-1, node.lastTransition(null, 0, 0));
        assertEquals(-1, node.transition(null, 0, 0, 0));
        assertEquals(0, node.transitionRange(null, 0));
        assertEquals(0, node.transitionDelta(null, 0, 0));
    }

    private void prepareSingleNode(long delta)
    {
        when(sn.childCount()).thenReturn(1);
        when(sn.transition(0)).thenReturn(123);
        when(sn.serializedPositionDelta(0, 0)).thenReturn(delta);
    }

    private void singleNodeAssertions(TrieNode node, int payloadFlags, int size, long pos)
    {
        assertEquals(payloadFlags, node.payloadFlags(out.asNewBuffer(), 0));
        assertEquals(size, node.sizeofNode(null));
        assertEquals(0, node.search(out.asNewBuffer(), 0, 123));
        assertEquals(-1, node.search(out.asNewBuffer(), 0, 122));
        assertEquals(-2, node.search(out.asNewBuffer(), 0, 124));
        assertEquals(123, node.transitionByte(out.asNewBuffer(), 0, 0));
        assertEquals(Integer.MAX_VALUE, node.transitionByte(out.asNewBuffer(), 0, 1));
        assertEquals(100, node.lesserTransition(out.asNewBuffer(), 0, 100 - pos, -2, 123));
        assertEquals(234, node.greaterTransition(null, 0, 0, 0, 234));
        assertEquals(234, node.greaterTransition(out.asNewBuffer(), 0, 100 - pos, -2, 234));
        assertEquals(100, node.greaterTransition(out.asNewBuffer(), 0, 100 - pos, -1, 234));
        assertEquals(234, node.greaterTransition(out.asNewBuffer(), 0, 100 - pos, 0, 234));
        assertEquals(100, node.lastTransition(out.asNewBuffer(), 0, 100 - pos));
        assertEquals(100, node.transition(out.asNewBuffer(), 0, 100 - pos, 0));
        assertEquals(1, node.transitionRange(out.asNewBuffer(), 0));
        assertEquals(pos, node.transitionDelta(out.asNewBuffer(), 0, 0));
    }

    @Test
    public void testSingle16Node() throws IOException
    {
        prepareSingleNode(-43210L);
        TrieNode.Types.SINGLE_16.serialize(out, sn, 1 | 4, 0);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 0);
        assertEquals(TrieNode.Types.SINGLE_16, node);
        singleNodeAssertions(node, 1 | 4, 4, -43210);
    }

    @Test
    public void testSingleNoPayload4Node() throws IOException
    {
        prepareSingleNode(-7L);
        TrieNode.Types.SINGLE_NOPAYLOAD_4.serialize(out, sn, 0, 0);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 0);
        assertEquals(TrieNode.Types.SINGLE_NOPAYLOAD_4, node);
        singleNodeAssertions(node, 0, 2, -7);
    }

    @Test
    public void testSingleNoPayload12Node() throws IOException
    {
        prepareSingleNode(-1234L);
        TrieNode.Types.SINGLE_NOPAYLOAD_12.serialize(out, sn, 0, 0);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 0);
        assertEquals(TrieNode.Types.SINGLE_NOPAYLOAD_12, node);
        singleNodeAssertions(node, 0, 3, -1234L);
    }

    private void prepareSparseNode(long delta) throws IOException
    {
        out.write(new byte[6]);
        when(sn.childCount()).thenReturn(3);
        when(sn.transition(0)).thenReturn(10);
        when(sn.transition(1)).thenReturn(20);
        when(sn.transition(2)).thenReturn(30);
        when(sn.serializedPositionDelta(0, 6)).thenReturn(delta);
        when(sn.serializedPositionDelta(1, 6)).thenReturn(delta + 2);
        when(sn.serializedPositionDelta(2, 6)).thenReturn(delta + 4);
    }

    private void sparseOrDenseNodeAssertions(TrieNode node, int payloadFlags, int size, long pos)
    {
        assertEquals(size, node.sizeofNode(sn));
        assertEquals(payloadFlags, node.payloadFlags(out.asNewBuffer(), 6));
        assertEquals(3, node.transitionRange(out.asNewBuffer(), 6));
        assertEquals(6 + size, node.payloadPosition(out.asNewBuffer(), 6));

        assertEquals(Integer.MAX_VALUE, node.transitionByte(out.asNewBuffer(), 6, 3));

        assertEquals(10, node.lesserTransition(out.asNewBuffer(), 6, 10 - pos, 1, 123));
        assertEquals(14, node.lesserTransition(out.asNewBuffer(), 6, 10 - pos, -4, 123));

        assertEquals(14, node.greaterTransition(out.asNewBuffer(), 6, 10 - pos, 1, 234));
        assertEquals(234, node.greaterTransition(out.asNewBuffer(), 6, 10 - pos, 2, 234));
        assertEquals(10, node.greaterTransition(out.asNewBuffer(), 6, 10 - pos, -1, 234));

        assertEquals(14, node.lastTransition(out.asNewBuffer(), 6, 10 - pos));

        assertEquals(pos, node.transitionDelta(out.asNewBuffer(), 6, 0));
        assertEquals(pos + 2, node.transitionDelta(out.asNewBuffer(), 6, 1));
        assertEquals(pos + 4, node.transitionDelta(out.asNewBuffer(), 6, 2));

        assertEquals(10, node.transition(out.asNewBuffer(), 6, 10 - pos, 0));
        assertEquals(12, node.transition(out.asNewBuffer(), 6, 10 - pos, 1));
        assertEquals(14, node.transition(out.asNewBuffer(), 6, 10 - pos, 2));
    }

    private void sparseNodeAssertions(TrieNode node, int payloadFlags, int size, long pos)
    {
        sparseOrDenseNodeAssertions(node, payloadFlags, size, pos);
        assertEquals(-1, node.search(out.asNewBuffer(), 6, 5));
        assertEquals(0, node.search(out.asNewBuffer(), 6, 10));
        assertEquals(-2, node.search(out.asNewBuffer(), 6, 15));
        assertEquals(-4, node.search(out.asNewBuffer(), 6, 35));

        assertEquals(10, node.transitionByte(out.asNewBuffer(), 6, 0));
        assertEquals(20, node.transitionByte(out.asNewBuffer(), 6, 1));
        assertEquals(30, node.transitionByte(out.asNewBuffer(), 6, 2));
    }

    @Test
    public void testSparse16Node() throws IOException
    {
        prepareSparseNode(-43210L);
        TrieNode.Types.SPARSE_16.serialize(out, sn, 1 | 4, 6);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 6);
        assertEquals(TrieNode.Types.SPARSE_16, node);
        sparseNodeAssertions(node, 1 | 4, 11, -43210L);
    }

    @Test
    public void testSparse12Node() throws IOException
    {
        prepareSparseNode(-1234L);
        TrieNode.Types.SPARSE_12.serialize(out, sn, 1 | 4, 6);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 6);
        assertEquals(TrieNode.Types.SPARSE_12, node);
        sparseNodeAssertions(node, 1 | 4, 10, -1234L);
    }

    private void prepareDenseNode(long delta) throws IOException
    {
        out.write(new byte[6]);
        when(sn.childCount()).thenReturn(3);
        when(sn.transition(0)).thenReturn(11);
        when(sn.transition(1)).thenReturn(12);
        when(sn.transition(2)).thenReturn(13);
        when(sn.serializedPositionDelta(0, 6)).thenReturn(delta);
        when(sn.serializedPositionDelta(1, 6)).thenReturn(delta + 2);
        when(sn.serializedPositionDelta(2, 6)).thenReturn(delta + 4);
    }

    private void denseNodeAssertions(TrieNode node, int payload, int size, long pos)
    {
        sparseOrDenseNodeAssertions(node, payload, size, pos);
        assertEquals(-1, node.search(out.asNewBuffer(), 6, 10));
        assertEquals(0, node.search(out.asNewBuffer(), 6, 11));
        assertEquals(-4, node.search(out.asNewBuffer(), 6, 14));

        assertEquals(11, node.transitionByte(out.asNewBuffer(), 6, 0));
        assertEquals(12, node.transitionByte(out.asNewBuffer(), 6, 1));
        assertEquals(13, node.transitionByte(out.asNewBuffer(), 6, 2));
    }

    @Test
    public void testDense16Node() throws IOException
    {
        prepareDenseNode(-43210L);
        TrieNode.Types.DENSE_16.serialize(out, sn, 1 | 4, 6);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 6);
        assertEquals(TrieNode.Types.DENSE_16, node);
        denseNodeAssertions(node, 1 | 4, 9, -43210L);
    }

    @Test
    public void testDense12Node() throws IOException
    {
        prepareDenseNode(-1234L);
        TrieNode.Types.DENSE_12.serialize(out, sn, 1 | 4, 6);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 6);
        assertEquals(TrieNode.Types.DENSE_12, node);
        denseNodeAssertions(node, 1 | 4, 8, -1234L);
    }

    @Test
    public void testLongDenseNode() throws IOException
    {
        prepareDenseNode(-0x7ffffffffffffffL);
        TrieNode.Types.LONG_DENSE.serialize(out, sn, 1 | 4, 6);
        TrieNode node = TrieNode.at(out.asNewBuffer(), 6);
        assertEquals(TrieNode.Types.LONG_DENSE, node);
        denseNodeAssertions(node, 1 | 4, 27, -0x7ffffffffffffffL);
    }

    private enum Ranges
    {
        BITS_4(0x1L, 0xfL),
        BITS_8(0x10L, 0xffL),
        BITS_12(0x100L, 0xfffL),
        BITS_16(0x1000L, 0xffffL),
        BITS_24(0x100000L, 0xffffffL),
        BITS_32(0x10000000L, 0xffffffffL),
        BITS_40(0x1000000000L, 0xffffffffffL),
        BITS_GT40(0x100000000000L, 0x7fffffffffffffffL);

        private final long min, max;

        Ranges(long min, long max)
        {
            this.min = min;
            this.max = max;
        }
    }
}