/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2002-2006
 *      Oracle Corporation.  All rights reserved.
 *
 * $Id: CursorEdgeTest.java,v 1.32 2006/09/12 19:17:12 cwl Exp $
 */

package com.sleepycat.je;

import java.io.File;

import junit.framework.TestCase;

import com.sleepycat.je.config.EnvironmentParams;
import com.sleepycat.je.junit.JUnitThread;
import com.sleepycat.je.util.TestUtils;

/**
 * Test edge case in cursor traversals. In particular, look at duplicates and 
 * sets of keys interspersed with deletions.
 */
public class CursorEdgeTest extends TestCase {

    private static final boolean DEBUG = false;
    private Environment env;
    private File envHome;
    private boolean operationStarted;

    public CursorEdgeTest() {
        envHome = new File(System.getProperty(TestUtils.DEST_DIR));
    }

    public void setUp()
	throws Exception {

        TestUtils.removeLogFiles("Setup", envHome, false);

        /* 
         * Create an environment w/transactions and a max node size of 6.
         * Be sure to disable the compressor, we want some holes in the
         * tree.
         */
        EnvironmentConfig envConfig = TestUtils.initEnvConfig();
        envConfig.setTransactional(true);
        envConfig.setConfigParam(EnvironmentParams.NODE_MAX.getName(), "6");
        envConfig.setConfigParam(EnvironmentParams.ENV_RUN_INCOMPRESSOR.getName(),
                                 "false");
        envConfig.setAllowCreate(true);
        env = new Environment(envHome, envConfig);
    }
    
    public void tearDown()
	throws Exception {
        
        try {
            env.close();
        } catch (Throwable e) {
            System.out.println("Exception during tearDown");
            e.printStackTrace();
        }
	env = null;
        TestUtils.removeLogFiles("TearDown", envHome, false);
    }

