/*
 *  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.directory.mavibot.btree;


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

import java.io.IOException;

import org.apache.directory.mavibot.btree.BTree;
import org.apache.directory.mavibot.btree.BorrowedFromLeftResult;
import org.apache.directory.mavibot.btree.BorrowedFromRightResult;
import org.apache.directory.mavibot.btree.DeleteResult;
import org.apache.directory.mavibot.btree.InsertResult;
import org.apache.directory.mavibot.btree.Leaf;
import org.apache.directory.mavibot.btree.MergedWithSiblingResult;
import org.apache.directory.mavibot.btree.ModifyResult;
import org.apache.directory.mavibot.btree.Node;
import org.apache.directory.mavibot.btree.NotPresentResult;
import org.apache.directory.mavibot.btree.Page;
import org.apache.directory.mavibot.btree.ReferenceHolder;
import org.apache.directory.mavibot.btree.RemoveResult;
import org.apache.directory.mavibot.btree.Tuple;
import org.apache.directory.mavibot.btree.exception.KeyNotFoundException;
import org.apache.directory.mavibot.btree.serializer.LongSerializer;
import org.apache.directory.mavibot.btree.serializer.StringSerializer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;


/**
 * A unit test class for Leaf
 * 
 * @author <a href="mailto:labs@labs.apache.org">Mavibot labs Project</a>
 */
public class LeafTest
{
    private BTree<Long, String> btree = null;


    /**
     * Create a btree
     */
    @Before
    public void setup() throws IOException
    {
        btree = new BTree<Long, String>( "test", new LongSerializer(), new StringSerializer() );
        btree.setPageSize( 8 );
    }


    @After
    public void shutdown() throws IOException
    {
        btree.close();
    }


    /**
     * A helper method to insert elements in a Leaf
     * @throws IOException 
     */
    private Leaf<Long, String> insert( Leaf<Long, String> leaf, long key, String value ) throws IOException
    {
        InsertResult<Long, String> result = leaf.insert( 1L, key, value );

        return ( Leaf<Long, String> ) ( ( ModifyResult<Long, String> ) result ).getModifiedPage();
    }


    /**
     * Test that deleting an entry from an empty page returns a NOT_PRESENT result
     * @throws IOException
     */
    @Test
    public void testDeleteFromEmptyLeaf() throws IOException
    {
        Leaf<Long, String> leaf = new Leaf<Long, String>( btree );

        DeleteResult<Long, String> result = leaf.delete( 1L, 1L, null, null, -1 );

        assertEquals( NotPresentResult.NOT_PRESENT, result );
    }


    /**
     * Test that deleting an entry which is not present in the leaf works
     * @throws IOException
     */
    @Test
    public void testDeleteNotPresentElementFromRootLeaf() throws IOException
    {
        Leaf<Long, String> leaf = new Leaf<Long, String>( btree );
        leaf = insert( leaf, 1L, "v1" );
        leaf = insert( leaf, 2L, "v2" );
        leaf = insert( leaf, 3L, "v3" );
        leaf = insert( leaf, 4L, "v4" );

        DeleteResult<Long, String> result = leaf.delete( 2L, 5L, null, null, -1 );

        assertEquals( NotPresentResult.NOT_PRESENT, result );
    }


    /**
     * Test that deleting an entry which is present in the leaf works
     * @throws IOException
     */
    @Test
    public void testDeletePresentElementFromRootLeaf() throws IOException
    {
        Leaf<Long, String> leaf = new Leaf<Long, String>( btree );
        leaf = insert( leaf, 1L, "v1" );
        leaf = insert( leaf, 2L, "v2" );
        leaf = insert( leaf, 3L, "v3" );
        leaf = insert( leaf, 4L, "v4" );

        DeleteResult<Long, String> result = leaf.delete( 4L, 3L, null, null, -1 );

        assertTrue( result instanceof RemoveResult );

        Tuple<Long, String> removedElement = ( ( RemoveResult<Long, String> ) result ).getRemovedElement();
        Page<Long, String> newLeaf = ( ( RemoveResult<Long, String> ) result ).getModifiedPage();

        assertEquals( Long.valueOf( 3L ), removedElement.getKey() );
        assertEquals( "v3", removedElement.getValue() );
        assertEquals( 3, newLeaf.getNbElems() );

        try
        {
            assertEquals( "v1", newLeaf.get( 1L ) );
            assertEquals( "v2", newLeaf.get( 2L ) );
            assertEquals( "v4", newLeaf.get( 4L ) );
        }
        catch ( KeyNotFoundException knfe )
        {
            fail();
        }

        try
        {
            newLeaf.get( 3L );
            fail();
        }
        catch ( KeyNotFoundException knfe )
        {
            // Expected
        }
    }


