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

import java.lang.annotation.ElementType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.util.HashMap;
import java.util.Map;

import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Path;
import jakarta.validation.TraversableResolver;

import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.validation.TraversableResolverImpl;
// The following 2 are dynamically loaded by loadPathImpl() from setUp()
// import org.hibernate.validator.engine.PathImpl;
// import com.agimatec.validation.jsr303.util.PathImpl;

import junit.framework.TestCase;

/**
 * Test the TraversableResolver methods
 *
 * First run several testcases from a user perspective. These test the methods
 * indirectly:
 *    1) testLoadedTitle()
 *    2} testUnloaded()
 *    3) testCascading()
 *
 * Then test the methods directly:
 *    1) testPages()
 *    2) testTitle
 */
public class TestTraversableResolver extends TestCase {
    private static OpenJPAEntityManagerFactorySPI emf = null;
    private Log log = null;
    private OpenJPAEntityManager em = null;
    private Book book;

    /**
     * Create a book with a title that is too long, and the embedded
     * publisher has a name that is also too long. However, use a
     * persistence unit with validation-mode set to NONE. Therefore,
     * the create should be successful. This is to setup a situation
     * where fields to be potentially validated are not necessarily loaded.
     */
    @Override
    public void setUp() {
        createBook(1, "long title", 234);
    }

    private void createEMF(String pu, String schemaAction) {
        Map<String, Object> map = new HashMap<>();
        map.put("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true,"
            + schemaAction);
        emf = (OpenJPAEntityManagerFactorySPI)OpenJPAPersistence.createEntityManagerFactory(
            pu,
            "org/apache/openjpa/integration/validation/persistence.xml",
            map);
        assertNotNull(emf);
        log = emf.getConfiguration().getLog("Tests");
    }

    private void closeEMF() {
        log = null;
        if (em != null) {
            em.close();
            em = null;
        }
        if (emf != null) {
            emf.close();
            emf = null;
        }
    }

    /**
     * By default, the title is not loaded. Make sure it gets loaded,
     * make a change in a different field, and commit. The isLoaded() method
     * of the TraversableResolver should return true, resulting in a validation
     * being performed and a ConstraintViolationException should be returned
     * because the title is too long.
     */
    public void testLoadedTitle() {
        createEMF("validation-pu", "SchemaAction='add')");
        em = emf.createEntityManager();
        em.getTransaction().begin();
        book = em.find(org.apache.openjpa.integration.validation.Book.class, 1);
        assertNotNull(book);
        book.setPages(124);
        // load the title
        String title = book.getTitle();
        assertEquals("long title", title);
        boolean exceptionCaught = false;
        try {
            em.getTransaction().commit();
        } catch (ConstraintViolationException e) {
            exceptionCaught = true;
        }
        assertTrue(exceptionCaught);
    }

    /**
     * By default, the title and publisher are not loaded. Make a change in a different field
     * and commit. The isLoaded() method of the TraversableResolver should return
     * false for both of these. Therefore a validation should not be performed.
     * The commit should succeed with no exception.
     */
    public void testUnloaded() {
        createEMF("non-validation-pu", "SchemaAction='add')");
        em = emf.createEntityManager();
        em.getTransaction().begin();
        book = em.find(org.apache.openjpa.integration.validation.Book.class, 1);
        assertNotNull(book);
        book.setPages(124);
        boolean exceptionCaught = false;
        try {
            em.getTransaction().commit();
        } catch (ConstraintViolationException e) {
            exceptionCaught = true;
        }
        assertFalse(exceptionCaught);
        closeEMF();
    }

    /**
     * By default, the publisher is not loaded. Make sure it gets loaded.
     * The isLoaded() and isCascadable() methods should both return true,
     * resulting in a validation being performed. A ConstraintViolation
     * should be thrown since the publisher name is too long.
     */
    public void testCascading() {
        createEMF("validation-pu", "SchemaAction='add')");
        em = emf.createEntityManager();
        em.getTransaction().begin();
        book = em.find(org.apache.openjpa.integration.validation.Book.class, 1);
        assertNotNull(book);
        book.setPages(124);
        // load the embedded publisher
        Publisher publisher = book.getPublisher();
        assertNotNull(publisher);
        publisher.setPublisherID("yyy");
        boolean exceptionCaught = false;
        try {
            em.getTransaction().commit();
        } catch (Exception e) {
            exceptionCaught = true;
        }
        assertTrue(exceptionCaught);
        closeEMF();
    }

