/*
 * Copyright 2015 the original author or authors.
 *
 * 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 org.powermock.core.transformers.impl;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.powermock.core.classloader.MockClassLoader;
import powermock.test.support.MainMockTransformerTestSupport.SupportClasses;

import org.powermock.core.IndicateReloadClass;

import static java.lang.reflect.Modifier.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

@RunWith(Parameterized.class)
public class TestClassTransformerTest {

    @Parameterized.Parameter(0)
    public MockClassLoaderCase classLoaderCase;

    @Parameterized.Parameters(name = "{0}")
    public static List<?> values() {
        MockClassLoaderCase[] factoryAlternatives = MockClassLoaderCase.values();
        List<Object[]> values = Arrays.asList(new Object[factoryAlternatives.length][]);
        for (MockClassLoaderCase eachFactoryAlternative : factoryAlternatives) {
            values.set(eachFactoryAlternative.ordinal(),
                    new Object[] {eachFactoryAlternative});
        }
        return values;
    }

    @Test
    public void preparedSubclassShouldNotGetPublicDeferConstructor() throws Exception {
        MockClassLoader mockClassLoader = classLoaderCase
                .createMockClassLoaderThatPrepare(SupportClasses.SubClass.class);
        final Class<?> clazz = Class.forName(SupportClasses.SubClass.class.getName(), true, mockClassLoader);
        assertEquals("Original number of constructoprs",
                1, SupportClasses.SubClass.class.getConstructors().length);
        try {
            fail("A public defer-constructor is not expected: "
                    + clazz.getConstructor(IndicateReloadClass.class));
        } catch (NoSuchMethodException is_expected) {}
        assertEquals("Number of (public) constructors in modified class",
                1, clazz.getConstructors().length);

        assertNotNull("But there should still be a non-public defer constructor!",
                clazz.getDeclaredConstructor(IndicateReloadClass.class));
    }

    @Test
    public void preparedClassConstructorsShouldKeepTheirAccessModifier() throws Exception {
        MockClassLoader mockClassLoader = classLoaderCase
                .createMockClassLoaderThatPrepare(SupportClasses.MultipleConstructors.class);
        final Class<?> clazz = Class.forName(
                SupportClasses.MultipleConstructors.class.getName(),
                true, mockClassLoader);
        for (Constructor<?> originalConstructor : SupportClasses
                .MultipleConstructors.class.getDeclaredConstructors()) {
            Class[] paramTypes = originalConstructor.getParameterTypes();
            int originalModifiers = originalConstructor.getModifiers();
            int newModifiers = clazz.getDeclaredConstructor(paramTypes).getModifiers();
            String constructorName = 0 == paramTypes.length
                    ? "Default constructor "
                    : paramTypes[0].getSimpleName() + " constructor ";
            assertEquals(constructorName + "is public?",
                    isPublic(originalModifiers), isPublic(newModifiers));
            assertEquals(constructorName + "is protected?",
                    isProtected(originalModifiers), isProtected(newModifiers));
            assertEquals(constructorName + "is private?",
                    isPrivate(originalModifiers), isPrivate(newModifiers));
        }
    }

    enum MockClassLoaderCase {
        WHEN_PREPARED_CLASS_IS_TESTCLASS {
            @Override Class<?> chooseTestClass(Class<?> prepare4test) {
                return prepare4test;
            }
            @Override String[] preparations(Class<?> prepare4test) {
                return new String[] {MockClassLoader.MODIFY_ALL_CLASSES};
            }
        },
        WHEN_ENCLOSING_CLASS_IS_TESTCLASS {
            @Override Class<?> chooseTestClass(Class<?> prepare4test) {
                return prepare4test.getDeclaringClass();
            }
            @Override String[] preparations(Class<?> prepare4test) {
                return new String[] {prepare4test.getName()};
            }
        };

        abstract Class<?> chooseTestClass(Class<?> prepare4test);
        abstract String[] preparations(Class<?> prepare4test);

        MockClassLoader createMockClassLoaderThatPrepare(Class<?> prepare4test) {
            TestClassTransformer testClassTransformer = TestClassTransformer
                    .forTestClass(chooseTestClass(prepare4test))
                    .removesTestMethodAnnotation(Test.class)
                    .fromMethods(Collections.<Method>emptyList());
            MockClassLoader mockClassLoader =
                    new MockClassLoader(preparations(prepare4test));
            mockClassLoader.setMockTransformerChain(Arrays.asList(
                    new ClassMockTransformer(),
                    testClassTransformer));
            return mockClassLoader;
        }
    }
}
