/*
 * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.testing.tests.distributedcache;

import java.rmi.registry.LocateRegistry;
import org.eclipse.persistence.testing.models.employee.domain.Employee;
import org.eclipse.persistence.testing.models.employee.relational.EmployeeProject;
import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy;
import org.eclipse.persistence.internal.helper.ConcurrencyManager;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.sessions.DatabaseLogin;
import org.eclipse.persistence.sessions.Project;
import org.eclipse.persistence.sessions.SessionEvent;
import org.eclipse.persistence.sessions.SessionEventAdapter;
import org.eclipse.persistence.sessions.SessionEventListener;
import org.eclipse.persistence.sessions.UnitOfWork;
import org.eclipse.persistence.testing.framework.TestCase;
import org.eclipse.persistence.testing.framework.TestErrorException;
import org.eclipse.persistence.sessions.coordination.RemoteCommandManager;
import org.eclipse.persistence.sessions.coordination.TransportManager;
import org.eclipse.persistence.sessions.server.Server;
import org.eclipse.persistence.sessions.server.ServerSession;

public abstract class DistributedCacheMergeTest extends TestCase {
    private OptimisticLockingPolicy policy1 = null;
    private OptimisticLockingPolicy policy2 = null;
    protected Server cluster1Session = null;
    protected Server cluster2Session = null;
    protected Session originalSession = null;
    public static ConcurrencyManager semaphore = new ConcurrencyManager();
    Object originalObject = null;
    int initialNumProjs;

    public DistributedCacheMergeTest() {
        setDescription("Testing");
    }

    @Override
    protected void setup() throws Exception {
        originalSession = getExecutor().getSession();
        originalSession.getIdentityMapAccessor().initializeAllIdentityMaps();
        createObject();

        try {
            LocateRegistry.createRegistry(41099);
        } catch (Exception e) {
            //hopefully this exception is just because the registry already exists
        }

        cluster1Session = buildSession("cluster1");
        cluster1Session.login();

        cluster2Session = buildSession("cluster2");
        cluster2Session.login();
        Thread.sleep(5000);// Let the Cache-sync get configured.

        policy1 = disableOptimisticLocking(cluster1Session);
        policy2 = disableOptimisticLocking(cluster2Session);
    }

    protected void createObject() {
        originalObject = getOriginalObject();
        UnitOfWork uow = originalSession.acquireUnitOfWork();
        Object newObj = uow.registerNewObject(originalObject);
        uow.commit();
    }

    protected Server buildSession(String sessionName) throws Exception {
        ServerSession session = null;
        Project p = getNewProject();


        DatabaseLogin theLogin = originalSession.getLogin();
        p.setLogin(originalSession.getLogin());
        session = (ServerSession)p.createServerSession();
        session.setSessionLog(getSession().getSessionLog());

        RemoteCommandManager cm = new RemoteCommandManager(session);

        // set propagate command asynchronously for testing
        cm.setShouldPropagateAsynchronously(true);
        cm.getDiscoveryManager().setAnnouncementDelay(0);
        // ovewrite default to use RMI registry naming service
        cm.getTransportManager().setNamingServiceType(TransportManager.REGISTRY_NAMING_SERVICE);
        // set full rmi URL of local host
        cm.setUrl("rmi://localhost:41099");
        // turn on cache sync with RCM
        session.setShouldPropagateChanges(true);
        cm.setServerPlatform(getSession().getServerPlatform());
        cm.initialize();

        // Sleep to allow RCM to startup and find each session.
        try {
            Thread.sleep(2000);
        } catch (Exception ignore) {
        }

        return session;
    }

    @Override
    public void reset() throws Exception {
        int depth = semaphore.getDepth();

        while (depth != 0) {
            semaphore.release();
            depth = semaphore.getDepth();
        }
        UnitOfWork uow = cluster1Session.acquireClientSession().acquireUnitOfWork();
        uow.deleteObject(originalObject);
        uow.commit();

        enableOptimisticLocking(cluster1Session, policy1);
        enableOptimisticLocking(cluster2Session, policy2);

        cluster1Session.logout();
        cluster1Session = null;
        cluster2Session.logout();
        cluster2Session = null;
    }

    private OptimisticLockingPolicy disableOptimisticLocking(Server server) {
        ClassDescriptor descriptor = server.getDescriptor(getRootClass());
        OptimisticLockingPolicy policy = descriptor.getOptimisticLockingPolicy();
        descriptor.setOptimisticLockingPolicy(null);
        return policy;
    }

    private void enableOptimisticLocking(Server server, OptimisticLockingPolicy policy) {
        ClassDescriptor descriptor = server.getDescriptor(getRootClass());
        descriptor.setOptimisticLockingPolicy(policy);
    }

    @Override
    protected void test() {
        Session clientSession1 = cluster1Session.acquireClientSession();
        Session clientSession2 = cluster2Session.acquireClientSession();

        cluster2Session.getIdentityMapAccessor().initializeAllIdentityMaps();
        cluster2Session.getEventManager().addListener(buildCacheMergeBlockingListener());

        Object object1 = findOriginalObject(clientSession1);

        if (object1 == null) {
            throw new TestErrorException("Employee on Server 1 exists");
        }
        initialNumProjs = getCollectionSize(object1);

        UnitOfWork uow = clientSession1.acquireUnitOfWork();
        Object newEmpWC = uow.registerObject(object1);
        modifyCollection(uow, newEmpWC);

        semaphore.acquire();

        uow.commit();

        if (getCollectionSize(object1) != (initialNumProjs + 1)) {
            throw new TestErrorException("Employee has the wrong number of items in the collection; expected:" + (initialNumProjs + 1) + " was:" + getCollectionSize(object1));
        }

        try {
            while (semaphore.getNumberOfWritersWaiting() == 0) {
                Thread.sleep(10);
            }
        } catch (Exception e) {
            throw new TestErrorException("Error while getting Thread to sleep", e);
        }

        // Make sure its not in server 2's cache
        Object object2 = clientSession2.getIdentityMapAccessor().getFromIdentityMap(object1);

        if (object2 != null) {
            throw new TestErrorException("Employee already exists in Server 2's cache");
        }

        object2 = findOriginalObject(clientSession2);

        if (object2 == null) {
            throw new TestErrorException("Employee does not exist in Server 2's cache");
        }

        if (getCollectionSize(object2) != (initialNumProjs + 1)) {
            throw new TestErrorException("Employee has the wrong number of items in the collection");
        }

        semaphore.release();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new TestErrorException("Error while getting Thread to sleep", e);
        }

        semaphore.acquire();

        if (getCollectionSize(object2) != (initialNumProjs + 1)) {
            if (getCollectionSize(object2) == (initialNumProjs + 2)) {
                throw new TestErrorException("Employee has one too many items in the collection after merge");
            }
            throw new TestErrorException("Employee has wrong number of items in the collection after merge");
        }
        semaphore.release();

    }

    protected Class<?> getRootClass() {
        return Employee.class;
    }

    protected Project getNewProject() {
        return new EmployeeProject();
    }

    protected abstract void modifyCollection(UnitOfWork uow, Object objectToModify);

    protected abstract int getCollectionSize(Object rootObject);

    protected abstract Object buildOriginalObject();

    public Object findOriginalObject(Session session) {
        ReadObjectQuery roq = new ReadObjectQuery(originalObject);
        return session.executeQuery(roq);
    }

    protected SessionEventListener buildCacheMergeBlockingListener() {
        return new SessionEventAdapter() {
                @Override
                public void preDistributedMergeUnitOfWorkChangeSet(SessionEvent event) {
                    try {
                        DistributedCacheMergeTest.semaphore.acquire();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }

                @Override
                public void postDistributedMergeUnitOfWorkChangeSet(SessionEvent event) {
                    DistributedCacheMergeTest.semaphore.release();
                }
            };
    }

    public Object getOriginalObject() {
        if (originalObject == null) {
            originalObject = buildOriginalObject();
        }
        return originalObject;
    }
}
