/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2009, 2013 Oracle and/or its affiliates.  All rights reserved.
 *
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;
using NUnit.Framework;
using BerkeleyDB;

namespace CsharpAPITest
{
	[TestFixture]
	public class TransactionTest : CSharpTestFixture
	{

		private DatabaseEnvironment deadLockEnv;

		[TestFixtureSetUp]
		public void SetUpTestFixture()
		{
			testFixtureName = "TransactionTest";
			base.SetUpTestfixture();

			DatabaseEnvironment.Remove(testFixtureHome);
		}

		[Test, ExpectedException(typeof(ExpectedTestException))]
		public void TestAbort()
		{
			testName = "TestAbort";
			SetUpTest(true);

			DatabaseEnvironment env;
			Transaction txn;
			BTreeDatabase db;

			/* 
			 * Open an environment and begin a transaction. Open
			 * a db and write a record the db within this transaction.
			 */ 
			PutRecordWithTxn(out env, testHome, testName, out txn);

			// Abort the transaction.
			txn.Abort();

			/* 
			 * Undo all operations in the transaction so the
			 * database couldn't be reopened.
			 */ 
			try
			{
				OpenBtreeDBInEnv(testName + ".db", env,
				    out db, false, null);
			}
			catch (DatabaseException)
			{
				throw new ExpectedTestException();
			}
			finally
			{
				env.Close();
			}
		}

		[Test]
		public void TestCommit()
		{
			testName = "TestCommit";
			SetUpTest(true);

			DatabaseEnvironment env;
			Transaction txn;
			BTreeDatabase db;

			/* 
			 * Open an environment and begin a transaction. Open
			 * a db and write a record the db within this transaction.
			 */ 
			PutRecordWithTxn(out env, testHome, testName, out txn);

			// Commit the transaction.
			txn.Commit();

			// Reopen the database.
			OpenBtreeDBInEnv(testName + ".db", env,
			    out db, false, null);

			/*
			 * Confirm that the record("key", "data") exists in the
			 * database.
			 */
			try
			{
				db.GetBoth(new DatabaseEntry(
				    ASCIIEncoding.ASCII.GetBytes("key")),
				    new DatabaseEntry(
				    ASCIIEncoding.ASCII.GetBytes("data")));
			}
			catch (DatabaseException)
			{
				throw new TestException();
			}
			finally
			{
				db.Close();
				env.Close();
			}
		}

		[Test]
		public void TestDiscard()
		{
			DatabaseEnvironment env;
			byte[] gid;

			testName = "TestDiscard";
			SetUpTest(true);

			/*
			 * Open an environment and begin a transaction
			 * called "transaction". Within the transacion, open a 
			 * database, write a record and close it. Then prepare 
			 * the transaction and panic the environment.
			 */
			PanicPreparedTxn(testHome, testName, out env, out gid);

			/*
			 * Recover the environment. 	Log and db files are not 
			 * destoyed so run normal recovery. Recovery should 
			 * use DB_CREATE and DB_INIT_TXN flags when 
			 * opening the environment.
			 */
			DatabaseEnvironmentConfig envConfig
			    = new DatabaseEnvironmentConfig();
			envConfig.RunRecovery = true;
			envConfig.Create = true;
			envConfig.UseTxns = true;
			envConfig.UseMPool = true;
			env = DatabaseEnvironment.Open(testHome, envConfig);

			PreparedTransaction[] preparedTxns
			    = new PreparedTransaction[10];
			preparedTxns = env.Recover(10, true);

			Assert.AreEqual(gid, preparedTxns[0].GlobalID);
			preparedTxns[0].Txn.Discard();
			try {
				preparedTxns[0].Txn.Commit();
				throw new TestException();
			} catch (AccessViolationException) {
			} finally {
				env.Close();
			}
		}

