/*
 * 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 org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.*;
import static org.springframework.config.java.internal.model.AutoBeanMethodTests.VALID_AUTOBEAN_METHOD;
import static org.springframework.config.java.internal.model.BeanMethodTests.VALID_BEAN_METHOD;
import static org.springframework.config.java.internal.util.AnnotationExtractionUtils.extractClassAnnotation;

import java.lang.reflect.Modifier;
import java.util.ArrayList;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.config.java.annotation.Configuration;
import org.springframework.config.java.annotation.ImportXml;
import org.springframework.config.java.internal.model.ConfigurationClass.FinalConfigurationError;
import org.springframework.config.java.internal.model.JavaConfigMethod.IncompatibleAnnotationError;
import org.springframework.config.java.internal.model.JavaConfigMethod.PrivateMethodError;
import org.springframework.config.java.internal.util.AnnotationExtractionUtils;


/**
 * Unit test for {@link ConfigurationClass}
 *
 * @author  Chris Beams
 */
public class ConfigurationClassTests {

    static final ConfigurationClass INVALID_CONFIGURATION_CLASS = new ConfigurationClass("c", Modifier.FINAL);
    private ConfigurationClass configClass;

    @Before
    public void setUp() { configClass = new ConfigurationClass("c"); }

    @Test
    public void modifiers() {
        assertEquals("should have no modifiers by default", 0, new ConfigurationClass("c").getModifiers());

        assertEquals("all modifiers should be preserved",
                     Modifier.ABSTRACT, new ConfigurationClass("c", Modifier.ABSTRACT).getModifiers());
    }

    /**
     * If a Configuration class is not explicitly annotated with
     * {@link Configuration @Configuration}, a default instance of the annotation should be applied.
     * Essentially, Configuration metadata should never be null.
     */
    @Test
    public void defaultConfigurationMetadataIsAlwaysPresent() {
        ConfigurationClass c = new ConfigurationClass("c");
        assertNotNull("default metadata is not present", c.getMetadata());
    }

    @Test
    public void getFinalBeanMethods() {
        BeanMethod finalBeanMethod = new BeanMethod("y", BeanMethodTests.FINAL_BEAN_ANNOTATION);
        configClass.add(new BeanMethod("x")).add(finalBeanMethod).add(new BeanMethod("z"));

        assertArrayEquals(new ValidatableMethod[] { finalBeanMethod }, configClass.getFinalBeanMethods());
    }

