/*
 * Copyright (c) 1998, 2022 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.framework;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.foundation.AbstractDirectMapping;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.sessions.server.ClientSession;

import java.util.Enumeration;
import java.util.Vector;

/**
 * <p>
 * <b>Purpose</b>: Define a generic test for writing an object to the database.
 * Should originalObject contain no changes to the original object from the
 * database (a TRIVIAL UPDATE), find and mutate a direct to field mapping before
 * writing the object to the database.  If originalObject is different from but
 * has the same primary key as an object on the database, do not mutate the
 * object as it has already been changed (a NON-TRIVIAL UPDATE).
 * <p>
 * <b>Responsibilities</b>:
 * <ul>
 * <li> Be independent of the class being tested.
 * <li> Attempt to mutate a Direct to Field mapping if instructed to do so.
 * <li> Execute the insert object query and verify no errors occurred.
 * <li> Verify the object written matches the object that was written.
 * </ul>
 */
public class WriteObjectTest extends TransactionalTestCase {

    /** Query to read the original object from the database */
    protected ReadObjectQuery query;

    /** The original object that is set thru example methods from the model */
    protected Object originalObject;

    /** The originalObject is read from the database and stored here */
    protected Object objectToBeWritten;

    /** The object from the database is read in verify to compare against the
    * objectToBeWritten.
    */
    protected Object objectFromDatabase;

    /** The following allows individual tests to be run with or
     * without bind all parameters on. */
    protected Boolean bindAllParameters = null;
    protected Boolean bindAllParametersOriginal = null;

    /** An option to test non-trivial updates. */
    protected boolean makesTrivialUpdate = true;

    /** This determines whether the test should attempt to mutate the object.
     * This defaults to true.  It can be set to false where changing the object
     * will cause failures.
     */
    protected boolean testShouldMutate = true;

    public WriteObjectTest() {
        setDescription("The test writing of the intended object from the "+
            "database and checks if it was inserted properly");
    }

    public WriteObjectTest(Object originalObject) {
        this.originalObject = originalObject;
        setName(getName() + "(" + originalObject.getClass() + ")");
        setDescription(
            "The test writing of the intended object, '"+originalObject
            +"', from the database and checks if it was inserted properly");
    }

    public Object getOriginalObject() {
        return originalObject;
    }

    /**
     * A method that takes an object and attempts to find a direct to field
     * mapping that can be mutated.  The value of the mapping is changed by
     * appending a mutation string to the existing value in the mapping.
     * Should no suitable mapping be found, the method simply returns the
     * object passed in.
     * This covers test case bug 2773036
     */
    public Object findAndMutateDirectToFieldMappingInObject(Object objectToBeMutated, boolean isInUOW) {
        DatabaseMapping dbMapping = null;
        DatabaseMapping mutatableMapping = null;

        String mutationString = "M"; // The string to append

        /**
         * Here the class of the object passed is used to get the corresponding
         * descriptor, which is then used to find the mappings and determine if
         * one exists that can be mutated
         */
        Class<?> objectClass = objectToBeMutated.getClass();
        ClassDescriptor descriptor = getSession().getProject().getClassDescriptor(objectClass);
        Vector<DatabaseMapping> mappings = descriptor.getMappings();

        if (isInUOW) {
            mutationString += "U";
        }

        Enumeration<DatabaseMapping> en = mappings.elements();

        /**
         * Parse the mappings for the object's descriptor to find a suitable
         * mapping that can be mutated.  The mapping must meet the conditions:
         * Not a primary key mapping
         * Must be direct to field
         * Must be a string field
         * Must not have a converter (i.e. Male--{@literal >}M, Female--{@literal >}F, Unknown--{@literal >}U)
         * The loop exits once an appropriate mapping is found or the list of
         * mappings has been fully parsed (whichever occurs first)
         */
        while (en.hasMoreElements ()) {
            dbMapping = en.nextElement();

            if (!dbMapping.isPrimaryKeyMapping()
                    && dbMapping.isDirectToFieldMapping()
                    &&!((AbstractDirectMapping) dbMapping).hasConverter()
                    && (dbMapping.getAttributeAccessor().getAttributeClass())
                    .getName().contains("String")) {
                mutatableMapping = dbMapping;
                break;
            }
        }

        /**
         * If a mapping was found that can be mutated, use TopLink methods to
         * modify the value stored in the object for that mapping.  Otherwise
         * do nothing.
         */
        if (mutatableMapping != null) {
            mutatableMapping.setAttributeValueInObject(
                objectToBeMutated,
                mutatableMapping.getAttributeValueFromObject(
                    objectToBeMutated) + mutationString);
        }
        else {
            // Can't necessarily throw error/warning as some projects
            // (i.e. LOB project) have descriptors that are not simple to mutate
        }

        return objectToBeMutated;
    }

