/**
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.openejb.core.cmp.jpa;

import org.apache.openejb.test.entity.cmp.BasicCmpBean;
import org.apache.openejb.test.entity.cmp.ComplexCmpBean;
import org.apache.openejb.test.entity.cmp.UnknownCmpBean;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.sql.DataSource;
import javax.transaction.Status;
import javax.transaction.TransactionManager;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Set;

public class JpaTestObject extends org.junit.Assert {
    private EntityManagerFactory entityManagerFactory;
    private TransactionManager transactionManager;
    private DataSource nonJtaDs;

    private EntityManager entityManager;
    private EntityTransaction transaction;
    private static final String MCLAUGHLIN = "Brett McLaughlin";
    private static final String FLANAGAN = "David Flanagan";
    private static final String TIGER = "Java 5.0 Tiger";
    private static final String JAVASCRIPT = "JavaScript";
    private static final String ENTERPRISE_APPS = "Building Java Enterprise Applications";

    public EntityManagerFactory getEntityManagerFactory() {
        return entityManagerFactory;
    }

    public void setEntityManagerFactory(final EntityManagerFactory entityManagerFactory) {
        this.entityManagerFactory = entityManagerFactory;
    }

    public TransactionManager getTransactionManager() {
        return transactionManager;
    }

    public void setTransactionManager(final TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public DataSource getNonJtaDs() {
        return nonJtaDs;
    }

    public void setNonJtaDs(final DataSource nonJtaDs) {
        this.nonJtaDs = nonJtaDs;
    }

    public void setUp() throws Exception {
    }

    public void tearDown() throws Exception {
        if (entityManager != null && entityManager.isOpen()) {
            if (transactionManager == null) {
                try {
                    if (entityManager.getTransaction().getRollbackOnly()) {
                        entityManager.getTransaction().rollback();
                    } else {
                        entityManager.getTransaction().commit();
                    }
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            } else {
                try {
                    if (transactionManager.getStatus() != Status.STATUS_NO_TRANSACTION) {
                        transactionManager.rollback();
                    }
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            }
            entityManager.close();
        }

        nonJtaDs = null;
        entityManager = null;
        entityManagerFactory = null;
        transactionManager = null;
    }

    public void jpaLifecycle() throws Exception {
        newDeleteNew();
        employeeTest();
        billTest();
        cmpText();
        complexCmpText();
        unknownCmpText();
        manytoMany();

//        beginTx();
//        ABean_ABean a = new ABean_ABean();
//        a.setField1(2);
//        entityManager.persist(a);
//        BBean_BBean b = new BBean_BBean();
//        b.setField1(22);
//        entityManager.persist(b);
//        commitTx();
//
//        beginTx();
//        b = entityManager.find(BBean_BBean.class, 22);
//        a = entityManager.find(ABean_ABean.class, 2);
//        a.OpenEJB_addCmr("b", b);
//        b.OpenEJB_addCmr("a", a);
//        commitTx();
//
////        dump();
//
//        beginTx();
//        b = entityManager.find(BBean_BBean.class, 22);
//        assertNotNull(b);
//        assertEquals(new Integer(22), b.getField1());
////        a = (ABean_ABean) b.OpenEJB_getCmr("a");
////        assertNotNull(a);
////        assertEquals(new Integer(2), a.getField1());
//        commitTx();
    }

    private void newDeleteNew() throws Exception {
        beginTx();

        // Create new
        Person dain = new Person();
        dain.setName("dain");
        assertFalse(entityManager.contains(dain));
        entityManager.persist(dain);
        entityManager.flush();
        dain = entityManager.merge(dain);
        assertTrue(entityManager.contains(dain));

        // Find and verify
        dain = entityManager.find(Person.class, "dain");
        assertNotNull(dain);
        assertEquals("dain", dain.getName());

        // Delete
        entityManager.remove(dain);
        assertFalse(entityManager.contains(dain));

        // Recreate
        dain = new Person();
        dain.setName("dain");
        assertFalse(entityManager.contains(dain));
        entityManager.persist(dain);
        entityManager.flush();
        dain = entityManager.merge(dain);
        assertTrue(entityManager.contains(dain));

        // Find and verify
        dain = entityManager.find(Person.class, "dain");
        assertNotNull(dain); // <<<<<<< FAILS
        assertEquals("dain", dain.getName());

        commitTx();
    }

    private void employeeTest() throws Exception {
        // get the pk for the inserted row
        int davidPk = -1;
        {
            Connection connection = null;
            Statement statement = null;
            try {
                connection = nonJtaDs.getConnection();
                statement = connection.createStatement();
                final ResultSet resultSet = statement.executeQuery("select * from employee");
                resultSet.next();
                davidPk = resultSet.getInt("id");
            } finally {
                close(statement);
                close(connection);
            }
        }

        beginTx();
        assertTrue(entityManager.isOpen());

        Employee david = entityManager.find(Employee.class, davidPk);
        assertTrue(entityManager.contains(david));

        assertEquals(david.getId(), davidPk);
        assertEquals(david.getFirstName(), "David");
        assertEquals(david.getLastName(), "Blevins");
        commitTx();

        beginTx();

        david = entityManager.find(Employee.class, davidPk);
        assertTrue(entityManager.contains(david));

        assertEquals(david.getId(), davidPk);
        assertEquals(david.getFirstName(), "David");
        assertEquals(david.getLastName(), "Blevins");

        commitTx();
        beginTx();

        david = (Employee) entityManager.createQuery("select e from Employee e where e.firstName='David'").getSingleResult();
        assertTrue(entityManager.contains(david));

        assertEquals(david.getId(), davidPk);
        assertEquals(david.getFirstName(), "David");
        assertEquals(david.getLastName(), "Blevins");

        commitTx();
        beginTx();

        david = entityManager.find(Employee.class, davidPk);
        entityManager.remove(david);
        assertFalse(entityManager.contains(david));
        david = entityManager.find(Employee.class, davidPk);
        assertNull(david);

        commitTx();
        beginTx();

        Employee dain = new Employee();
        dain.setFirstName("Dain");
        dain.setLastName("Sundstrom");
        assertFalse(entityManager.contains(dain));

        entityManager.persist(dain);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        dain = entityManager.merge(dain);
        final int dainId = dain.getId();

        assertTrue(entityManager.contains(dain));

        commitTx();


        beginTx();

        dain = entityManager.find(Employee.class, dainId);
        assertTrue(entityManager.contains(dain));

        assertEquals(dain.getId(), dainId);
        assertEquals(dain.getFirstName(), "Dain");
        assertEquals(dain.getLastName(), "Sundstrom");

        commitTx();
        beginTx();

        dain = (Employee) entityManager.createQuery("select e from Employee e").getSingleResult();
        assertTrue(entityManager.contains(dain));

        assertEquals(dain.getId(), dainId);
        assertEquals(dain.getFirstName(), "Dain");
        assertEquals(dain.getLastName(), "Sundstrom");

        commitTx();
    }

    public void billTest() throws Exception {
        // get the pk for the inserted row
        BillPk basicPk;

        {
            Connection connection = null;
            Statement statement = null;
            try {
                connection = nonJtaDs.getConnection();
                statement = connection.createStatement();
                final ResultSet resultSet = statement.executeQuery("select * from bill");
                resultSet.next();

                basicPk = new BillPk(resultSet.getLong("billNumber"), resultSet.getLong("billVersion"), resultSet.getLong("billRevision"));
            } finally {
                close(statement);
                close(connection);
            }
        }

        beginTx();
        assertTrue(entityManager.isOpen());

        Bill basic = entityManager.find(Bill.class, basicPk);
        assertTrue(entityManager.contains(basic));

        assertTrue(basicPk.equals(new BillPk(1, 0, 0)));

        assertEquals(basic.getBillNumber(), 1);
        assertEquals(basic.getBillVersion(), 0);
        assertEquals(basic.getBillRevision(), 0);
        assertEquals(basic.getBillDescription(), "Basic Model");

        commitTx();
        beginTx();

        basic = (Bill) entityManager.createQuery("select e from Bill e where e.billDescription='Basic Model'").getSingleResult();
        assertTrue(entityManager.contains(basic));

        assertTrue(basicPk.equals(new BillPk(1, 0, 0)));

        assertEquals(basic.getBillNumber(), 1);
        assertEquals(basic.getBillVersion(), 0);
        assertEquals(basic.getBillRevision(), 0);
        assertEquals(basic.getBillDescription(), "Basic Model");

        commitTx();
        beginTx();

        basic = entityManager.find(Bill.class, basicPk);
        entityManager.remove(basic);
        assertFalse(entityManager.contains(basic));
        basic = entityManager.find(Bill.class, basicPk);
        assertNull(basic);

        commitTx();
        beginTx();

        Bill advanced = new Bill();
        advanced.setBillNumber(1000);
        advanced.setBillVersion(2000);
        advanced.setBillRevision(3000);
        advanced.setBillDescription("This is so advanced!!");
        assertFalse(entityManager.contains(advanced));

        entityManager.persist(advanced);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        advanced = entityManager.merge(advanced);

        final BillPk advancedPk = new BillPk(advanced.getBillNumber(), advanced.getBillVersion(), advanced.getBillRevision());

        assertTrue(entityManager.contains(advanced));

        commitTx();
        beginTx();

        advanced = entityManager.find(Bill.class, advancedPk);
        assertTrue(entityManager.contains(advanced));

        assertTrue(advancedPk.equals(new BillPk(1000, 2000, 3000)));
        assertEquals(advanced.getBillNumber(), 1000);
        assertEquals(advanced.getBillVersion(), 2000);
        assertEquals(advanced.getBillRevision(), 3000);
        assertEquals(advanced.getBillDescription(), "This is so advanced!!");

        commitTx();
        beginTx();

        advanced = (Bill) entityManager.createQuery("select e from Bill e").getSingleResult();
        assertTrue(entityManager.contains(advanced));

        assertTrue(advancedPk.equals(new BillPk(1000, 2000, 3000)));
        assertEquals(advanced.getBillNumber(), 1000);
        assertEquals(advanced.getBillVersion(), 2000);
        assertEquals(advanced.getBillRevision(), 3000);
        assertEquals(advanced.getBillDescription(), "This is so advanced!!");

        commitTx();
    }

    private void cmpText() throws Exception {
        beginTx();

        BasicCmpBean basicCmpBean = new BasicCmpBean_Subclass();
        basicCmpBean.ejbCreateObject("Joe Blow");
        assertFalse(entityManager.contains(basicCmpBean));

        entityManager.persist(basicCmpBean);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        basicCmpBean = entityManager.merge(basicCmpBean);
        final int joeId = basicCmpBean.getPrimaryKey();

        assertTrue(entityManager.contains(basicCmpBean));

        commitTx();
        beginTx();

        basicCmpBean = new BasicCmpBean_Subclass();
        basicCmpBean.ejbCreateObject("Lloyd Dobler");
        assertFalse(entityManager.contains(basicCmpBean));

        entityManager.persist(basicCmpBean);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        basicCmpBean = entityManager.merge(basicCmpBean);
        final int lloydId = basicCmpBean.getPrimaryKey();

        assertTrue(entityManager.contains(basicCmpBean));
        commitTx();


        beginTx();

        final BasicCmpBean joe = (BasicCmpBean) entityManager.createQuery("select e from BasicCmpBean_Subclass e where e.primaryKey=" + joeId).getSingleResult();
        assertTrue(entityManager.contains(joe));

        assertEquals(joe.getPrimaryKey(), joeId);
        assertEquals(joe.getFirstName(), "Joe");
        assertEquals(joe.getLastName(), "Blow");

        final BasicCmpBean lloyd = (BasicCmpBean) entityManager.createQuery("select e from BasicCmpBean_Subclass e where e.primaryKey=" + lloydId).getSingleResult();
        assertTrue(entityManager.contains(lloyd));

        assertEquals(lloyd.getPrimaryKey(), lloydId);
        assertEquals(lloyd.getFirstName(), "Lloyd");
        assertEquals(lloyd.getLastName(), "Dobler");

        commitTx();
    }

    private void complexCmpText() throws Exception {
        beginTx();

        ComplexCmpBean complexCmpBean = new ComplexCmpBean_Subclass();
        complexCmpBean.ejbCreateObject("Joe Blow");
        assertFalse(entityManager.contains(complexCmpBean));

        entityManager.persist(complexCmpBean);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        complexCmpBean = entityManager.merge(complexCmpBean);

        assertTrue(entityManager.contains(complexCmpBean));

        commitTx();
        beginTx();

        complexCmpBean = new ComplexCmpBean_Subclass();
        complexCmpBean.ejbCreateObject("Lloyd Dobler");
        assertFalse(entityManager.contains(complexCmpBean));

        entityManager.persist(complexCmpBean);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        complexCmpBean = entityManager.merge(complexCmpBean);

        assertTrue(entityManager.contains(complexCmpBean));
        commitTx();


        beginTx();

        final ComplexCmpBean joe = (ComplexCmpBean) entityManager.createQuery("select e from ComplexCmpBean_Subclass e where e.firstName='Joe'").getSingleResult();
        assertTrue(entityManager.contains(joe));

        assertEquals(joe.getFirstName(), "Joe");
        assertEquals(joe.getLastName(), "Blow");

        final ComplexCmpBean lloyd = (ComplexCmpBean) entityManager.createQuery("select e from ComplexCmpBean_Subclass e where e.firstName='Lloyd'").getSingleResult();
        assertTrue(entityManager.contains(lloyd));

        assertEquals(lloyd.getFirstName(), "Lloyd");
        assertEquals(lloyd.getLastName(), "Dobler");

        commitTx();
    }

    private void unknownCmpText() throws Exception {
        beginTx();

        UnknownCmpBean unknownCmpBean = new UnknownCmpBean_Subclass();
        unknownCmpBean.ejbCreateObject("Joe Blow");
        assertFalse(entityManager.contains(unknownCmpBean));

        entityManager.persist(unknownCmpBean);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        unknownCmpBean = entityManager.merge(unknownCmpBean);
        final Object joeId = unknownCmpBean.getPrimaryKey();

        assertTrue(entityManager.contains(unknownCmpBean));

        commitTx();
        beginTx();

        unknownCmpBean = new UnknownCmpBean_Subclass();
        unknownCmpBean.ejbCreateObject("Lloyd Dobler");
        assertFalse(entityManager.contains(unknownCmpBean));

        entityManager.persist(unknownCmpBean);

        // extract primary key seems to require a flush followed by a merge
        entityManager.flush();
        unknownCmpBean = entityManager.merge(unknownCmpBean);
        final Object lloydId = unknownCmpBean.getPrimaryKey();

        assertTrue(entityManager.contains(unknownCmpBean));
        commitTx();


        beginTx();

        final UnknownCmpBean joe = (UnknownCmpBean) entityManager.createQuery("select e from UnknownCmpBean_Subclass e where e.firstName='Joe'").getSingleResult();
        assertTrue(entityManager.contains(joe));

        assertEquals(joe.getPrimaryKey(), joeId);
        assertEquals(joe.getFirstName(), "Joe");
        assertEquals(joe.getLastName(), "Blow");

        final UnknownCmpBean lloyd = (UnknownCmpBean) entityManager.createQuery("select e from UnknownCmpBean_Subclass e where e.firstName='Lloyd'").getSingleResult();
        assertTrue(entityManager.contains(lloyd));

        assertEquals(lloyd.getPrimaryKey(), lloydId);
        assertEquals(lloyd.getFirstName(), "Lloyd");
        assertEquals(lloyd.getLastName(), "Dobler");

        commitTx();
    }

    private void manytoMany() throws Exception {
        beginTx();

        final AuthorBean mcLaughlin = new AuthorBean(MCLAUGHLIN);
        final AuthorBean flanagan = new AuthorBean(FLANAGAN);

        final BookBean tiger = new BookBean(TIGER);
        final BookBean javaScript = new BookBean(JAVASCRIPT);

        final BookBean enterpriseApps = new BookBean(ENTERPRISE_APPS);

        entityManager.persist(mcLaughlin);
        entityManager.persist(flanagan);
        entityManager.persist(tiger);
        entityManager.persist(javaScript);
        entityManager.persist(enterpriseApps);

        commitTx();

        link(MCLAUGHLIN, TIGER);
        link(MCLAUGHLIN, ENTERPRISE_APPS);
        link(FLANAGAN, TIGER);
        link(FLANAGAN, JAVASCRIPT);

        beginTx();

        final BookBean book = entityManager.find(BookBean.class, TIGER);
        assertTrue(entityManager.contains(book));

        assertEquals(book.getTitle(), TIGER);
        final Set<AuthorBean> authors = book.getAuthors();
        assertEquals(authors.size(), 2);

        commitTx();
    }

    private void link(final String authorName, final String title) throws Exception {
        beginTx();
        final AuthorBean author = entityManager.find(AuthorBean.class, authorName);
        assertNotNull("Author not found " + authorName, author);

        final BookBean book = entityManager.find(BookBean.class, title);
        assertNotNull("Book not found " + title, book);

        book.OpenEJB_addCmr("authors", author);
        author.OpenEJB_addCmr("books", book);
        commitTx();
    }

    private void createEntityManager() {
        entityManager = entityManagerFactory.createEntityManager();

//        OpenJPAEntityManager openjpaEM = (OpenJPAEntityManager) entityManager;
//        openjpaEM.addLifecycleListener(new JpaTestObject.OpenJPALifecycleListener(), (Class[])null);
    }

    private void beginTx() throws Exception {
        createEntityManager();

        final String msg = "BEGIN_TX";
        log(msg);
        if (transactionManager != null) {
            transactionManager.begin();
            entityManager.joinTransaction();
        } else {
            transaction = entityManager.getTransaction();
            transaction.begin();
        }
    }

    public void log(final String msg) {
//        System.out.println(msg);
    }

    private void commitTx() throws Exception {
        log("  BEFORE_COMMIT_TX");
        try {
            if (transactionManager != null) {
                transactionManager.commit();
            } else {
                transaction.commit();
            }
        } finally {
            if (entityManager != null && entityManager.isOpen()) {
                entityManager.close();
            }
            entityManager = null;
            log("AFTER_COMMIT_TX");
        }
    }

//    private class OpenJPALifecycleListener extends AbstractLifecycleListener {
//        protected void eventOccurred(LifecycleEvent event) {
//            int type = event.getType();
//            switch (type) {
//                case LifecycleEvent.BEFORE_PERSIST:
//                    log("  BEFORE_PERSIST");
//                    break;
//                case LifecycleEvent.AFTER_PERSIST:
//                    log("  AFTER_PERSIST");
//                    break;
//                case LifecycleEvent.AFTER_LOAD:
//                    log("  AFTER_LOAD");
//                    break;
//                case LifecycleEvent.BEFORE_STORE:
//                    log("  BEFORE_STORE");
//                    break;
//                case LifecycleEvent.AFTER_STORE:
//                    log("  AFTER_STORE");
//                    break;
//                case LifecycleEvent.BEFORE_CLEAR:
//                    log("  BEFORE_CLEAR");
//                    break;
//                case LifecycleEvent.AFTER_CLEAR:
//                    log("  AFTER_CLEAR");
//                    break;
//                case LifecycleEvent.BEFORE_DELETE:
//                    log("  BEFORE_DELETE");
//                    break;
//                case LifecycleEvent.AFTER_DELETE:
//                    log("  AFTER_DELETE");
//                    break;
//                case LifecycleEvent.BEFORE_DIRTY:
//                    log("  BEFORE_DIRTY");
//                    break;
//                case LifecycleEvent.AFTER_DIRTY:
//                    log("  AFTER_DIRTY");
//                    break;
//                case LifecycleEvent.BEFORE_DIRTY_FLUSHED:
//                    log("  BEFORE_DIRTY_FLUSHED");
//                    break;
//                case LifecycleEvent.AFTER_DIRTY_FLUSHED:
//                    log("  AFTER_DIRTY_FLUSHED");
//                    break;
//                case LifecycleEvent.BEFORE_DETACH:
//                    log("  BEFORE_DETACH");
//                    break;
//                case LifecycleEvent.AFTER_DETACH:
//                    log("  AFTER_DETACH");
//                    break;
//                case LifecycleEvent.BEFORE_ATTACH:
//                    log("  BEFORE_ATTACH");
//                    break;
//                case LifecycleEvent.AFTER_ATTACH:
//                    log("  AFTER_ATTACH");
//                    break;
//                case LifecycleEvent.AFTER_REFRESH:
//                    log("  AFTER_REFRESH");
//                    break;
//                default:
//                    log("  default");
//                    break;
//            }
//            super.eventOccurred(event);
//        }
//
//        public void beforePersist(LifecycleEvent event) {
//            eventOccurred(event);
//        }
//
//        public void afterRefresh(LifecycleEvent event) {
//            eventOccurred(event);
//        }
//
//        public void beforeDetach(LifecycleEvent event) {
//            eventOccurred(event);
//        }
//
//        public void afterDetach(LifecycleEvent event) {
//            eventOccurred(event);
//        }
//
//        public void beforeAttach(LifecycleEvent event) {
//            eventOccurred(event);
//        }
//
//        public void afterAttach(LifecycleEvent event) {
//            eventOccurred(event);
//        }
//    }

    protected void dump() throws SQLException {
        JpaTestObject.dumpTable(nonJtaDs, "employee");
        JpaTestObject.dumpTable(nonJtaDs, "OneToOneA");
        JpaTestObject.dumpTable(nonJtaDs, "OneToOneB");
    }

    public static void dumpTable(final DataSource ds, final String table) throws SQLException {
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        try {
            connection = ds.getConnection();
            statement = connection.createStatement();
            resultSet = statement.executeQuery("SELECT * FROM " + table);
            final ResultSetMetaData setMetaData = resultSet.getMetaData();
            final int columnCount = setMetaData.getColumnCount();
            while (resultSet.next()) {
                final StringBuilder row = new StringBuilder();
                for (int i = 1; i <= columnCount; i++) {
                    if (i > 1) {
                        row.append(", ");
                    }
                    final String name = setMetaData.getColumnName(i);
                    final Object value = resultSet.getObject(i);
                    row.append(name).append("=").append(value);
                }
                System.out.println(row);
            }
        } finally {
            JpaTestObject.close(resultSet);
            JpaTestObject.close(statement);
            JpaTestObject.close(connection);
        }
    }

    private static void close(final ResultSet resultSet) {
        if (resultSet == null) return;
        try {
            resultSet.close();
        } catch (final SQLException e) {
        }
    }

    private static void close(final Statement statement) {
        if (statement == null) return;
        try {
            statement.close();
        } catch (final SQLException e) {
        }
    }

    private static void close(final Connection connection) {
        if (connection == null) return;
        try {
            connection.close();
        } catch (final SQLException e) {
        }
    }
}