    @Test
    public void equality() {
        { // unlike names causes inequality
            ConfigurationClass c1 = new ConfigurationClass("a");
            ConfigurationClass c2 = new ConfigurationClass("b");

            Assert.assertThat(c1, not(equalTo(c2)));
        }

        { // like names causes equality
            ConfigurationClass c1 = new ConfigurationClass("a");
            ConfigurationClass c2 = new ConfigurationClass("a");
            Assert.assertThat(c1, equalTo(c2));
        }

        {                                                            // order of bean methods is not
                                                                     // significant
            ConfigurationClass c1 = new ConfigurationClass("a").add(new BeanMethod("m")).add(new BeanMethod("n"));
            ConfigurationClass c2 =
                new ConfigurationClass("a").add(new BeanMethod("n")) // only difference is order
                                           .add(new BeanMethod("m"));
            Assert.assertThat(c1, equalTo(c2));
        }

        {                                                                                                         // but different bean methods is significant
            ConfigurationClass c1 = new ConfigurationClass("a").add(new BeanMethod("a")).add(new BeanMethod("b"));
            ConfigurationClass c2 = new ConfigurationClass("a").add(new BeanMethod("a")).add(new BeanMethod("z")) // only difference
                                    ;
            Assert.assertThat(c1, not(equalTo(c2)));
        }

        { // same object instance causes equality
            ConfigurationClass c1 = new ConfigurationClass("a");
            Assert.assertThat(c1, equalTo(c1));
        }

        { // null comparison causes inequality
            ConfigurationClass c1 = new ConfigurationClass("a");
            Assert.assertThat(c1, not(equalTo(null)));
        }

        { // is declaring class considered when evaluating equality?
            ConfigurationClass c1 = new ConfigurationClass("c").setDeclaringClass(new ConfigurationClass("p"));
            ConfigurationClass c2 = new ConfigurationClass("c");
            Assert.assertThat(c1, not(equalTo(c2)));
            c2.setDeclaringClass(new ConfigurationClass("p"));
            Assert.assertThat(c1, equalTo(c2));
            Assert.assertThat(c2, equalTo(c1));
            c2.getDeclaringClass().add(new BeanMethod("f"));
            Assert.assertThat(c1, not(equalTo(c2)));
        }

        { // is @Configuration metadata considered when evaluating equality?
            @Configuration(defaultAutowire = Autowire.BY_TYPE)
            class Prototype { }

            Configuration metadata = extractClassAnnotation(Configuration.class, Prototype.class);

            ConfigurationClass c1 = new ConfigurationClass("c", metadata);
            ConfigurationClass c2 = new ConfigurationClass("c");
            Assert.assertThat(c1, not(equalTo(c2)));
            Assert.assertThat(c2, not(equalTo(c1)));
            c2 = new ConfigurationClass("c", metadata);
            Assert.assertThat(c1, equalTo(c2));
            Assert.assertThat(c2, equalTo(c1));
        }

        { // are @AutoBean methods considered when evaluating evaluating equality?
            ConfigurationClass c1 = new ConfigurationClass("c").add(VALID_AUTOBEAN_METHOD);
            ConfigurationClass c2 = new ConfigurationClass("c");
            Assert.assertThat(c1, not(equalTo(c2)));
            Assert.assertThat(c2, not(equalTo(c1)));
            c2.add(VALID_AUTOBEAN_METHOD);
            Assert.assertThat(c1, equalTo(c2));
            Assert.assertThat(c2, equalTo(c1));
        }

        { // are @Plugin annotations considered when evaluating equality?
            @ImportXml(locations="foo.xml")
            class c { }

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

            ConfigurationClass c1 = new ConfigurationClass();
            ConfigurationClass c2 = new ConfigurationClass();

            assertThat(c1, equalTo(c2));
            assertThat(c2, equalTo(c1));

            c1.addPluginAnnotation(importAnno);

            assertThat(c1, not(equalTo(c2)));
            assertThat(c2, not(equalTo(c1)));

            c2.addPluginAnnotation(importAnno);

            assertThat(c1, equalTo(c2));
            assertThat(c2, equalTo(c1));
        }
    }

    @Test
    public void containsBeanMethod() {
        configClass.add(new BeanMethod("x")).add(new BeanMethod("y")).add(new BeanMethod("z"));

        assertTrue(configClass.containsBeanMethod("x"));
        assertTrue(configClass.containsBeanMethod("y"));
        assertTrue(configClass.containsBeanMethod("z"));

        assertFalse(configClass.containsBeanMethod("n"));

        try {
            assertFalse(configClass.containsBeanMethod(""));
            fail("should throw when given invalid input");
        } catch (IllegalArgumentException ex) { /* expected */ }
    }

    @Test
    public void validateConfigurationMayDeclareZeroBeans() {
        ConfigurationClass configClass = new ConfigurationClass("a");
        assertContainsNoUsageErrors(configClass);
    }

    @Test
    public void validateConfigurationDeclaringOneBeanIsValid() {
        ConfigurationClass configClass = new ConfigurationClass("a").add(VALID_BEAN_METHOD);
        assertContainsNoUsageErrors(configClass);
    }

    @Test
    public void validateConfigurationDeclaringOneAutoBeanIsValid() {
        ConfigurationClass configClass = new ConfigurationClass("a").add(VALID_AUTOBEAN_METHOD);
        assertContainsNoUsageErrors(configClass);
    }

    @Test
    public void validateConfigurationMayDeclareAtLeastOneBeanOrImport() {
        ConfigurationClass configClass =
            new ConfigurationClass("c1").addImportedClass(new ConfigurationClass("c2").add(new BeanMethod("m")));

        assertContainsNoUsageErrors(configClass);
    }

    @Test
    public void validateEmptyAbstractConfigurationIsValid() {
        ConfigurationClass configClass = new ConfigurationClass("a", Modifier.ABSTRACT).add(new BeanMethod("m"));

        assertContainsNoUsageErrors(configClass);
    }

