/*
 * 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.factory;

import static org.easymock.EasyMock.*;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.sameInstance;

import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;

import static org.springframework.config.java.internal.factory.BeanVisibility.PUBLIC;
import static org.springframework.config.java.internal.util.Constants.VALUE_SOURCE_BEAN_NAME;

import org.junit.Ignore;
import org.junit.Test;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;

import org.springframework.config.java.annotation.Configuration;
import org.springframework.config.java.annotation.ExternalValue;
import org.springframework.config.java.internal.model.ConfigurationClass;
import org.springframework.config.java.valuesource.ValueResolutionException;
import org.springframework.config.java.valuesource.ValueResolver;

import test.common.beans.ITestBean;

import java.lang.reflect.Field;

import java.util.HashMap;


/**
 * Unit test for {@link ExternalValueInjectingBeanPostProcessor}.
 *
 * @author  Chris Beams
 */
@Ignore // broke mocking when switching away from VALUE_SOURCE_BEAN_NAME approach
public class ExternalValueInjectingBeanPostProcessorTests {

    private final Object dummyBean = dummyBean();
    private final String dummyName = "dummyBean";
    private final JavaConfigBeanFactory fullyConfiguredBeanFactory = fullyConfiguredBeanFactory();
    private final JavaConfigBeanFactory dummyBeanFactory = dummyBeanFactory();

    /** prove that beforeInit is a no-op. */
    @Test
    public void testBeforeInitialization() {
        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(dummyBeanFactory);
        assertSame(dummyBean, bpp.postProcessBeforeInitialization(dummyBean, dummyName));
    }

    /** prove that a null beanName causes early return. */
    @Test
    public void testAfterInitWithNullBeanName() {
        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(dummyBeanFactory);
        assertSame(dummyBean, bpp.postProcessAfterInitialization(dummyBean, null));
    }

    @Test
    public void testAfterInitWithNoBeanDefinition() {
        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(emptyBeanFactory());
        assertSame(dummyBean, bpp.postProcessAfterInitialization(dummyBean, dummyName));
    }

    @Test
    public void testAfterInitWithNonConfigurationClassBeanDefinition() {
        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(beanFactoryContainingNonConfigClass());
        assertSame(dummyBean, bpp.postProcessAfterInitialization(dummyBean, dummyName));
    }

    @Test
    public void testAfterInitNoExternalValueFields() {
        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        assertSame(dummyBean, bpp.postProcessAfterInitialization(dummyBean, dummyName));
    }

    @Test
    public void testAfterInitOneExternalValueField() {
        class Config {
            @ExternalValue
            String name;
        }

        Config originalBean = new Config();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        bpp.postProcessAfterInitialization(originalBean, dummyName);
        assertThat(originalBean.name, equalTo("Chris Beams"));
    }

    @Test
    public void testAfterInitWithTwoExternalValueFields() {
        class Config {
            @ExternalValue
            String name;
            @ExternalValue
            String password;
        }

        Config originalBean = new Config();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        bpp.postProcessAfterInitialization(originalBean, dummyName);
        assertThat(originalBean.name, equalTo("Chris Beams"));
        assertThat(originalBean.password, equalTo("s3cr3t"));
    }

    @Test
    public void testAfterInitWithOneUnmatchedExternalValueFieldWithDefaultValue() {
        class Config {
            @ExternalValue
            String unknown = "defaultValue";
        }

        Config originalBean = new Config();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        Object returnedBean = bpp.postProcessAfterInitialization(originalBean, dummyName);
        assertThat(originalBean, sameInstance(returnedBean));
        assertThat(originalBean.unknown, equalTo("defaultValue"));
    }

    @Test(expected = ValueResolutionException.class)
    public void testAfterInitWithOneUnmatchedExternalValueFieldWithoutDefaultValue() {
        @SuppressWarnings("unused")
        class Config {
            @ExternalValue
            String unknown;
        }

        Config originalBean = new Config();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        bpp.postProcessAfterInitialization(originalBean, dummyName);
    }

    @Test
    public void testAfterInitWithOneExplicitlyNamedExternalValueField() {
        class Config {
            @ExternalValue("jdbc.url")
            String url;
        }

        Config originalBean = new Config();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        Object returnedBean = bpp.postProcessAfterInitialization(originalBean, dummyName);
        assertThat(originalBean, sameInstance(returnedBean));
        assertThat(originalBean.url, equalTo("jdbc:localhost..."));
    }

    @Test(expected = IllegalStateException.class)
    public void testAfterInitWithOneExternalValueFieldButNoRegisteredValueSourceBean() {
        @SuppressWarnings("unused")
        class Config {
            @ExternalValue
            String name;
        }

        Config originalBean = new Config();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(beanFactoryWithoutValueSource());
        bpp.postProcessAfterInitialization(originalBean, dummyName);
    }

    @Test
    public void testAfterInitWithClassHierarchy() {
        class Config {
            @ExternalValue
            String name;
        }

        class SubConfig extends Config { }

        Config originalBean = new SubConfig();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        bpp.postProcessAfterInitialization(originalBean, dummyName);
        assertThat(originalBean.name, equalTo("Chris Beams"));
    }

