/*
 * Copyright (c) 1998, 2018 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:
//     05/28/2008-1.0M8 Andrei Ilitchev.
//       - New file introduced for bug 224964: Provide support for Proxy Authentication through JPA.
package org.eclipse.persistence.testing.tests.proxyauthentication.thin;

import java.util.Map;
import java.util.Properties;

import oracle.jdbc.OracleConnection;

import org.eclipse.persistence.sessions.*;
import org.eclipse.persistence.sessions.server.ClientSession;
import org.eclipse.persistence.sessions.server.ConnectionPolicy;
import org.eclipse.persistence.sessions.server.ServerSession;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.internal.sessions.ExclusiveIsolatedClientSession;
import org.eclipse.persistence.queries.DataModifyQuery;
import org.eclipse.persistence.queries.ValueReadQuery;

import org.eclipse.persistence.testing.framework.*;
import org.eclipse.persistence.testing.framework.oracle.SessionExchanger;

// Test verifying that connectionUser and proxyUser are used as expected.
public class ProxyAuthenticationConnectionCustomizerTestCase extends TestCase {
    // true -> DatabaseSession;  false -> ServerSession.
    boolean shouldUseDatabaseSession;
    // indicates whether external connection pooling should be used.
    boolean shouldUseExternalConnectionPooling;

    Map mainSessionProxyProperties;
    String expectedMainSessionUser;

    // (valid on ServerSession only) indicates whether EclusiveIsolatedClientSession should be used.
    boolean shoulUseExclusiveIsolatedSession;

    Map clientSessionProxyProperties;
    String expectedClientSessionUser;

    // Substitutes the original session with the one required for the test; restores the original one after the test.
    SessionExchanger exchanger = new SessionExchanger();

    // Set to true to test for Bug 267880: JPA/ProxyAuthentication tests failed with "java.sql.SQLException: Closed Statement":
    // the bug caused tests using internal connection pools to fail.
    boolean shouldEnableStatementCaching = true;

    private String writeUser;
    class Listener extends SessionEventAdapter {
        public void outputParametersDetected(SessionEvent event) {
            writeUser = (String)((Map)event.getResult()).get("OUT");
        }
    }
    SessionEventListener listener = new Listener();

    static ValueReadQuery readQuery;
    static DataModifyQuery writeQuery;
    static {
        // Used to return the schema name used by reading connection.
        readQuery = new ValueReadQuery();
        readQuery.setSQLString("SELECT SYS_CONTEXT ('USERENV', 'CURRENT_SCHEMA') FROM DUAL");

        // Used to return (through event) the schema name used by writing connection.
        writeQuery = new DataModifyQuery();
        writeQuery.setSQLString("BEGIN SELECT SYS_CONTEXT ('USERENV', 'CURRENT_SCHEMA') INTO ###OUT FROM DUAL; END;");
        writeQuery.setIsUserDefined(true);
    }

    String errorMsg = "";

    public static ProxyAuthenticationConnectionCustomizerTestCase createDatabaseSessionTest(boolean shouldUseExternalConnectionPooling, Map databaseSessionProxyProperties) {
        return new ProxyAuthenticationConnectionCustomizerTestCase(shouldUseExternalConnectionPooling, databaseSessionProxyProperties);
    }

    public static ProxyAuthenticationConnectionCustomizerTestCase createServerSessionTest(boolean shouldUseExternalConnectionPooling, Map serverSessionProxyProperties, boolean shoulUseExclusiveIsolatedSession, Map clientSessionProxyProperties) {
        return new ProxyAuthenticationConnectionCustomizerTestCase(shouldUseExternalConnectionPooling, serverSessionProxyProperties, shoulUseExclusiveIsolatedSession, clientSessionProxyProperties);
    }

    protected ProxyAuthenticationConnectionCustomizerTestCase(boolean shouldUseExternalConnectionPooling, Map databaseSessionProxyProperties) {
        this(true, shouldUseExternalConnectionPooling, databaseSessionProxyProperties, false, null);
    }

    protected ProxyAuthenticationConnectionCustomizerTestCase(boolean shouldUseExternalConnectionPooling, Map serverSessionProxyProperties, boolean shoulUseExclusiveIsolatedSession, Map clientSessionProxyProperties) {
        this(false, shouldUseExternalConnectionPooling, serverSessionProxyProperties, shoulUseExclusiveIsolatedSession, clientSessionProxyProperties);
    }

    protected ProxyAuthenticationConnectionCustomizerTestCase(boolean shouldUseDatabaseSession, boolean shouldUseExternalConnectionPooling, Map mainSessionProxyProperties, boolean shoulUseExclusiveIsolatedSession, Map clientSessionProxyProperties) {
        this.shouldUseDatabaseSession = shouldUseDatabaseSession;
        this.shouldUseExternalConnectionPooling = shouldUseExternalConnectionPooling;
        this.mainSessionProxyProperties = mainSessionProxyProperties;
        // in case no proxy properties specified on the main session it uses connectionUser.
        expectedMainSessionUser = ProxyAuthenticationUsersAndProperties.connectionUser;
        if(mainSessionProxyProperties != null) {
            expectedMainSessionUser = getExpectedUserName(mainSessionProxyProperties);
        }
        this.shoulUseExclusiveIsolatedSession = shoulUseExclusiveIsolatedSession;
        if(!shouldUseDatabaseSession) {
            this.clientSessionProxyProperties = clientSessionProxyProperties;
            // in case no proxy properties specified on the ClientSession it uses the same use as the ServerSession.
            expectedClientSessionUser = expectedMainSessionUser;
            if(clientSessionProxyProperties != null) {
                expectedClientSessionUser = getExpectedUserName(clientSessionProxyProperties);
            }
        }

        // generate the test name
        String mainPropsStr = mainSessionProxyProperties != null ? " Proxy properties on" : "No proxy properties on";
        String sessionStr = shouldUseDatabaseSession ? " DatabaseSession" : " ServerSession";
        String poolingStr = shouldUseExternalConnectionPooling ? " usingExternalConnectionPooling" : "";
        if(shouldUseDatabaseSession) {
            this.setName(mainPropsStr + sessionStr + poolingStr);
        } else {
            String clientPropsStr = "; no proxy properties on";
            if(clientSessionProxyProperties != null) {
                if(mainSessionProxyProperties == null) {
                    clientPropsStr = "; proxy properties on";
                } else {
                    clientPropsStr = expectedClientSessionUser.equals(ProxyAuthenticationUsersAndProperties.connectionUser) ? "; canceled on" : "; overridden on";
                }
            }
            String clientSessionStr = shoulUseExclusiveIsolatedSession ? " ExclusiveIsolatedClientSession" : " ClientSession";
            this.setName(mainPropsStr + sessionStr + poolingStr + clientPropsStr + clientSessionStr);
        }
    }

    // If the session's proxyProperties PersistenceUnitProperties.ORACLE_PROXY_TYPE property has an empty string value
    // then the session should use connectionUser;
    // otherwise it should use proxyUser specified in the proxyProperties.
    static String getExpectedUserName(Map proxyProperties) {
        Object proxytype = proxyProperties.get(PersistenceUnitProperties.ORACLE_PROXY_TYPE);
        boolean proxytypeIsAnEmptyString = proxytype instanceof String && ((String)proxytype).length()==0;
        if(proxytypeIsAnEmptyString) {
            // PersistenceUnitProperties.ORACLE_PROXY_TYPE -> "" means that proxyProperies are to be ignored and therefore connectionUser should be used.
            return ProxyAuthenticationUsersAndProperties.connectionUser;
        } else {
            return (String)proxyProperties.get(OracleConnection.PROXY_USER_NAME);
        }
    }

    public void setup() {
        if (!getSession().getPlatform().isOracle()) {
            throw new TestWarningException("Supports Oracle platform only");
        }
        Properties loginProperties = ProxyAuthenticationUsersAndProperties.connectionProperties;
        // In case of ServerSession with internal connection pools each pool will use just one connection.
        getExecutor().setSession(exchanger.createNewSession((DatabaseSession)getSession(), shouldUseDatabaseSession, shouldUseExternalConnectionPooling, loginProperties, mainSessionProxyProperties, 1, 1, 1, 1));
        getSession().getEventManager().addListener(listener);
        if(!shouldUseDatabaseSession) {
            if(shoulUseExclusiveIsolatedSession) {
                ((ServerSession)getSession()).getDefaultConnectionPolicy().setExclusiveMode(ConnectionPolicy.ExclusiveMode.Always);

            }
        }
    }

    String getReadUser(Session session) {
        return (String)session.executeQuery(readQuery);
    }

    String getWriteUser(Session session) {
        session.executeQuery(writeQuery);
        String user = writeUser;
        writeUser = null;
        return user;
    }

    public void test() {
        if(this.shouldUseDatabaseSession) {
            testDatabaseSession();
        } else {
            testServerSession();
        }
    }

    void testDatabaseSession() {
        // DatabaseSession was created with proxy properties - should always use proxyUser.
        verifyUser("DatabaseSession read", getReadUser(getSession()), ProxyAuthenticationUsersAndProperties.proxyUser);
        verifyUser("DatabaseSession wrote", getWriteUser(getSession()), ProxyAuthenticationUsersAndProperties.proxyUser);
    }

    void testServerSession() {
        ServerSession ss = (ServerSession)getSession();
        if(shouldEnableStatementCaching) {
            ss.getPlatform().setShouldCacheAllStatements(true);
        }
        verifyUser("ServerSession first read", getReadUser(ss), expectedMainSessionUser);

        // The first ClientSession created without proxy properties - should always use the same user as ServerSession.
        ClientSession cs1 = ss.acquireClientSession();
        if(shoulUseExclusiveIsolatedSession) {
            // verify that IsolatedSession is used if required.
            if(!(cs1 instanceof ExclusiveIsolatedClientSession)) {
                throw new TestProblemException("The ClientSession must be ExclusiveIsolatedClientSession");
            }
        }
        verifyUser("ClientSession1 before transaction read", getReadUser(cs1), expectedMainSessionUser);
        cs1.beginTransaction();
        verifyUser("ClientSession1in transaction read", getReadUser(cs1), expectedMainSessionUser);
        verifyUser("ClientSession1 wrote", getWriteUser(cs1), expectedMainSessionUser);
        cs1.rollbackTransaction();
        verifyUser("ClientSession1 after transaction read", getReadUser(cs1), expectedMainSessionUser);
        cs1.release();

        // The second ClientSession created with proxy properties.
        ClientSession cs2 = ss.acquireClientSession(clientSessionProxyProperties);
        if(shoulUseExclusiveIsolatedSession) {
            verifyUser("ExclusiveIsolatedClientSession2 before transaction read", getReadUser(cs2), expectedClientSessionUser);
        } else {
            verifyUser("ClientSession2 before transaction read", getReadUser(cs2), expectedMainSessionUser);
        }
        cs2.beginTransaction();
        verifyUser("ClientSession2 in transaction read", getReadUser(cs2), expectedClientSessionUser);
        verifyUser("ClientSession2 wrote", getWriteUser(cs2), expectedClientSessionUser);
        cs2.rollbackTransaction();
        if(shoulUseExclusiveIsolatedSession) {
            verifyUser("ExclusiveIsolatedClientSession2 after transaction read", getReadUser(cs2), expectedClientSessionUser);
        } else {
            verifyUser("ClientSession2 after transaction read", getReadUser(cs2), expectedMainSessionUser);
        }
        cs2.release();

        // verify that ServerSession still uses the correct user.
        // Because there is one one connection in each connection pool (internal pool case) this would fail in case proxy customizer
        // wasn't removed by cs2.
        verifyUser("ServerSession second read", getReadUser(ss), expectedMainSessionUser);

        // The third ClientSession created without proxy properties again - should always use the same user as ServerSession.
        // Because there is one one connection in each connection pool (internal pool case) this would fail in case proxy customizer
        // wasn't removed by cs2.
        ClientSession cs3 = ss.acquireClientSession();
        verifyUser("ClientSession3 before transaction read", getReadUser(cs3), expectedMainSessionUser);
        cs3.beginTransaction();
        verifyUser("ClientSession3 in transaction read", getReadUser(cs3), expectedMainSessionUser);
        verifyUser("ClientSession3 wrote", getWriteUser(cs3), expectedMainSessionUser);
        cs3.rollbackTransaction();
        verifyUser("ClientSession3 after transaction read", getReadUser(cs3), expectedMainSessionUser);
        cs3.release();

    }

    void verifyUser(String msg, String user, String expectedUser) {
        if(!user.equals(expectedUser)) {
            errorMsg += msg + " through wrong user " + user + " - " + expectedUser + " was expected \n";
        }
    }

    public void verify() {
        if(errorMsg.length() > 0) {
            throw new TestErrorException(errorMsg);
        }
    }

    public void reset() {
        getSession().getEventManager().removeListener(listener);
        getExecutor().setSession(exchanger.returnOriginalSession());
        errorMsg = "";
    }
}