    /*
     * Covers the following case:
     *   @Configuration
     *   class c {
     *      @ExternalBean @Bean
     *      public TestBean m() { ... }
     *   }
     */
    @Test
    public void validateMultiJavaConfigMethodNotValid() {
        ConfigurationClass configClass = new ConfigurationClass("c")
            .add(new ExternalBeanMethod("m"))
            .add(new BeanMethod("m"));

        assertContainsUsageError(configClass, IncompatibleAnnotationError.class);
    }

    /*
     * Covers the following case:
     *   @Configuration class Parent {
     *      public @Bean TestBean m() { ... }
     *   }
     *   @Configuration class Child extends Parent {
     *      public @Bean TestBean m() { ... }
     *   }
     */
    @Test
    public void validateMultiJavaConfigMethodPolymorphicMethodsAreValid() {
        ConfigurationClass configClass = new ConfigurationClass("Child")
            .add(new BeanMethod("m"))
            .add(new BeanMethod("m"));

        assertContainsNoUsageErrors(configClass);
    }

    @Test
    public void validationCascadesToImportedClasses() {
        configClass.add(new BeanMethod("m"))
                   .addImportedClass(INVALID_CONFIGURATION_CLASS);

        assertContainsUsageError(configClass, FinalConfigurationError.class);
    }

    @Test
    public void validationCascadesToBeanMethods() {
        // create any simple, invalid bean method definition
        configClass.add(new BeanMethod("m", Modifier.PRIVATE));

        assertContainsUsageError(configClass, PrivateMethodError.class);
    }

    /** See JavaDoc for {@link ConfigurationClass#getSelfAndAllImports()}. */
    @Test
    public void getSelfAndAllImports() {
        ConfigurationClass A = new ConfigurationClass("A");
        ConfigurationClass B = new ConfigurationClass("B");
        ConfigurationClass Y = new ConfigurationClass("Y");
        ConfigurationClass Z = new ConfigurationClass("Z");
        ConfigurationClass M = new ConfigurationClass("M");

        A.addImportedClass(B);
        Y.addImportedClass(Z);

        M.addImportedClass(A);
        M.addImportedClass(Y);

        ConfigurationClass[] expected = new ConfigurationClass[] { B, A, Z, Y, M };

        assertArrayEquals(expected, M.getSelfAndAllImports().toArray());
    }

    private static void assertContainsNoUsageErrors(ConfigurationClass configClass) {
        ArrayList<UsageError> errors = new ArrayList<UsageError>();
        configClass.detectUsageErrors(errors);
        assertEquals(0, errors.size());
    }

    private static void assertContainsUsageError(ConfigurationClass configClass,
                                                 Class<? extends UsageError> errorType) {
        ArrayList<UsageError> errors = getErrors(configClass);

        for (UsageError error : errors)
            if (error.getClass().isAssignableFrom(errorType))
                return;

        fail("Couldn't find an annotation of type " + errorType);
    }

    private static ArrayList<UsageError> getErrors(ConfigurationClass configClass) {
        ArrayList<UsageError> errors = new ArrayList<UsageError>();
        configClass.detectUsageErrors(errors);
        assertTrue("expected errors during validation", errors.size() > 0);
        return errors;
    }

    // -------------------------------------------------------------------------
    // UsageError tests
    // -------------------------------------------------------------------------
    @Test
    public void testFinalConfigurationError() {
        UsageError error = new ConfigurationClass("org.foo.Bar").new FinalConfigurationError();
        String expected = "@Configuration class may not be final. Remove the final modifier to continue.";
        String actual = error.getDescription();
        assertEquals(expected, actual);
    }

    @Test
    public void testIllegalBeanOverrideError() {
        ConfigurationClass authoritativeClass = new ConfigurationClass("org.foo.Foo");
        ConfigurationClass violatingClass = new ConfigurationClass("org.foo.Bar");
        BeanMethod finalMethod = new BeanMethod("m");

        UsageError error = authoritativeClass.new IllegalBeanOverrideError(violatingClass, finalMethod);
        String expected =
            "Illegal attempt by 'm' to override bean definition originally "
            + "specified by Foo.m. Consider removing 'allowOverride=false' from original method.";
        String actual = error.getDescription();
        assertEquals(expected, actual);
    }

}
