/** Tests around the logic for savepoints. */
@Category(ArchitectureSpecific.class)
public class SavepointsTest {

  private static final byte[] DESTINATION_TABLE = Bytes.toBytes("1216");

  private static SITestEnv testEnv;
  private static TestTransactionSetup transactorSetup;

  private TxnLifecycleManager control;
  private final List<Txn> createdParentTxns = Lists.newArrayList();
  private TxnStore txnStore;

  @Before
  public void setUp() throws Exception {
    if (testEnv == null) {
      testEnv = SITestEnvironment.loadTestEnvironment();
      transactorSetup = new TestTransactionSetup(testEnv, true);
    }
    control =
        new ForwardingLifecycleManager(transactorSetup.txnLifecycleManager) {
          @Override
          protected void afterStart(Txn txn) {
            createdParentTxns.add(txn);
          }
        };
    txnStore = transactorSetup.txnStore;
  }

  @After
  public void tearDown() throws Exception {
    for (Txn txn : createdParentTxns) {
      txn.rollback(); // rollback the transaction to prevent contamination
    }
  }

  @Test
  public void testCreateSavepoint() throws Exception {
    Txn parent = control.beginTransaction(DESTINATION_TABLE);
    TransactionImpl transaction = new TransactionImpl("user", parent, false, control);

    int res = transaction.setSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);

    res = transaction.setSavePoint("second", null);

    Assert.assertEquals("Wrong txn stack size", 3, res);

    res = transaction.rollbackToSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);
  }

  @Test(expected = SavePointNotFoundException.class)
  public void testSavepointNotFound() throws Exception {
    Txn parent = control.beginTransaction(DESTINATION_TABLE);
    TransactionImpl transaction = new TransactionImpl("user", parent, false, control);

    int res = transaction.setSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);

    transaction.rollbackToSavePoint("second", null);
  }

  @Test
  public void testSavepointsAreActive() throws Exception {
    Txn parent = control.beginTransaction(DESTINATION_TABLE);
    TransactionImpl transaction = new TransactionImpl("user", parent, false, control);

    int res = transaction.setSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);

    transaction.elevate(DESTINATION_TABLE);

    Txn parent2 = control.beginTransaction();

    long[] ids = txnStore.getActiveTransactionIds(parent2, DESTINATION_TABLE);
    Assert.assertEquals("Incorrect size", 2, ids.length);
    Assert.assertArrayEquals(
        "Incorrect values", new long[] {parent.getTxnId(), transaction.getTxn().getTxnId()}, ids);

    res = transaction.rollbackToSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);

    ids = txnStore.getActiveTransactionIds(parent2, DESTINATION_TABLE);
    Assert.assertEquals("Incorrect size", 1, ids.length);
    Assert.assertArrayEquals("Incorrect values", new long[] {parent.getTxnId()}, ids);
  }

  @Test
  public void testReleaseSavepoint() throws Exception {
    Txn parent = control.beginTransaction(DESTINATION_TABLE);
    TransactionImpl transaction = new TransactionImpl("user", parent, false, control);

    int res = transaction.setSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);
    transaction.elevate(DESTINATION_TABLE);
    TxnView first = transaction.getTxn();

    res = transaction.setSavePoint("second", null);

    Assert.assertEquals("Wrong txn stack size", 3, res);
    transaction.elevate(DESTINATION_TABLE);
    Txn second = transaction.getTxn();

    // release first, should also commit second
    res = transaction.releaseSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 1, res);

    Assert.assertEquals(Txn.State.COMMITTED, first.getState());
    Assert.assertEquals(Txn.State.COMMITTED, second.getState());
  }

  @Test
  public void testRollbackSavepoint() throws Exception {
    Txn parent = control.beginTransaction(DESTINATION_TABLE);
    TransactionImpl transaction = new TransactionImpl("user", parent, false, control);

    int res = transaction.setSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);
    transaction.elevate(DESTINATION_TABLE);
    TxnView first = transaction.getTxn();

    res = transaction.setSavePoint("second", null);

    Assert.assertEquals("Wrong txn stack size", 3, res);
    transaction.elevate(DESTINATION_TABLE);
    Txn second = transaction.getTxn();

    // rollback to first, should rollback second
    res = transaction.rollbackToSavePoint("first", null);

    Assert.assertEquals("Wrong txn stack size", 2, res);

    Assert.assertEquals(Txn.State.ROLLEDBACK, first.getState());
    Assert.assertEquals(Txn.State.ROLLEDBACK, second.getState());
  }

  @Test
  public void testElevateWholeStack() throws Exception {
    Txn parent = control.beginTransaction();
    TransactionImpl transaction = new TransactionImpl("user", parent, false, control);
    Assert.assertFalse(transaction.getTxn().allowsWrites());

    int res = transaction.setSavePoint("first", null);
    Assert.assertEquals("Wrong txn stack size", 2, res);
    Assert.assertFalse(transaction.getTxn().allowsWrites());

    res = transaction.setSavePoint("second", null);
    Assert.assertEquals("Wrong txn stack size", 3, res);
    Assert.assertFalse(transaction.getTxn().allowsWrites());

    transaction.elevate(DESTINATION_TABLE);
    Assert.assertTrue(transaction.getTxn().allowsWrites());

    res = transaction.releaseSavePoint("second", null);
    Assert.assertEquals("Wrong txn stack size", 2, res);
    Assert.assertTrue(transaction.getTxn().allowsWrites());

    res = transaction.releaseSavePoint("first", null);
    Assert.assertEquals("Wrong txn stack size", 1, res);
    Assert.assertTrue(transaction.getTxn().allowsWrites());

    transaction.commit();
  }
}