/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */

package org.apache.asterix.test.om.typecomputer;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.asterix.om.functions.BuiltinFunctionInfo;
import org.apache.asterix.om.functions.BuiltinFunctions;
import org.apache.asterix.om.typecomputer.base.IResultTypeComputer;
import org.apache.asterix.om.types.ATypeTag;
import org.apache.asterix.om.types.BuiltinType;
import org.apache.asterix.om.types.IAType;
import org.apache.commons.lang3.mutable.Mutable;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
import org.apache.hyracks.algebricks.core.algebra.expressions.IVariableTypeEnvironment;
import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
import org.apache.hyracks.algebricks.core.algebra.metadata.IMetadataProvider;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;

/**
 * This test passes all possible combinations of types to each type computer, to ensure each type computer handles
 * them properly, and in case of an exception, the exception falls within the defined exception codes.
 *
 * Care needs to be taken when deciding the number of arguments, below is how the tests are calculated:
 * The number of combinations is = n ^ m
 * n = number of types available
 * m = number of arguments to pass
 *
 * Example:
 * 2 arguments, 40 types = 40 ^ 2 = 1,600 combinations per type computer
 * 3 arguments, 40 types = 40 ^ 3 = 64,000 combinations per type computer
 * 4 arguments, 40 types = 40 ^ 4 = 2,560,000 combinations per type computer
 *
 * Choice of number of arguments:
 * 1- To know the proper number of arguments for each type computer, we are going over all the available functions,
 *    getting their arity and their type computer and comparing them as follows:
 *     - If a function has VARARGS, that type computer is marked as VARARGS.
 *     - If multiple functions use the same type computer, the highest arity of all functions is used for that computer.
 * 2- In case of VARARGS or type computers that handle more than 3 arguments, we will use only 3 arguments, for the
 *    following reasons:
 *     - Going over 3 arguments will result in over 2,500,000 test cases, which will time out and fail the test.
 *     - (As of now) all the type computers using VARARGS need a minimum of 3 arguments or less to function properly,
 *       so passing 3 arguments will suffice to include all cases.
 *     - (As of now) all the type computers that handle more than 3 arguments return a constant type, nothing to test.
 */

@RunWith(Parameterized.class)
public class ExceptionTest {

    private static final Logger LOGGER = LogManager.getLogger();

    // Caution: Using more than 3 arguments will generate millions of combinations and the test will timeout and fail
    private static final int MAXIMUM_ARGUMENTS_ALLOWED = 3;

    // This map will hold each used type computer and its highest arity, which represents arguments number to pass
    private static Map<String, Integer> typeComputerToArgsCountMap = new HashMap<>();

    // Test parameters
    @Parameter
    public String testName;

    @Parameter(1)
    public Class<? extends IResultTypeComputer> clazz;

    @Parameters(name = "TypeComputerTest {index}: {0}")
    public static Collection<Object[]> tests() {

        // Prepare tests
        List<Object[]> tests = new ArrayList<>();

        // First, we will go over all the functions available, get their arity and type computers to know the number
        // of arguments to pass for each type computer
        Map<FunctionIdentifier, BuiltinFunctionInfo> registeredFunctions;
        try {
            Field field = BuiltinFunctions.class.getDeclaredField("registeredFunctions");
            field.setAccessible(true);
            registeredFunctions = (Map<FunctionIdentifier, BuiltinFunctionInfo>) field.get(null);
        } catch (Exception ex) {
            // this should never fail
            throw new IllegalStateException();
        }

        // Note: If a type computer handles both known and VARARGS, VARARGS will be used
        for (BuiltinFunctionInfo finfo : registeredFunctions.values()) {
            FunctionIdentifier fid = finfo.getFunctionIdentifier();
            IResultTypeComputer typeComputer = finfo.getResultTypeComputer();

            // Type computer does not exist, add it with its arity to the map
            if (typeComputerToArgsCountMap.get(typeComputer.getClass().getSimpleName()) == null) {
                typeComputerToArgsCountMap.put(typeComputer.getClass().getSimpleName(), fid.getArity());
                continue;
            }

            // VARARGS functions, these are kept as is, put/update, no need for any comparison
            if (fid.getArity() == FunctionIdentifier.VARARGS) {
                typeComputerToArgsCountMap.put(typeComputer.getClass().getSimpleName(), fid.getArity());
                continue;
            }

            // We have it already, and it is of type VARARGS, we are not going to change it
            if (typeComputerToArgsCountMap.get(typeComputer.getClass().getSimpleName()) == FunctionIdentifier.VARARGS) {
                continue;
            }

            // We have it already, if it has larger arity than our existing one, we will update it
            if (typeComputerToArgsCountMap.get(typeComputer.getClass().getSimpleName()) < fid.getArity()) {
                typeComputerToArgsCountMap.put(typeComputer.getClass().getSimpleName(), fid.getArity());
            }
        }

        // Tests all usual type computers.
        Reflections reflections = new Reflections("org.apache.asterix.om.typecomputer", new SubTypesScanner(false));
        Set<Class<? extends IResultTypeComputer>> classes = reflections.getSubTypesOf(IResultTypeComputer.class);

        for (Class<? extends IResultTypeComputer> clazz : classes) {
            if (Modifier.isAbstract(clazz.getModifiers())) {
                LOGGER.log(Level.INFO, "Excluding " + clazz.getSimpleName() + " (Abstract class)");
                continue;
            }

            tests.add(new Object[] { clazz.getSimpleName(), clazz });
        }

        return tests;
    }

