/*
 * Copyright 2002-2008 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.springframework.config.java.internal.model;

import static java.lang.reflect.Modifier.FINAL;
import static java.lang.reflect.Modifier.PUBLIC;
import static org.junit.Assert.*;
import static org.springframework.config.java.internal.util.AnnotationExtractionUtils.extractClassAnnotation;
import static org.springframework.config.java.internal.util.AnnotationExtractionUtils.extractMethodAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Modifier;

import org.aspectj.lang.annotation.Aspect;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.asm.ClassReader;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.config.java.annotation.AutoBean;
import org.springframework.config.java.annotation.Bean;
import org.springframework.config.java.annotation.Configuration;
import org.springframework.config.java.annotation.DependencyCheck;
import org.springframework.config.java.annotation.ExternalBean;
import org.springframework.config.java.annotation.ExternalValue;
import org.springframework.config.java.annotation.Import;
import org.springframework.config.java.annotation.ImportXml;
import org.springframework.config.java.annotation.Lazy;
import org.springframework.config.java.annotation.Meta;
import org.springframework.config.java.annotation.Primary;
import org.springframework.config.java.internal.parsing.ConfigurationParser;
import org.springframework.config.java.internal.parsing.asm.AsmConfigurationParser;
import org.springframework.config.java.internal.parsing.asm.AsmUtils;
import org.springframework.config.java.internal.util.AnnotationExtractionUtils;
import org.springframework.config.java.internal.util.MethodAnnotationPrototype;
import org.springframework.config.java.model.ModelClass;
import org.springframework.config.java.plugin.ConfigurationPlugin;
import org.springframework.config.java.plugin.Plugin;
import org.springframework.config.java.util.DefaultScopes;
import org.springframework.util.ClassUtils;

import test.common.beans.ITestBean;
import test.common.beans.TestBean;


/**
 * TODO: JAVADOC - out of sync, used to be a base class
 * TCK-style unit test for any/all {@link ConfigurationParser} implementations. Explore
 * subclass hierarchy to discover concrete implementations.
 *
 * <p>Contract for each test:
 * <ul>
 * <li>define a configuration class and assign that class literal to <tt>configClass</tt></li>
 * <li>populate <tt>expectedModel</tt> with the expected results of processing <tt>configClass</tt></li>
 * </ul>
 *
 * {@link #initializeModelsAndParser()} will initialize model objects and call back to
 * {@link newPopulator()} to instantiate a new parser.
 * {@link #populateResultModelAndCompareToTargetModel()} will ensure that the result of processing
 * <tt>configClass</tt> against the parser produces a model equivalent to <tt>expectedModel</tt>
 *
 * @author Chris Beams
 */
// suppress warnings about local @Configuration class methods that are never referenced
@SuppressWarnings("unused")
public class AsmConfigurationParserTests {

    private Class<?> configClass;

    private ConfigurationModel expectedModel;

    private ConfigurationModel actualModel;

    private ConfigurationParser parser;

    /**
     * Load <var>clazz</var> as an ASM ClassReader.
     */
    protected ClassReader loadAsConfigurationSource(Class<?> clazz) {
        String fqClassName = clazz.getName();
        String resourcePath = ClassUtils.convertClassNameToResourcePath(fqClassName);
        return AsmUtils.newClassReader(resourcePath);
    }

    @Before
    public void initializeModelsAndParser() {
        actualModel = new ConfigurationModel();
        expectedModel = new ConfigurationModel();
        parser = new AsmConfigurationParser(actualModel);
    }

    @After
    public void populateResultModelAndCompareToTargetModel() throws  Exception {
        assertNotNull("configClass has not been set for this test", configClass);
        assertTrue("expectedModel has not been populated for this test (or was empty)",
                expectedModel.getConfigurationClasses().length > 0);

        parser.parse(loadAsConfigurationSource(configClass));

        assertEquals("models were not equivalent", expectedModel, actualModel);
    }


    // -----------------------------------------------
    // Individual tests
    // -----------------------------------------------

    public @Test void simplestPossibleConfigDefinition() {
        @Configuration class Config { }
        configClass = Config.class;

        expectedModel.add(new ConfigurationClass(Config.class.getName()));
    }