    /**
     * Test that deleting the first element return the correct result
     * @throws IOException
     */
    @Test
    public void testDeleteFirstElementFromRootLeaf() throws IOException
    {
        Leaf<Long, String> leaf = new Leaf<Long, String>( btree );
        leaf = insert( leaf, 1L, "v1" );
        leaf = insert( leaf, 2L, "v2" );
        leaf = insert( leaf, 3L, "v3" );
        leaf = insert( leaf, 4L, "v4" );

        DeleteResult<Long, String> result = leaf.delete( 4L, 1L, null, null, -1 );

        assertTrue( result instanceof RemoveResult );

        RemoveResult<Long, String> removeResult = ( RemoveResult<Long, String> ) result;

        Tuple<Long, String> removedElement = removeResult.getRemovedElement();
        Page<Long, String> newLeaf = removeResult.getModifiedPage();

        assertEquals( Long.valueOf( 1L ), removedElement.getKey() );
        assertEquals( "v1", removedElement.getValue() );
        assertEquals( 3, newLeaf.getNbElems() );

        try
        {
            newLeaf.get( 1L );
            fail();
        }
        catch ( KeyNotFoundException knfe )
        {
            // expected
        }

        try
        {
            assertEquals( "v2", newLeaf.get( 2L ) );
            assertEquals( "v3", newLeaf.get( 3L ) );
            assertEquals( "v4", newLeaf.get( 4L ) );
        }
        catch ( KeyNotFoundException knfe )
        {
            fail();
        }
    }


    /**
     * Check that deleting an element from a leaf with N/2 element works when we borrow
     * an element in a left page with more than N/2 elements
     * @throws IOException 
     */
    @Test
    public void testDeleteBorrowingFromLeftSibling() throws IOException
    {
        Node<Long, String> parent = new Node<Long, String>( btree, 1L, 2 );
        Leaf<Long, String> left = new Leaf<Long, String>( btree );
        Leaf<Long, String> target = new Leaf<Long, String>( btree );
        Leaf<Long, String> right = new Leaf<Long, String>( btree );

        // Fill the left page
        left = insert( left, 1L, "v1" );
        left = insert( left, 2L, "v2" );
        left = insert( left, 3L, "v3" );
        left = insert( left, 4L, "v4" );
        left = insert( left, 5L, "v5" );

        // Fill the target page
        target = insert( target, 6L, "v6" );
        target = insert( target, 7L, "v7" );
        target = insert( target, 8L, "v8" );
        target = insert( target, 9L, "v9" );

        // Fill the right page
        right = insert( right, 10L, "v10" );
        right = insert( right, 11L, "v11" );
        right = insert( right, 12L, "v12" );
        right = insert( right, 13L, "v13" );

        parent.children[0] = new ReferenceHolder<Page<Long, String>, Long, String>( null, left,
            left.getOffset(), left.getLastOffset() );
        parent.children[1] = new ReferenceHolder<Page<Long, String>, Long, String>( null, target,
            target.getOffset(), target.getLastOffset() );
        parent.children[2] = new ReferenceHolder<Page<Long, String>, Long, String>( null, right,
            right.getOffset(), right.getLastOffset() );

        // Update the parent
        parent.keys[0] = 6L;
        parent.keys[1] = 10L;

        // Now, delete the element from the target page
        DeleteResult<Long, String> result = target.delete( 2L, 7L, null, parent, 1 );

        assertTrue( result instanceof BorrowedFromLeftResult );

        BorrowedFromLeftResult<Long, String> borrowed = ( BorrowedFromLeftResult<Long, String> ) result;
        Tuple<Long, String> removedKey = borrowed.getRemovedElement();

        assertEquals( Long.valueOf( 7L ), removedKey.getKey() );

        // Check the modified leaf
        Leaf<Long, String> newLeaf = ( Leaf<Long, String> ) borrowed.getModifiedPage();

        assertEquals( 4, newLeaf.nbElems );
        assertEquals( Long.valueOf( 5L ), newLeaf.keys[0] );
        assertEquals( Long.valueOf( 6L ), newLeaf.keys[1] );
        assertEquals( Long.valueOf( 8L ), newLeaf.keys[2] );
        assertEquals( Long.valueOf( 9L ), newLeaf.keys[3] );

        // Check the sibling
        Leaf<Long, String> leftSibling = ( Leaf<Long, String> ) borrowed.getModifiedSibling();

        assertEquals( 4, leftSibling.nbElems );
        assertEquals( Long.valueOf( 1L ), leftSibling.keys[0] );
        assertEquals( Long.valueOf( 2L ), leftSibling.keys[1] );
        assertEquals( Long.valueOf( 3L ), leftSibling.keys[2] );
        assertEquals( Long.valueOf( 4L ), leftSibling.keys[3] );
    }


