/*
 * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2021 IBM Corporation. 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 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;

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

/**
 * <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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    public void outputParametersDetected(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the client session after creation/acquiring.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    public void postCommitUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC: This event is raised on the unit of work after a flush.
     */
    @Override
    public void postFlushUnitOfWork(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.
     */
    @Override
    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.
     */
    @Override
    public void postExecuteCall(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.
     */
    @Override
    public void postExecuteQuery(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the client session after releasing.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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
     */
    @Override
    public void postDistributedMergeUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This even will be raised after a UnitOfWorkChangeSet has been merged
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    public void preCommitUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC: This event is raised on the unit of work before a flush.
     */
    @Override
    public void preFlushUnitOfWork(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.
     */
    @Override
    public void preExecuteCall(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.
     */
    @Override
    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.
     */
    @Override
    public void prepareUnitOfWork(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

    /**
     * PUBLIC:
     * This event is raised on the client session before releasing.
     */
    @Override
    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.
     */
    @Override
    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
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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
     */
    @Override
    public void preDistributedMergeUnitOfWorkChangeSet(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

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

    /**
     * PUBLIC:
     * This Event is raised before the session logs in.
     */
    @Override
    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.
     */
    @Override
    public void preLogout(SessionEvent event) {
        if (!isTrackingEvent(event)) {
            return;
        }
        log(event);
    }

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

    /**
     * PUBLIC:
     * This Event is raised after the session logs in.
     */
    @Override
    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() + ")";
    }
}