    /**
     * Insert a number of duplicates, making sure that the duplicate tree
     * has multiple bins. Make sure that we can skip over the duplicates and
     * find the right value.
     */
    public void testSearchOnDuplicatesWithDeletions()
	throws Throwable {

        Database myDb = null;
        Cursor cursor = null;
	try {
            /* Set up a db */
            DatabaseConfig dbConfig = new DatabaseConfig();
            dbConfig.setTransactional(true);
            dbConfig.setSortedDuplicates(true);
            dbConfig.setAllowCreate(true);
            myDb = env.openDatabase(null, "foo", dbConfig);

            /* 
             * Insert k1/d1, then a duplicate range of k2/d1 -> k2/d15, then
             * k3/d1. Now delete the beginning part of the duplicate
             * range, trying to get more than a whole bin's worth
             * (k2/d1 -> k2/d7). Because the compressor is not
             * enabled, there will be a hole in the k2 range. While
             * we're at it, delete k2/d10 - k2/d13 too, make sure we
             * can traverse a hole in the middle of the duplicate
             * range.
             */
            DatabaseEntry key = new DatabaseEntry();
            DatabaseEntry data = new DatabaseEntry();
            key.setData(TestUtils.getTestArray(1));  
            data.setData(TestUtils.getTestArray(1)); 
            myDb.put(null, key, data);          // k1/d1
            key.setData(TestUtils.getTestArray(3));  
            myDb.put(null, key, data);          // k3/d1

            /* insert k2 range */
            key.setData(TestUtils.getTestArray(2));  
            for (int i = 1; i <= 15; i++) {
                data.setData(TestUtils.getTestArray(i));
                myDb.put(null, key, data);
            }

            /* Now delete k2/d1 -> k2/d7 */
            Transaction txn =
		env.beginTransaction(null, TransactionConfig.DEFAULT);
            cursor = myDb.openCursor(txn, CursorConfig.DEFAULT);
            assertEquals(OperationStatus.SUCCESS,
			 cursor.getSearchKey(key, data, LockMode.DEFAULT));
            for (int i = 0; i < 7; i ++) {
                assertEquals(OperationStatus.SUCCESS, cursor.delete());
                assertEquals(OperationStatus.SUCCESS,
			     cursor.getNext(key, data, LockMode.DEFAULT));
            }

            /* Also delete k2/d10 - k2/d13 */
            data.setData(TestUtils.getTestArray(10));
            assertEquals(OperationStatus.SUCCESS,
			 cursor.getSearchBoth(key, data, LockMode.DEFAULT));
            for (int i = 0; i < 3; i ++) {
                assertEquals(OperationStatus.SUCCESS, cursor.delete());
                assertEquals(OperationStatus.SUCCESS,
			     cursor.getNext(key, data, LockMode.DEFAULT));
            }

            /* Double check what's in the tree */
            if (DEBUG) {
                Cursor checkCursor = myDb.openCursor(txn,
						     CursorConfig.DEFAULT);
                while (checkCursor.getNext(key, data, LockMode.DEFAULT) ==
		       OperationStatus.SUCCESS) {
                    System.out.println("key=" +
                                       TestUtils.getTestVal(key.getData()) +
                                       " data=" +
                                       TestUtils.getTestVal(data.getData()));
                }
                checkCursor.close();
            }
            cursor.close();
            cursor = null;
            txn.commit();

            /* 
             * Now make sure we can find k2/d8 
             */
            Cursor readCursor = myDb.openCursor(null, CursorConfig.DEFAULT);
            key.setData(TestUtils.getTestArray(2));
            
            /* Use key search */
            assertEquals(OperationStatus.SUCCESS,
			 readCursor.getSearchKey(key, data, LockMode.DEFAULT));
            assertEquals(2, TestUtils.getTestVal(key.getData()));
            assertEquals(8, TestUtils.getTestVal(data.getData()));

            /* Use range search */
            assertEquals(OperationStatus.SUCCESS,
			 readCursor.getSearchKeyRange(key, data,
						      LockMode.DEFAULT));
            assertEquals(2, TestUtils.getTestVal(key.getData()));
            assertEquals(8, TestUtils.getTestVal(data.getData()));

            /* Use search both */
            data.setData(TestUtils.getTestArray(8));
            assertEquals(OperationStatus.SUCCESS,
			 readCursor.getSearchBoth(key, data,
						  LockMode.DEFAULT));
            assertEquals(2, TestUtils.getTestVal(key.getData()));
            assertEquals(8, TestUtils.getTestVal(data.getData()));

            /* Use search both range, starting data at 8 */
            data.setData(TestUtils.getTestArray(8));
            assertEquals(OperationStatus.SUCCESS,
			 readCursor.getSearchBothRange(key, data,
						       LockMode.DEFAULT));
            assertEquals(2, TestUtils.getTestVal(key.getData()));
            assertEquals(8, TestUtils.getTestVal(data.getData()));

            /* Use search both range, starting at 1 */
            data.setData(TestUtils.getTestArray(1));
            assertEquals(OperationStatus.SUCCESS,
			 readCursor.getSearchBothRange(key, data,
						       LockMode.DEFAULT));
            assertEquals(2, TestUtils.getTestVal(key.getData()));
            assertEquals(8, TestUtils.getTestVal(data.getData()));

            /* 
             * Make sure we can find k2/d13 with a range search.
             */

            /* 
	     * Insert a set of duplicates, k5/d0 -> k5/d9, then delete
             * all of them (but don't compress). Make sure no form of
             * search every finds them.
             */
            key.setData(TestUtils.getTestArray(5));  
            for (int i = 0; i < 10; i++) {
                data.setData(TestUtils.getTestArray(i));
                myDb.put(null, key, data);
            }
            myDb.delete(null, key);  // delete all k5's

            /* All searches on key 5 should fail */
            assertFalse(readCursor.getSearchKey(key, data, LockMode.DEFAULT) ==
			OperationStatus.SUCCESS);
            assertFalse(readCursor.getSearchKeyRange(key, data,
						     LockMode.DEFAULT) ==
			OperationStatus.SUCCESS);
            data.setData(TestUtils.getTestArray(0));
            assertFalse(readCursor.getSearchBoth(key, data,
						 LockMode.DEFAULT) ==
			OperationStatus.SUCCESS);
            assertFalse(readCursor.getSearchBothRange(key, data,
						      LockMode.DEFAULT) ==
			OperationStatus.SUCCESS);

            /* All ranges on key 4 should also fail. */
            key.setData(TestUtils.getTestArray(4));
            assertFalse(readCursor.getSearchKeyRange(key, data,
						     LockMode.DEFAULT) ==
			OperationStatus.SUCCESS);
            assertFalse(readCursor.getSearchBothRange(key, data,
                                                      LockMode.DEFAULT) ==
			OperationStatus.SUCCESS);

            readCursor.close();
	} catch (Throwable t) {

	    t.printStackTrace();
	    throw t;
	} finally {
            if (cursor != null) {
                cursor.close();
            }
            myDb.close();
        }
    }

