/*
 * 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.graph;

import java.sql.Date;
import java.util.Arrays;
import java.util.List;

import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException;

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.FieldStrategy;
import org.apache.openjpa.jdbc.meta.MappingRepository;
import org.apache.openjpa.jdbc.meta.ValueHandler;
import org.apache.openjpa.jdbc.meta.strats.HandlerFieldStrategy;
import org.apache.openjpa.jdbc.meta.strats.HandlerHandlerMapTableFieldStrategy;
import org.apache.openjpa.jdbc.meta.strats.UntypedPCValueHandler;
import org.apache.openjpa.kernel.QueryHints;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/**
 * Tests basic create and query on generic persistent graph. The test creates a
 * graph of People and Cities. Then different queries on the graph are verified.
 *
 * @author Pinaki Poddar
 *
 */
public class TestPersistentGraph extends SingleEMFTestCase {
    private static enum Emotion {
        LOVES, HATES, KNOWS
    }

    // Identity of People is their SSN
    private static final long[] SSN = { 123456781, 123456782, 123456783, 123456784, 123456785 };
    private static final String[] PERSON_NAMES = { "P1", "P2", "P3", "P4", "P5" };
    private static final String[] CITY_NAMES = { "San Francisco", "Paris", "Rome" };

    private static final String ATTR_SINCE = "since";
    private static final Date SINCE = new Date(90, 1, 27);

    private static final String ATTR_EMOTION = "feels";
    private static final Emotion[][] EMOTIONS = {
                            /*    P1             P2             P3             P4             P5       */
    /* P1 */new Emotion[] {          null, Emotion.LOVES, Emotion.HATES,          null, Emotion.KNOWS },
    /* P2 */new Emotion[] { Emotion.LOVES,          null, Emotion.LOVES,          null, Emotion.LOVES },
    /* P3 */new Emotion[] { Emotion.HATES, Emotion.LOVES,          null,          null, Emotion.KNOWS },
    /* P4 */new Emotion[] { Emotion.LOVES, Emotion.HATES, Emotion.KNOWS, Emotion.LOVES, Emotion.LOVES },
    /* P5 */new Emotion[] {          null, Emotion.LOVES, Emotion.KNOWS, Emotion.KNOWS,          null },
    };

    private static final String ATTR_DISTANCE = "distance";
    private static final int[][] ATTR_DISTANCE_VALUE = {
                       /* C1 C2 C3 */
    /* C1 */new int[] { 0, 200, 400 },
    /* C2 */new int[] { 200, 0, 500 },
    /* C3 */new int[] { 400, 500, 0 }
    };

    private EntityManager em;
    private PersistentGraph<Object> graph;

    @Override
    public void setUp() throws Exception {
        super.setUp(CLEAR_TABLES, PersistentGraph.class, RelationGraph.class,
                PersistentRelation.class, People.class, City.class);
        em = emf.createEntityManager();
        graph = createData();
        em.clear();
    }

    /**
     * Verifies that fields are mapped with expected strategy or value handlers.
     */
    public void testMapping() {
        assertStrategy(PersistentRelation.class, "source", HandlerFieldStrategy.class, UntypedPCValueHandler.class);
        assertStrategy(PersistentRelation.class, "target", HandlerFieldStrategy.class, UntypedPCValueHandler.class);
        assertStrategy(PersistentRelation.class, "attrs", HandlerHandlerMapTableFieldStrategy.class, null);
    }

    private void printMapping(FieldMapping fm) {
        System.err.println("Field :" + fm.getName());
        System.err.println("Type :" + fm.getTypeCode() + " " + fm.getType());
        System.err.println("Type (declared):" + fm.getDeclaredTypeCode() + " " + fm.getDeclaredType());
        System.err.println("Type Override :" + fm.getTypeOverride());
        System.err.println("Key type  :" + fm.getKey().getType());
        System.err.println("Key declared type  :" + fm.getKey().getDeclaredType());
        System.err.println("Element type  :" + fm.getElement().getType());
        System.err.println("Element declared type  :" + fm.getElement().getDeclaredType());
    }

    FieldMapping getFieldMapping(Class<?> pcClass, String field) {
        MappingRepository repos = (MappingRepository) emf.getConfiguration()
                .getMetaDataRepositoryInstance();
        ClassMapping cmd = repos.getMapping(pcClass, null, true);
        assertNotNull("No metadata found for " + pcClass, cmd);
        FieldMapping fmd = cmd.getFieldMapping(field);
        assertNotNull("No metadata found for " + pcClass.getName() + "." + field + " Fields are "
                + Arrays.toString(cmd.getFieldNames()), fmd);
        return fmd;
    }

