#region License

/*
/// Copyright 2002-2006 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.
 */

#endregion

using System;
using System.Collections;
using System.Reflection;

using NUnit.Framework;

using Spring.Context;
using Spring.Context.Support;
using Spring.Objects.Factory;
using Spring.Objects.Factory.Config;
using Spring.Objects.Factory.Support;

namespace Spring.Testing.NUnit
{
    /// <summary>
    /// Convenient superclass for tests depending on a Spring context.
    /// The test instance itself is populated by Dependency Injection.
    /// </summary>
    /// 
    /// <remarks>
    /// <p>Really for integration testing, not unit testing.
    /// You should <i>not</i> normally use the Spring container
    /// for unit tests: simply populate your objects in plain NUnit tests!</p>
    ///
    /// <p>This supports two modes of populating the test:
    /// <ul>
    /// <li>Via Property Dependency Injection. Simply express dependencies on objects
    /// in the test fixture, and they will be satisfied by autowiring by type.</li>
    /// <li>Via Field Injection. Declare protected variables of the required type
    /// which match named beans in the context. This is autowire by name,
    /// rather than type. This approach is based on an approach originated by
    /// Ara Abrahmian. Property Dependency Injection is the default: set the
    /// "populateProtectedVariables" property to true in the constructor to switch
    /// on Field Injection.</li>
    /// </ul></p>
    ///
    /// <p>This class will normally cache contexts based on a <i>context key</i>:
    /// normally the config locations String array describing the Spring resource
    /// descriptors making up the context. Unless the <code>SetDirty()</code> method
    /// is called by a test, the context will not be reloaded, even across different
    /// subclasses of this test. This is particularly beneficial if your context is
    /// slow to construct, for example if you are using Hibernate and the time taken
    /// to load the mappings is an issue.</p>
    ///
    /// <p>If you don't want this behavior, you can override the <code>ContextKey</code>
    /// property, most likely to return the test class. In conjunction with this you would
    /// probably override the <code>GetContext</code> method, which by default loads
    /// the locations specified by the <code>ConfigLocations</code> property.</p>
    ///
    /// <p><b>WARNING:</b> When doing integration tests from within VS.NET, only use
    /// assembly resource URLs. Else, you may see misleading failures when changing
    /// context locations.</p>
    /// </remarks>
    /// 
    /// <author>Rod Johnson</author>
    /// <author>Rob Harrop</author>
    /// <author>Rick Evans</author>
    /// <author>Aleksandar Seovic (.NET)</author>
    public abstract class AbstractDependencyInjectionSpringContextTests : AbstractSpringContextTests
    {
        private bool populateProtectedVariables = false;
        private AutoWiringMode autowireMode = AutoWiringMode.ByType;
        private bool dependencyCheck = true;

        /// <summary>
        /// Application context this test will run against.
        /// </summary>
        protected IConfigurableApplicationContext applicationContext;

        /// <summary>
        /// Holds names of the fields that should be used for field injection.
        /// </summary>
        protected string[] managedVariableNames;
        private int loadCount = 0;

        /// <summary>
        /// Default constructor for AbstractDependencyInjectionSpringContextTests.
        /// </summary>
        public AbstractDependencyInjectionSpringContextTests()
        {}

        /// <summary>
        /// Gets or sets a flag specifying whether to populate protected 
        /// variables of this test case.
        /// </summary>
        /// <value>
        /// A flag specifying whether to populate protected variables of this test case. 
        /// Default is <b>false</b>.
        /// </value>
        public bool PopulateProtectedVariables
        {
            get { return populateProtectedVariables; }
            set { populateProtectedVariables = value; }
        }

        /// <summary>
        /// Gets or sets the autowire mode for test properties set by Dependency Injection.
        /// </summary>
        /// <value>
        /// The autowire mode for test properties set by Dependency Injection.
        /// The default is <see cref="AutoWiringMode.ByType"/>.
        /// </value>
        public AutoWiringMode AutowireMode
        {
            get { return autowireMode; }
            set { autowireMode = value; }
        }

        /// <summary>
        /// Gets or sets a flag specifying whether or not dependency checking 
        /// should be performed for test properties set by Dependency Injection.
        /// </summary>
        /// <value>
        /// <p>A flag specifying whether or not dependency checking 
        /// should be performed for test properties set by Dependency Injection.</p>
        /// <p>The default is <b>true</b>, meaning that tests cannot be run
        /// unless all properties are populated.</p>
        /// </value>
        public bool DependencyCheck
        {
            get { return dependencyCheck; }
            set { dependencyCheck = value; }
        }

        /// <summary>
        /// Gets the current number of context load attempts.
        /// </summary>
        public int LoadCount
        {
            get { return loadCount; }
        }

        /// <summary>
        /// Called to say that the "applicationContext" instance variable is dirty and
        /// should be reloaded. We need to do this if a test has modified the context
        /// (for example, by replacing an object definition).
        /// </summary>
        public void SetDirty()
        {
            SetDirty(ConfigLocations);
        }

        /// <summary>
        /// Test setup method.
        /// </summary>
        [SetUp]
        public virtual void SetUp()
        {
            this.applicationContext = GetContext(ContextKey);
            InjectDependencies();
            try
            {
                OnSetUp();
            }
            catch (Exception ex)
            {
                logger.Error("Setup error", ex);
                throw;
            }
        }

