/*******************************************************************************
 * Copyright (c) 2005, 2014 SAP, Oracle.  All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     SAP - initial API and implementation
 ******************************************************************************/

package org.eclipse.persistence.testing.framework.wdf;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import javax.sql.DataSource;

import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.testing.framework.junit.JUnitTestCaseHelper;
import org.eclipse.persistence.testing.framework.server.JEEPlatform;
import org.eclipse.persistence.testing.framework.server.ServerPlatform;
import org.eclipse.persistence.testing.tests.feature.TestDataSource;
import org.junit.Assert;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized.Parameters;

@SuppressWarnings("unchecked")
@RunWith(SkipBugzillaTestRunner.class)
public abstract class AbstractBaseTest {

    // /** System variable to set the tests to run on the server. */
    // public static final String RUN_ON_SERVER = "server.run";
    //
    private static Map<String, EntityManagerFactory> emfNamedPersistenceUnits = new Hashtable<String, EntityManagerFactory>();

    /** Determine if the test is running on a JEE server, or in JSE. */

    private static ServerPlatform serverPlatform;

    private final JPAEnvironment environment;
    private final String puName;
    private final static DataSource dataSource;
    protected final static Map EMF_PROPERTIES;

    private static boolean seesJPA2 = (LockModeType.values().length > 2);

    static {
        final DataSource aDataSource;

        if (!ServerInfoHolder.isOnServer()) {
            Map<String, String> properties = JUnitTestCaseHelper.getDatabaseProperties();
            String driver = properties.get(PersistenceUnitProperties.JDBC_DRIVER);
            String url = properties.get(PersistenceUnitProperties.JDBC_URL);
            String user = properties.get(PersistenceUnitProperties.JDBC_USER);
            String password = properties.get(PersistenceUnitProperties.JDBC_PASSWORD);

            Properties userPasswd = new Properties();
            userPasswd.put("user", user);
            userPasswd.put("password", password);

            DataSource ds = new TestDataSource(driver, url, userPasswd);
            aDataSource = new PooledDataSource(ds);

        } else {
            Context context;
            try {
                context = new InitialContext();
                aDataSource = (DataSource) context.lookup(ServerInfoHolder.getDataSourceName()); 
            } catch (NamingException e) {
                throw new RuntimeException(e);
            }
        }
        dataSource = aDataSource;
        EMF_PROPERTIES = new HashMap();
        EMF_PROPERTIES.put("delimited-identifiers", "true");
        EMF_PROPERTIES.put(PersistenceUnitProperties.NON_JTA_DATASOURCE, dataSource);
    }

    public AbstractBaseTest(String name) {
        environment = new ResourceLocalEnvironment();
        puName = name;
    }

    protected final JPAEnvironment getEnvironment() {
        return environment;
    }

    @Parameters
    public static void parameters() {

    }

    final class ResourceLocalEnvironment implements JPAEnvironment {

        @Override
        public void beginTransaction(EntityManager em) {
            em.getTransaction().begin();
        }

        @Override
        public void commitTransaction(EntityManager em) {
            em.getTransaction().commit();
        }

        @Override
        public void commitTransactionAndClear(EntityManager em) {
            try {
                commitTransaction(em);
            } finally {
                em.clear();
            }
        }

        @Override
        public EntityManagerFactory createNewEntityManagerFactory() throws NamingException {
            AbstractBaseTest.closeEntityManagerFactory(puName);
            return Persistence.createEntityManagerFactory(puName, EMF_PROPERTIES);
        }

        @Override
        public EntityManager getEntityManager() {
            return getEntityManagerFactory().createEntityManager();
            // return AbstractBaseTest.createEntityManager(puName,
            // EMF_PROPERTIES);
        }

        @Override
        public EntityManagerFactory getEntityManagerFactory() {
            return AbstractBaseTest.getEntityManagerFactory(puName, EMF_PROPERTIES);
        }

        @Override
        public boolean isTransactionActive(EntityManager em) {
            return em.getTransaction().isActive();
        }

        @Override
        public boolean isTransactionMarkedForRollback(EntityManager em) {
            return em.getTransaction().getRollbackOnly();
        }

        @Override
        public void markTransactionForRollback(EntityManager em) {
            em.getTransaction().setRollbackOnly();
        }

        @Override
        public void rollbackTransaction(EntityManager em) {
            em.getTransaction().rollback();
        }

        @Override
        public void rollbackTransactionAndClear(EntityManager em) {
            try {
                rollbackTransaction(em);
            } finally {
                em.clear();
            }
        }

        @Override
        public boolean usesExtendedPC() {
            return true;
        }