    public @Test void classModifiersArePropagated() {
        @Configuration abstract class Config { }
        configClass = Config.class;

        expectedModel.add(new ConfigurationClass(Config.class.getName(), Modifier.ABSTRACT));
    }


    @Configuration
    public class PublicConfig { }
    public @Test void classModifiersArePropagated_public() {
        configClass = PublicConfig.class;

        expectedModel.add(new ConfigurationClass(PublicConfig.class.getName(), Modifier.PUBLIC));
    }


    @Configuration
    static class StaticConfig { }
    @Ignore // TODO: Bizarre: Works in Eclipse, but not at local or CI command line.
    public @Test void classModifiersArePropagated_static() {
        configClass = StaticConfig.class;

        // interestingly, the static modifier is intentionally NOT propagated by ASM on classes
        expectedModel.add(new ConfigurationClass(StaticConfig.class.getName(), 0));
    }


    public @Test void beanMethodsAreRecognized() {
        @Configuration class Config { @Bean TestBean alice() { return new TestBean(); } }
        configClass = Config.class;

        expectedModel.add(
            new ConfigurationClass(Config.class.getName())
                .add(new BeanMethod("alice")));
    }


    /**
     * Wire up a Configuration annotation with all the attributes,
     * all set to non-default values.
     */
    public @Test void configurationAnnotationAttributesArePropagated() {
        @Configuration(checkRequired=true, defaultAutowire=Autowire.NO,
                       defaultDependencyCheck=DependencyCheck.ALL,
                       defaultLazy=Lazy.TRUE, names={"foo", "bar"},
                       useFactoryAspects=true)
        class Config { }
        configClass = Config.class;

        Configuration targetAnno = extractClassAnnotation(Configuration.class, Config.class);

        expectedModel.add(
            new ConfigurationClass(Config.class.getName(), targetAnno));
    }

    public @Test void publicBeanMethodModifiersArePropagated() {
        @Configuration
        class Config implements MethodAnnotationPrototype {
            @Bean
            public void targetMethod() { }
        }
        configClass = Config.class;

        Bean targetAnno = extractMethodAnnotation(Bean.class, Config.class);

        expectedModel.add(
            new ConfigurationClass(Config.class.getName())
                .add(new BeanMethod("targetMethod", PUBLIC, targetAnno)));
    }

    public @Test void finalBeanMethodModifiersArePropagated() {
        @Configuration
        class Config implements MethodAnnotationPrototype {
            @Bean
            public final void targetMethod() { }
        }
        configClass = Config.class;

        Bean targetAnno = extractMethodAnnotation(Bean.class, Config.class);

        expectedModel.add(
            new ConfigurationClass(Config.class.getName())
                .add(new BeanMethod("targetMethod", PUBLIC+FINAL, targetAnno)));
    }

    public @Test void beanAnnotationAttributesArePropagated() {
        @Configuration
        class Config implements MethodAnnotationPrototype {
            @Bean(allowOverriding = false,
                  lazy=Lazy.TRUE,
                  primary=Primary.TRUE,
                  autowire=Autowire.NO,
                  dependencyCheck=DependencyCheck.ALL,
                  destroyMethodName="destroy",
                  initMethodName="init",
                  scope=DefaultScopes.PROTOTYPE,
                  aliases={"one, two"},
                  meta={@Meta(key="i", value="1"), @Meta(key="j", value="2") },
                  dependsOn={"b1", "b2"})
            public void targetMethod() { }
        }
        configClass = Config.class;

        Bean targetAnno = extractMethodAnnotation(Bean.class, Config.class);

        expectedModel.add(
            new ConfigurationClass(Config.class.getName())
                .add(new BeanMethod("targetMethod", PUBLIC, targetAnno)));
    }

    public @Test void nonJavaConfigMethodsAreRecognized() {
        @Configuration class Config {
            @Bean TestBean alice() { return new TestBean(); }
            TestBean knave() { return new TestBean(); }
        }
        configClass = Config.class;

        expectedModel.add(
            new ConfigurationClass(Config.class.getName())
                .add(new BeanMethod("alice"))
                .add(new NonJavaConfigMethod("knave")));
    }