    @Test
    public void test() throws Exception {
        // If a type computer is not found in the map, then it is not used by any function
        if (typeComputerToArgsCountMap.get(clazz.getSimpleName()) == null) {
            LOGGER.log(Level.INFO, "TypeComputer " + clazz.getSimpleName() + " is not used by any functions");
            return;
        }

        // Refer to documentation on the top for the details of choosing the arguments count
        // Arguments list
        int typeComputerMaxArgs = typeComputerToArgsCountMap.get(clazz.getSimpleName());

        // If type computer takes zero arguments, there is nothing to do
        if (typeComputerMaxArgs == 0) {
            return;
        }

        // Calculate number of arguments and number of combinations
        int numberOfArguments =
                (typeComputerMaxArgs > MAXIMUM_ARGUMENTS_ALLOWED || typeComputerMaxArgs == FunctionIdentifier.VARARGS)
                        ? MAXIMUM_ARGUMENTS_ALLOWED : typeComputerMaxArgs;
        int numberOfCombinations = (int) Math.pow(ATypeTag.values().length, numberOfArguments);

        Object[] opaqueParameters = new Object[numberOfArguments];
        List<Mutable<ILogicalExpression>> argumentsList = new ArrayList<>(numberOfArguments);
        for (int i = 0; i < numberOfArguments; i++) {
            opaqueParameters[i] = BuiltinType.ANY;
        }

        // Sets arguments for the mocked function expression.
        for (int index = 0; index < numberOfCombinations; ++index) {

            argumentsList.clear();

            // Type environment and metadata provider
            IVariableTypeEnvironment typeEnv = mock(IVariableTypeEnvironment.class);
            IMetadataProvider metadataProvider = mock(IMetadataProvider.class);

            // Function identifier
            FunctionIdentifier functionIdentifier = mock(FunctionIdentifier.class);
            when(functionIdentifier.getName()).thenReturn("testFunction");

            // Function call expression
            AbstractFunctionCallExpression functionCallExpression = mock(AbstractFunctionCallExpression.class);
            when(functionCallExpression.getFunctionIdentifier()).thenReturn(functionIdentifier);

            try {
                for (int argIndex = 0; argIndex < numberOfArguments; ++argIndex) {
                    int base = (int) Math.pow(ATypeTag.values().length, argIndex);
                    int serializedTypeTag = (index / base) % ATypeTag.values().length + 1;

                    ATypeTag typeTag = ATypeTag.VALUE_TYPE_MAPPING[serializedTypeTag];
                    if (typeTag == null) {
                        // If any tags have been removed or are missing, replace them with any
                        typeTag = ATypeTag.ANY;
                    }

                    // Set the types
                    IAType iaType = mock(IAType.class);
                    when(iaType.getTypeTag()).thenReturn(typeTag);

                    ILogicalExpression argument = mock(ILogicalExpression.class);
                    when(typeEnv.getType(argument)).thenReturn(iaType);

                    // Add argument to the arguments list
                    argumentsList.add(new MutableObject<>(argument));
                }

                // Sets up arguments for the mocked expression.
                when(functionCallExpression.getArguments()).thenReturn(argumentsList);
                when(functionCallExpression.getOpaqueParameters()).thenReturn(opaqueParameters);

                // Invokes a type computer.
                IResultTypeComputer instance;
                Field[] fields = clazz.getFields();

                for (Field field : fields) {
                    if (field.getType().equals(clazz)) {
                        instance = (IResultTypeComputer) field.get(null);
                        instance.computeType(functionCallExpression, typeEnv, metadataProvider);
                    }
                }
            } catch (AlgebricksException ae) {
                String msg = ae.getMessage();
                if (msg.startsWith("ASX")) {
                    // Verifies the error code.
                    int errorCode = Integer.parseInt(msg.substring(3, 7));
                    Assert.assertTrue(errorCode >= 1000 && errorCode < 2000);
                } else {
                    // Any root-level compilation exceptions thrown from type computers should have an error code.
                    Assert.assertTrue((ae.getCause() != null));
                }
            } catch (ClassCastException e) {
                // Do nothing
            }
        }
    }
}