    /**
     * Check that deleting an element from a leaf with N/2 element works when we borrow
     * an element in a right page with more than N/2 elements
     * @throws IOException 
     */
    @Test
    public void testDeleteBorrowingFromRightSibling() throws IOException
    {
        Node<Long, String> parent = new Node<Long, String>( btree, 1L, 2 );
        Leaf<Long, String> left = new Leaf<Long, String>( btree );
        Leaf<Long, String> target = new Leaf<Long, String>( btree );
        Leaf<Long, String> right = new Leaf<Long, String>( btree );

        // Fill the left page
        left = insert( left, 1L, "v1" );
        left = insert( left, 2L, "v2" );
        left = insert( left, 3L, "v3" );
        left = insert( left, 4L, "v4" );

        // Fill the target page
        target = insert( target, 6L, "v6" );
        target = insert( target, 7L, "v7" );
        target = insert( target, 8L, "v8" );
        target = insert( target, 9L, "v9" );

        // Fill the right page
        right = insert( right, 10L, "v10" );
        right = insert( right, 11L, "v11" );
        right = insert( right, 12L, "v12" );
        right = insert( right, 13L, "v13" );
        right = insert( right, 14L, "v14" );

        parent.children[0] = new ReferenceHolder<Page<Long, String>, Long, String>( null, left,
            left.getOffset(), left.getLastOffset() );
        parent.children[1] = new ReferenceHolder<Page<Long, String>, Long, String>( null, target,
            target.getOffset(), target.getLastOffset() );
        parent.children[2] = new ReferenceHolder<Page<Long, String>, Long, String>( null, right,
            right.getOffset(), right.getLastOffset() );

        // Update the parent
        parent.keys[0] = 6L;
        parent.keys[1] = 10L;

        // Now, delete the element from the target page
        DeleteResult<Long, String> result = target.delete( 2L, 7L, null, parent, 1 );

        assertTrue( result instanceof BorrowedFromRightResult );

        BorrowedFromRightResult<Long, String> borrowed = ( BorrowedFromRightResult<Long, String> ) result;
        assertEquals( Long.valueOf( 11L ), borrowed.getModifiedSibling().getKey( 0 ) );
        Tuple<Long, String> removedKey = borrowed.getRemovedElement();

        assertEquals( Long.valueOf( 7L ), removedKey.getKey() );

        // Check the modified leaf
        Leaf<Long, String> newLeaf = ( Leaf<Long, String> ) borrowed.getModifiedPage();

        assertEquals( 4, newLeaf.nbElems );
        assertEquals( Long.valueOf( 6L ), newLeaf.keys[0] );
        assertEquals( Long.valueOf( 8L ), newLeaf.keys[1] );
        assertEquals( Long.valueOf( 9L ), newLeaf.keys[2] );
        assertEquals( Long.valueOf( 10L ), newLeaf.keys[3] );

        // Check the sibling
        Leaf<Long, String> rightSibling = ( Leaf<Long, String> ) borrowed.getModifiedSibling();

        assertEquals( 4, rightSibling.nbElems );
        assertEquals( Long.valueOf( 11L ), rightSibling.keys[0] );
        assertEquals( Long.valueOf( 12L ), rightSibling.keys[1] );
        assertEquals( Long.valueOf( 13L ), rightSibling.keys[2] );
        assertEquals( Long.valueOf( 14L ), rightSibling.keys[3] );
    }


