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

import java.io.InputStream;
import java.sql.SQLException;
import java.util.List;

import jakarta.persistence.EntityExistsException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.OptimisticLockException;
import jakarta.persistence.Query;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.SQLErrorCodeReader;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/**
 * Tests proper JPA exceptions are raised by the implementation.
 * Actual runtime type of the raised exception is a subclass of JPA-defined
 * exception.
 * The raised exception may nest the expected exception.
 *
 * @author Pinaki Poddar
 */
public class TestException extends SingleEMFTestCase {
	private static long ID_COUNTER = System.currentTimeMillis();

	@Override
    public void setUp() {
        super.setUp(PObject.class, CLEAR_TABLES);
    }

	/**
	 * Tests that when Optimistic transaction consistency is violated, the
     * exception thrown is an instance of jakarta.persistence.OptimisticException.
	 */
	// TODO: Re-enable this test once OPENJPA-991 issue is corrected.
	public void disabledTestThrowsOptimisticException() {

        boolean supportsQueryTimeout = ((JDBCConfiguration) emf
            .getConfiguration()).getDBDictionaryInstance().supportsQueryTimeout;

		EntityManager em1 = emf.createEntityManager();
		EntityManager em2 = emf.createEntityManager();
		assertNotEquals(em1, em2);

		em1.getTransaction().begin();
		PObject pc = new PObject();
		long id = ++ID_COUNTER;
		pc.setId(id);
		em1.persist(pc);
		em1.getTransaction().commit();
		em1.clear();

		em1.getTransaction().begin();
		em2.getTransaction().begin();

		PObject pc1 = em1.find(PObject.class, id);
		PObject pc2 = em2.find(PObject.class, id);

		assertTrue(pc1 != pc2);

		pc1.setName("Modified in TXN1");
        if (supportsQueryTimeout) {
            em1.flush();
            try {
                pc2.setName("Modified in TXN2");
                em2.flush();
                fail("Expected " + OptimisticLockException.class);
            } catch (Throwable t) {
                assertException(t, OptimisticLockException.class);
            }
        } else {
            pc2.setName("Modified in TXN2");
        }

		em1.getTransaction().commit();
		try {
			em2.getTransaction().commit();
			fail("Expected " + OptimisticLockException.class);
		} catch (Throwable t) {
			assertException(t, OptimisticLockException.class);
		}
	}

	public void testThrowsEntityExistsException() {
		EntityManager em = emf.createEntityManager();

		em.getTransaction().begin();
		PObject pc = new PObject();
		long id = ++ID_COUNTER;
		pc.setId(id);
		em.persist(pc);
		em.getTransaction().commit();
		em.clear();

		em.getTransaction().begin();
		PObject pc2 = new PObject();
		pc2.setId(id);
		em.persist(pc2);
		try {
			em.getTransaction().commit();
			fail("Expected " + EntityExistsException.class);
		} catch (Throwable t) {
			assertException(t, EntityExistsException.class);
		}
		em.close();
	}

	public void testThrowsEntityNotFoundException() {
		EntityManager em = emf.createEntityManager();

		em.getTransaction().begin();
		PObject pc = new PObject();
		long id = ++ID_COUNTER;
		pc.setId(id);
		em.persist(pc);
		em.getTransaction().commit();

		EntityManager em2 = emf.createEntityManager();
		em2.getTransaction().begin();
		PObject pc2 = em2.find(PObject.class, id);
		assertNotNull(pc2);
		em2.remove(pc2);
		em2.getTransaction().commit();

		try {
			em.refresh(pc);
			fail("Expected " + EntityNotFoundException.class);
		} catch (Throwable t) {
			assertException(t, EntityNotFoundException.class);
		}
		em.close();
	}

	public void testErrorCodeConfigurationHasAllKnownDictionaries() {
		SQLErrorCodeReader reader = new SQLErrorCodeReader();
		InputStream in = DBDictionary.class.getResourceAsStream
			("sql-error-state-codes.xml");
		assertNotNull(in);
		List<String> names = reader.getDictionaries(in);
		assertTrue(names.size()>=18);
		for (String name:names) {
			try {
                Class.forName(name, false, Thread.currentThread()
                        .getContextClassLoader());
			} catch (Throwable t) {
                fail("DB dictionary " + name + " can not be loaded");
				t.printStackTrace();
			}
		}
	}

	/**
	 * Invalid query throws IllegalArgumentException on construction
	 * as per JPA spec.
	 */
	public void testIllegalArgumennExceptionOnInvalidQuery() {
	    EntityManager em = emf.createEntityManager();
	    try {
	      em.createQuery("This is not a valid JPQL query");
	      fail("Did not throw IllegalArgumentException for invalid query.");
	    } catch (Throwable t) {
		   assertException(t, IllegalArgumentException.class);
	    }
	    em.close();
	}

	/**
	 * Invalid named query fails as per spec on factory based construction.
	 */
     public void testIllegalArgumennExceptionOnInvalidNamedQuery() {
         EntityManager em = emf.createEntityManager();
         try {
             Query query = em.createNamedQuery("This is invalid Named query");
             fail("Did not throw IllegalArgumentException for invalid query.");
         } catch (Throwable t) {
             assertException(t, IllegalArgumentException.class);
         }
         em.close();
      }

	/**
     * Asserts that the given expected type of the exception is equal to or a
	 * subclass of the given throwable or any of its nested exception.
     * Otherwise fails assertion and prints the given throwable and its nested
	 * exception on the console.
	 */
	@Override
    public void assertException(Throwable t, Class expectedType) {
		if (!isExpectedException(t, expectedType)) {
		    getLog().error("TestException.assertException() - unexpected exception type", t);
			//t.printStackTrace();
			print(t, 0);
            fail(t + " or its cause is not instanceof " + expectedType);
		} else {
		    if (getLog().isTraceEnabled()) {
	            getLog().trace("TestException.assertException() - caught expected exception type=" +
	                expectedType, t);
		    }
		}
	}

	/**
	 * Affirms if the given expected type of the exception is equal to or a
	 * subclass of the given throwable or any of its nested exception.
	 */
	boolean isExpectedException(Throwable t, Class expectedType) {
		if (t == null)
			return false;
		if (expectedType.isAssignableFrom(t.getClass()))
				return true;
		return isExpectedException(t.getCause(), expectedType);
	}

	void print(Throwable t, int tab) {
		if (t == null) return;
		StringBuilder str = new StringBuilder(80);
		for (int i=0; i<tab*4;i++)
		    str.append(" ");
		String sqlState = (t instanceof SQLException) ?
			"(SQLState=" + ((SQLException)t).getSQLState() + ":"
				+ t.getMessage() + ")" : "";
		str.append(t.getClass().getName() + sqlState);
		getLog().error(str);
		if (t.getCause() == t)
			return;
		print(t.getCause(), tab+1);
	}
}
