/*
 * 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:
//     Oracle - initial API and implementation from Oracle TopLink


package org.eclipse.persistence.testing.framework;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.OneToOneMapping;
import org.eclipse.persistence.queries.UpdateAllQuery;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.queries.ReportQuery;
import org.eclipse.persistence.sessions.UnitOfWork;
import org.eclipse.persistence.queries.ReportQueryResult;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.expressions.DataExpression;

public class UpdateAllQueryTestHelper {

    public static String execute(Session mainSession, Class referenceClass, HashMap updateClauses, Expression selectionExpression) {
        return execute(mainSession, referenceClass, updateClauses, selectionExpression, true);
    }

    public static String execute(Session mainSession, Class referenceClass, HashMap updateClauses, Expression selectionExpression, boolean handleChildren) {
        return execute(mainSession, createUpdateAllQuery(referenceClass, updateClauses, selectionExpression), true);
    }

    public static String execute(Session mainSession, UpdateAllQuery uq) {
        return execute(mainSession, uq,  true);
    }

    // Compares results of updating multiple objects yusing traditionalTopLink update one-by-one approach
    // with updating using UpdateAllQuery. If results differ retuns a non-null error message.
    // To use this method:
    // 1. Populate db with the objects on which update should be performed;
    // 2. In case handleChildren == true the similar query will also be tested with all subclasses:
    //      example: for uq.referenceClass()==Project, the test will also run for SmallProject and LargeProject.
    // 3. After the test is completed it leaves the db in the original state (uses rollbacks).
    public static String execute(Session mainSession, UpdateAllQuery uq,  boolean handleChildren) {
        // Find the inheritance root class - the test will compare all instances of this class
        // after traditional TopLink update (one-by-one) with all instances of this class
        // after UpdateAllQuery. The test succeeds if the two collections are equal.
        Class rootClass = uq.getReferenceClass();
        ClassDescriptor descriptor = mainSession.getClassDescriptor(uq.getReferenceClass());
        if(descriptor.hasInheritance()) {
            ClassDescriptor parentDescriptor = descriptor;
            while(!parentDescriptor.getInheritancePolicy().isRootParentDescriptor()) {
                parentDescriptor = parentDescriptor.getInheritancePolicy().getParentDescriptor();
            }
            rootClass = parentDescriptor.getJavaClass();
        }

        String errorMsg = execute(mainSession, uq, handleChildren, rootClass);

        if(errorMsg.length() == 0) {
            return null;
        } else {
            return errorMsg;
        }
    }

    protected static String execute(Session mainSession, UpdateAllQuery uq, boolean handleChildren,
                                  Class rootClass) {
        String errorMsg = "";
        ClassDescriptor descriptor = mainSession.getDescriptor(uq.getReferenceClass());

        clearCache(mainSession);

        // original objects
        Vector objects = mainSession.readAllObjects(rootClass);

        // first update using the original TopLink approach - one by one.
        // That will be done using report query - it will use the same selection criteria
        // as UpdateAllQuery and each attribute will correspond to an update item.
        ReportQuery rq = new ReportQuery(uq.getReferenceClass(), uq.getExpressionBuilder());
        rq.setSelectionCriteria(uq.getSelectionCriteria());
        rq.setShouldRetrievePrimaryKeys(true);
        // some db platforms don't allow nulls in select clause - so add the fields with null values to the query result.
        Vector fieldsWithNullValues = new Vector();
        Iterator itEntrySets = uq.getUpdateClauses().entrySet().iterator();
        while(itEntrySets.hasNext()) {
            Map.Entry entry = (Map.Entry)itEntrySets.next();
            Expression valueExpression;
            String keyString = getQualifiedFieldNameFromKey(entry.getKey(), rq.getReferenceClass(), descriptor, mainSession);
            Object value = entry.getValue();
            DatabaseMapping mapping = descriptor.getObjectBuilder().getMappingForField(new DatabaseField(keyString));
            if(mapping != null && mapping.isOneToOneMapping() && value != null) {
                // Note that this only works in case the reference PK is not compound
                if(((OneToOneMapping)mapping).getSourceToTargetKeyFields().size() > 1) {
                    errorMsg = "Attribute "+ mapping.getAttributeName() + " mapped with 1to1 mapping that has more than one targetKeyField. UpdateAllQueryTestHelper currently doesn't support that.";
                }
                DatabaseField targetField = ((OneToOneMapping)mapping).getSourceToTargetKeyFields().get(new DatabaseField(keyString));
                if(value instanceof Expression) {
                    valueExpression = ((Expression)(((Expression)value).clone())).getField(targetField);
                } else {
                    ClassDescriptor targetDescriptor = ((OneToOneMapping)mapping).getReferenceDescriptor();
                    Object fieldValue = targetDescriptor.getObjectBuilder().extractValueFromObjectForField(value, targetField, (org.eclipse.persistence.internal.sessions.AbstractSession)mainSession);
                    valueExpression = rq.getExpressionBuilder().value(fieldValue);
                }
            } else {
                if(value instanceof Expression) {
                    valueExpression = (Expression)value;
                } else {
                    valueExpression = rq.getExpressionBuilder().value(value);
                }
            }
            if(value == null) {
                fieldsWithNullValues.add(keyString);
            } else {
                rq.addAttribute(keyString, valueExpression);
            }
        }

        UnitOfWork uow = mainSession.acquireUnitOfWork();
        // mainSession could be a ServerSession
        Session session = uow.getParent();

        // report query results contain the values to be assigned for each object to be updated.
        Vector result = (Vector)session.executeQuery(rq);
        Vector objectsAfterOneByOneUpdate = new Vector(objects.size());
        ((org.eclipse.persistence.internal.sessions.AbstractSession)session).beginTransaction();
        try {
            for (int i = 0; i < result.size(); i++) {
                // read through uow the object(clone) to be updated
                ReportQueryResult reportResult = (ReportQueryResult)result.elementAt(i);
                // hammer into the object the updated values
                Object obj = reportResult.readObject(rq.getReferenceClass(), uow);
                DatabaseRecord row = new DatabaseRecord();
                for (int j = 0; j < reportResult.getNames().size(); j++) {
                    String name = reportResult.getNames().get(j);
                    DatabaseField field = new DatabaseField(name);
                    Object value = reportResult.getResults().get(j);
                    row.add(field, value);
                }
                // some db platforms don't allow nulls in select clause - so add the fields with null values to the query result
                for (int j = 0; j < fieldsWithNullValues.size(); j++) {
                    String name = (String)fieldsWithNullValues.elementAt(j);
                    DatabaseField field = new DatabaseField(name);
                    row.add(field, null);
                }
                rq.getDescriptor().getObjectBuilder().assignReturnRow(obj, (AbstractSession)uow, row, null);
            }
            // uow committed - objects updated.
            uow.commit();

            // Because the transaction will be rolled back (to return to the original state to execute UpdateAllQuery)
            // objects are copied into another vector - later it will be compared with UpdateAllQuery result.
            for(int i=0; i < objects.size(); i++) {
                Object original = objects.elementAt(i);
                Object copy = buildCopy(descriptor, original, uow);
                objectsAfterOneByOneUpdate.add(copy);
            }
        } finally {
            // transaction rolled back - objects back to the original state in the db.
            ((org.eclipse.persistence.internal.sessions.AbstractSession)session).rollbackTransaction();
        }
        clearCache(mainSession);

        // now use UpdateAllQuery
        uow = mainSession.acquireUnitOfWork();
        // mainSession could be a ServerSession
        session = uow.getParent();
        Vector objectsAfterUpdateAll = new Vector(objects.size());
        ((org.eclipse.persistence.internal.sessions.AbstractSession)session).beginTransaction();
        try {
            uow.executeQuery(uq);
            // uow committed - objects updated.
            uow.commit();

            // Because the transaction will be rolled back (to return to the original state)
            // objects are copied into another vector - it will be compared with update one-by-one result.
            for(int i=0; i < objects.size(); i++) {
                Object original = objects.elementAt(i);
                Object copy = buildCopy(descriptor, original, uow);
                objectsAfterUpdateAll.add(copy);
            }
        } finally {
            // transaction rolled back - objects back to the original state in the db.
            ((org.eclipse.persistence.internal.sessions.AbstractSession)session).rollbackTransaction();
        }
        clearCache(mainSession);

        // verify
        String classErrorMsg = "";
        for(int i=0; i < objects.size(); i++) {
            Object obj = objects.elementAt(i);
            Object obj1 = objectsAfterOneByOneUpdate.elementAt(i);
            Object obj2 = objectsAfterUpdateAll.elementAt(i);
            boolean equal = rq.getDescriptor().getObjectBuilder().compareObjects(obj, obj2, (org.eclipse.persistence.internal.sessions.AbstractSession)session);
            if(!equal) {
                classErrorMsg = classErrorMsg + "Difference: original = " + obj.toString() + "; afterOneByOneUpdate = " + obj1.toString() +"; afterUpdateAll = " + obj2.toString() + ";";
            }
        }
        if(classErrorMsg.length() > 0) {
            errorMsg = errorMsg + classErrorMsg;
        }

        if(handleChildren) {
            if(descriptor.hasInheritance() && descriptor.getInheritancePolicy().hasChildren()) {
                Iterator it = descriptor.getInheritancePolicy().getChildDescriptors().iterator();
                while(it.hasNext()) {
                    ClassDescriptor childDescriptor = (ClassDescriptor)it.next();
                    Class childReferenceClass = childDescriptor.getJavaClass();
                    UpdateAllQuery childUq = (UpdateAllQuery)uq.clone();
                    childUq.setReferenceClass(childReferenceClass);
                    childUq.setIsPrepared(false);
                    errorMsg += execute(mainSession, childUq, handleChildren, rootClass);
                }
            }
        }
        return errorMsg;
    }

    protected static void clearCache(Session mainSession) {
        mainSession.getIdentityMapAccessor().initializeAllIdentityMaps();
    }

    public static UpdateAllQuery createUpdateAllQuery(Class referenceClass, HashMap updateClauses, Expression selectionExpression) {
        // Construct UpdateAllQuery
        UpdateAllQuery uq = new UpdateAllQuery(referenceClass, selectionExpression);
        Iterator itEntrySets = updateClauses.entrySet().iterator();
        while(itEntrySets.hasNext()) {
            Map.Entry entry = (Map.Entry)itEntrySets.next();
            uq.addUpdate((String)entry.getKey(), entry.getValue());
        }
        return uq;
    }

    static protected Object buildCopy(ClassDescriptor descriptor, Object original, UnitOfWork uow) {
        Object copy = descriptor.getCopyPolicy().buildClone(original, uow);
        descriptor.getObjectBuilder().copyInto(original, copy, true);
        return copy;
    }

    static protected String getQualifiedFieldNameFromKey(Object key, Class referenceClass, ClassDescriptor descriptor, Session session) {
        DatabaseField field = null;
        if(key instanceof String) {
            // attribute name
            String name = (String)key;
            DatabaseMapping mapping = descriptor.getObjectBuilder().getMappingForAttributeName(name);
            if(mapping != null) {
                field = mapping.getFields().firstElement();
            }
        } else if(key instanceof DataExpression) {
            DataExpression fieldExpression = (DataExpression)key;
            field = descriptor.getObjectBuilder().getFieldForQueryKeyName(fieldExpression.getName());
            if(field == null) {
                DataExpression fieldExpressionClone = (DataExpression)fieldExpression.clone();
                fieldExpressionClone.getBuilder().setQueryClass(referenceClass);
                fieldExpressionClone.getBuilder().setSession((org.eclipse.persistence.internal.sessions.AbstractSession)session);
                field = fieldExpressionClone.getField();
            }
        }

        if(field != null) {
            return field.getQualifiedName();
        }

        // should never happen
        return null;
    }
}
