/*
 * Licensed 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 com.facebook.presto.operator.scalar;

import com.facebook.presto.metadata.FunctionListBuilder;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.function.IsNull;
import com.facebook.presto.spi.function.ScalarFunction;
import com.facebook.presto.spi.function.SqlNullable;
import com.facebook.presto.spi.function.SqlType;
import com.facebook.presto.spi.function.TypeParameter;
import com.facebook.presto.spi.type.StandardTypes;
import com.facebook.presto.spi.type.Type;
import org.testng.annotations.Test;

import javax.annotation.Nullable;

@SuppressWarnings("UtilityClassWithoutPrivateConstructor")
public class TestScalarValidation
{
    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Parametric class method .* is annotated with @ScalarFunction")
    public void testBogusParametricMethodAnnotation()
    {
        extractParametricScalar(BogusParametricMethodAnnotation.class);
    }

    @ScalarFunction
    public static final class BogusParametricMethodAnnotation
    {
        @ScalarFunction
        public static void bad() {}
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Parametric class .* does not have any annotated methods")
    public void testNoParametricMethods()
    {
        extractParametricScalar(NoParametricMethods.class);
    }

    @SuppressWarnings("EmptyClass")
    @ScalarFunction
    public static final class NoParametricMethods {}

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* is missing @SqlType annotation")
    public void testMethodMissingReturnAnnotation()
    {
        extractScalars(MethodMissingReturnAnnotation.class);
    }

    public static final class MethodMissingReturnAnnotation
    {
        @ScalarFunction
        public static void bad() {}
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* annotated with @SqlType is missing @ScalarFunction or @ScalarOperator")
    public void testMethodMissingScalarAnnotation()
    {
        extractScalars(MethodMissingScalarAnnotation.class);
    }

    public static final class MethodMissingScalarAnnotation
    {
        @SuppressWarnings("unused")
        @SqlType
        public static void bad() {}
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has wrapper return type Long but is missing @SqlNullable")
    public void testPrimitiveWrapperReturnWithoutNullable()
    {
        extractScalars(PrimitiveWrapperReturnWithoutNullable.class);
    }

    public static final class PrimitiveWrapperReturnWithoutNullable
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static Long bad()
        {
            return 0L;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* annotated with @SqlNullable has primitive return type long")
    public void testPrimitiveReturnWithNullable()
    {
        extractScalars(PrimitiveReturnWithNullable.class);
    }

    public static final class PrimitiveReturnWithNullable
    {
        @ScalarFunction
        @SqlNullable
        @SqlType(StandardTypes.BIGINT)
        public static long bad()
        {
            return 0L;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has parameter with wrapper type Boolean that is missing @SqlNullable")
    public void testPrimitiveWrapperParameterWithoutNullable()
    {
        extractScalars(PrimitiveWrapperParameterWithoutNullable.class);
    }

    public static final class PrimitiveWrapperParameterWithoutNullable
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(@SqlType(StandardTypes.BOOLEAN) Boolean boxed)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has parameter with primitive type double annotated with @SqlNullable")
    public void testPrimitiveParameterWithNullable()
    {
        extractScalars(PrimitiveParameterWithNullable.class);
    }

    public static final class PrimitiveParameterWithNullable
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(@SqlNullable @SqlType(StandardTypes.DOUBLE) double primitive)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* is missing @SqlType annotation for parameter")
    public void testParameterWithoutType()
    {
        extractScalars(ParameterWithoutType.class);
    }

    public static final class ParameterWithoutType
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(long missing)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* annotated with @ScalarFunction must be public")
    public void testNonPublicAnnnotatedMethod()
    {
        extractScalars(NonPublicAnnnotatedMethod.class);
    }

    public static final class NonPublicAnnnotatedMethod
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        private static long bad()
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* is annotated with @Nullable but not @SqlNullable")
    public void testMethodWithLegacyNullable()
    {
        extractScalars(MethodWithLegacyNullable.class);
    }

    public static final class MethodWithLegacyNullable
    {
        @ScalarFunction
        @Nullable
        @SqlType(StandardTypes.BIGINT)
        public static Long bad()
        {
            return 0L;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has parameter annotated with @Nullable but not @SqlNullable")
    public void testParameterWithLegacyNullable()
    {
        extractScalars(ParameterWithLegacyNullable.class);
    }

    public static final class ParameterWithLegacyNullable
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(@Nullable @SqlType(StandardTypes.DOUBLE) Double value)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has @IsNull parameter that does not follow a @SqlType parameter")
    public void testParameterWithConnectorAndIsNull()
    {
        extractScalars(ParameterWithConnectorAndIsNull.class);
    }

    public static final class ParameterWithConnectorAndIsNull
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(ConnectorSession session, @IsNull boolean isNull)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has @IsNull parameter that does not follow a @SqlType parameter")
    public void testParameterWithOnlyIsNull()
    {
        extractScalars(ParameterWithOnlyIsNull.class);
    }

    public static final class ParameterWithOnlyIsNull
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(@IsNull boolean isNull)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has non-boolean parameter with @IsNull")
    public void testParameterWithNonBooleanIsNull()
    {
        extractScalars(ParameterWithNonBooleanIsNull.class);
    }

    public static final class ParameterWithNonBooleanIsNull
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(@SqlType(StandardTypes.BIGINT) long value, @IsNull int isNull)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* uses @IsNull following a parameter with boxed primitive type: Long")
    public void testParameterWithBoxedPrimitiveIsNull()
    {
        extractScalars(ParameterWithBoxedPrimitiveIsNull.class);
    }

    public static final class ParameterWithBoxedPrimitiveIsNull
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(@SqlNullable @SqlType(StandardTypes.BIGINT) Long value, @IsNull boolean isNull)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has @IsNull parameter that has other annotations")
    public void testParameterWithOtherAnnotationsWithIsNull()
    {
        extractScalars(ParameterWithOtherAnnotationsWithIsNull.class);
    }

    public static final class ParameterWithOtherAnnotationsWithIsNull
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long bad(@SqlType(StandardTypes.BIGINT) long value, @IsNull @SqlNullable boolean isNull)
        {
            return 0;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter to only contain A-Z and 0-9 \\(starting with A-Z\\), but got bad on method .*")
    public void testNonUpperCaseTypeParameters()
    {
        extractScalars(TypeParameterWithNonUpperCaseAnnotation.class);
    }

    public static final class TypeParameterWithNonUpperCaseAnnotation
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        @TypeParameter("bad")
        public static long bad(@TypeParameter("array(bad)") Type type, @SqlType(StandardTypes.BIGINT) long value)
        {
            return value;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter to only contain A-Z and 0-9 \\(starting with A-Z\\), but got 1E on method .*")
    public void testLeadingNumericTypeParameters()
    {
        extractScalars(TypeParameterWithLeadingNumbers.class);
    }

    public static final class TypeParameterWithLeadingNumbers
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        @TypeParameter("1E")
        public static long bad(@TypeParameter("array(1E)") Type type, @SqlType(StandardTypes.BIGINT) long value)
        {
            return value;
        }
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter not to take parameters, but got E on method .*")
    public void testNonPrimitiveTypeParameters()
    {
        extractScalars(TypeParameterWithNonPrimitiveAnnotation.class);
    }

    public static final class TypeParameterWithNonPrimitiveAnnotation
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        @TypeParameter("E")
        public static long bad(@TypeParameter("E(VARCHAR)") Type type, @SqlType(StandardTypes.BIGINT) long value)
        {
            return value;
        }
    }

    @Test
    public void testValidTypeParameters()
    {
        extractScalars(ValidTypeParameter.class);
    }

    public static final class ValidTypeParameter
    {
        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        public static long good1(
                @TypeParameter("ROW(ARRAY(BIGINT),MAP(INTEGER,DECIMAL),SMALLINT,CHAR,BOOLEAN,DATE,TIMESTAMP,VARCHAR)") Type type,
                @SqlType(StandardTypes.BIGINT) long value)
        {
            return value;
        }

        @ScalarFunction
        @SqlType(StandardTypes.BIGINT)
        @TypeParameter("E12")
        @TypeParameter("F34")
        public static long good2(
                @TypeParameter("ROW(ARRAY(E12),JSON,TIME,VARBINARY,ROW(ROW(F34)))") Type type,
                @SqlType(StandardTypes.BIGINT) long value)
        {
            return value;
        }
    }

    @Test
    public void testValidTypeParametersForConstructors()
    {
        extractScalars(ConstructorWithValidTypeParameters.class);
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter not to take parameters, but got K on method .*")
    public void testInvalidTypeParametersForConstructors()
    {
        extractScalars(ConstructorWithInvalidTypeParameters.class);
    }

    private static void extractParametricScalar(Class<?> clazz)
    {
        new FunctionListBuilder().scalar(clazz);
    }

    private static void extractScalars(Class<?> clazz)
    {
        new FunctionListBuilder().scalars(clazz);
    }
}