    /**
     * Asserts that the given field of the given class has been mapped with the
     * given strategy or value handler.
     */
    void assertStrategy(Class<?> pcClass, String field, Class<? extends FieldStrategy> strategy,
            Class<? extends ValueHandler> handler) {

        FieldMapping fmd = getFieldMapping(pcClass, field);
        FieldStrategy actualStrategy = ((FieldMapping) fmd).getStrategy();
        assertEquals(strategy, actualStrategy.getClass());
        ValueHandler actualHandler = fmd.getHandler();
        if (handler == null) {
            if (actualHandler != null) {
                printMapping(fmd);
                fail("Expected no value handler for " + pcClass.getName() + "." + field +
                        " but found " + actualHandler);
            }
        } else {
            if (actualHandler == null) {
                printMapping(fmd);
                fail("Expected a value handler for " + pcClass.getName() + "." + field + " but found null");
            }
            if (!handler.equals(actualHandler.getClass())) {
                printMapping(fmd);
                assertEquals(handler, fmd.getHandler().getClass());
            }
        }
    }

    FieldStrategy getStrategy(Class<?> cls, String field) {
        MetaDataRepository repos = emf.getConfiguration().getMetaDataRepositoryInstance();
        ClassMetaData cmd = repos.getMetaData(cls, null, true);
        assertNotNull("No metadat found for " + cls, cmd);
        FieldMetaData fmd = cmd.getField(field);
        assertNotNull("No metadata found for " + cls.getName() + "." + field + " Fields are "
                + Arrays.toString(cmd.getFieldNames()), fmd);
        FieldStrategy strategy = ((FieldMapping) fmd).getStrategy();
        System.err.println(cls.getName() + "." + field + ":" + strategy.getClass().getSimpleName());
        return strategy;
    }

    /**
     * Tests that the nodes retrieved from the database meets the same
     * assertions of the created graph.
     */
    public void testCreateGraph() {
        em.getTransaction().begin();
        assertFalse(em.contains(graph));
        graph = em.find(PersistentGraph.class, graph.getId());
        assertNotNull(graph);
        People[] people = new People[SSN.length];
        for (int i = 0; i < SSN.length; i++) {
            People p = em.find(People.class, SSN[i]);
            assertNotNull(p);
            people[i] = p;
        }
        City[] cities = new City[CITY_NAMES.length];
        for (int i = 0; i < CITY_NAMES.length; i++) {
            City c = em.find(City.class, CITY_NAMES[i]);
            assertNotNull(c);
            cities[i] = c;
        }
        assertDataEquals(graph, people, cities);

        em.getTransaction().rollback();
    }

    /**
     * Tests that relation can be queried and their references are set
     * correctly.
     */
    public void testQueryRelation() {
        String jpql = "select r from PersistentRelation r";
        List<PersistentRelation> relations = em.createQuery(jpql, PersistentRelation.class).getResultList();
        for (Relation<?, ?> r : relations) {
            Object source = r.getSource();
            Object target = r.getTarget();
            if (source instanceof People) {
                int i = indexOf((People) source);
                if (target instanceof People) {
                    int j = indexOf((People) target);
                    assertNotNull(EMOTIONS[i][j]);
                    assertEquals(EMOTIONS[i][j].toString(), r.getAttribute(ATTR_EMOTION));
                } else if (target instanceof City) {
                    int j = indexOf((City) target);
                    assertEquals(i % CITY_NAMES.length, j);
                    assertTrue(r.getAttributes().isEmpty());
                } else if (target != null){
                    fail("Unexpected relation " + r);
                }
            } else if (source instanceof City) {
                int i = indexOf((City) source);
                if (target instanceof City) {
                    int j = indexOf((City) target);
                    assertEquals(""+ATTR_DISTANCE_VALUE[i][j], r.getAttribute(ATTR_DISTANCE));
                } else if (target != null) {
                    fail("Unexpected relation " + r);
                }
            }
        }
    }

