#region License

/*
 * Copyright © 2002-2005 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

#region Imports

using System.Collections;
using System.Reflection;
using AopAlliance.Intercept;
using DotNetMock.Dynamic;
using NUnit.Framework;
using Spring.Caching;
using Spring.Context;

#endregion

namespace Spring.Aspects.Cache
{
    /// <summary>
    /// Unit tests for the CacheResultAdvice class.
    /// </summary>
    /// <author>Aleksandar Seovic</author>
    [TestFixture]
    public sealed class CacheResultAdviceTests
    {
        object[] IGNORED_ARGS = null;

        private IDynamicMock mockInvocation;
        private IDynamicMock mockContext;
        private CacheResultAdvice advice;
        private ICache resultCache;
        private ICache itemCache;
        private CacheResultTarget cacheResultTarget = new CacheResultTarget();

        [SetUp]
        public void SetUp()
        {
            mockInvocation = new DynamicMock( typeof( IMethodInvocation ) );
            mockContext = new DynamicMock( typeof( IApplicationContext ) );

            advice = new CacheResultAdvice();
            advice.ApplicationContext = (IApplicationContext)mockContext.Object;

            resultCache = new NonExpiringCache();
            itemCache = new NonExpiringCache();
        }

        /// <summary>
        /// Change History:
        /// 2007-08-22 (oakinger): changed behaviour to cache null values as well
        /// </summary>
        [Test]
        public void CacheResultOfMethodThatReturnsNull()
        {
            MethodInfo method = new VoidMethod( cacheResultTarget.ReturnsNothing ).Method;
            object expectedReturnValue = null;

            ExpectAttributeRetrieval( method );
            ExpectCacheKeyGeneration( method, null );
            ExpectCacheInstanceRetrieval( "results", resultCache );
            ExpectCallToProceed( expectedReturnValue );

            // check that the null retVal is cached as well - it might be 
            // the result of an expensive webservice/database call etc.
            object returnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, returnValue );
            Assert.AreEqual( 1, resultCache.Count );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheResultOfMethodThatReturnsObject()
        {
            MethodInfo method = new IntMethod( cacheResultTarget.ReturnsScalar ).Method;
            object expectedReturnValue = CacheResultTarget.Scalar;

            ExpectAttributeRetrieval( method );
            ExpectCacheKeyGeneration( method, null );
            ExpectCacheInstanceRetrieval( "results", resultCache );
            ExpectCallToProceed( expectedReturnValue );

            // return value should be added to cache
            object returnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, returnValue );
            Assert.AreEqual( 1, resultCache.Count );

            // and again, but without Proceed()...
            ExpectAttributeRetrieval( method );
            ExpectCacheKeyGeneration( method, null );
            ExpectCacheInstanceRetrieval( "results", resultCache );

            // cached value should be returned
            object cachedValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, cachedValue );
            Assert.AreEqual( 1, resultCache.Count );
            Assert.AreSame( returnValue, cachedValue );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheResultOfMethodThatReturnsCollection()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.ReturnsCollection ).Method;
            object expectedReturnValue = new object[] {"one", "two", "three"};

            ExpectAttributeRetrieval(method);
            ExpectCacheKeyGeneration(method, 5, expectedReturnValue);
            ExpectCacheInstanceRetrieval("results", resultCache);
            ExpectCallToProceed(new object[] { "one", "two", "three" });

            // return value should be added to cache
            object returnValue = advice.Invoke((IMethodInvocation)mockInvocation.Object);
            Assert.AreEqual(expectedReturnValue, returnValue);
            Assert.AreEqual(1, resultCache.Count);

            // and again, but without Proceed()...
            ExpectAttributeRetrieval(method);
            ExpectCacheKeyGeneration(method, 5, expectedReturnValue);
            ExpectCacheInstanceRetrieval("results", resultCache);

            // cached value should be returned
            object cachedValue = advice.Invoke((IMethodInvocation)mockInvocation.Object);
            Assert.AreEqual(expectedReturnValue, cachedValue);
            Assert.AreNotSame(expectedReturnValue, cachedValue);
            Assert.AreEqual(expectedReturnValue, resultCache.Get(5));
            Assert.AreEqual( 1, resultCache.Count );
            Assert.AreSame( returnValue, cachedValue );
            Assert.AreSame( cachedValue, resultCache.Get( 5 ) );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheResultAndItemsOfMethodThatReturnsCollection()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.ReturnsCollectionAndItems ).Method;
            object expectedReturnValue = new object[] { "one", "two", "three" };

            ExpectAttributeRetrieval(method);
            ExpectCacheKeyGeneration(method, 5, expectedReturnValue);
            ExpectCacheInstanceRetrieval("results", resultCache);
            ExpectCallToProceed(new object[] { "one", "two", "three" });
            ExpectCacheInstanceRetrieval("items", itemCache);

            // return value should be added to result cache and each item to item cache
            object returnValue = advice.Invoke((IMethodInvocation)mockInvocation.Object);
            Assert.AreEqual(expectedReturnValue, returnValue);
            Assert.AreEqual(1, resultCache.Count);
            Assert.AreEqual(3, itemCache.Count);

            // and again, but without Proceed() and item cache access...
            ExpectAttributeRetrieval(method);
            ExpectCacheKeyGeneration(method, 5, expectedReturnValue);
            ExpectCacheInstanceRetrieval("results", resultCache);

            // cached value should be returned
            object cachedValue = advice.Invoke((IMethodInvocation)mockInvocation.Object);
            Assert.AreEqual(expectedReturnValue, cachedValue);
            Assert.AreNotSame(expectedReturnValue, cachedValue);
            Assert.AreEqual(expectedReturnValue, resultCache.Get(5));
            Assert.AreEqual( 1, resultCache.Count );
            Assert.AreEqual( 3, itemCache.Count );
            Assert.AreSame( returnValue, cachedValue );
            Assert.AreSame( cachedValue, resultCache.Get( 5 ) );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheOnlyItemsOfMethodThatReturnsCollection()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.ReturnsItems ).Method;
            object expectedReturnValue = new object[] { "one", "two", "three" };

            ExpectAttributeRetrieval(method);
            ExpectCallToProceed(new object[] { "one", "two", "three" });
            ExpectCacheInstanceRetrieval("items", itemCache);

            // return value should be added to result cache and each item to item cache
            object returnValue = advice.Invoke((IMethodInvocation)mockInvocation.Object);
            Assert.AreEqual(expectedReturnValue, returnValue);
            Assert.AreEqual(0, resultCache.Count);
            Assert.AreEqual(3, itemCache.Count);

            // and again, but without Proceed() and item cache access...
            ExpectAttributeRetrieval(method);
            ExpectCallToProceed(new object[] { "one", "two", "three" });
            ExpectCacheInstanceRetrieval( "items", itemCache );

            // new return value should be returned
            object newReturnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, newReturnValue );
            Assert.AreEqual( 0, resultCache.Count );
            Assert.AreEqual( 3, itemCache.Count );
            Assert.AreEqual( "two", itemCache.Get( "two" ) );
            Assert.AreNotSame( returnValue, newReturnValue );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheOnlyItemsOfMethodThatReturnsCollectionWithinTwoDifferentCaches()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.MultipleCacheResultItems ).Method;
            object expectedReturnValue = new object[] { "one", "two", "three" };

            ExpectAttributeRetrieval(method);
            ExpectCallToProceed(new object[] { "one", "two", "three" });
            ExpectCacheInstanceRetrieval( "items", itemCache );
            ExpectCacheInstanceRetrieval( "items", itemCache );

            // return value should be added to result cache and each item to item cache
            object returnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, returnValue );
            Assert.AreEqual( 0, resultCache.Count );
            Assert.AreEqual( 6, itemCache.Count );

            // and again, but without Proceed() and item cache access...
            ExpectAttributeRetrieval( method );
            ExpectCallToProceed( new object[] { "one", "two", "three" } );
            ExpectCacheInstanceRetrieval( "items", itemCache );
            ExpectCacheInstanceRetrieval( "items", itemCache );

            // new return value should be returned
            object newReturnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, newReturnValue );
            Assert.AreEqual( 0, resultCache.Count );
            Assert.AreEqual( 6, itemCache.Count );
            Assert.AreEqual( "two", itemCache.Get( "two" ) );
            Assert.AreEqual( "two", itemCache.Get( "TWO" ) );
            Assert.AreNotSame( returnValue, newReturnValue );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheOnlyItemsOfMethodThatReturnsCollectionOnCondition()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.CacheResultItemsWithCondition ).Method;
            object expectedReturnValue = new object[] { "one", "two", "three" };

            ExpectAttributeRetrieval(method);
            ExpectCallToProceed(new object[] { "one", "two", "three" });
            ExpectCacheInstanceRetrieval( "items", itemCache );

            // return value should be added to result cache and each item to item cache
            object returnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, returnValue );
            Assert.AreEqual( 2, itemCache.Count );
            Assert.AreEqual( "two", itemCache.Get( "two" ) );
            Assert.AreEqual( "three", itemCache.Get( "three" ) );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheResultOfMethodThatReturnsCollectionOnCondition()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.CacheResultWithCondition ).Method;
            object expectedReturnValue = new object[] { };

            ExpectAttributeRetrieval(method);
            ExpectCacheKeyGeneration(method, 5, expectedReturnValue);
            ExpectCacheInstanceRetrieval("results", resultCache);
            ExpectCallToProceed(new object[] { });

            // return value should not be added to cache
            object returnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreEqual( expectedReturnValue, returnValue );
            Assert.AreEqual( 0, resultCache.Count );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void AcceptsEnumerableOnlyReturn()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.ReturnsEnumerableOnlyAndItems ).Method;
            object[] args = new object[] { "one", "two", "three" };
            EnumerableOnlyResult expectedReturnValue = new EnumerableOnlyResult(args);

            ExpectAttributeRetrieval(method);
            ExpectCacheKeyGeneration(method, 5, expectedReturnValue.InnerArray);
            ExpectCacheInstanceRetrieval("results", resultCache);
            ExpectCallToProceed(expectedReturnValue);
            ExpectCacheInstanceRetrieval("items", itemCache);

            // return value should be added to result cache and each item to item cache
            object returnValue = advice.Invoke((IMethodInvocation)mockInvocation.Object);
            Assert.AreEqual(1, resultCache.Count);
            Assert.AreEqual(3, itemCache.Count);
            Assert.AreSame(expectedReturnValue, returnValue);
            Assert.AreSame(expectedReturnValue, resultCache.Get(5));

            // and again, but without Proceed() and item cache access...
            ExpectAttributeRetrieval(method);
            ExpectCacheKeyGeneration(method, 5, IGNORED_ARGS);
            ExpectCacheInstanceRetrieval("results", resultCache);

            // cached value should be returned, cache remains unchanged
            object cachedValue = advice.Invoke((IMethodInvocation)mockInvocation.Object);
            Assert.AreSame(expectedReturnValue, cachedValue);
            Assert.AreSame(returnValue, cachedValue );
            Assert.AreSame(expectedReturnValue, resultCache.Get(5));
            Assert.AreEqual( 1, resultCache.Count );
            Assert.AreEqual( 3, itemCache.Count );

            mockInvocation.Verify();
            mockContext.Verify();
        }

        [Test]
        public void CacheResultOfMethodThatReturnsCollectionContainingNullItems()
        {
            MethodInfo method = new EnumerableResultMethod( cacheResultTarget.ReturnsEnumerableOnlyAndItems ).Method;
            object expectedReturnValue = new object[] { null, "two", null };

            ExpectAttributeRetrieval( method );
            ExpectCacheKeyGeneration( method, 5, expectedReturnValue );
            ExpectCacheInstanceRetrieval( "results", resultCache );
            ExpectCallToProceed( expectedReturnValue );
            ExpectCacheInstanceRetrieval( "items", itemCache );

            // return value should be added to result cache and each item to item cache
            object returnValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreSame( expectedReturnValue, returnValue );
            Assert.AreEqual( 1, resultCache.Count );
            Assert.AreEqual( 2, itemCache.Count ); // 2 null items result into 1 cached item

            // and again, but without Proceed() and item cache access...
            ExpectAttributeRetrieval( method );
            ExpectCacheKeyGeneration( method, 5, IGNORED_ARGS );
            ExpectCacheInstanceRetrieval( "results", resultCache );

            // cached value should be returned
            object cachedValue = advice.Invoke( (IMethodInvocation)mockInvocation.Object );
            Assert.AreSame(expectedReturnValue, cachedValue);
            Assert.AreSame(returnValue, cachedValue );
            Assert.AreSame(expectedReturnValue, resultCache.Get(5));
            Assert.AreEqual( 1, resultCache.Count );
            Assert.AreEqual( 2, itemCache.Count );

            mockInvocation.Verify();
            mockContext.Verify();
        }


        #region Helper methods

        private void ExpectAttributeRetrieval( MethodInfo method )
        {
            mockInvocation.ExpectAndReturn( "Method", method );
            mockInvocation.ExpectAndReturn( "Method", method );
        }

        private void ExpectCacheKeyGeneration( MethodInfo method, params object[] arguments )
        {
            mockInvocation.ExpectAndReturn( "Method", method );
            mockInvocation.ExpectAndReturn( "Arguments", arguments );
        }

        private void ExpectCacheInstanceRetrieval( string cacheName, ICache cache )
        {
            mockContext.ExpectAndReturn( "GetObject", cache, cacheName );
        }

        private void ExpectCallToProceed( object expectedReturnValue )
        {
            mockInvocation.ExpectAndReturn( "Proceed", expectedReturnValue );
        }

        #endregion

    }

    #region Inner Class : CacheResultTarget

    public delegate void VoidMethod();
    public delegate int IntMethod();
    public delegate IEnumerable EnumerableResultMethod( int key, params object[] elements );

    public class EnumerableOnlyResult : IEnumerable
    {
        private object[] _args;

        public EnumerableOnlyResult( params object[] args )
        {
            _args = args;
        }

        public IEnumerator GetEnumerator()
        {
            return _args.GetEnumerator();
        }

        public override bool Equals( object obj )
        {
            Assert.AreEqual(_args, ((EnumerableOnlyResult)obj)._args );
            return true;
        }

        public override int GetHashCode()
        {
            return _args.GetHashCode();
        }

        public override string ToString()
        {
            return _args.ToString();
        }

        public int Length { get { return _args.Length; } }

        public object[] InnerArray
        {
            get { return _args; }
        }
    }

    public interface ICacheResultTarget
    {
        void ReturnsNothing();
        int ReturnsScalar();
        IEnumerable ReturnsCollection( int key, params object[] elements );
        IEnumerable ReturnsCollectionAndItems( int key, params object[] elements );
        IEnumerable ReturnsItems( int key, params object[] elements );
    }

    public sealed class CacheResultTarget : ICacheResultTarget
    {
        public const int Scalar = int.MaxValue;

        [CacheResult( "results", "'key'" )]
        public void ReturnsNothing()
        {
        }

        [CacheResult( "results", "'key'" )]
        public int ReturnsScalar()
        {
            return Scalar;
        }

        [CacheResult( "results", "#key" )]
        public IEnumerable ReturnsCollection( int key, params object[] elements )
        {
            return elements;
        }

        [CacheResult( "results", "#key" )]
        [CacheResultItems( "items", "''+#this" )]
        public IEnumerable ReturnsCollectionAndItems( int key, params object[] elements )
        {
            return elements;
        }

        [CacheResult( "results", "#key" )]
        [CacheResultItems( "items", "''+#this" )]
        public IEnumerable ReturnsEnumerableOnlyAndItems( int key, params object[] elements )
        {
            return new EnumerableOnlyResult(elements);
        }

        [CacheResultItems( "items", "#this" )]
        public IEnumerable ReturnsItems( int key, params object[] elements )
        {
            return elements;
        }

        [CacheResultItems( "items", "#this" )]
        [CacheResultItems( "items", "#this.ToUpper()" )]
        public IEnumerable MultipleCacheResultItems( int key, params object[] elements )
        {
            return elements;
        }

        [CacheResultItems( "items", "#this", Condition = "#this.StartsWith('t')" )]
        public IEnumerable CacheResultItemsWithCondition( int key, params object[] elements )
        {
            return elements;
        }

        [CacheResult( "results", "#key", Condition = "#this.Length > 0" )]
        public IEnumerable CacheResultWithCondition( int key, params object[] elements )
        {
            return elements;
        }
    }

    #endregion
}