    @Configuration
    class BeanMethodOrderConfig {
        @Bean TestBean alice() { return new TestBean(); }
        @Bean TestBean knave() { return new TestBean(); }
    }
    public @Test void beanMethodOrderIsNotSignificantA() {
        configClass = BeanMethodOrderConfig.class;

        expectedModel.add(
            new ConfigurationClass(BeanMethodOrderConfig.class.getName())
                .add(new BeanMethod("alice"))
                .add(new BeanMethod("knave"))
            );
    }
    public @Test void beanMethodOrderIsNotSignificantB() {
        configClass = BeanMethodOrderConfig.class;

        expectedModel.add(
            new ConfigurationClass(BeanMethodOrderConfig.class.getName())
                .add(new BeanMethod("knave"))
                .add(new BeanMethod("alice"))
            );
    }

    public @Test void importIsRecognized() {
        @Configuration class Imported { @Bean TestBean alice() { return new TestBean(); } }
        @Configuration @Import(Imported.class)
        class Config { @Bean TestBean knave() { return new TestBean(); } }
        configClass = Config.class;

        expectedModel
            .add(new ConfigurationClass(Config.class.getName()).add(new BeanMethod("knave"))
                .addImportedClass(new ConfigurationClass(Imported.class.getName()).add(new BeanMethod("alice"))));
    }

    public @Test void multipleImportsAreSupported() {
        @Configuration class Imported1 { @Bean TestBean alice() { return new TestBean(); } }
        @Configuration class Imported2 { @Bean TestBean queen() { return new TestBean(); } }
        @Configuration @Import({Imported1.class, Imported2.class})
        class Config { @Bean TestBean knave() { return new TestBean(); } }
        configClass = Config.class;

        expectedModel
            .add(new ConfigurationClass(Config.class.getName()).add(new BeanMethod("knave"))
                .addImportedClass(new ConfigurationClass(Imported1.class.getName()).add(new BeanMethod("alice")))
                .addImportedClass(new ConfigurationClass(Imported2.class.getName()).add(new BeanMethod("queen"))));
    }

    public @Test void nestedImportsAreSupported() {
        @Configuration class Imported2 { @Bean TestBean queen() { return new TestBean(); } }
        @Configuration @Import(Imported2.class)
        class Imported1 { @Bean TestBean alice() { return new TestBean(); } }
        @Configuration @Import(Imported1.class)
        class Config { @Bean TestBean knave() { return new TestBean(); } }
        configClass = Config.class;

        expectedModel
            .add(new ConfigurationClass(Config.class.getName()).add(new BeanMethod("knave"))
                .addImportedClass(new ConfigurationClass(Imported1.class.getName()).add(new BeanMethod("alice"))
                    .addImportedClass(new ConfigurationClass(Imported2.class.getName()).add(new BeanMethod("queen")))));
    }

    public @Test void nestedImportsAreSupported2() {
        @Configuration class Imported3 { @Bean TestBean rabbit() { return new TestBean(); } }
        @Configuration class Imported2 { @Bean TestBean queen() { return new TestBean(); } }
        @Configuration @Import(Imported2.class)
        class Imported1 { @Bean TestBean alice() { return new TestBean(); } }
        @Configuration @Import({Imported1.class, Imported3.class})
        class Config { @Bean TestBean knave() { return new TestBean(); } }
        configClass = Config.class;

        expectedModel
            .add(new ConfigurationClass(Config.class.getName()).add(new BeanMethod("knave"))
                .addImportedClass(new ConfigurationClass(Imported1.class.getName()).add(new BeanMethod("alice"))
                    .addImportedClass(new ConfigurationClass(Imported2.class.getName()).add(new BeanMethod("queen"))))
                .addImportedClass(new ConfigurationClass(Imported3.class.getName()).add(new BeanMethod("rabbit"))))
            ;
    }


    public @Test void variousBeanMethodModifiersAreSupported() {
        @Configuration class Config {
            public @Bean TestBean a() { return new TestBean(); }
            public final @Bean TestBean b() { return new TestBean(); }
            private strictfp @Bean TestBean c() { return new TestBean(); }
        }
        configClass = Config.class;
        expectedModel
            .add(new ConfigurationClass(Config.class.getName())
                    .add(new BeanMethod("a", Modifier.PUBLIC))
                    .add(new BeanMethod("b", Modifier.PUBLIC + Modifier.FINAL))
                    .add(new BeanMethod("c", Modifier.PRIVATE + Modifier.STRICT)));
    }