    /**
     * Tests that a relation can be queried predicated on its source vertex.
     */
    public void testQueryRelationOnSourceParameter() {
        People p1 = em.find(People.class, SSN[0]);
        String jpql = "select r from PersistentRelation r where r.source = :node";
        List<PersistentRelation> result = em.createQuery(jpql, PersistentRelation.class)
                                  .setParameter("node", p1)
                                  .getResultList();
        assertFalse("Result of [" + jpql + "] on source = " + p1 + " should not be empty", result.isEmpty());
    }

    /**
     * Tests that a relation can be queried predicated on its attribute key.
     */
    public void testQueryRelationOnSingleAttributeKey() {
        String jpql = "select r from PersistentRelation r join r.attrs a where key(a) = :key";
        List<PersistentRelation> result = em.createQuery(jpql, PersistentRelation.class)
                                  .setParameter("key", ATTR_EMOTION)
                                  .getResultList();

        assertFalse("Result of [" + jpql + "] on key = " + ATTR_EMOTION + " should not be empty", result.isEmpty());
    }

    /**
     * Tests that a relation can be queried predicated on a single attribute
     * key-value pair.
     */
    public void testQueryRelationOnSingleAttributeKeyValue() {
        String jpql = "select r from PersistentRelation r join r.attrs a where key(a) = :key and value(a) = :value";
        String value = EMOTIONS[0][2].toString();
        List<PersistentRelation> result = em.createQuery(jpql, PersistentRelation.class)
                                  .setParameter("key", ATTR_EMOTION)
                                  .setParameter("value", value)
                                  .getResultList();

        assertFalse("Result of [" + jpql + "] on key-value (" + ATTR_EMOTION + "," + value + ") should not be empty",
                result.isEmpty());
    }

    /**
     * Tests that a relation can be queried predicated on a multiple attribute
     * key-value pair. This requires multiple joins. Single join will produce
     * wrong result.
     */
    public void testQueryRelationOnMultipleAttributeKeyValuePairs() {
        String jpql = "select r from PersistentRelation r join r.attrs a1 join r.attrs a2 "
                    + "where key(a1) = :key1 and value(a1) = :value1 "
                    + "and key(a2) = :key2 and value(a2) = :value2";
        String value = EMOTIONS[0][2].toString();
        List<PersistentRelation> result = em.createQuery(jpql, PersistentRelation.class)
                                  .setParameter("key1", ATTR_EMOTION)
                                  .setParameter("value1", value)
                                  .setParameter("key2", ATTR_SINCE)
                                  .setParameter("value2", SINCE.toString())
                                  .getResultList();

        assertFalse("Result of [" + jpql + "] on key-value = (" + ATTR_EMOTION + "," + value
                + ") and key-value=("  + ATTR_SINCE + "," + SINCE + ") should not be empty",
                result.isEmpty());

        String wrongJPQL = "select r from PersistentRelation r join r.attrs a "
                         + "where key(a) = :key1 and value(a) = :value1 "
                         + "and key(a) = :key2 and value(a) = :value2";
        List<PersistentRelation> result2 = em.createQuery(wrongJPQL, PersistentRelation.class)
                                   .setParameter("key1", ATTR_EMOTION)
                                   .setParameter("value1", value)
                                   .setParameter("key2", ATTR_SINCE)
                                   .setParameter("value2", SINCE.toString())
                                   .getResultList();

        assertTrue("Result of [" + jpql + "] on key-value = (" + ATTR_EMOTION + "," + value
                + ") and key-value=("+ ATTR_SINCE + "," + SINCE + ") should be empty",
                result2.isEmpty());
    }

    public void testAddRemoveAttribute() {
        em.getTransaction().begin();
        People p1 = em.find(People.class, SSN[0]);
        String jpql = "select r from PersistentRelation r where r.source = :node";
        List<PersistentRelation> r = em.createQuery(jpql, PersistentRelation.class)
                            .setHint(QueryHints.HINT_IGNORE_PREPARED_QUERY, true)
                            .setParameter("node", p1)
                            .getResultList();
        assertFalse(r.isEmpty());
        r.get(0).addAttribute("new-key", "new-value");
        em.getTransaction().commit();
        em.clear();

        em.getTransaction().begin();
        jpql = "select r from PersistentRelation r join r.attrs a where key(a) = :key";
        Relation newR = em.createQuery(jpql, PersistentRelation.class)
                          .setParameter("key", "new-key")
                          .getSingleResult();
        assertNotNull(newR);
        assertEquals("new-value", newR.getAttribute("new-key"));
        newR.removeAttribute("new-key");
        em.getTransaction().commit();

        em.getTransaction().begin();
        jpql = "select r from PersistentRelation r join r.attrs a where key(a) = :key";
        try {
            newR = em.createQuery(jpql, PersistentRelation.class)
                          .setParameter("key", "new-key")
                          .getSingleResult();
            fail(jpql + " with new-key expected no result");
        } catch (NoResultException nre) {
            // this is what is expected
        } finally {
            em.getTransaction().rollback();
        }
    }