    /**
     * Test the case where we allow duplicates in the database, but
     * don't actually insert a duplicate.  So we have a key/value pair
     * and do a getSearchBothRange using key and data-1 (i.e. we land
     * on the key, but just before the data in the dup set (which isn't
     * a dup set since there's only one).  getSearchBothRange should land
     * on the key/value pair in this case.  See SR #9248.
     */
    public void testSearchBothWithOneDuplicate()
	throws Throwable {

        Database myDb = null;
        Cursor cursor = null;
	try {
            /* Set up a db */
            DatabaseConfig dbConfig = new DatabaseConfig();
            dbConfig.setTransactional(true);
            dbConfig.setSortedDuplicates(true);
            dbConfig.setAllowCreate(true);
            myDb = env.openDatabase(null, "foo", dbConfig);

            /* Put one record */
            DatabaseEntry key = new DatabaseEntry();
            DatabaseEntry data = new DatabaseEntry();
            key.setData(TestUtils.getTestArray(1));  
            data.setData(TestUtils.getTestArray(1)); 
            myDb.put(null, key, data);
            
            key.setData(TestUtils.getTestArray(1));  
            data.setData(TestUtils.getTestArray(0)); 
            cursor = myDb.openCursor(null, CursorConfig.DEFAULT);
            OperationStatus status =
		cursor.getSearchBothRange(key, data, LockMode.DEFAULT);
            assertSame(status, OperationStatus.SUCCESS);
            assertEquals(1, TestUtils.getTestVal(key.getData()));
            assertEquals(1, TestUtils.getTestVal(data.getData()));
	} finally {
            if (cursor != null) {
                cursor.close();
            }
            if (myDb != null) {
                myDb.close();
            }
        }
    }

