#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;
using System.Reflection;
using System.Runtime.CompilerServices;
using NUnit.Framework;
using Spring.Aop.Framework;
using Spring.Aop.Interceptor;
using Spring.Objects;

#endregion

namespace Spring.Aop.Support
{
	/// <summary>
	/// Unit tests for the ControlFlowPointcut class.
	/// </summary>
	/// <author>Rod Johnson</author>
	/// <author>Simon White (.NET)</author>
	[TestFixture]
	public sealed class ControlFlowPointcutTests
	{
		[Test]
		[Category("Integration")]
		public void Matches()
		{
			SerializablePerson target = new SerializablePerson();
			target.SetAge(27);
			ControlFlowPointcut cflow = new ControlFlowPointcut(typeof(One), "GetAge");
			ProxyFactory factory = new ProxyFactory(target);
			NopInterceptor nop = new NopInterceptor();
			IPerson proxied = (IPerson) factory.GetProxy();
			factory.AddAdvisor(new DefaultPointcutAdvisor(cflow, nop));

			// not advised, not under One...
			Assert.AreEqual(target.GetAge(), proxied.GetAge());
			Assert.AreEqual(0, nop.Count, "Whoops, appear to be advising when not under One's cflow.");

			// will be advised...
			One one = new One();
			Assert.AreEqual(27, one.GetAge(proxied));
			Assert.AreEqual(1, nop.Count, "Not advising when under One's cflow (must be).");

			// won't be advised...
			Assert.AreEqual(target.GetAge(), new One().NoMatch(proxied));
			Assert.AreEqual(1, nop.Count, "Whoops, appear to be advising when under One's cflow scope, BUT NOT under a target method's cflow scope.");
			Assert.AreEqual(3, cflow.EvaluationCount, "Pointcut not invoked the correct number of times.");
		}

		private sealed class One
		{
            // attribute is required so that the delegated call is NOT jitted away...
            [MethodImpl(MethodImplOptions.NoInlining)]
			public int GetAge(IPerson proxied)
			{
				return proxied.GetAge();
			}

			public int NoMatch(IPerson proxied)
			{
				return proxied.GetAge();
			}

            // similarly, attribute is required so that the delegated call is NOT jitted away...
            [MethodImpl(MethodImplOptions.NoInlining)]
			public void Set(IPerson proxied)
			{
				proxied.SetAge(5);
			}
		}

		/// <summary>
		/// Check that we can use a cflow pointcut only in conjunction with
		/// a static pointcut: e.g. all setter methods that are invoked under
		/// a particular class.
		/// </summary>
		/// <remarks>
		/// This greatly reduces the number of calls to the cflow pointcut,
		/// meaning that it's not so prohibitively expensive.
		/// </remarks>
		[Test]
		[Category("Integration")]
		public void SelectiveApplication()
		{
			SerializablePerson target = new SerializablePerson();
			target.SetAge(27);
			NopInterceptor nop = new NopInterceptor();
			ControlFlowPointcut cflow = new ControlFlowPointcut(typeof (One));
			IPointcut settersUnderOne = Pointcuts.Intersection(SetterPointcut.Instance, cflow);
			ProxyFactory pf = new ProxyFactory(target);
			IPerson proxied = (IPerson) pf.GetProxy();
			pf.AddAdvisor(new DefaultPointcutAdvisor(settersUnderOne, nop));

			// Not advised, not under One
			target.SetAge(16);
			Assert.AreEqual(0, nop.Count);

			// Not advised; under One but not a setter
			Assert.AreEqual(16, new One().GetAge(proxied));
			Assert.AreEqual(0, nop.Count);

			// Won't be advised
			new One().Set(proxied);
			Assert.AreEqual(1, nop.Count);

			// We saved most evaluations
			Assert.AreEqual(1, cflow.EvaluationCount);
		}

