/*****************************************************************
 *   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
 *
 *    https://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.cayenne;

import org.apache.cayenne.access.ToManyList;
import org.apache.cayenne.configuration.server.ServerRuntime;
import org.apache.cayenne.di.Inject;
import org.apache.cayenne.query.ObjectSelect;
import org.apache.cayenne.test.jdbc.DBHelper;
import org.apache.cayenne.test.jdbc.TableHelper;
import org.apache.cayenne.testdo.testmap.ArtGroup;
import org.apache.cayenne.testdo.testmap.Artist;
import org.apache.cayenne.testdo.testmap.Gallery;
import org.apache.cayenne.testdo.testmap.Painting;
import org.apache.cayenne.testdo.testmap.PaintingInfo;
import org.apache.cayenne.unit.di.server.CayenneProjects;
import org.apache.cayenne.unit.di.server.ServerCase;
import org.apache.cayenne.unit.di.server.UseServerRuntime;
import org.junit.Before;
import org.junit.Test;

import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
public class CayenneDataObjectRelationshipsIT extends ServerCase {

    @Inject
    private ObjectContext context;

    @Inject
    private ServerRuntime runtime;

    @Inject
    private DBHelper dbHelper;

    private TableHelper tArtist;
    private TableHelper tPaintingInfo;
    private TableHelper tPainting;

    @Before
    public void setUp() throws Exception {
        tArtist = new TableHelper(dbHelper, "ARTIST");
        tArtist.setColumns("ARTIST_ID", "ARTIST_NAME");

        tPainting = new TableHelper(dbHelper, "PAINTING");
        tPainting.setColumns(
                "PAINTING_ID",
                "PAINTING_TITLE",
                "ARTIST_ID");

        tPaintingInfo = new TableHelper(dbHelper, "PAINTING_INFO");
        tPaintingInfo.setColumns("PAINTING_ID", "TEXT_REVIEW");
    }

    private void createArtistWithPaintingDataSet() throws Exception {
        tArtist.insert(8, "aX");
        tPainting.insert(6, "pW", 8);
    }

    private void createArtistWithPaintingAndInfoDataSet() throws Exception {
        tArtist.insert(8, "aX");
        tPainting.insert(6, "pW", 8);
        tPaintingInfo.insert(6, "mE");
    }

    @Test
    public void testReadNestedProperty1() throws Exception {
        createArtistWithPaintingDataSet();

        Painting p1 = Cayenne.objectForPK(context, Painting.class, 6);
        assertEquals("aX", p1.readNestedProperty("toArtist.artistName"));
    }

    @Test
    public void testReadNestedProperty2() throws Exception {
        createArtistWithPaintingDataSet();

        Painting p1 = Cayenne.objectForPK(context, Painting.class, 6);
        assertTrue(p1.getToArtist().readNestedProperty("paintingArray") instanceof List<?>);
    }

    @Test
    public void testReciprocalRel1() throws Exception {
        createArtistWithPaintingDataSet();

        Painting p1 = Cayenne.objectForPK(context, Painting.class, 6);
        Artist a1 = p1.getToArtist();

        assertNotNull(a1);
        assertEquals("aX", a1.getArtistName());

        List<Painting> paintings = a1.getPaintingArray();
        assertEquals(1, paintings.size());
        Painting p2 = paintings.get(0);
        assertSame(p1, p2);
    }

    @Test
    public void testReadToOneRel1() throws Exception {
        createArtistWithPaintingDataSet();

        Painting p1 = Cayenne.objectForPK(context, Painting.class, 6);
        Artist a1 = p1.getToArtist();

        assertNotNull(a1);
        assertEquals(PersistenceState.HOLLOW, a1.getPersistenceState());
        assertEquals("aX", a1.getArtistName());
        assertEquals(PersistenceState.COMMITTED, a1.getPersistenceState());
    }

    @Test
    public void testReadToOneRel2() throws Exception {
        // test chained calls to read relationships
        createArtistWithPaintingAndInfoDataSet();

        PaintingInfo pi1 = Cayenne.objectForPK(context, PaintingInfo.class, 6);
        Painting p1 = pi1.getPainting();
        p1.getPaintingTitle();

        Artist a1 = p1.getToArtist();

        assertNotNull(a1);
        assertEquals(PersistenceState.HOLLOW, a1.getPersistenceState());
        assertEquals("aX", a1.getArtistName());
        assertEquals(PersistenceState.COMMITTED, a1.getPersistenceState());
    }

    @Test
    public void testReadToOneRel3() throws Exception {
        createArtistWithPaintingDataSet();

        Painting p1 = Cayenne.objectForPK(context, Painting.class, 6);
        Gallery g1 = p1.getToGallery();
        assertNull(g1);
    }

    @Test
    public void testReadToManyRel1() throws Exception {
        createArtistWithPaintingDataSet();

        Artist a1 = Cayenne.objectForPK(context, Artist.class, 8);
        List<Painting> plist = a1.getPaintingArray();

        assertNotNull(plist);
        assertEquals(1, plist.size());
        assertEquals(PersistenceState.COMMITTED, plist.get(0).getPersistenceState());
        assertEquals("pW", plist.get(0).getPaintingTitle());
    }

    @Test
    public void testReadToManyRel2() throws Exception {
        // test empty relationship
        tArtist.insert(11, "aX");

        Artist a1 = Cayenne.objectForPK(context, Artist.class, 11);
        List<Painting> plist = a1.getPaintingArray();

        assertNotNull(plist);
        assertEquals(0, plist.size());
    }

    @Test
    public void testReflexiveRelationshipInsertOrder1() {

        ArtGroup parentGroup = context.newObject(ArtGroup.class);
        parentGroup.setName("parent");

        ArtGroup childGroup1 = context.newObject(ArtGroup.class);
        childGroup1.setName("child1");
        childGroup1.setToParentGroup(parentGroup);
        context.commitChanges();

        childGroup1.setToParentGroup(null);
        context.commitChanges();
    }

    @Test
    public void testReflexiveRelationshipInsertOrder2() {

        ArtGroup childGroup1 = context.newObject(ArtGroup.class);
        childGroup1.setName("child1");

        ArtGroup parentGroup = context.newObject(ArtGroup.class);
        parentGroup.setName("parent");

        childGroup1.setToParentGroup(parentGroup);

        context.commitChanges();

        childGroup1.setToParentGroup(null);
        context.commitChanges();
    }

    @Test
    public void testReflexiveRelationshipInsertOrder3() {
        // multiple children, one created before parent, one after

        ArtGroup childGroup1 = context.newObject(ArtGroup.class);
        childGroup1.setName("child1");

        ArtGroup parentGroup = context.newObject(ArtGroup.class);
        parentGroup.setName("parent");

        childGroup1.setToParentGroup(parentGroup);

        ArtGroup childGroup2 = context.newObject(ArtGroup.class);
        childGroup2.setName("child2");
        childGroup2.setToParentGroup(parentGroup);

        context.commitChanges();

        childGroup1.setToParentGroup(null);
        context.commitChanges();

        childGroup2.setToParentGroup(null);
        context.commitChanges();
    }

    @Test
    public void testReflexiveRelationshipInsertOrder4() {
        // multiple children, one created before parent, one after

        ArtGroup childGroup1 = context.newObject(ArtGroup.class);
        childGroup1.setName("child1");

        ArtGroup parentGroup = context.newObject(ArtGroup.class);
        parentGroup.setName("parent");

        childGroup1.setToParentGroup(parentGroup);

        ArtGroup childGroup2 = context.newObject(ArtGroup.class);
        childGroup2.setName("subchild");
        childGroup2.setToParentGroup(childGroup1);

        context.commitChanges();

        childGroup1.setToParentGroup(null);
        context.commitChanges();

        childGroup2.setToParentGroup(null);
        context.commitChanges();
    }

    @Test
    public void testCrossContextRelationshipException() {

        // Create this object in one context...
        Artist artist = context.newObject(Artist.class);

        // ...and this object in another context
        Painting painting = runtime.newContext().newObject(Painting.class);

        // Check setting a toOne relationship
        try {
            painting.setToArtist(artist);
            fail("Should have failed to set a cross-context relationship");
        }
        catch (CayenneRuntimeException e) {
            // Fine.. it should throw an exception
        }

        assertNull(painting.getToArtist()); // Make sure it wasn't set

        // Now try the reverse (toMany) relationship
        try {
            artist.addToPaintingArray(painting);
            fail("Should have failed to add a cross-context relationship");
        }
        catch (CayenneRuntimeException e) {
            // Fine.. it should throw an exception
        }

        assertEquals(0, artist.getPaintingArray().size());
    }

    @Test
    public void testComplexInsertUpdateOrdering() {
        Artist artist = context.newObject(Artist.class);
        artist.setArtistName("a name");

        context.commitChanges();

        // Cause an update and an insert that need correct ordering
        Painting painting = context.newObject(Painting.class);
        painting.setPaintingTitle("a painting");
        artist.addToPaintingArray(painting);

        context.commitChanges();

        context.deleteObjects(artist);
        context.commitChanges();
    }

    @Test
    public void testNewToMany() throws Exception {
        Artist artist = context.newObject(Artist.class);
        artist.setArtistName("test");
        assertTrue(artist.readPropertyDirectly("paintingArray") instanceof ToManyList);

        ToManyList list = (ToManyList) artist.readPropertyDirectly("paintingArray");
        assertFalse(list.isFault());

        context.commitChanges();

        assertFalse(list.isFault());
    }

    @Test
    public void testTransientInsertAndDeleteOfToManyRelationship() throws Exception {
        createArtistWithPaintingDataSet();

        Artist artist = ObjectSelect.query(Artist.class).selectOne(context);

        // create and then immediately delete a to-many relationship value
        Painting object2 = context.newObject(Painting.class);
        artist.addToPaintingArray(object2);
        artist.removeFromPaintingArray(object2);
        context.deleteObject(object2);
        assertEquals(1, artist.getPaintingArray().size());

        artist.setArtistName("updated artist name"); // this will force the commit to actually execute some SQL
        context.commitChanges();
    }
    
    @Test
    public void testTransientSetAndNullOfToOneRelationship() throws Exception {
        createArtistWithPaintingDataSet();

        Artist artist = ObjectSelect.query(Artist.class).selectOne(context);

        Painting object2 = context.newObject(Painting.class);
        object2.setPaintingTitle("Title");
        object2.setToArtist(artist);
        object2.setToArtist(null);
        context.commitChanges();
        
        context.invalidateObjects(object2);
        assertNull(object2.getToArtist());
    }
    
}
