/*
 * 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.openjpa.persistence.detachment;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.openjpa.conf.Compatibility;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.detachment.model.DMCustomer;
import org.apache.openjpa.persistence.detachment.model.DMCustomerInventory;
import org.apache.openjpa.persistence.detachment.model.DMItem;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/**
 * Tests detachment behavior according to JPA 2.0 Specification. The primary
 * changes in detachment behavior from the existing OpenJPA behavior are
 * i. detach(x) does not flush if x is dirty
 * ii. detach(x) removes x from persistence context
 * iii. detach(x) propagates via CascadeType.DETACH. It is not clear how that
 * impacts the detach graph. So currently, detach graph is same as 'loaded'.
 *
 * The test uses a 'domain model' with following cascade relation
 *
 *            ALL                 PERSIST, MERGE, REFRESH
 * Customer --------> Inventory   ----------------------> Item
 *          <-------
 *            MERGE
 *
 * @author Pinaki Poddar
 *
 */
public class TestDetach extends SingleEMFTestCase {
    OpenJPAEntityManager em;
    DMCustomer root;

    @Override
    public void setUp() {
        super.setUp(DMCustomer.class, DMCustomerInventory.class, DMItem.class,
            CLEAR_TABLES);

        Compatibility compat =
            emf.getConfiguration().getCompatibilityInstance();
        compat.setCopyOnDetach(false);
        compat.setFlushBeforeDetach(false);

        em = emf.createEntityManager();
        root = createData();
    }

    public void testDetachCascadeIsSet() {
        MetaDataRepository repos = emf.getConfiguration()
                                      .getMetaDataRepositoryInstance();
        ClassMetaData meta = repos.getCachedMetaData(DMCustomer.class);
        assertEquals(ValueMetaData.CASCADE_NONE,
                meta.getField("firstName").getCascadeDetach());
        assertEquals(ValueMetaData.CASCADE_IMMEDIATE, meta.getField(
                "customerInventories").getElement().getCascadeDetach());

        meta = repos.getCachedMetaData(DMCustomerInventory.class);
        assertEquals(ValueMetaData.CASCADE_NONE,
                meta.getField("customer").getCascadeDetach());
        assertEquals(ValueMetaData.CASCADE_NONE,
                meta.getField("item").getCascadeDetach());

    }

    public void testDetachRemovesEntityAndCascadedRelationFromContext() {
        em.getTransaction().begin();

        DMCustomer pc = em.find(DMCustomer.class, root.getId());
        List<DMCustomerInventory> inventories = pc.getCustomerInventories();
        DMItem item = inventories.get(0).getItem();

        assertNotDetached(pc);
        for (DMCustomerInventory i : inventories) assertNotDetached(i);
        assertNotDetached(item);

        em.detach(pc);

        assertDetached(pc);
        for (DMCustomerInventory i : inventories) assertDetached(i);

        em.getTransaction().rollback();

        assertNotNull(pc.getFirstName());
    }

    public void testDetachingDirtyEntityDoesNotImplicitlyFlush() {
        em.getTransaction().begin();
        DMCustomer pc = em.find(DMCustomer.class, root.getId());
        String original = pc.getLastName();
        pc.setLastName("Changed That Should not be Saved");

        em.detach(pc);
        em.getTransaction().commit();

        DMCustomer pc2 = em.find(DMCustomer.class, root.getId());
        assertNotNull(pc2);
        assertEquals(original, pc2.getLastName());
    }

    public void testDetachingNewEntityIsIgnored() {
        em.getTransaction().begin();
        DMCustomer pc = em.find(DMCustomer.class, root.getId());
        List<DMCustomerInventory> inventories = pc.getCustomerInventories();

        DMCustomer newPC = new DMCustomer();
        newPC.setCustomerInventories(inventories);
        for (DMCustomerInventory inventory : inventories)
            inventory.setCustomer(newPC);

        em.detach(newPC);
        for (DMCustomerInventory inventory : inventories) {
            assertNotDetached(inventory);
        }
        em.getTransaction().rollback();
    }

    public void testDetachingDetachedEntityIsIgnored() {
        em.getTransaction().begin();
        DMCustomer pc = em.find(DMCustomer.class, root.getId());
        List<DMCustomerInventory> inventories = pc.getCustomerInventories();

        em.detach(pc);
        DMCustomer detached = pc;
        assertDetached(detached);
        for (DMCustomerInventory inventory : inventories) {
            assertDetached(inventory);
        }

        List<DMCustomerInventory> newInventories =
                new ArrayList<>(inventories);
        DMCustomerInventory newInventory = new DMCustomerInventory();
        newInventory.setCustomer(detached);
        newInventories.add(newInventory);
        detached.setCustomerInventories(newInventories);
        em.persist(newInventory);
        assertNotDetached(newInventory);

        em.detach(detached);
        assertDetached(detached);
        assertEquals(inventories.size()+1, newInventories.size());
        for (DMCustomerInventory inventory : newInventories) {
            if (inventory == newInventory)
                assertNotDetached(inventory);
            else
                assertDetached(inventory);
        }
        em.getTransaction().rollback();
    }


    public void testFlushingBeforeDetachingSavesChange() {

    }

    public void testManagedEntityContinuesToReferDetachedEntities() {
        em.getTransaction().begin();

        DMCustomer pc = em.find(DMCustomer.class, root.getId());
        List<DMCustomerInventory> inventories = pc.getCustomerInventories();
        DMItem item = inventories.get(1).getItem();

        em.detach(inventories.get(0));

        DMCustomerInventory attached0 = inventories.get(0);
        DMCustomerInventory attached1 = inventories.get(1);

        assertSame(pc.getCustomerInventories().get(0), attached0);
        assertSame(pc.getCustomerInventories().get(1), attached1);

        em.getTransaction().rollback();
    }

    DMCustomer createData() {
        DMItem item1 = new DMItem();
        DMItem item2 = new DMItem();
        item1.setName("item-1"); item1.setPrice(100.0);
        item2.setName("item-2"); item2.setPrice(200.0);

        DMCustomerInventory inventory1 = new DMCustomerInventory();
        DMCustomerInventory inventory2 = new DMCustomerInventory();
        inventory1.setItem(item1); inventory1.setQuantity(10);
        inventory2.setItem(item2); inventory2.setQuantity(20);
        DMCustomer customer = new DMCustomer();
        customer.setFirstName("Detached"); customer.setLastName("Customer");
        customer.setCustomerInventories(Arrays.asList(
            new DMCustomerInventory[]{inventory1,inventory2}));
        inventory1.setCustomer(customer);
        inventory2.setCustomer(customer);

        em.getTransaction().begin();
        em.persist(customer);
        em.getTransaction().commit();
        em.clear();

        return customer;
    }

    void assertDetached(Object pc) {
        assertTrue(pc + " should be detached", em.isDetached(pc));
        assertFalse(pc + " should not be in cache", em.contains(pc));
    }

    void assertNotDetached(Object pc) {
        assertFalse(pc + " should not be detached", em.isDetached(pc));
        assertTrue(pc + " should be in cache", em.contains(pc));
    }
}