    /**
     * @see #setMakesTrivialUpdate(boolean)
     */
    public boolean makesTrivialUpdate() {
        return makesTrivialUpdate;
    }

    /**
     * @see #setTestShouldMutate(boolean)
     */
    public boolean testShouldMutate() {
        return testShouldMutate;
    }

    @Override
    public void reset() {
        if (bindAllParametersOriginal != null) {
            getSession().getLogin().setShouldBindAllParameters(bindAllParametersOriginal);
        }
        super.reset();
    }

    /**
     * A trivial update is writing the same version of an object to the
     * database as exists on the database.  Normally in this test the object
     * passed to the constructor is used only to read the version of itself on
     * the database.  This version from the database is then written to the
     * database: a trivial update.  If this flag is set and originalObject is
     * different from but has the same primary key as an object on the database,
     * then a non-trivial update will occur.
     * Warning: If you modify the objects passed to this
     * test, do not obtain them from the population manager, as they may become
     * corrupted for other users.
     */
    public void setMakesTrivialUpdate(boolean value) {
        this.makesTrivialUpdate = value;
    }

    /**
     * Some subclasses of WriteObjectTest will not return the correct results if
     * the object is mutated, for example tests that pass null values and expect
     * nulls to be returned.  If this flag is set then the object will not be
     * mutated before attempting to write to the database.
     */
    public void setTestShouldMutate(boolean value) {
        this.testShouldMutate = value;
    }

    /**
     * Allows one to set bindAllParameters to true on a test by test basis.
     * This works only for simple sessions, and could cause this simple test to
     * run much slower than before.
     */
    public void setShouldBindAllParameters(boolean value) {
        bindAllParameters = value;
    }

    @Override
    protected void setup() {
        if (shouldBindAllParameters() != null) {
            bindAllParametersOriginal = getSession().getLogin().shouldBindAllParameters();
            getSession().getLogin().setShouldBindAllParameters(shouldBindAllParameters());
        }
        super.setup();

        this.query = new ReadObjectQuery();
        this.query.setSelectionObject(this.originalObject);

        /* Must ensure that the object is from the database for updates. */
        this.objectToBeWritten = getSession().executeQuery(this.query);
        if (this.query.getDescriptor().shouldIsolateProtectedObjectsInUnitOfWork() && getSession().isClientSession()){
            //must get entity from server session and not the isolated session.
            this.objectToBeWritten = ((ClientSession)getSession()).getParent().getIdentityMapAccessor().getFromIdentityMap(this.objectToBeWritten);
        }
        if (this.objectToBeWritten == null) {
            this.objectToBeWritten = this.originalObject;
            this.query = new ReadObjectQuery();
            this.query.setSelectionObject(this.originalObject);
        }

        if (!makesTrivialUpdate()) {
            this.objectToBeWritten = this.originalObject;
        }
    }

    public Boolean shouldBindAllParameters() {
        return bindAllParameters;
    }


    /**
     * The test() method will, if required, pass the object to the
     * findAndMutateDirectToFieldMappingInObject method, and will then attempt
     * to write the object to the database.
     */
    @Override
    protected void test() {

        if (makesTrivialUpdate() && testShouldMutate()) {
            // Only want to do this if the update is trivial
            // Otherwise there are already changes in the object that
            // will generate SQL
            this.objectToBeWritten =
                this.findAndMutateDirectToFieldMappingInObject(
                    this.objectToBeWritten, false);
        }

        getDatabaseSession().writeObject(this.objectToBeWritten);
    }

    /**
     * Verify if the objects match completely through allowing the session
     * to use the descriptors.  This will compare the objects and all of
     * their privately owned parts.
     */
    @Override
    protected void verify() {
        getSession().getIdentityMapAccessor().initializeIdentityMaps();
        this.objectFromDatabase = getSession().executeQuery(this.query);

        if (!(compareObjects(this.objectToBeWritten, this.objectFromDatabase))) {
            throw new TestErrorException("The object inserted into the database, '"
                + this.objectFromDatabase + "' does not match the original, '"
                + this.objectToBeWritten + "'.");
        }
    }
}
