/*
 * Copyright (c) 2011, 2020 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:
//     July 13, 2011 - Andrei Ilitchev (Oracle) - initial API and implementation
package org.eclipse.persistence.testing.framework;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.sessions.SessionEvent;
import org.eclipse.persistence.sessions.SessionEventListener;
import org.eclipse.persistence.sessions.SessionEventManager;

/**
 * <p><b>Purpose</b>: Used to test handling of session events.
 *
 * Create several instances of the class;
 * name them to distinguish from each other;
 * add them to different sessions' EventManagers.
 *
 * In the test:
 *   // clear the previously logged event handlings
 *   SessionEventTracker.clearLog();
 *   // define which events should be tracked, for instance:
 *   // first clear all the previously set events,
 *   SessionEventTracker.noneEvents();
 *   // then add events to be tracked:
 *   SessionEventTracker.addEvent(SessionEvent.PreLogin);
 *   SessionEventTracker.addEvent(SessionEvent.PostLogin);
 *   // start tracking
 *   SessionEventTracker.startTracking();
 *
 *   // do something
 *
 *   SessionEventTracker.stopTracking();
 *   // analyze listeners and events Lists.
 *
 *   clean up (optional)
 *   SessionEventTracker.clearLog();
 *   // return back to the default setting - handling all events
 *   SessionEventTracker.allEvents();
 *
 *   In static handlings list contains instances of Handling class -
 *   each holds an event and a listener that has handled it.
 *
 *   It's possible to set error on a Handling (see examples preLogin and postLogin) -
 *   that sends the erroneous Handling to errors list (still kept on handlings list, too).
 *
 * @see SessionEventManager#addListener(SessionEventListener)
 * @see Session#getEventManager()
 * @see SessionEvent
 */
public class SessionEventTracker implements SessionEventListener {

    // in order from 1 to (currently) 35 (no holes!).
    // should be kept in sync with SessionEvent codes.
    public static final String[] eventNames = {
        "PreExecuteQuery ",
        "PostExecuteQuery",
        "PreBeginTransaction",
        "PostBeginTransaction",
        "PreCommitTransaction",
        "PostCommitTransaction",
        "PreRollbackTransaction",
        "PostRollbackTransaction",
        "PostAcquireUnitOfWork",
        "PreCommitUnitOfWork",
        "PostCommitUnitOfWork",
        "PreReleaseUnitOfWork",
        "PostReleaseUnitOfWork",
        "PrepareUnitOfWork",
        "PostResumeUnitOfWork",
        "PostAcquireClientSession",
        "PreReleaseClientSession",
        "PostReleaseClientSession",
        "OutputParametersDetected",
        "MoreRowsDetected",
        "PostConnect",
        "PostAcquireConnection",
        "PreReleaseConnection",
        "PreLogin",
        "PostLogin",
        "PreMergeUnitOfWorkChangeSet",
        "PreDistributedMergeUnitOfWorkChangeSet",
        "PostMergeUnitOfWorkChangeSet",
        "PostDistributedMergeUnitOfWorkChangeSet",
        "PreCalculateUnitOfWorkChangeSet",
        "PostCalculateUnitOfWorkChangeSet",
        "MissingDescriptor",
        "PostAcquireExclusiveConnection",
        "PreReleaseExclusiveConnection",
        "NoRowsModified"
    };

    public static int nEvents = eventNames.length;

    public static boolean isTracking;
    public static boolean[] shouldTrackEvent = new boolean[nEvents];
    static {
        allEvents();
    }

    public static class Handling {
        public Handling(SessionEventTracker listener, SessionEvent event) {
            this.time = System.currentTimeMillis();
            this.listener = listener;
            this.event = event;
        }
        long time;
        SessionEventTracker listener;
        SessionEvent event;
        String error = "";
        public String toString() {
            return listener.name + " -> " + eventToString(event) + (error.length()==0 ? "" : " Error: " + error);
        }
        public void setError(String error) {
            this.error = error;
            if (error.length() > 0) {
                synchronized (errors) {
                    errors.add(this);
                }
            } else {
                synchronized (errors) {
                    errors.remove(this);
                }
            }
        }
        public String getError() {
            return error;
        }
        public SessionEvent getEvent() {
            return event;
        }
        public SessionEventTracker getListener() {
            return listener;
        }
    }