        @Override
        public DataSource getDataSource() {
            return AbstractBaseTest.this.getDataSource();
        }

        @Override
        public EntityManagerFactory createNewEntityManagerFactory(Map properties) throws NamingException {
            Map mergedProperties = new HashMap(); 
            mergedProperties.putAll(EMF_PROPERTIES); 
            mergedProperties.putAll(properties); 
            
            AbstractBaseTest.closeEntityManagerFactory(puName); 
            return Persistence.createEntityManagerFactory(puName, mergedProperties); 
        }

        @Override
        public Object getPropertyValue(EntityManager em, String key) {
            Object delegate = em.getDelegate();
            Method getEntityManagerFactoryMethod;
            try {
                getEntityManagerFactoryMethod = delegate.getClass().getMethod("getEntityManagerFactory");
                Object emf = getEntityManagerFactoryMethod.invoke(delegate);
                
                Method getPropertiesMethod = emf.getClass().getMethod("getProperties");
                @SuppressWarnings("rawtypes")
                Map map = (Map) getPropertiesMethod.invoke(emf);
                
                return map.get(key);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
            
        }
        
        @Override
        public void evict(EntityManager em, Class<?> clazz) {
            Object delegate = em.getDelegate();
            Method getEntityManagerFactoryMethod;
            try {
                getEntityManagerFactoryMethod = delegate.getClass().getMethod("getEntityManagerFactory");
                Object emf = getEntityManagerFactoryMethod.invoke(delegate);
                
                Method getCacheMethod = emf.getClass().getMethod("getCache");
                Object cache =  getCacheMethod.invoke(emf);
                
                Method evictClassMethod = cache.getClass().getMethod("evict", Class.class);
                
                evictClassMethod.invoke(cache, clazz);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
        
        @Override
        public void evictAll(EntityManager em) {
            Object delegate = em.getDelegate();
            Method getEntityManagerFactoryMethod;
            try {
                getEntityManagerFactoryMethod = delegate.getClass().getMethod("getEntityManagerFactory");
                Object emf = getEntityManagerFactoryMethod.invoke(delegate);
                
                Method getCacheMethod = emf.getClass().getMethod("getCache");
                Object cache =  getCacheMethod.invoke(emf);
                
                Method evictClassMethod = cache.getClass().getMethod("evictAll", new Class[]{});
                
                evictClassMethod.invoke(cache, new Object[]{});
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }

    }

    final class JTATxScopedEnvironment implements JPAEnvironment {

        @Override
        public void beginTransaction(EntityManager em) {
            AbstractBaseTest.this.beginTransaction(em);
        }

        @Override
        public void commitTransaction(EntityManager em) {
            AbstractBaseTest.this.commitTransaction(em);
        }

        @Override
        public void commitTransactionAndClear(EntityManager em) {
            try {
                commitTransaction(em);
            } finally {
                em.clear();
            }
        }

        @Override
        public EntityManagerFactory createNewEntityManagerFactory() throws NamingException {
            AbstractBaseTest.closeEntityManagerFactory(puName);
            return Persistence.createEntityManagerFactory(puName, EMF_PROPERTIES);
        }

        @Override
        public EntityManager getEntityManager() {
            return AbstractBaseTest.createEntityManager(puName, EMF_PROPERTIES);
        }

        @Override
        public EntityManagerFactory getEntityManagerFactory() {
            return AbstractBaseTest.getEntityManagerFactory(puName, EMF_PROPERTIES);
        }

        @Override
        public boolean isTransactionActive(EntityManager em) {
            return AbstractBaseTest.this.isTransactionActive(em);
        }

        @Override
        public boolean isTransactionMarkedForRollback(EntityManager em) {
            return AbstractBaseTest.this.getRollbackOnly(em);
        }

        @Override
        public void markTransactionForRollback(EntityManager em) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void rollbackTransaction(EntityManager em) {
            AbstractBaseTest.this.rollbackTransaction(em);
        }

        @Override
        public void rollbackTransactionAndClear(EntityManager em) {
            try {
                rollbackTransaction(em);
            } finally {
                em.clear();
            }
        }

        @Override
        public boolean usesExtendedPC() {
            return true;
        }

        @Override
        public DataSource getDataSource() {
            return AbstractBaseTest.this.getDataSource();
        }

        @Override
        public EntityManagerFactory createNewEntityManagerFactory(Map<String, Object> properties) throws NamingException {
            throw new UnsupportedOperationException();
        }

        @Override
        public Object getPropertyValue(EntityManager em, String key) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void evict(EntityManager em, Class<?> clazz) {
            throw new UnsupportedOperationException();
        }
        
        @Override
        public void evictAll(EntityManager em) {
            throw new UnsupportedOperationException();
        }

    }

    public DataSource getDataSource() {
        return dataSource;
    }

    final public void closeEntityManager(EntityManager em) {
        if (!em.isOpen()) {
            return;
        }
        if (environment.isTransactionActive(em)/*
                                                * ||environment.
                                                * isTransactionMarkedForRollback
                                                * (em)
                                                */) { // FIXME discuss if tx is
            // active if marked for
            // rollback
            environment.rollbackTransaction(em);
        }
        // // close application-managed entity manager
        // if (!(getEnvironment() instanceof JTASharedPCEnvironment)) {
        // em.close(); FIXME
        // }
    }

    protected static void verify(boolean condition, String string) {
        Assert.assertTrue(string, condition);
    }

    protected final void flop(final String msg) {
        Assert.fail(msg);
    }

    abstract protected String[] getClearableTableNames();

    @Before
    public void clearAllTablesAndSetup() throws SQLException {
        clearAllTables();
        
        setup();
    }

    protected void clearAllTables() throws SQLException {
        Connection conn = getDataSource().getConnection();
        try {
            conn.setAutoCommit(false);

            Set<String> existingTables = new HashSet<String>();
            DatabaseMetaData metaData = conn.getMetaData();
            String schemaPattern = null;
            if (metaData.getDriverName().toLowerCase().contains("oracle")) {
                schemaPattern = metaData.getUserName().toUpperCase();
            }
            ResultSet rs = metaData.getTables(null, schemaPattern, "TMP_%", null);
            try {
                while (rs.next()) {
                    existingTables.add(rs.getString("TABLE_NAME").toUpperCase(Locale.ENGLISH));
                }
            } finally {
                rs.close();
            }

            Statement statement = conn.createStatement();
            try {
                if (existingTables.contains("TMP_CASC_NODE")) {
                    statement.executeUpdate("update TMP_CASC_NODE set PARENT = null");
                }
                if (existingTables.contains("TMP_NODE")) {
                    statement.executeUpdate("update TMP_NODE set PARENT = null");
                }
                if (existingTables.contains("TMP_COP")) {
                    statement.executeUpdate("update TMP_COP set PARTNER_ID = null");
                }
                if (existingTables.contains("TMP_UCOFFICE_CUBICLE")) {
                    statement.executeUpdate("delete from TMP_UCOFFICE_CUBICLE");
                    existingTables.remove("TMP_UCOFFICE_CUBICLE");
                }
                for (String name : getClearableTableNames()) {
                    if (existingTables.contains(name.toUpperCase(Locale.ENGLISH))) {
                        statement.executeUpdate("delete from " + name);
                    }
                }
            } catch (SQLException ex) {
                System.err.println(ex.getMessage());
                throw ex;
            } finally {
                statement.close();
            }
            conn.commit();
        } finally {
            conn.close();
        }
        
        // TODO evictAll
        
    }

    /**
     * intended to be overwritten by subclasses
     */
    protected void setup() throws SQLException {

    }

    public static final <T extends Serializable> T serializeDeserialize(T serializable) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
        try {
            objectOutputStream.writeObject(serializable);
        } finally {
            objectOutputStream.close();
        }
        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
        return (T) new ObjectInputStream(in).readObject();
    }

    public static boolean isInsideEngine() {
        return false;
    }

    /**
     * Checks whether the given throwable is of type
     * java.lang.IllegalStateException, or otherwise if the throwable contains a
     * java.lang.IllegalStateException somewhere in the cause stack.
     * 
     * @param e
     *            The throwable to check
     * @return <code>true</code> if the throwable is instance of or caused by
     *         java.lang.IllegalStateException
     */
    protected final boolean checkForIllegalStateException(Throwable e) {
        boolean contained = false;
        while (e != null) {
            if (e instanceof IllegalStateException) {
                contained = true;
                break;
            }
            e = e.getCause();
        }
        return contained;
    }

    /**
     * Checks whether the given throwable is of type java.sql.SQLException, or
     * otherwise if the throwable contains a java.sql.SQLException somewhere in
     * the cause stack.
     * 
     * @param e
     *            The throwable to check
     * @return <code>true</code> if the throwable is instance of or caused by
     *         java.sql.SQLException
     */
    protected final boolean checkForSQLException(Throwable e) {
        boolean contained = false;
        while (e != null) {
            if (e instanceof SQLException) {
                contained = true;
                break;
            }
            e = e.getCause();
        }
        return contained;
    }

    /**
     * Return if the transaction is active. This allows the same code to be used
     * on the server where JTA is used.
     */
    public boolean isTransactionActive(EntityManager entityManager) {
        if (ServerInfoHolder.isOnServer()) {
            return getServerPlatform().isTransactionActive();
        } else {
            return entityManager.getTransaction().isActive();
        }
    }

    /**
     * Return if the transaction is roll back only. This allows the same code to
     * be used on the server where JTA is used.
     */
    public boolean getRollbackOnly(EntityManager entityManager) {
        if (ServerInfoHolder.isOnServer()) {
            return getServerPlatform().getRollbackOnly();
        } else {
            return entityManager.getTransaction().getRollbackOnly();
        }
    }

    /**
     * Begin a transaction on the entity manager. This allows the same code to
     * be used on the server where JTA is used.
     */
    public void beginTransaction(EntityManager entityManager) {
        if (ServerInfoHolder.isOnServer()) {
            getServerPlatform().beginTransaction();
        } else {
            entityManager.getTransaction().begin();
        }
    }

    /**
     * Commit a transaction on the entity manager. This allows the same code to
     * be used on the server where JTA is used.
     */
    public void commitTransaction(EntityManager entityManager) {
        if (ServerInfoHolder.isOnServer()) {
            getServerPlatform().commitTransaction();
        } else {
            entityManager.getTransaction().commit();
        }
    }

    /**
     * Rollback a transaction on the entity manager. This allows the same code
     * to be used on the server where JTA is used.
     */
    public void rollbackTransaction(EntityManager entityManager) {
        if (ServerInfoHolder.isOnServer()) {
            getServerPlatform().rollbackTransaction();
        } else {
            entityManager.getTransaction().rollback();
        }
    }

    /**
     * Return the server platform if running in JEE.
     */
    public static ServerPlatform getServerPlatform() {
        if (serverPlatform == null) {
            serverPlatform = new JEEPlatform();
        }
        return serverPlatform;
    }

    /**
     * Create a new entity manager for the "default" persistence unit. If in JEE
     * this will create or return the active managed entity manager.
     */
    public static EntityManager createEntityManager() {
        if (ServerInfoHolder.isOnServer()) {
            return getServerPlatform().getEntityManager("default");
        } else {
            return getEntityManagerFactory().createEntityManager();
        }
    }

    /**
     * Create a new entity manager for the persistence unit using the
     * properties. The properties will only be used the first time this entity
     * manager is accessed. If in JEE this will create or return the active
     * managed entity manager.
     */
    public static EntityManager createEntityManager(String persistenceUnitName, Map<String, String> properties) {
        if (ServerInfoHolder.isOnServer()) {
            return getServerPlatform().getEntityManager(persistenceUnitName);
        } else {
            return getEntityManagerFactory(persistenceUnitName, properties).createEntityManager();
        }
    }

    public static EntityManagerFactory getEntityManagerFactory(String persistenceUnitName) {
        return getEntityManagerFactory(persistenceUnitName, JUnitTestCaseHelper.getDatabaseProperties());
    }

    public static EntityManagerFactory getEntityManagerFactory(String persistenceUnitName, Map<String, String> properties) {
        if (ServerInfoHolder.isOnServer()) {
            return getServerPlatform().getEntityManagerFactory(persistenceUnitName);
        } else {
            EntityManagerFactory emfNamedPersistenceUnit = (EntityManagerFactory) emfNamedPersistenceUnits.get(persistenceUnitName);
            if (emfNamedPersistenceUnit == null) {
                emfNamedPersistenceUnit = Persistence.createEntityManagerFactory(persistenceUnitName, properties);
                emfNamedPersistenceUnits.put(persistenceUnitName, emfNamedPersistenceUnit);
            }
            return emfNamedPersistenceUnit;
        }
    }

    public static EntityManagerFactory getEntityManagerFactory() {
        return getEntityManagerFactory("default");
    }

    public static void closeEntityManagerFactory() {
        closeEntityManagerFactory("default");
    }

    public static void closeEntityManagerFactory(String persistenceUnitName) {
        EntityManagerFactory emfNamedPersistenceUnit = (EntityManagerFactory) emfNamedPersistenceUnits.get(persistenceUnitName);
        if (emfNamedPersistenceUnit != null) {
            if (emfNamedPersistenceUnit.isOpen()) {
                emfNamedPersistenceUnit.close();
            }
            emfNamedPersistenceUnits.remove(persistenceUnitName);
        }
    }

    public static boolean seesJPA2() {
        return seesJPA2;
    }

    public static Map<String, String> getTestProperties() {
        if (ServerInfoHolder.isOnServer()) {
            return ServerInfoHolder.getTestProperties();
        } else {
            return JUnitTestCaseHelper.getDatabaseProperties();
        }

    }
}