    public @Test void externalBeanMethodsAreSupported() {
        @Configuration class Config {
            @Bean TestBean bean() { return new TestBean(); }
            @ExternalBean TestBean extbean() { return new TestBean(); }
        }
        configClass = Config.class;
        expectedModel
            .add(new ConfigurationClass(Config.class.getName())
                    .add(new BeanMethod("bean"))
                    .add(new ExternalBeanMethod("extbean")));
    }


    public @Test void externalValueMethodsAreSupported() {
        @Configuration abstract class Config {
            @Bean TestBean bean() { return new TestBean(); }
            abstract @ExternalValue String extval();
        }

        configClass = Config.class;

        expectedModel
            .add(new ConfigurationClass(Config.class.getName(), Modifier.ABSTRACT)
                    .add(new BeanMethod("bean"))
                    .add(new ExternalValueMethod("extval", Modifier.ABSTRACT)));
    }

    public @Test void autoBeanMethodsAreSupported() {
        @Configuration abstract class Config {
            public abstract @AutoBean TestBean bean();
        }

        configClass = Config.class;

        ModelClass returnType = new ModelClass(TestBean.class.getName());

        expectedModel
            .add(new ConfigurationClass(Config.class.getName(), Modifier.ABSTRACT)
                    .add(new AutoBeanMethod("bean", returnType, Modifier.PUBLIC + Modifier.ABSTRACT)));
    }

    public @Test void autoBeanMethodsWithInterfaceReturnTypesAreDetected() {
        @Configuration abstract class Config {
            public abstract @AutoBean ITestBean bean();
        }

        configClass = Config.class;

        ModelClass expectedReturnType = new ModelClass(ITestBean.class.getName());
        expectedReturnType.setInterface(true);

        expectedModel
            .add(new ConfigurationClass(Config.class.getName(), Modifier.ABSTRACT)
                    .add(new AutoBeanMethod("bean", expectedReturnType, Modifier.PUBLIC + Modifier.ABSTRACT)));
    }


    public @Test void declaringClassesAreSupported() {
        class Main {
            @Configuration class OuterConfig {
                @Bean TestBean b() { return new TestBean(); }
                @Configuration class InnerConfig {
                    @Bean TestBean a() { return new TestBean(); }
                }
            }
        }
        configClass = Main.OuterConfig.InnerConfig.class;
        expectedModel.add(
            new ConfigurationClass(Main.OuterConfig.InnerConfig.class.getName())
                .add(new BeanMethod("a"))
                .setDeclaringClass(
                    new ConfigurationClass(Main.OuterConfig.class.getName())
                        .add(new BeanMethod("b"))));
    }


    public @Test void ignoreDeclaringClassIfNotAtConfigurationAnnotated() {
        abstract class DeclaringClass {
            @Configuration class MemberConfigClass { @Bean TestBean m() { return new TestBean(); } }
        }
        configClass = DeclaringClass.MemberConfigClass.class;

        // note: expectedModel does NOT include a declaring class
        expectedModel.add(new ConfigurationClass(DeclaringClass.MemberConfigClass.class.getName()).add(new BeanMethod("m")));
    }


    /**
     * Configuration classes must be annotated with JavaConfig's {@link Configuration @Configuration}
     * annotation.  If so, the annotation should be preserved in the configuration model object as
     * 'metadata' (this nomenclature is consistent across different model objects)
     * @see {@link ConfigurationClass#getMetadata()}
     * @see {@link BeanMethod#getMetadata()}
     */
    public @Test void configurationMetadataIsPreserved() {
        @Configuration(checkRequired=true)
        class Config { @Bean TestBean alice() { return new TestBean(); } }

        configClass = Config.class;

        Configuration expectedMetadata = extractClassAnnotation(Configuration.class, Config.class);

        expectedModel.add(new ConfigurationClass(Config.class.getName(), expectedMetadata).add(new BeanMethod("alice")));
    }