    protected static List<Handling> handlings = new ArrayList();
    protected static List<Handling> errors = new ArrayList();

    protected String name = "";

    public SessionEventTracker() {
        super();
    }

    public SessionEventTracker(String name) {
        this.name = name;
    }

    public static void startTracking() {
        isTracking = true;
    }

    public static void stopTracking() {
        isTracking = false;
    }

    public static boolean isTracking() {
        return isTracking;
    }

    public static boolean isTrackingEvent(SessionEvent event) {
        if (isTracking) {
            return shouldTrackEvent[event.getEventCode()];
        } else {
            return false;
        }
    }

    public static int size() {
        return handlings.size();
    }

    public static void clearLog() {
        handlings = new ArrayList();
        errors = new ArrayList();
    }

    public static void allEvents() {
        for (int i=1; i<nEvents; i++) {
            shouldTrackEvent[i] = true;
        }
    }

    public static void noneEvents() {
        for (int i=1; i<nEvents; i++) {
            shouldTrackEvent[i] = false;
        }
    }

    public static void addEvent(int eventCode) {
        if (eventCode <= 0) {
            throw new TestErrorException("event code " + eventCode + " is wrong - should be a positive number");
        } else if (eventCode > eventNames.length) {
            throw new TestErrorException("event code " + eventCode + " is unknown - add event name for it to SessionEventTracker.eventNames array");
        }
        shouldTrackEvent[eventCode] = true;
    }

    public static void removeEvent(int eventCode) {
        if (eventCode <= 0) {
            throw new TestErrorException("event code " + eventCode + " is wrong - should be a positive number");
        } else if (eventCode > eventNames.length) {
            throw new TestErrorException("event code " + eventCode + " is unknown - add event name for it to SessionEventTracker.eventNames array");
        }
        shouldTrackEvent[eventCode] = false;
    }

    public static List<Handling> getHandlings() {
        return handlings;
    }