    /**
     * Tests a bug fix to CursorImpl.fetchCurrent [#11195].
     *
     * T1 inserts K1-D1 and holds WRITE on K1-D1 (no dup tree yet)
     * T2 calls getFirst and waits for READ on K1-D1
     * T1 inserts K1-D2 which creates the dup tree
     * T1 commits, allowing T2 to proceed
     *
     * T2 is in the middle of CursorImpl.fetchCurrent, and assumes incorrectly
     * that it has a lock on an LN in BIN; actually the LN was replaced by a
     * DIN and a ClassCastException occurs.
     */
    public void testGetCurrentDuringDupTreeCreation()
	throws Throwable {

        /* Set up a db */
        DatabaseConfig dbConfig = new DatabaseConfig();
        dbConfig.setTransactional(true);
        dbConfig.setSortedDuplicates(true);
        dbConfig.setAllowCreate(true);
        final Database myDb = env.openDatabase(null, "foo", dbConfig);

        /* T1 inserts K1-D1. */
        Transaction t1 = env.beginTransaction(null, null);
        DatabaseEntry key = new DatabaseEntry();
        DatabaseEntry data = new DatabaseEntry();
        key.setData(TestUtils.getTestArray(1));  
        data.setData(TestUtils.getTestArray(1)); 
        myDb.put(t1, key, data);

        /* T2 calls getFirst. */
        JUnitThread thread = new JUnitThread("getFirst") {
            public void testBody() 
                throws DatabaseException {
                DatabaseEntry key = new DatabaseEntry();
                DatabaseEntry data = new DatabaseEntry();
                Transaction t2 = env.beginTransaction(null, null);
                operationStarted = true;
                Cursor cursor = myDb.openCursor(t2, null);
                OperationStatus status = cursor.getFirst(key, data, null);
                assertEquals(1, TestUtils.getTestVal(key.getData()));
                assertEquals(1, TestUtils.getTestVal(data.getData()));
                assertEquals(OperationStatus.SUCCESS, status);
                cursor.close();
                t2.commitNoSync();
            }
        };
        thread.start();
        while (!operationStarted) {
            Thread.yield();
        }
        Thread.sleep(10);

        /* T1 inserts K1-D2. */
        key.setData(TestUtils.getTestArray(1));  
        data.setData(TestUtils.getTestArray(2)); 
        myDb.put(t1, key, data);
        t1.commitNoSync();

        try {
            thread.finishTest();
        } catch (Throwable e) {
            e.printStackTrace();
            fail(e.toString());
        }
        myDb.close();
    }

    /**
     * Tests a bug fix to CursorImpl.fetchCurrent [#11700] that caused
     * ArrayIndexOutOfBoundsException.
     */
    public void testGetPrevNoDupWithEmptyTree()
	throws Throwable {

        OperationStatus status;

        /*
         * Set up a db
         */
        DatabaseConfig dbConfig = new DatabaseConfig();
        dbConfig.setSortedDuplicates(true);
        dbConfig.setAllowCreate(true);
        Database myDb = env.openDatabase(null, "foo", dbConfig);

        /*
         * Insert two sets of duplicates.
         */
        DatabaseEntry key = new DatabaseEntry();
        DatabaseEntry data = new DatabaseEntry();

        key.setData(TestUtils.getTestArray(1));  
        data.setData(TestUtils.getTestArray(1)); 
        myDb.put(null, key, data);
        data.setData(TestUtils.getTestArray(2)); 
        myDb.put(null, key, data);

        key.setData(TestUtils.getTestArray(2));  
        data.setData(TestUtils.getTestArray(1)); 
        myDb.put(null, key, data);
        data.setData(TestUtils.getTestArray(2)); 
        myDb.put(null, key, data);

        /*
         * Delete all duplicates with a cursor.
         */
        Cursor cursor = myDb.openCursor(null, null);
        while ((status = cursor.getNext(key, data, null)) ==
                OperationStatus.SUCCESS) {
            cursor.delete();
        }

        /*
         * Compress to empty the two DBINs.  The BIN will not be deleted
         * because a cursor is attached to it.  This causes a cursor to be
         * positioned on an empty DBIN, which brings out the bug.
         */
        env.compress();

        /*
         * Before the bug fix, getPrevNoDup caused
         * ArrayIndexOutOfBoundsException.
         */
        status = cursor.getPrevNoDup(key, data, null);
        assertEquals(OperationStatus.NOTFOUND, status);

        cursor.close();
        myDb.close();
    }