    boolean isPopulated() {
        return em.createQuery("select count(p) from People p", Long.class).getSingleResult() > 0;
    }

    /**
     * Creates a typical graph of People and Cities. The tests are sensitive to
     * the actual values and relations set in in this method.
     */
    PersistentGraph<Object> createData() {
        PersistentGraph<Object> graph = new RelationGraph<>();

        em.getTransaction().begin();

        People[] people = new People[SSN.length];
        for (int i = 0; i < SSN.length; i++) {
            People p = new People();
            graph.add(p);
            p.setSsn(SSN[i]);
            p.setName(PERSON_NAMES[i]);
            people[i] = p;
        }
        City[] cities = new City[CITY_NAMES.length];
        for (int i = 0; i < CITY_NAMES.length; i++) {
            City c = new City();
            graph.add(c);
            c.setName(CITY_NAMES[i]);
            cities[i] = c;
        }
        for (int i = 0; i < people.length; i++) {
            for (int j = 0; j < people.length; j++) {
                if (EMOTIONS[i][j] != null) {
                    Relation<People, People> r = graph.link(people[i], people[j])
                                 .addAttribute(ATTR_EMOTION, EMOTIONS[i][j]);
                    if (i == 0 && j == 2) {
                        r.addAttribute(ATTR_SINCE, SINCE);
                    }
                }
            }
        }
        for (int i = 0; i < cities.length; i++) {
            for (int j = 0; j < cities.length; j++) {
                graph.link(cities[i], cities[j]).addAttribute(ATTR_DISTANCE, ATTR_DISTANCE_VALUE[i][j]);
            }
        }

        for (int i = 0; i < people.length; i++) {
            graph.link(people[i], cities[i % CITY_NAMES.length]);
        }
        em.persist(graph);
        em.getTransaction().commit();

        return graph;
    }

    void assertDataEquals(Graph<Object> graph, People[] people, City[] cities) {
        assertEquals(SSN.length, people.length);
        assertEquals(CITY_NAMES.length, cities.length);

        for (int i = 0; i < people.length; i++) {
            People p = people[i];
            assertEquals(SSN[i], p.getSsn());
            assertEquals(PERSON_NAMES[i], p.getName());
        }
        for (int i = 0; i < cities.length; i++) {
            City c = cities[i];
            assertEquals(CITY_NAMES[i], c.getName());
        }
        for (int i = 0; i < people.length; i++) {
            People p1 = people[i];
            for (int j = 0; j < people.length; j++) {
                People p2 = people[j];
                Relation<People, People> r = graph.getRelation(p1,p2);
                if (EMOTIONS[i][j] != null) {
                    assertNotNull(r);
                    assertEquals(EMOTIONS[i][j].toString(), r.getAttribute(ATTR_EMOTION));
                } else {
                    assertNull(r);
                }
            }
        }
        for (int i = 0; i < cities.length; i++) {
            City c1 = cities[i];
            for (int j = 0; j < cities.length; j++) {
                City c2 = cities[j];
                Relation<City, City> r12 = graph.getRelation(c1,c2);
                assertNotNull(r12);
                assertEquals(""+ATTR_DISTANCE_VALUE[i][j], r12.getAttribute(ATTR_DISTANCE));
            }
        }

        for (int i = 0; i < people.length; i++) {
            People p = people[i];
            for (int j = 0; j < cities.length; j++) {
                City c = cities[j];
                Relation<People, City> r = graph.getRelation(p,c);
                if (i % CITY_NAMES.length == j) {
                    assertNotNull(r);
                } else {
                    assertNull(r);
                }
            }

        }
    }

    int indexOf(People p) {
        for (int i = 0; i < SSN.length; i++) {
            if (SSN[i] == p.getSsn())
                return i;
        }
        return -1;
    }

    int indexOf(City c) {
        for (int i = 0; i < CITY_NAMES.length; i++) {
            if (CITY_NAMES[i].equals(c.getName()))
                return i;
        }
        return -1;
    }
}