    @Test
    public void testAfterInitWithClassHierarchyAndPrivateExternalValueField() throws Exception {
        @SuppressWarnings("unused")
        class Config {
            @ExternalValue
            private String name;
        }

        class SubConfig extends Config { }

        Config originalBean = new SubConfig();

        BeanPostProcessor bpp = new ExternalValueInjectingBeanPostProcessor(fullyConfiguredBeanFactory);
        bpp.postProcessAfterInitialization(originalBean, dummyName);
        Field privateField = Config.class.getDeclaredField("name");
        privateField.setAccessible(true);
        String value = (String) privateField.get(originalBean);
        assertThat(value, equalTo("Chris Beams"));
    }


    // --- helper methods -----------------------------------------------------

    /**
     * Return a {@link BeanDefinition} with metadata indicating it is a {@link Configuration} bean.
     */
    private BeanDefinition configClassBeanDef() {
        BeanDefinition beanDef = new RootBeanDefinition();
        beanDef.setBeanClassName("com.foo.NotImportant");
        beanDef.setAttribute(ConfigurationClass.IS_CONFIGURATION_CLASS, true);
        return beanDef;
    }

    /**
     * Return a mock bean that will fail if any methods are called on it.
     */
    private Object dummyBean() {
        ITestBean originalBean = createMock(ITestBean.class);
        replay(originalBean);
        return originalBean;
    }

    /**
     * Return a {@link JavaConfigBeanFactory} mock awaiting further expectations.
     */
    private JavaConfigBeanFactory createOpenBeanFactoryMock() {
        JavaConfigBeanFactory beanFactory = createMock(JavaConfigBeanFactory.class);
        return beanFactory;
    }

    /**
     * Return {@link JavaConfigBeanFactory} mock that will fail if any methods are called on it.
     */
    private JavaConfigBeanFactory dummyBeanFactory() {
        JavaConfigBeanFactory beanFactory = createOpenBeanFactoryMock();
        replay(beanFactory);
        return beanFactory;
    }

    /**
     * Return a {@link JavaConfigBeanFactory} mock guaranteed to return true when asked if it
     * contains {@link #dummyName}.
     */
    private JavaConfigBeanFactory createOpenBeanFactoryMockWithTargetBeanDefinition() {
        JavaConfigBeanFactory beanFactory = createOpenBeanFactoryMock();
        expect(beanFactory.containsBeanDefinition(dummyName, PUBLIC)).andReturn(true);
        return beanFactory;
    }

    /**
     * Return a {@link JavaConfigBeanFactory} mock that returns {@link #configClassBeanDef()} when
     * asked for {@link #dummyName}.
     */
    private JavaConfigBeanFactory createOpenBeanFactoryMockWithTargetConfigBeanDefinition() {
        JavaConfigBeanFactory beanFactory = createOpenBeanFactoryMockWithTargetBeanDefinition();
        expect(beanFactory.getBeanDefinition(dummyName, PUBLIC)).andReturn(configClassBeanDef());
        return beanFactory;
    }

    /**
     * Return a {@link JavaConfigBeanFactory} mock that contains {@link #dummyName} and
     * {@link ValueResolver} beans.
     */
    private JavaConfigBeanFactory fullyConfiguredBeanFactory() {
        JavaConfigBeanFactory beanFactory = createOpenBeanFactoryMockWithTargetConfigBeanDefinition();
        expect(beanFactory.containsBean(VALUE_SOURCE_BEAN_NAME)).andReturn(true);
        expect(beanFactory.getBean(VALUE_SOURCE_BEAN_NAME)).andReturn(new StubValueSource());
        replay(beanFactory);
        return beanFactory;
    }

    /**
     * Return a {@link JavaConfigBeanFactory} mock that contains a {@link #dummyName} bean, but it
     * is a non-configuration class.
     */
    private JavaConfigBeanFactory beanFactoryContainingNonConfigClass() {
        JavaConfigBeanFactory beanFactory = createOpenBeanFactoryMockWithTargetBeanDefinition();
        expect(beanFactory.getBeanDefinition(dummyName, PUBLIC)).andReturn(new RootBeanDefinition());
        replay(beanFactory);
        return beanFactory;
    }

    /**
     * Return a {@link JavaConfigBeanFactory} mock that does not contain a {@link ValueResolver} bean.
     */
    private JavaConfigBeanFactory beanFactoryWithoutValueSource() {
        JavaConfigBeanFactory beanFactory = createOpenBeanFactoryMockWithTargetConfigBeanDefinition();
        expect(beanFactory.containsBean(VALUE_SOURCE_BEAN_NAME)).andReturn(false);
        replay(beanFactory);
        return beanFactory;
    }

    /**
     * Return a {@link JavaConfigBeanFactory} that does not contain any bean definitions.
     */
    private JavaConfigBeanFactory emptyBeanFactory() {
        JavaConfigBeanFactory beanFactory = createOpenBeanFactoryMock();
        expect(beanFactory.containsBeanDefinition(dummyName, PUBLIC)).andReturn(false);
        replay(beanFactory);
        return beanFactory;
    }

}


/**
 * Stub implementation of {@link ValueResolver} interface. Returns hard-coded values, throws
 * {@link ValueResolutionException} if asked for a value it does not know about.
 *
 * @author  Chris Beams
 */
class StubValueSource implements ValueResolver {
    private static final HashMap<String, Object> values = new HashMap<String, Object>();

    static {
        values.put("name", "Chris Beams");
        values.put("password", "s3cr3t");
        values.put("jdbc.url", "jdbc:localhost...");
    }

    @SuppressWarnings("unchecked")
    public <T> T resolve(String name, Class<?> requiredType) throws ValueResolutionException {
        if (!values.containsKey(name))
            throw new ValueResolutionException(name, "no matching key found");

        return (T) values.get(name);
    }
}