    /**
     * Test the isReachable() and isCascadable() methods on the pages element of Book,
     * which is eagerly fetched by default.
     */
    public void testPages() {
        createEMF("validation-pu", "SchemaAction='add')");
        em = emf.createEntityManager();
        em.getTransaction().begin();
        book = em.find(org.apache.openjpa.integration.validation.Book.class, 1);
        assertNotNull(book);
        // PathImpl path = PathImpl.createPathFromString("org.apache.openjpa.integration.validation.Book.pages");
        // Path.Node node = path.getLeafNode();
        Path.Node node = getLeafNodeFromString("org.apache.openjpa.integration.validation.Book.pages");
        TraversableResolver tr = new TraversableResolverImpl();
        assertTrue(tr.isReachable(book, node, Book.class, null, ElementType.METHOD));
        assertTrue(tr.isCascadable(book, node, Book.class, null, ElementType.METHOD));
        em.getTransaction().commit();
        closeEMF();
    }

    /**
     * Test the isReachable() method on the title.
     * It is configured with fetch=FetvhType.LAZY.
     */
    public void testTitle() {
        createEMF("validation-pu", "SchemaAction='add')");
        em = emf.createEntityManager();
        em.getTransaction().begin();
        book = em.find(org.apache.openjpa.integration.validation.Book.class, 1);
        assertNotNull(book);
        // PathImpl path = PathImpl.createPathFromString("org.apache.openjpa.integration.validation.Book.title");
        // Path.Node node = path.getLeafNode();
        Path.Node node = getLeafNodeFromString("org.apache.openjpa.integration.validation.Book.title");
        TraversableResolver tr = new TraversableResolverImpl();
        assertFalse(tr.isReachable(book, node, Book.class, null, ElementType.FIELD));
        em.getTransaction().commit();
        closeEMF();
    }

    private void createBook(int id, String title, int pages) {
        createEMF("non-validation-pu", "SchemaAction='drop,add')");
        em = emf.createEntityManager();
        book = new Book(id);
        book.setTitle(title);
        book.setPages(pages);
        Publisher publisher = new Publisher();
        publisher.setName("long name");
        publisher.setPublisherID("xxx");
        book.setPublisher(publisher);
        em.getTransaction().begin();
        em.persist(book);
        em.getTransaction().commit();
        closeEMF();
    }

    private Path.Node getLeafNodeFromString(String s) {
        Class<?> PathImpl = null;
        Path.Node node = null;

        // dynamically load PathImpl depending on the Bean Validation provider
        try {
            PathImpl = Class.forName("org.hibernate.validator.engine.PathImpl",
                true, AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction()));
        } catch (ClassNotFoundException e) {
            log.trace("getLeafNodeFromPath: Did not find org.hibernate.validator.engine.PathImpl");
        }
        if (PathImpl == null) {
            try {
                PathImpl = Class.forName("org.apache.bval.jsr303.util.PathImpl",
                    true, AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction()));
            } catch (ClassNotFoundException e) {
                log.trace("getLeafNodeFromPath: Did not find org.apache.bval.jsr303.util.PathImpl");
            }
        }
        if (PathImpl == null) {
            try {
                PathImpl = Class.forName("com.agimatec.validation.jsr303.util.PathImpl",
                    true, AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction()));
            } catch (ClassNotFoundException e) {
                log.trace("getLeafNodeFromPath: Did not find com.agimatec.validation.jsr303.util.PathImpl");
            }
        }
        assertNotNull("Could not load a Bean Validation provider specific PathImpl", PathImpl);
        try {
            Method createPathFromString = PathImpl.getMethod("createPathFromString", String.class);
            assertNotNull(createPathFromString);
            Method getLeafNode = PathImpl.getMethod("getLeafNode");
            assertNotNull(getLeafNode);
            Object path = createPathFromString.invoke(null, s);
            node = (Path.Node) getLeafNode.invoke(path, null);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
        }
        return node;
    }

}