    /**
     * Check that deleting an element from a leaf with N/2 element works when we merge
     * it with one of its sibling, if both has N/2 elements
     * @throws IOException 
     */
    @Test
    public void testDeleteMergeWithSibling() throws IOException
    {
        Node<Long, String> parent = new Node<Long, String>( btree, 1L, 2 );
        Leaf<Long, String> left = new Leaf<Long, String>( btree );
        Leaf<Long, String> target = new Leaf<Long, String>( btree );
        Leaf<Long, String> right = new Leaf<Long, String>( btree );

        // Fill the left page
        left = insert( left, 1L, "v1" );
        left = insert( left, 2L, "v2" );
        left = insert( left, 3L, "v3" );
        left = insert( left, 4L, "v4" );

        // Fill the target page
        target = insert( target, 5L, "v5" );
        target = insert( target, 6L, "v6" );
        target = insert( target, 7L, "v7" );
        target = insert( target, 8L, "v8" );

        // Fill the right page
        right = insert( right, 9L, "v9" );
        right = insert( right, 10L, "v10" );
        right = insert( right, 11L, "v11" );
        right = insert( right, 12L, "v12" );

        parent.children[0] = new ReferenceHolder<Page<Long, String>, Long, String>( null, left,
            left.getOffset(), left.getLastOffset() );
        parent.children[1] = new ReferenceHolder<Page<Long, String>, Long, String>( null, target,
            target.getOffset(), target.getLastOffset() );;
        parent.children[2] = new ReferenceHolder<Page<Long, String>, Long, String>( null, right,
            right.getOffset(), right.getLastOffset() );

        // Update the parent
        parent.keys[0] = 5L;
        parent.keys[1] = 9L;

        // Now, delete the element from the target page
        DeleteResult<Long, String> result = target.delete( 2L, 7L, null, parent, 1 );

        assertTrue( result instanceof MergedWithSiblingResult );

        MergedWithSiblingResult<Long, String> merged = ( MergedWithSiblingResult<Long, String> ) result;
        Tuple<Long, String> removedKey = merged.getRemovedElement();

        assertEquals( Long.valueOf( 7L ), removedKey.getKey() );

        // Check the modified leaf
        Leaf<Long, String> newLeaf = ( Leaf<Long, String> ) merged.getModifiedPage();

        assertEquals( 7, newLeaf.nbElems );
        assertEquals( Long.valueOf( 1L ), newLeaf.keys[0] );
        assertEquals( Long.valueOf( 2L ), newLeaf.keys[1] );
        assertEquals( Long.valueOf( 3L ), newLeaf.keys[2] );
        assertEquals( Long.valueOf( 4L ), newLeaf.keys[3] );
        assertEquals( Long.valueOf( 5L ), newLeaf.keys[4] );
        assertEquals( Long.valueOf( 6L ), newLeaf.keys[5] );
        assertEquals( Long.valueOf( 8L ), newLeaf.keys[6] );
    }


    /**
     * Test the findPos() method
     * @throws Exception
     */
    @Test
    public void testFindPos() throws Exception
    {
        Leaf<Long, String> leaf = new Leaf<Long, String>( btree );

        // Inject the values
        for ( long i = 0; i < 8; i++ )
        {
            long value = i + i + 1;
            leaf = ( Leaf<Long, String> ) ( ( ModifyResult<Long, String> ) leaf.insert( 0L, value, "V" + value ) )
                .getModifiedPage();
        }

        // Check the findPos() method now
        for ( long i = 0; i < 17; i++ )
        {
            if ( i % 2 == 1 )
            {
                assertEquals( -( i / 2 + 1 ), leaf.findPos( i ) );
            }
            else
            {
                assertEquals( i / 2, leaf.findPos( i ) );
            }
        }
    }
}