    /* 
     * Check that non transactional cursors can't do update operations
     * against a transactional database.
     */
    public void testNonTxnalCursorNoUpdates() 
        throws Throwable {

        Database myDb = null;
        SecondaryDatabase mySecDb = null;
        Cursor cursor = null;
        SecondaryCursor secCursor = null;
	try {
            /* Set up a db with a secondary, insert something. */
            DatabaseConfig dbConfig = new DatabaseConfig();
            dbConfig.setTransactional(true);
            dbConfig.setAllowCreate(true);
            myDb = env.openDatabase(null, "foo", dbConfig);

            SecondaryConfig secConfig = new SecondaryConfig();
            secConfig.setTransactional(true); 
            secConfig.setAllowCreate(true);
            secConfig.setKeyCreator(new KeyCreator());
            mySecDb = env.openSecondaryDatabase(null, "fooSecDb", myDb,
                                                secConfig);

            /* Insert something. */
            DatabaseEntry key = new DatabaseEntry(new byte[1]);
            assertEquals(myDb.put(null, key, key), OperationStatus.SUCCESS);

            /* Open a non-txnal cursor on the primary database. */
            cursor = myDb.openCursor(null, null);
            DatabaseEntry data = new DatabaseEntry();
            assertEquals(OperationStatus.SUCCESS,
                         cursor.getNext(key, data, LockMode.DEFAULT));

            /* All updates should be prohibited. */
            updatesShouldBeProhibited(cursor);

            /* Open a secondary non-txnal cursor */
            secCursor = mySecDb.openSecondaryCursor(null, null);
            assertEquals(OperationStatus.SUCCESS,
                         secCursor.getNext(key, data, LockMode.DEFAULT));

            /* All updates should be prohibited. */
            updatesShouldBeProhibited(secCursor);

	} catch (Throwable t) {
	    t.printStackTrace();
	    throw t;
	} finally {
            if (secCursor != null) {
                secCursor.close();
            }

            if (cursor != null) {
                cursor.close();
            }

            if (mySecDb != null) {
                mySecDb.close();
            }

            myDb.close();
        }
    }

    /* Updates should not be possible with this cursor. */
    private void updatesShouldBeProhibited(Cursor cursor) 
        throws Exception {
        
        try {
            cursor.delete();
            fail("Should not be able to do a delete");
        } catch (DatabaseException e) {
            checkForTransactionException(e);
        }

        DatabaseEntry key = new DatabaseEntry(new byte[0]);
        DatabaseEntry data = new DatabaseEntry(new byte[0]);

        try {
            cursor.put(key, data);
            fail("Should not be able to do a put");
        } catch (UnsupportedOperationException e) {
            /* disregard for secondary cursors */
        } catch (DatabaseException e) {
            checkForTransactionException(e);
        }


        try {
            cursor.putCurrent(data);
            fail("Should not be able to do a putCurrent");
        } catch (UnsupportedOperationException e) {
            /* disregard for secondary cursors */
        } catch (DatabaseException e) {
            checkForTransactionException(e);
        }

        try {
            cursor.putNoDupData(key, data);
            fail("Should not be able to do a putNoDupData");
        } catch (UnsupportedOperationException e) {
            /* disregard for secondary cursors */
        } catch (DatabaseException e) {
            checkForTransactionException(e);
        }

        try {
            cursor.putNoOverwrite(key, data);
            fail("Should not be able to do a putNoOverwrite");
        } catch (UnsupportedOperationException e) {
            /* disregard for secondary cursors */
        } catch (DatabaseException e) {
            checkForTransactionException(e);
        }
    }

    private void checkForTransactionException(DatabaseException e) {
        /* 
         * Check that it's a transaction problem. Crude, but since we
         * don't want to add exception types, necessary.
         */
        String eMsg = e.getMessage();
        assertTrue(TestUtils.skipVersion(e).startsWith("A transaction was not supplied"));
    }

    private static class KeyCreator implements SecondaryKeyCreator {
        public boolean createSecondaryKey(SecondaryDatabase secondaryDb,
                                          DatabaseEntry keyEntry,
                                          DatabaseEntry dataEntry,
                                          DatabaseEntry resultEntry) {
            resultEntry.setData(dataEntry.getData());
            return true;
        }
    }
}