    public static List<Handling> getErrors() {
        return errors;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * PUBLIC:
     * This event is raised on the session if a descriptor is missing for a class being persisted.
     * This can be used to lazy register the descriptor or set of descriptors.
     */
    public void missingDescriptor(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the session after read object query detected more than a single row back from the database.
     * The "result" of the event will be the call.  Some applications may want to interpret this as an error or warning condition.
     */
    public void moreRowsDetected(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the session after update or delete SQL has been sent to the database
     * but a row count of zero was returned.
     */
    public void noRowsModified(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the session after a stored procedure call has been executed that had output parameters.
     * If the proc was used to override an insert/update/delete operation then EclipseLink will not be expecting any return value.
     * This event mechanism allows for a listener to be registered before the proc is call to process the output values.
     * The event "result" will contain a Record of the output values, and property "call" will be the StoredProcedureCall.
     */
    public void outputParametersDetected(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the client session after creation/acquiring.
     */
    public void postAcquireClientSession(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on when using the server/client sessions.
     * This event is raised after a connection is acquired from a connection pool.
     */
    public void postAcquireConnection(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised when a ClientSession, with Isolated data, acquires
     * an exclusive connection.  The event will contain the ClientSession that
     * is being acquired.  Users can set properties within the ConnectionPolicy
     * of that ClientSession for access within this event.
     */
    public void postAcquireExclusiveConnection(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the unit of work after creation/acquiring.
     * This will be raised on nest units of work.
     */
    public void postAcquireUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised after a database transaction is started.
     * It is not raised for nested transactions.
     */
    public void postBeginTransaction(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised after the commit has begun on the UnitOfWork but before
     * the changes are calculated.
     */
    public void preCalculateUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised after the commit has begun on the UnitOfWork and
     * after the changes are calculated.  The UnitOfWorkChangeSet, at this point,
     * will contain changeSets without the version fields updated and without
     * IdentityField type primary keys.  These will be updated after the insert, or
     * update, of the object
     */
    public void postCalculateUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised after a database transaction is commited.
     * It is not raised for nested transactions.
     */
    public void postCommitTransaction(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the unit of work after commit.
     * This will be raised on nest units of work.
     */
    public void postCommitUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised after the session connects to the database.
     * In a server session this event is raised on every new connection established.
     */
    public void postConnect(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised after the execution of every query against the session.
     * The event contains the query and query result.
     */
    public void postExecuteQuery(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the client session after releasing.
     */
    public void postReleaseClientSession(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the unit of work after release.
     * This will be raised on nest units of work.
     */
    public void postReleaseUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the unit of work after resuming.
     * This occurs after pre/postCommit.
     */
    public void postResumeUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised after a database transaction is rolledback.
     * It is not raised for nested transactions.
     */
    public void postRollbackTransaction(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This even will be raised after a UnitOfWorkChangeSet has been merged
     * When that changeSet has been received from a distributed session
     */
    public void postDistributedMergeUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This even will be raised after a UnitOfWorkChangeSet has been merged
     */
    public void postMergeUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised before a database transaction is started.
     * It is not raised for nested transactions.
     */
    public void preBeginTransaction(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised before a database transaction is commited.
     * It is not raised for nested transactions.
     */
    public void preCommitTransaction(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the unit of work before commit.
     * This will be raised on nest units of work.
     */
    public void preCommitUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised before the execution of every query against the session.
     * The event contains the query to be executed.
     */
    public void preExecuteQuery(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the unit of work after the SQL has been flushed, but the commit transaction has not been executed.
     * It is similar to the JTS prepare phase.
     */
    public void prepareUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the client session before releasing.
     */
    public void preReleaseClientSession(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on when using the server/client sessions.
     * This event is raised before a connection is released into a connection pool.
     */
    public void preReleaseConnection(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is fired just before a Client Session, with isolated data,
     * releases its Exclusive Connection
     */
    public void preReleaseExclusiveConnection(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the unit of work before release.
     * This will be raised on nest units of work.
     */
    public void preReleaseUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised before a database transaction is rolledback.
     * It is not raised for nested transactions.
     */
    public void preRollbackTransaction(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This even will be raised before a UnitOfWorkChangeSet has been merged
     * When that changeSet has been received from a distributed session
     */
    public void preDistributedMergeUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This even will be raised before a UnitOfWorkChangeSet has been merged
     */
    public void preMergeUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This Event is raised before the session logs in.
     */
    public void preLogin(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        Handling handling = log(event);
        if (((DatabaseSessionImpl)event.getSession()).isLoggedIn()) {
            handling.setError("session is already logged in");
        }
    }

    /**
     * PUBLIC:
     * This Event is raised after the session logs out.
     */
    public void preLogout(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This Event is raised after the session logs out.
     */
    public void postLogout(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This Event is raised after the session logs in.
     */
    public void postLogin(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        Handling handling = log(event);
        String errorMsg = "";
        Iterator<ClassDescriptor> it = event.getSession().getDescriptors().values().iterator();
        while (it.hasNext()) {
            ClassDescriptor descriptor = it.next();
            if (!descriptor.isAggregateDescriptor()) {
                if (!descriptor.isFullyInitialized()) {
                    errorMsg += descriptor.getJavaClass().getName() + "; ";
                }
            }
        }
        if (errorMsg.length() > 0) {
            errorMsg = "Some descriptors are not initialized: " + errorMsg;
            handling.setError(errorMsg);
        }
    }

    protected Handling log(SessionEvent event) {
        Handling handling = new Handling(this, event);
        synchronized (handlings) {
            handlings.add(handling);
        }
        return handling;
    }

    public String toString() {
        return Helper.getShortClassName(this) + "(" + (name != null ? name : "") + ")";
    }

    public static String eventToString(SessionEvent event) {
        return getEventName(event.getEventCode()) + "[" + sessionToString(event.getSession()) + "]";
    }

    public static String getEventName(int eventCode) {
        if (eventCode <= 0) {
            throw new TestErrorException("event code " + eventCode + " is wrong - should be a positive number");
        } else if (eventCode > eventNames.length) {
            throw new TestErrorException("event code " + eventCode + " is unknown - add event name for it to SessionEventTracker.eventNames array");
        }
        return eventNames[eventCode - 1];
    }

    public static String sessionToString(Session session) {
        return Helper.getShortClassName(session) + "(" + session.getName() + ")";
    }
}