		[Test]
		public void TestPrepare()
		{
			testName = "TestPrepare";
			SetUpTest(true);

			DatabaseEnvironment env;
			byte[] gid;

			/*
			 * Open an environment and begin a transaction
			 * called "transaction". Within the transacion, open a 
			 * database, write a record and close it. Then prepare 
			 * the transaction and panic the environment.
			 */
			PanicPreparedTxn(testHome, testName, out env, out gid);

			/*
			 * Recover the environment. 	Log and db files are not 
			 * destoyed so run normal recovery. Recovery should 
			 * use DB_CREATE and DB_INIT_TXN flags when 
			 * opening the environment.
			 */
			DatabaseEnvironmentConfig envConfig
			    = new DatabaseEnvironmentConfig();
			envConfig.RunRecovery = true;
			envConfig.Create = true;
			envConfig.UseTxns = true;
			envConfig.UseMPool = true;
			env = DatabaseEnvironment.Open(testHome, envConfig);

			// Reopen the database.
			BTreeDatabase db;
			OpenBtreeDBInEnv(testName + ".db", env, out db, 
			    false, null);

			/*
			 * Confirm that record("key", "data") exists in the 
			 * database.
			 */
			DatabaseEntry key, data;
			key = new DatabaseEntry(
			    ASCIIEncoding.ASCII.GetBytes("key"));
			data = new DatabaseEntry(
			    ASCIIEncoding.ASCII.GetBytes("data"));
			try
			{
				db.GetBoth(key, data);
			}
			catch (DatabaseException)
			{
				throw new TestException();
			}
			finally
			{
				db.Close();
				env.Close();
			}
			
		}

		public void PanicPreparedTxn(string home, string dbName,
		    out DatabaseEnvironment env, out byte[] globalID)
		{
			Transaction txn;

			// Put record into database within transaction.
			PutRecordWithTxn(out env, home, dbName, out txn);

			/*
			 * Generate global ID for the transaction. Copy 
			 * transaction ID to the first 4 tyes in global ID.
			 */
			globalID = new byte[Transaction.GlobalIdLength];
			byte[] txnID = new byte[4];
			txnID = BitConverter.GetBytes(txn.Id);
			for (int i = 0; i < txnID.Length; i++)
				globalID[i] = txnID[i];

			// Prepare the transaction.
			txn.Prepare(globalID);

			// Panic the environment.
			env.Panic();

		}

		[Test]
		public void TestTxnName()
		{
			DatabaseEnvironment env;
			Transaction txn;

			testName = "TestTxnName";
			SetUpTest(true);

			SetUpTransactionalEnv(testHome, out env);
			txn = env.BeginTransaction();
			txn.Name = testName;
			Assert.AreEqual(testName, txn.Name);
			txn.Commit();
			env.Close();
		}

		[Test]
		public void TestTxnPriority() {
			DatabaseEnvironment env;
			Transaction txn;

			testName = "TestTxnPriority";
			SetUpTest(true);

			SetUpTransactionalEnv(testHome, out env);
			txn = env.BeginTransaction();
			txn.Priority = 555;
			Assert.AreEqual(555, txn.Priority);
			txn.Commit();
			env.Close();
		}

		[Test]
		public void TestSetLockTimeout()
		{
			testName = "TestSetLockTimeout";
			SetUpTest(true);

			// Set lock timeout.
			TestTimeOut(true);

		}

		[Test]
		public void TestSetTxnTimeout()
		{
			testName = "TestSetTxnTimeout";
			SetUpTest(true);

			// Set transaction time out.
			TestTimeOut(false);

		}

		/*
		 * ifSetLock is used to indicate which timeout function 
		 * is used, SetLockTimeout or SetTxnTimeout.
		 */
		public void TestTimeOut(bool ifSetLock)
		{
			// Open environment and begin transaction.
			Transaction txn;
			deadLockEnv = null;
			SetUpEnvWithTxnAndLocking(testHome,
			    out deadLockEnv, out txn, 0, 0, 0, 0);

			// Define deadlock detection and resolve policy.
			deadLockEnv.DeadlockResolution =
			    DeadlockPolicy.YOUNGEST;
			if (ifSetLock == true)
				txn.SetLockTimeout(10);
			else
				txn.SetTxnTimeout(10);

			txn.Commit();
			deadLockEnv.Close();
		}

