﻿// <copyright file="DoesNotThrowAssertion.cs" company="natsnudasoft">
// Copyright (c) Adrian John Dunstan. All rights reserved.
//
// 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.
// </copyright>

namespace Natsnudasoft.NatsnudaLibrary.TestExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using AutoFixture.Idioms;
    using AutoFixture.Kernel;
    using Natsnudasoft.NatsnudaLibrary;

    /// <summary>
    /// Encapsulates a unit test that verifies a method or constructor does not throw any
    /// exceptions.
    /// </summary>
    /// <seealso cref="IdiomaticAssertion" />
    public sealed class DoesNotThrowAssertion : IdiomaticAssertion
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DoesNotThrowAssertion"/> class.
        /// </summary>
        /// <param name="specimenBuilder">The anonymous object creation service to use to create new
        /// specimens.</param>
        /// <exception cref="ArgumentNullException"><paramref name="specimenBuilder"/> is
        /// <see langword="null"/>.</exception>
        public DoesNotThrowAssertion(ISpecimenBuilder specimenBuilder)
        {
            ParameterValidation.IsNotNull(specimenBuilder, nameof(specimenBuilder));

            this.SpecimenBuilder = specimenBuilder;
        }

        /// <summary>
        /// Gets the anonymous object creation service.
        /// </summary>
        public ISpecimenBuilder SpecimenBuilder { get; }

        /// <summary>
        /// Verifies that the specified constructor does not throw an exception when invoked. Any
        /// needed parameters are auto generated by the <see cref="ISpecimenBuilder" /> instance.
        /// </summary>
        /// <param name="constructorInfo">The constructor to verify.</param>
        /// <exception cref="ArgumentNullException"><paramref name="constructorInfo"/> is
        /// <see langword="null"/>.</exception>
        public override void Verify(ConstructorInfo constructorInfo)
        {
            ParameterValidation.IsNotNull(constructorInfo, nameof(constructorInfo));

            constructorInfo.Invoke(this.GetParameters(constructorInfo).ToArray());
        }

        /// <summary>
        /// Verifies that the specified method does not throw an exception when invoked. Any needed
        /// parameters are auto generated by the <see cref="ISpecimenBuilder" /> instance.
        /// </summary>
        /// <param name="methodInfo">The method to verify.</param>
        /// <exception cref="ArgumentNullException"><paramref name="methodInfo"/> is
        /// <see langword="null"/>.</exception>
        public override void Verify(MethodInfo methodInfo)
        {
            ParameterValidation.IsNotNull(methodInfo, nameof(methodInfo));

            var parameters = this.GetParameters(methodInfo);
            if (methodInfo.IsStatic)
            {
                methodInfo.Invoke(null, parameters.ToArray());
            }
            else
            {
                var owner = this.CreateOwner(methodInfo.ReflectedType);
                methodInfo.Invoke(owner, parameters.ToArray());
            }
        }

        /// <summary>
        /// Verifies that the public get and set methods (if available) on this property do not
        /// throw an exception.
        /// </summary>
        /// <param name="propertyInfo">The property.</param>
        /// <exception cref="ArgumentNullException"><paramref name="propertyInfo"/> is
        /// <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException">Could not find a public get or set method on
        /// property specified by <paramref name="propertyInfo"/>.</exception>
        public override void Verify(PropertyInfo propertyInfo)
        {
            ParameterValidation.IsNotNull(propertyInfo, nameof(propertyInfo));

            var getMethod = propertyInfo.GetGetMethod();
            var setMethod = propertyInfo.GetSetMethod();

            if (getMethod == null && setMethod == null)
            {
                throw new ArgumentException(
                    "Could not find a public get or set method on property.",
                    nameof(propertyInfo));
            }

            if (getMethod != null)
            {
                this.Verify(getMethod);
            }

            if (setMethod != null)
            {
                this.Verify(setMethod);
            }
        }

        private IEnumerable<object> GetParameters(MethodBase method)
        {
            var context = new SpecimenContext(this.SpecimenBuilder);
            return method.GetParameters().Select(p => context.Resolve(p));
        }

        private object CreateOwner(Type type)
        {
            var context = new SpecimenContext(this.SpecimenBuilder);
            return context.Resolve(type);
        }
    }
}