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


import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.List;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Persistence;

import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;
import org.apache.openjpa.persistence.OpenJPAPersistence;

import junit.framework.TestCase;

/**
 * A test for audit facility.
 *
 * @author Pinaki Poddar
 *
 */
public class TestAudit extends TestCase {
	private static OpenJPAEntityManagerFactory emf;
	private static Auditor auditor;
	private static Object oid;

	EntityManager em;

    @Override
    public void setUp() {
    	if (emf == null) {
    		emf = OpenJPAPersistence.cast(Persistence.createEntityManagerFactory("audit"));
    		assertNotNull(emf);
    		auditor = emf.getConfiguration().getAuditorInstance();
            em = emf.createEntityManager();
    		clearAuditedEntries();
    		oid = createManagedObject();
    	} else {
    		em = emf.createEntityManager();
    	}
    }

    private Object createManagedObject() {
    	em.getTransaction().begin();
    	X x = new X();
    	x.setName("New Object");
    	x.setPrice(100);
    	em.persist(x);
    	em.getTransaction().commit();

    	return emf.getPersistenceUnitUtil().getIdentifier(x);
    }

    private void clearAuditedEntries() {
    	em.getTransaction().begin();
    	em.createQuery("delete from AuditedEntry a").executeUpdate();
    	em.getTransaction().commit();
    }

    public void testAuditorIsConfigured() {
    	assertNotNull(auditor);
    }

    public void testIsEntityAuditable() {
    	assertNotNull(X.class.getAnnotation(Auditable.class));
    }

    public void testNewInstancesAreAudited() {
    	X x = em.find(X.class, oid);
    	assertNotNull(x);

    	AuditedEntry entry = findLastAuditedEntry(AuditableOperation.CREATE);

    	assertNotNull(entry);
    	assertEquals(x, entry.getAudited());
    	assertEquals(AuditableOperation.CREATE, entry.getOperation());
    	assertEquals(X.class, entry.getAudited().getClass());
    	assertTrue(entry.getUpdatedFields().isEmpty());
    }

    public void testUpdateOutsideTransactionAreAudited() {
    	X x = em.find(X.class, oid);
    	assertNotNull(x);

    	x.setName("Updated Object outside transaction");

    	em.getTransaction().begin();
    	x = em.merge(x);
    	em.getTransaction().commit();

    	AuditedEntry entry = findLastAuditedEntry(AuditableOperation.UPDATE);

    	assertNotNull(entry);
    	assertEquals(x, entry.getAudited());
    	assertEquals(AuditableOperation.UPDATE, entry.getOperation());
    	assertTrue(entry.getUpdatedFields().contains("name"));
    	assertFalse(entry.getUpdatedFields().contains("price"));
    }

    public void testUpdateInsideTransactionAreAudited() {
    	X x = em.find(X.class, oid);
    	assertNotNull(x);


    	em.getTransaction().begin();
    	x.setPrice(x.getPrice()+100);
    	x = em.merge(x);
    	em.getTransaction().commit();

    	AuditedEntry entry = findLastAuditedEntry(AuditableOperation.UPDATE);


    	assertNotNull(entry);
    	assertEquals(x, entry.getAudited());
    	assertEquals(AuditableOperation.UPDATE, entry.getOperation());
    	assertFalse(entry.getUpdatedFields().contains("name"));
    	assertTrue(entry.getUpdatedFields().contains("price"));
    }

    public void testAuditDoesNotLeakMemory() {
    	int N = 1000;
    	EntityManager em = emf.createEntityManager();
	   	long m2 = insert(N, em);
    	em = Persistence.createEntityManagerFactory("no-audit").createEntityManager();
    	assertNull(OpenJPAPersistence.cast(em).getEntityManagerFactory().getConfiguration().getAuditorInstance());
		long m0 = insert(N, em);
    	System.err.println("Memory used with no auditor " + m0);
    	System.err.println("Memory used with auditor " + m2);
		double pct = 100.0*(m2-m0)/m0;
		System.err.println("Extra memory with auditor " + pct);
    	assertTrue(pct < 10.0);
    }

    private long insert(int N, EntityManager em) {
    	assertTrue(ensureGarbageCollection());
    	long m1 = Runtime.getRuntime().freeMemory();
    	em.getTransaction().begin();
    	for (int i = 0; i < N; i++) {
    		X x = new X();
    		x.setName("X"+System.currentTimeMillis());
    		em.persist(x);
    	}
    	em.getTransaction().commit();
    	assertTrue(ensureGarbageCollection());
    	long m2 = Runtime.getRuntime().freeMemory();
    	long mused = m1-m2;

    	return mused;
    }

    /**
     * Finds the latest audit entry of the given operation type.
     * The <em>latest</em> is determined by a sort on identifier which is assumed to be monotonically ascending.
     *
     */
    AuditedEntry findLastAuditedEntry(AuditableOperation op) {
        List<AuditedEntry> entry =
            em.createQuery("select a from AuditedEntry a where a.operation=:op order by a.id desc", AuditedEntry.class)
                .setMaxResults(1).setParameter("op", op).getResultList();
        return entry.get(0);
    }

	public boolean ensureGarbageCollection() {
		ReferenceQueue<Object> detector = new ReferenceQueue<>();
		Object marker = new Object();
		WeakReference<Object> ref = new WeakReference<>(marker, detector);
		marker = null;
		System.gc();
		try {
			return detector.remove() == ref;
		} catch (InterruptedException e) {

		}
		return false;
	}



}