		[Test]
		public void EvaluationCountIncrementedEvenIfPointcutDoesNotMatch()
		{
			ControlFlowPointcut cut = new ControlFlowPointcut(typeof(One));
			cut.Matches(null, null, null); // args are ingored in this impl...
			Assert.AreEqual(1, cut.EvaluationCount);
			cut.Matches(null, null, null); // args are ingored in this impl...
			Assert.AreEqual(2, cut.EvaluationCount);
		}

		[Test]
		public void EvaluationCountIncrementedOnEveryMatch()
		{
			Type oneType = typeof(One);
			ControlFlowPointcut cut = new ControlFlowPointcut(oneType);
			MethodInfo method = oneType.GetMethod("GetAge");
			cut.Matches(method, oneType, null);
			Assert.AreEqual(1, cut.EvaluationCount);
			cut.Matches(method, oneType, null);
			Assert.AreEqual(2, cut.EvaluationCount);
		}

		[Test]
		public void DefaultClassFilterImplAlwaysMatchesRegardless()
		{
			Type oneType = typeof(One);
			ControlFlowPointcut cut = new ControlFlowPointcut(oneType);
			ITypeFilter filter = cut.TypeFilter;
			Assert.IsTrue(filter.Matches(oneType),
				"Must always match regardless of the supplied argument Type.");
			Assert.IsTrue(filter.Matches(GetType()),
				"Must always match even if the supplied argument Type is not " +
				"a match for the Type supplied in the ctor.");
			Assert.IsTrue(filter.Matches(null), // args are ingored in this impl...
				"Must always match even if the supplied argument Type is null");
		}

		[Test]
		public void StaticMethodMatchImplAlwaysMatchesRegardless()
		{
			Type oneType = typeof(One);
			ControlFlowPointcut cut = new ControlFlowPointcut(oneType);
			IMethodMatcher filter = cut.MethodMatcher;
			MethodInfo method = oneType.GetMethod("GetAge");
			Assert.IsTrue(filter.Matches(method, oneType),
				"Must always match regardless of the supplied arguments.");
			Assert.IsTrue(filter.Matches(method, GetType()),
				"Must always match even if the supplied argument method and Type are not " +
				"a match for the name and Type supplied in the ctor.");
			Assert.IsTrue(filter.Matches(null, null), // args are ingored in this impl...
				"Must always match even if the supplied arguments are null");
		}

		[Test]
		public void DynamicMethodMatchWithJustTypeSpecifiedInCtor()
		{
			ControlFlowPointcut cut = new ControlFlowPointcut(GetType());
			IMethodMatcher filter = cut.MethodMatcher;
			Assert.IsTrue(filter.Matches(null, null, null), // args are ingored in this impl...
				"Must match - under cflow of Type specified in ctor");
		}

		[Test]
		public void DynamicMethodMatchWithTypeAndMethodNameSpecifiedInCtor()
		{
			ControlFlowPointcut cut = new ControlFlowPointcut(
				GetType(), "DynamicMethodMatchWithTypeAndMethodNameSpecifiedInCtor");
			IMethodMatcher filter = cut.MethodMatcher;
			Assert.IsTrue(filter.Matches(null, null, null), // args are ingored in this impl...
				"Must match - under cflow of Type specified in ctor");
		}

		[Test]
		public void DynamicMethodMatchWithTypeAndMethodNameSpecifiedInCtorNoMatch()
		{
			ControlFlowPointcut cut = new ControlFlowPointcut(GetType(), "KiloRiley");
			IMethodMatcher filter = cut.MethodMatcher;
			Assert.IsFalse(filter.Matches(null, null, null), // args are ingored in this impl...
				"Must not match - under cflow of Type specified in ctor, but no match on method name.");
		}

		#region Helper Classes

		/// <summary>
		/// Pointcut to catch all methods beginning with 'Set'.
		/// </summary>
		private class SetterPointcut : StaticMethodMatcherPointcut
		{
			public static SetterPointcut Instance = new SetterPointcut();

			public override bool Matches(MethodInfo methodBase, Type targetType)
			{
				return methodBase.Name.StartsWith("Set");
			}
		}

		#endregion
	}
}