    /**
     * If Configuration class B extends Configuration class A, where A
     * is annotated with {@link Configuration @Configuration} and B is not,
     * an instance of B should have A's metadata.
     */
    public @Test void configurationMetadataIsInherited() {
        @Configuration(defaultAutowire=Autowire.NO)
        class A { }
        class B extends A { @Bean TestBean m() { return null; } }

        configClass = B.class;

        Configuration metadataA = extractClassAnnotation(Configuration.class, A.class);

        expectedModel.add(new ConfigurationClass(B.class.getName(), metadataA).add(new BeanMethod("m")));
    }

    /**
     * If Configuration class B extends Configuration class A, where A
     * declares a Bean method m(), B should be processed as if it declared m() as well.
     */
    public @Test void configurationMetadataAndBeanMethodsAreInherited() {
        @Configuration(defaultAutowire=Autowire.NO)
        class A { @Bean TestBean m() { return null; } }
        class B extends A { }

        configClass = B.class;

        Configuration metadataA = extractClassAnnotation(Configuration.class, A.class);

        expectedModel.add(new ConfigurationClass(B.class.getName(), metadataA).add(new BeanMethod("m")));
    }

    /**
     * If Configuration class B extends Configuration class A, where A
     * is annotated with {@link Configuration @Configuration} and B is also
     * annotated with {@link Configuration @Configuration}, the most specific
     * annotation should take precence.  That is, B's annotation shoud prevail.
     */
    public @Test void configurationMetadataIsOverridable() {
        @Configuration(defaultAutowire=Autowire.NO, defaultDependencyCheck=DependencyCheck.ALL)
        class A { @Bean TestBean m() { return null; } }
        @Configuration(defaultAutowire=Autowire.BY_NAME, checkRequired=true)
        class B extends A { }

        configClass = B.class;

        Configuration expectedMetadata = extractClassAnnotation(Configuration.class, B.class);

        expectedModel.add(new ConfigurationClass(B.class.getName(), expectedMetadata).add(new BeanMethod("m")));
    }

    /**
     * If a Configuration class B extends Configuration class A where A defines
     * a Bean method M, parsing class B should result in a model containing B and
     * method M.
     */
    public @Test void beanMethodsAreInherited() {
        @Configuration class A { @Bean TestBean M() { return null; } }
        class B extends A { }

        configClass = B.class;

        expectedModel.add(new ConfigurationClass(B.class.getName()).add(new BeanMethod("M")));
    }


    public @Test void aspectScenario() {
        @Configuration
        abstract class BaseConfig {
            public @Bean TestBean bean() { return new TestBean(); }
        }
        @Aspect
        abstract class BaseAspectConfig extends BaseConfig { }
        class ConcreteAspectConfig extends BaseAspectConfig { }

        configClass = ConcreteAspectConfig.class;

        expectedModel.add(new ConfigurationClass(ConcreteAspectConfig.class.getName())
                                               .add(new BeanMethod("bean", Modifier.PUBLIC)));
    }

    // -- tests that are specific to AsmConfigurationParser, i.e. they do not belong in
    // -- AbstractConfigurationParserTests because there is no equivalent support in
    // -- ReflectiveConfigurationParser

    public @Test void testRegistrationOfPluginAnnotation() {
        @Configuration
        @MyPlugin
        class Config { }

        MyPlugin myPlugin = extractClassAnnotation(MyPlugin.class, Config.class);

        configClass = Config.class;

        expectedModel.add(
            new ConfigurationClass(Config.class.getName())
                .addPluginAnnotation(myPlugin)
        );
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Plugin(handler=MyPluginHandler.class)
    static @interface MyPlugin { }

    static class MyPluginHandler implements ConfigurationPlugin<MyPlugin> {
        public void handle(MyPlugin annotation, BeanDefinitionRegistry registry) {
            // no-op
        }
    }

    // -------------------------------------------------------------------------

    public @Test void testRegistrationOfXmlImportAnnotation() {
        @Configuration
        @ImportXml(locations="foo.xml")
        class Config { }

        ImportXml importAnno = AnnotationExtractionUtils.extractClassAnnotation(ImportXml.class, Config.class);

        configClass = Config.class;

        expectedModel.add(
            new ConfigurationClass(Config.class.getName())
                .addPluginAnnotation(importAnno));
    }

}
