#region License

// Copyright (c) 2004 Griffin Caprio & Choy Rim. All rights reserved.

#endregion

#region Imports

using System;
using System.Reflection;

#endregion

namespace DotNetMock.Dynamic
{
	/// <summary>
	/// Expected method call used for building dynamic mocks
	/// </summary>
	public class ExpectationMethod : IMethodCallExpectation
	{
		/// <summary>
		/// Expected Method Name
		/// </summary>
		private string _expectedMethodName;
		/// <summary>
		/// Expectations on the arguments of the expected method call.
		/// </summary>
		private object[] _argumentExpectations;
		/// <summary>
		/// Expected return value for this method call if any.
		/// </summary>
		private object _expectedReturnValue;
		/// <summary>
		/// Exception to throw when the method is called
		/// </summary>
		private Exception _expectedException;
		/// <summary>
		/// Actual <see cref="MethodCall"/>.
		/// </summary>
		private MethodCall _actualMethodCall = null;
		/// <summary>
		/// Default Constructor
		/// </summary>
		/// <param name="methodName">Method name to expect</param>
		public ExpectationMethod( string methodName )
			: this( methodName, null, null, null )
		{
		}
		/// <summary>
		/// Default Constructor
		/// </summary>
		/// <param name="methodName">Method name to expect</param>
		/// <param name="returnValue">return value when expectation is called</param>
		public ExpectationMethod( string methodName, object returnValue )
			: this( methodName, returnValue, null, null )
		{
		}
		/// <summary>
		/// Default Constructor
		/// </summary>
		/// <param name="methodName">Method name to expect</param>
		/// <param name="returnValue">return value when expectation is called</param>
		/// <param name="argumentExpectations">Expectations on the arguments</param>
		public ExpectationMethod(
			string methodName,
			object returnValue,
			object[] argumentExpectations
			)
			: this( methodName, returnValue, argumentExpectations, null )
		{
		}
		/// <summary>
		/// Default Constructor
		/// </summary>
		/// <param name="methodName">Method name to expect</param>
		/// <param name="returnValue">return value when expectation is called</param>
		/// <param name="argumentExpectations">Expectations on the arguments</param>
		/// <param name="expectedException">Exception to throw when called.</param>
		public ExpectationMethod(
			string methodName,
			object returnValue,
			object[] argumentExpectations,
			Exception expectedException
			)
		{
			_expectedMethodName = methodName;
			_argumentExpectations = argumentExpectations;
			_expectedReturnValue = returnValue;
			_expectedException = expectedException;
		}
		/// <summary>
		/// Expected Method Name
		/// </summary>
		public string ExpectedMethodName
		{
			get
			{
				return _expectedMethodName;
			}
		}
		/// <summary>
		/// Expected Return Value
		/// </summary>
		public object ReturnValue
		{
			get
			{
				return _expectedReturnValue;
			}
		}
		/// <summary>
		/// True if actual method call was set.
		/// <seealso cref="ActualMethodCall"/>
		/// </summary>
		public bool ActualMethodCallWasSet
		{
			get
			{
				return _actualMethodCall != null;
			}
		}
		/// <summary>
		/// <see cref="MethodInfo"/> of actual method called.
		/// </summary>
		public MethodInfo ActualMethod
		{
			get
			{
				return ActualMethodCall.Method;
			}
		}
		/// <summary>
		/// Name of actual method called.
		/// </summary>
		public string ActualMethodName
		{
			get
			{
				return ActualMethodCall.MethodName;
			}
		}
		/// <summary>
		/// Actual method call.
		/// </summary>
		public MethodCall ActualMethodCall
		{
			get
			{
				if ( ! ActualMethodCallWasSet )
				{
					throw new InvalidOperationException(
						"Cannot get property ActualMethodCall " +
							"before setting it."
						);
				}
				return _actualMethodCall;
			}
			set
			{
				if ( value == null )
				{
					throw new ArgumentNullException(
						"Cannot set ActualMethodCall property to null"
						);
				}
				if ( ActualMethodCallWasSet )
				{
					throw new InvalidOperationException(
						"Cannot set property ActualMethodCall " +
							"more than once."
						);
				}
				_actualMethodCall = value;
			}
		}
		//TODO: Refactor methods
		private string argsToString( object[] args )
		{
			if ( args != null && args.Length > 0 )
			{
				string[] argText = new string[args.Length];
				for ( int i = 0; i < args.Length; ++i )
				{
					object arg = args[ i ];
					if ( object.ReferenceEquals( arg, null ) )
					{
						argText[ i ] = "null";
					}
					else if ( arg is string )
					{
						argText[ i ] = "\"" + arg.ToString( ) + "\"";
					}
					else
					{
						argText[ i ] = arg.ToString( );
					}
				}
				return String.Join( ", ", argText );
			}
			return String.Empty;
		}
		/// <summary>
		/// Verifies this expectation
		/// </summary>
		public void Verify( )
		{
			if ( ! ActualMethodCallWasSet )
			{
				Assertion.Fail( String.Format(
					"{0}({1}) expected but never called.",
					ExpectedMethodName,
					argsToString( _argumentExpectations )
					) );
			}
			if ( ExpectedMethodName != ActualMethodName )
			{
				Assertion.Fail( String.Format(
					"{0}({1}) expected, but {2} called.",
					ExpectedMethodName, argsToString( _argumentExpectations ),
					ActualMethodCall
					) );
			}
			if ( _argumentExpectations == null )
			{
				return;
			}
			object[] actualArguments = ActualMethodCall.Arguments;
			// actual arguments must be equal to expectations
			if ( actualArguments.Length != _argumentExpectations.Length )
			{
				Assertion.Fail( String.Format(
					"Expected {0} arguments but received {1} " +
						"in method call {2}.",
					_argumentExpectations.Length,
					actualArguments.Length,
					ActualMethodCall
					) );
			}
			// assert that each passed in arg is validated by the appropriate predicate.					
			for ( int i = 0; i < _argumentExpectations.Length; i++ )
			{
				object argumentExpectation = _argumentExpectations[ i ];
				object actualArgument = actualArguments[ i ];
				// evaluate whether input expectations have been met
				IPredicate predicate =
					PredicateUtils.ConvertFrom( argumentExpectation );
				bool isPredicateSatisfied =
					predicate.Eval( actualArgument );
				if ( ! isPredicateSatisfied )
				{
					Assertion.Fail( String.Format(
						"Failed to satisfy '{0}' on argument[{1}] " +
							"of method call {2}",
						predicate,
						i,
						ActualMethodCall
						) );
				}
				// return output expectations if specified
				IArgumentMutator mutator =
					argumentExpectation as IArgumentMutator;
				if ( mutator != null )
				{
					mutator.Mutate( ref actualArguments[ i ] );
				}
			}
			// if exception setup to be thrown, throw it
			if ( _expectedException != null )
			{
				throw _expectedException;
			}
		}
		/// <summary>
		/// Check actual incoming method call and return expected outgoing response.
		/// <see cref="IMethodCallExpectation.CheckCallAndSendResponse"/>
		/// </summary>
		public object CheckCallAndSendResponse( MethodCall call )
		{
			_actualMethodCall = call;
			this.Verify( );
			return _expectedReturnValue;
		}
	}
}