/*
 * 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 com.facebook.presto.bytecode.expression;

import com.facebook.presto.bytecode.BytecodeUtils;
import org.junit.jupiter.api.Test;

import static com.facebook.presto.bytecode.expression.BytecodeExpressionAssertions.assertBytecodeExpression;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.getStatic;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

public class TestCastBytecodeExpression {
    public static final Object OBJECT_FIELD = "foo";
    public static final boolean BOOLEAN_FIELD = true;
    public static final byte BYTE_FIELD = 99;
    public static final char CHAR_FIELD = 11;
    public static final short SHORT_FIELD = 22;
    public static final int INT_FIELD = 33;
    public static final long LONG_FIELD = 44;
    public static final float FLOAT_FIELD = 3.3f;
    public static final double DOUBLE_FIELD = 4.4;

    @Test
    @SuppressWarnings("OverlyStrongTypeCast")
    public void testDownCastObject()
        throws Exception {
        assertBytecodeExpression(getStatic(getClass(), "OBJECT_FIELD").cast(String.class).invoke("length", int.class),
            ((String)OBJECT_FIELD).length(),
            "((String) " + getClass().getSimpleName() + ".OBJECT_FIELD).length()");
    }

    @Test
    public void testCastBetweenObjectAndPrimitive()
        throws Exception {
        assertCast(getStatic(getClass(), "INT_FIELD"), 33, Object.class);
        assertCast(getStatic(getClass(), "INT_FIELD").cast(Object.class), 33, int.class);
    }

    @Test
    public void testInvalildCast() {
        // Cast between a boxed primitive and a primitive that are different
        assertInvalidCast(getStatic(getClass(), "INT_FIELD"), Double.class);
        assertInvalidCast(getStatic(getClass(), "INT_FIELD").cast(Integer.class), double.class);

        // Cast between two different boxed primitives
        assertInvalidCast(getStatic(getClass(), "INT_FIELD").cast(Integer.class), Double.class);

        // Cast between a primitive and an object (that is not java.lang.Object)
        assertInvalidCast(getStatic(getClass(), "OBJECT_FIELD").cast(String.class), double.class);
        assertInvalidCast(getStatic(getClass(), "INT_FIELD"), String.class);
    }

    @Test
    public void testCastPrimitive()
        throws Exception {
        assertPrimitiveCast("BOOLEAN_FIELD", boolean.class, BOOLEAN_FIELD);

        assertPrimitiveCast("BYTE_FIELD", byte.class, BYTE_FIELD);
        assertPrimitiveCast("BYTE_FIELD", char.class, (char)BYTE_FIELD);
        assertPrimitiveCast("BYTE_FIELD", short.class, (short)BYTE_FIELD);
        assertPrimitiveCast("BYTE_FIELD", int.class, (int)BYTE_FIELD);
        assertPrimitiveCast("BYTE_FIELD", long.class, (long)BYTE_FIELD);
        assertPrimitiveCast("BYTE_FIELD", float.class, (float)BYTE_FIELD);
        assertPrimitiveCast("BYTE_FIELD", double.class, (double)BYTE_FIELD);

        assertPrimitiveCast("CHAR_FIELD", byte.class, (byte)CHAR_FIELD);
        assertPrimitiveCast("CHAR_FIELD", char.class, CHAR_FIELD);
        assertPrimitiveCast("CHAR_FIELD", short.class, (short)CHAR_FIELD);
        assertPrimitiveCast("CHAR_FIELD", int.class, (int)CHAR_FIELD);
        assertPrimitiveCast("CHAR_FIELD", long.class, (long)CHAR_FIELD);
        assertPrimitiveCast("CHAR_FIELD", float.class, (float)CHAR_FIELD);
        assertPrimitiveCast("CHAR_FIELD", double.class, (double)CHAR_FIELD);

        assertPrimitiveCast("SHORT_FIELD", byte.class, (byte)SHORT_FIELD);
        assertPrimitiveCast("SHORT_FIELD", char.class, (char)SHORT_FIELD);
        assertPrimitiveCast("SHORT_FIELD", short.class, SHORT_FIELD);
        assertPrimitiveCast("SHORT_FIELD", int.class, (int)SHORT_FIELD);
        assertPrimitiveCast("SHORT_FIELD", long.class, (long)SHORT_FIELD);
        assertPrimitiveCast("SHORT_FIELD", float.class, (float)SHORT_FIELD);
        assertPrimitiveCast("SHORT_FIELD", double.class, (double)SHORT_FIELD);

        assertPrimitiveCast("INT_FIELD", byte.class, (byte)INT_FIELD);
        assertPrimitiveCast("INT_FIELD", char.class, (char)INT_FIELD);
        assertPrimitiveCast("INT_FIELD", short.class, (short)INT_FIELD);
        assertPrimitiveCast("INT_FIELD", int.class, INT_FIELD);
        assertPrimitiveCast("INT_FIELD", long.class, (long)INT_FIELD);
        assertPrimitiveCast("INT_FIELD", float.class, (float)INT_FIELD);
        assertPrimitiveCast("INT_FIELD", double.class, (double)INT_FIELD);

        assertPrimitiveCast("LONG_FIELD", byte.class, (byte)LONG_FIELD);
        assertPrimitiveCast("LONG_FIELD", char.class, (char)LONG_FIELD);
        assertPrimitiveCast("LONG_FIELD", short.class, (short)LONG_FIELD);
        assertPrimitiveCast("LONG_FIELD", int.class, (int)LONG_FIELD);
        assertPrimitiveCast("LONG_FIELD", long.class, LONG_FIELD);
        assertPrimitiveCast("LONG_FIELD", float.class, (float)LONG_FIELD);
        assertPrimitiveCast("LONG_FIELD", double.class, (double)LONG_FIELD);

        assertPrimitiveCast("FLOAT_FIELD", byte.class, (byte)FLOAT_FIELD);
        assertPrimitiveCast("FLOAT_FIELD", char.class, (char)FLOAT_FIELD);
        assertPrimitiveCast("FLOAT_FIELD", short.class, (short)FLOAT_FIELD);
        assertPrimitiveCast("FLOAT_FIELD", int.class, (int)FLOAT_FIELD);
        assertPrimitiveCast("FLOAT_FIELD", long.class, (long)FLOAT_FIELD);
        assertPrimitiveCast("FLOAT_FIELD", float.class, FLOAT_FIELD);
        assertPrimitiveCast("FLOAT_FIELD", double.class, (double)FLOAT_FIELD);

        assertPrimitiveCast("DOUBLE_FIELD", byte.class, (byte)DOUBLE_FIELD);
        assertPrimitiveCast("DOUBLE_FIELD", char.class, (char)DOUBLE_FIELD);
        assertPrimitiveCast("DOUBLE_FIELD", short.class, (short)DOUBLE_FIELD);
        assertPrimitiveCast("DOUBLE_FIELD", int.class, (int)DOUBLE_FIELD);
        assertPrimitiveCast("DOUBLE_FIELD", long.class, (long)DOUBLE_FIELD);
        assertPrimitiveCast("DOUBLE_FIELD", float.class, (float)DOUBLE_FIELD);
        assertPrimitiveCast("DOUBLE_FIELD", double.class, DOUBLE_FIELD);
    }

    public void assertPrimitiveCast(String fieldName, Class<?> castToType, Object expected)
        throws Exception {
        // simple cast
        BytecodeExpression baseExpression = getStatic(getClass(), fieldName);
        assertCast(baseExpression, expected, castToType);

        // box result
        baseExpression = baseExpression.cast(castToType);
        Class<?> boxedType = BytecodeUtils.wrap(castToType);
        assertCast(baseExpression, expected, boxedType);

        // unbox the boxed result
        baseExpression = baseExpression.cast(boxedType);
        assertCast(baseExpression, expected, castToType);
    }

    public static void assertCast(BytecodeExpression expression, Object expectedValue, Class<?> castToType)
        throws Exception {
        BytecodeExpression castExpression = expression.cast(castToType);
        assertBytecodeExpression(castExpression, expectedValue, expectedCastRendering(expression.toString(), castToType));
        assertEquals(castExpression.getType().getJavaClassName(), castToType.getName());
    }

    public static void assertInvalidCast(BytecodeExpression expression, Class<?> castToType) {
        try {
            // Exception must be thrown here.
            // An exception that is thrown at actual byte code generation time is too late. At that point, stack trace is generally not useful.
            expression.cast(castToType);
            fail();
        }
        catch (IllegalArgumentException ignored) {
        }
    }

    public static String expectedCastRendering(String expectedRendering, Class<?> castToType) {
        return "((" + castToType.getSimpleName() + ") " + expectedRendering + ")";
    }
}
