package org.ovirt.engine.core.utils;

import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import junit.framework.Assert;

import org.jboss.embedded.Bootstrap;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

import org.ovirt.engine.core.common.businessentities.bookmarks;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.compat.TransactionScopeOption;
import org.ovirt.engine.core.dal.dbbroker.DbFacade;
import org.ovirt.engine.core.utils.ejb.ContainerManagedResourceType;
import org.ovirt.engine.core.utils.ejb.EjbUtils;
import org.ovirt.engine.core.utils.ejb.JBossEmbeddedEJBUtilsStrategy;
import org.ovirt.engine.core.utils.transaction.TransactionMethod;
import org.ovirt.engine.core.utils.transaction.TransactionSupport;

@Ignore("Running these tests requires manual configuration of timeouts. They should be revisited when TransactionSupport is changed")
public class TransactionSupportTest {

    private static TransactionManager tm;
    private static Bootstrap bootstrap;
    private Transaction outerTransaction;
    private Transaction innerTransaction;

    /**
     * Initializing the embadded Jboss and looking-up the transaction manager.
     *
     * @throws Exception
     */
    @BeforeClass
    public static void initState() throws Exception {
        try {
            bootstrap = Bootstrap.getInstance();
            if (!bootstrap.isStarted()) {
                bootstrap.bootstrap();
                EjbUtils.setStrategy(new JBossEmbeddedEJBUtilsStrategy());
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            throw ex;
        }

        tm = EjbUtils.findResource(ContainerManagedResourceType.TRANSACTION_MANAGER);
    }

    @Before
    public void cleanState() {
        outerTransaction = null;
        innerTransaction = null;
    }

    /**
     * Testing that opening a transaction within another transaction indeed opens a new transaction. Testing that the
     * transactions status are as expected during the whole test.
     */
    @Test
    public void runInNewTransaction() {
        TransactionSupport.executeInScope(TransactionScopeOption.RequiresNew, new TransactionMethod<Object>() {
            @Override
            public Object runInTransaction() {
                try {
                    outerTransaction = tm.getTransaction();
                } catch (SystemException e) {
                    e.printStackTrace();
                }
                validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                // open a new transaction
                TransactionSupport.executeInNewTransaction(new TransactionMethod<Object>() {
                    @Override
                    public Object runInTransaction() {
                        try {
                            innerTransaction = tm.getTransaction();
                        } catch (SystemException e) {
                            e.printStackTrace();
                        }
                        // validate that the inner and outer
                        // transactions are not equals
                        Assert.assertFalse(outerTransaction.equals(innerTransaction));
                        validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                        validateStatus(Status.STATUS_ACTIVE, innerTransaction);
                        return null;
                    }
                });
                validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                validateStatus(Status.STATUS_COMMITTED, innerTransaction);
                return null;
            }
        });
        validateStatus(Status.STATUS_COMMITTED, outerTransaction);
    }

    /**
     * This test validates that changing inner transaction status to STATUS_MARKED_ROLLBACK The inner transaction is
     * rolled back even if no exception was thrown.
     */
    @Test
    public void runInNewTransactionAndRollBack() {
        TransactionSupport.executeInScope(TransactionScopeOption.RequiresNew, new TransactionMethod<Object>() {
            @Override
            public Object runInTransaction() {
                try {
                    outerTransaction = tm.getTransaction();
                } catch (SystemException e) {
                    e.printStackTrace();
                }
                validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                // open a new transaction
                TransactionSupport.executeInNewTransaction(new TransactionMethod<Object>() {
                    @Override
                    public Object runInTransaction() {
                        try {
                            innerTransaction = tm.getTransaction();
                            innerTransaction.setRollbackOnly();
                        } catch (SystemException e) {
                            e.printStackTrace();
                        }
                        // validate that the inner and outer
                        // transactions are not equals
                        Assert.assertFalse(outerTransaction.equals(innerTransaction));
                        validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                        validateStatus(Status.STATUS_MARKED_ROLLBACK, innerTransaction);
                        return null;

                    }
                });
                validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                validateStatus(Status.STATUS_ROLLEDBACK, innerTransaction);
                return null;
            }
        });
        validateStatus(Status.STATUS_COMMITTED, outerTransaction);
    }

    /**
     * Testing that if inner transaction fails - the outer transaction still commits.
     */
    @Test
    public void runInNewTransactionWithInnerErrors() {
        TransactionSupport.executeInScope(TransactionScopeOption.RequiresNew, new TransactionMethod<Object>() {
            @Override
            public Object runInTransaction() {
                try {
                    outerTransaction = tm.getTransaction();
                } catch (SystemException e) {
                    e.printStackTrace();
                }
                validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                try {
                    // open a new transaction
                    TransactionSupport.executeInNewTransaction(new TransactionMethod<Object>() {
                        @Override
                        public Object runInTransaction() {
                            try {
                                innerTransaction = tm.getTransaction();
                            } catch (SystemException e) {
                                e.printStackTrace();
                            }
                            // validate that the inner and outer
                            // transactions are not equals
                            Assert.assertFalse(outerTransaction.equals(innerTransaction));
                            validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                            validateStatus(Status.STATUS_ACTIVE, innerTransaction);
                            throw new RuntimeException("failing inner transaction");
                        }
                    });
                } catch (Exception e) {
                    System.out.println("Exception for inner transaction with msg: " + e.getMessage());
                } finally {
                    validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                    validateStatus(Status.STATUS_ROLLEDBACK, innerTransaction);
                }
                return null;
            }
        });
        validateStatus(Status.STATUS_COMMITTED, outerTransaction);

    }

    /**
     * Testing that if inner transaction throws exception and fails - the outer transaction fails as well.
     */
    @Test
    public void runInNewTransactionWithErrors() {
        try {
            TransactionSupport.executeInScope(TransactionScopeOption.RequiresNew, new TransactionMethod<Object>() {
                @Override
                public Object runInTransaction() {
                    try {
                        outerTransaction = tm.getTransaction();
                    } catch (SystemException e) {
                        e.printStackTrace();
                    }
                    validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                    try {
                        // open a new transaction
                        TransactionSupport.executeInNewTransaction(new TransactionMethod<Object>() {
                            @Override
                            public Object runInTransaction() {
                                try {
                                    innerTransaction = tm.getTransaction();
                                } catch (SystemException e) {
                                    e.printStackTrace();
                                }
                                // validate that the inner and
                                // outer transactions are not
                                // equals
                                Assert.assertFalse(outerTransaction.equals(innerTransaction));
                                validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                                validateStatus(Status.STATUS_ACTIVE, innerTransaction);
                                throw new RuntimeException("failing inner transaction");
                            }
                        });
                    } finally {
                        validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                        validateStatus(Status.STATUS_ROLLEDBACK, innerTransaction);
                    }
                    return null;
                }
            });

        } catch (Exception e) {
            validateStatus(Status.STATUS_ROLLEDBACK, outerTransaction);
        }
    }

    private DbFacade dbFacade;
    private DbFacade dbFacade2;

    private void createMsFacade() throws ClassNotFoundException {
        this.getClass().getClassLoader().loadClass("com.microsoft.sqlserver.jdbc.SQLServerDriver");
        SingleConnectionDataSource dataSource = new SingleConnectionDataSource(
                "jdbc:sqlserver://10.35.113.39\\SQLEXPRESS:1433;databaseName=engine", "sa", "ENGINEadmin2009!", true);
        dbFacade = new DbFacade();
                dbFacade.setTemplate(new JdbcTemplate(dataSource));
        SingleConnectionDataSource dataSource2 = new SingleConnectionDataSource(
                "jdbc:sqlserver://10.35.113.39\\SQLEXPRESS:1433;databaseName=engine", "sa", "ENGINEadmin2009!", true);
        dbFacade2 = new DbFacade();
                dbFacade2.setTemplate(new JdbcTemplate(dataSource2));
        // org.springframework.jdbc.datasource.DataSourceTransactionManager
    }

    /**
     * Testing that opening a transaction within another transaction indeed opens a new transaction. Testing that the
     * transactions status are as expected during the whole test.
     */
    // @Test
    public void runInNewDBTransaction() throws Exception {

        createMsFacade();

        TransactionSupport.executeInScope(TransactionScopeOption.RequiresNew, new TransactionMethod<Object>() {
            @Override
            public Object runInTransaction() {
                try {
                    outerTransaction = tm.getTransaction();
                } catch (SystemException e) {
                    e.printStackTrace();
                }
                // validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                // TAKE DB lOCK
                bookmarks b = new bookmarks();
                b.setbookmark_id(Guid.NewGuid());
                b.setbookmark_name("MyBookmark");
                b.setbookmark_value("test");
                dbFacade.getBookmarkDAO().save(b);
                // open a new transaction
                TransactionSupport.executeInNewTransaction(new TransactionMethod<Object>() {
                    @Override
                    public Object runInTransaction() {
                        try {
                            innerTransaction = tm.getTransaction();
                        } catch (SystemException e) {
                            e.printStackTrace();
                        }
                        // TAKE DB lOCK
                        // open a new transaction
                        bookmarks c = dbFacade2.getBookmarkDAO().getByName("MyBookmark");
                        c.setbookmark_value("test2");
                        dbFacade2.getBookmarkDAO().update(c);
                        throw new RuntimeException();
                        // validate that the inner and outer
                        // transactions are not equals
                        // Assert.assertFalse(outerTransaction.equals(innerTransaction));
                        // validateStatus(Status.STATUS_ACTIVE,
                        // outerTransaction);
                        // validateStatus(Status.STATUS_ACTIVE,
                        // innerTransaction);
                        // return null;
                    }
                });
                // validateStatus(Status.STATUS_ACTIVE, outerTransaction);
                // validateStatus(Status.STATUS_COMMITTED, innerTransaction);
                return null;
            }
        });
        // validateStatus(Status.STATUS_COMMITTED, outerTransaction);
    }

    private static void validateStatus(int expected, Transaction t) {
        try {
            Assert.assertEquals(expected, t.getStatus());
        } catch (SystemException e) {
            e.printStackTrace();
        }
    }
}