        /// <summary>
        /// Inject dependencies into 'this' instance (that is, this test instance).
        /// </summary>
        /// <remarks>
        /// <p>The default implementation populates protected variables if the
        /// <see cref="PopulateProtectedVariables"/> property is set, else
        /// uses autowiring if autowiring is switched on (which it is by default).</p>
        /// <p>You can certainly override this method if you want to totally control
        /// how dependencies are injected into 'this' instance.</p>
        /// </remarks>
        protected virtual void InjectDependencies()
        {
            if (PopulateProtectedVariables)
            {
                if (this.managedVariableNames == null)
                {
                    InitManagedVariableNames();
                }
                InjectProtectedVariables();
            }
            else if (AutowireMode != AutoWiringMode.No)
            {
                IConfigurableListableObjectFactory factory = this.applicationContext.ObjectFactory;
                ((AbstractObjectFactory) factory).IgnoreDependencyType(typeof(AutoWiringMode));
                factory.AutowireObjectProperties(this, AutowireMode, DependencyCheck);
            }
        }

        /// <summary>
        /// Gets a key for this context. Usually based on config locations, but
        /// a subclass overriding buildContext() might want to return its class.
        /// </summary>
        protected virtual object ContextKey
        {
            get { return ConfigLocations; }
        }

        /// <summary>
        /// Loads application context from the specified resource locations.
        /// </summary>
        /// <param name="locations">Resources to load object definitions from.</param>
        protected override IConfigurableApplicationContext LoadContextLocations(string[] locations)
        {
            ++this.loadCount;
            return base.LoadContextLocations(locations);
        }

        /// <summary>
        /// Retrieves the names of the fields that should be used for field injection.
        /// </summary>
        protected virtual void InitManagedVariableNames()
        {
            ArrayList managedVarNames = new ArrayList();
            Type type = GetType();

            do
            {
                FieldInfo[] fields =
                    type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Instance);
                if (logger.IsDebugEnabled)
                {
                    logger.Debug("Found " + fields.Length + " fields on " + type);
                }

                for (int i = 0; i < fields.Length; i++)
                {
                    FieldInfo field = fields[i];
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Candidate field: " + field);
                    }
                    if (IsProtectedInstanceField(field))
                    {
                        object oldValue = field.GetValue(this);
                        if (oldValue == null)
                        {
                            managedVarNames.Add(field.Name);
                            if (logger.IsDebugEnabled)
                            {
                                logger.Debug("Added managed variable '" + field.Name + "'");
                            }
                        }
                        else
                        {
                            if (logger.IsDebugEnabled)
                            {
                                logger.Debug("Rejected managed variable '" + field.Name + "'");
                            }
                        }
                    }
                }
                type = type.BaseType;
            } while (type != typeof (AbstractDependencyInjectionSpringContextTests));

            this.managedVariableNames = (string[]) managedVarNames.ToArray(typeof (string));
        }

        private static bool IsProtectedInstanceField(FieldInfo field)
        {
            return field.IsFamily;
        }

        /// <summary>
        /// Injects protected fields using Field Injection.
        /// </summary>
        protected virtual void InjectProtectedVariables()
        {
            for (int i = 0; i < this.managedVariableNames.Length; i++)
            {
                string fieldName = this.managedVariableNames[i];
                Object obj = null;
                try
                {
                    FieldInfo field = GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
                    if (field != null)
                    {
                        BeforeProtectedVariableInjection(field);
                        obj = this.applicationContext.GetObject(fieldName, field.FieldType);
                        field.SetValue(this, obj);
                        if (logger.IsDebugEnabled)
                        {
                            logger.Debug("Populated field: " + field);
                        }
                    }
                    else
                    {
                        if (logger.IsWarnEnabled)
                        {
                            logger.Warn("No field with name '" + fieldName + "'");
                        }
                    }
                }
                catch (NoSuchObjectDefinitionException)
                {
                    if (logger.IsWarnEnabled)
                    {
                        logger.Warn("No object definition with name '" + fieldName + "'");
                    }
                }
            }
        }

        /// <summary>
        /// Called right before a field is being injected
        /// </summary>
        protected virtual void BeforeProtectedVariableInjection(FieldInfo fieldInfo)
        {

        }

        /// <summary>
        /// Subclasses can override this method in order to 
        /// add custom test setup logic.
        /// </summary>
        protected virtual void OnSetUp()
        {}

        /// <summary>
        /// Test teardown method.
        /// </summary>
        [TearDown]
        public void TearDown()
        {
            try
            {
                OnTearDown();
            }
            catch (Exception ex)
            {
                logger.Error("OnTearDown error", ex);
            }
        }

        /// <summary>
        /// Subclasses can override this method in order to 
        /// add custom test teardown logic.
        /// </summary>
        protected virtual void OnTearDown()
        {}


        /// <summary>
        /// Subclasses must implement this property to return the locations of their
        /// config files. A plain path will be treated as a file system location.
        /// </summary>
        /// <value>An array of config locations</value>
        protected abstract string[] ConfigLocations { get; }
    }
}