		public static void SetUpEnvWithTxnAndLocking(string envHome,
		    out DatabaseEnvironment env, out Transaction txn,
		    uint maxLock, uint maxLocker, uint maxObject, uint partition)
		{
			// Configure env and locking subsystem.
			LockingConfig lkConfig = new LockingConfig();

			/*
			 * If the maximum number of locks/lockers/objects
			 * is given, then the LockingConfig is set. Unless, 
			 * it is not set to any value.  
			 */
			if (maxLock != 0)
				lkConfig.MaxLocks = maxLock;
			if (maxLocker != 0)
				lkConfig.MaxLockers = maxLocker;
			if (maxObject != 0)
				lkConfig.MaxObjects = maxObject;
			if (partition != 0)
				lkConfig.Partitions = partition;

			DatabaseEnvironmentConfig envConfig =
				new DatabaseEnvironmentConfig();
			envConfig.Create = true;
			envConfig.UseTxns = true;
			envConfig.UseMPool = true;
			envConfig.LockSystemCfg = lkConfig;
			envConfig.UseLocking = true;
			envConfig.NoLocking = false;
			env = DatabaseEnvironment.Open(envHome, envConfig);
			txn = env.BeginTransaction();
		}

		public void PutRecordWithTxn(out DatabaseEnvironment env,
		    string home, string dbName, out Transaction txn)
		{
			BTreeDatabase db;

			// Open a new environment and begin a transaction.
			SetUpTransactionalEnv(home, out env);
			TransactionConfig txnConfig = new TransactionConfig();
			txnConfig.Name = "Transaction";
			txn = env.BeginTransaction(txnConfig);
			Assert.AreEqual("Transaction", txn.Name);

			// Open a new database within the transaction.
			OpenBtreeDBInEnv(dbName + ".db", env, out db, true, txn);

			// Write to the database within the transaction.
			WriteOneIntoBtreeDBWithTxn(db, txn);

			// Close the database.
			db.Close();
		}

		public void SetUpTransactionalEnv(string home, 
		    out DatabaseEnvironment env)
		{
			DatabaseEnvironmentConfig envConfig = 
			    new DatabaseEnvironmentConfig();
			envConfig.Create = true;
			envConfig.UseLogging = true;
			envConfig.UseLocking = true;
			envConfig.UseMPool = true;
			envConfig.UseTxns = true;
			env = DatabaseEnvironment.Open(
			    home, envConfig);
		}

		public void OpenBtreeDBInEnv(string dbName,
		    DatabaseEnvironment env, out BTreeDatabase db,
		    bool create, Transaction txn)
		{
			BTreeDatabaseConfig btreeDBConfig =
			    new BTreeDatabaseConfig();
			btreeDBConfig.Env = env;
			if (create == true)
				btreeDBConfig.Creation = CreatePolicy.IF_NEEDED;
			else
				btreeDBConfig.Creation = CreatePolicy.NEVER;
			if (txn == null)
				db = BTreeDatabase.Open(dbName,
				    btreeDBConfig);
			else
				db = BTreeDatabase.Open(dbName,
				    btreeDBConfig, txn);
		}

		public void WriteOneIntoBtreeDBWithTxn(BTreeDatabase db,
		    Transaction txn)
		{
			DatabaseEntry key, data;

			key = new DatabaseEntry(
			    ASCIIEncoding.ASCII.GetBytes("key"));
			data = new DatabaseEntry(
			    ASCIIEncoding.ASCII.GetBytes("data"));
			db.Put(key, data, txn);
		}
	}
}
