text: ﻿using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Linq.Expressions;
  using System.Reflection;
  using AutoFixture.Kernel;
  
  namespace AutoFixture.Dsl
  {
      /// <summary>
      /// Enables composition customization of a single type of specimen.
      /// </summary>
      /// <typeparam name="T">The type of specimen.</typeparam>
      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
          Justification = "The main responsibility of this class isn't to be a 'collection' (which, by the way, it isn't - it's just an Iterator).")]
      public class NodeComposer<T> :
          ICustomizationComposer<T>,
          ISpecimenBuilderNode
      {
          /// <summary>Gets the encapsulated builder.</summary>
          /// <value>The encapsulated builder.</value>
          /// <seealso cref="NodeComposer{T}(ISpecimenBuilder)" />
          public ISpecimenBuilder Builder { get; }
  
          /// <summary>
          /// Initializes a new instance of the <see cref="NodeComposer{T}" />
          /// class.
          /// </summary>
          /// <param name="builder">
          /// A builder to which specimen creation responsibilities are
          /// delegated.
          /// </param>
          /// <remarks>
          /// A new <see cref="NodeComposer{T}" /> instance with an appropriate
          /// initial <paramref name="builder" /> can be easily produced by
          /// <see cref="SpecimenBuilderNodeFactory.CreateComposer{T}()" />.
          /// </remarks>
          /// <seealso cref="SpecimenBuilderNodeFactory.CreateComposer{T}()" />
          /// <seealso cref="Builder" />
          public NodeComposer(ISpecimenBuilder builder)
          {
              this.Builder = builder;
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> FromSeed(Func<T, T> factory)
          {
              return this.WithFactory(new SeededFactory<T>(factory));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> FromFactory(ISpecimenBuilder factory)
          {
              return this.WithFactory(factory);
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> FromFactory(Func<T> factory)
          {
              return this.WithFactory(new SpecimenFactory<T>(factory));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> FromFactory<TInput>(Func<TInput, T> factory)
          {
              return this.WithFactory(new SpecimenFactory<TInput, T>(factory));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> FromFactory<TInput1, TInput2>(Func<TInput1, TInput2, T> factory)
          {
              return this.WithFactory(new SpecimenFactory<TInput1, TInput2, T>(factory));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> FromFactory<TInput1, TInput2, TInput3>(Func<TInput1, TInput2, TInput3, T> factory)
          {
              return this.WithFactory(new SpecimenFactory<TInput1, TInput2, TInput3, T>(factory));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> FromFactory<TInput1, TInput2, TInput3, TInput4>(Func<TInput1, TInput2, TInput3, TInput4, T> factory)
          {
              return this.WithFactory(new SpecimenFactory<TInput1, TInput2, TInput3, TInput4, T>(factory));
          }
  
          /// <summary>
          /// Composes a new <see cref="ISpecimenBuilder"/> instance.
          /// </summary>
          /// <returns>
          /// A new <see cref="ISpecimenBuilder"/> instance which can be used to
          /// produce specimens according to the behavior specified by previous
          /// method calls.
          /// </returns>
          public ISpecimenBuilder Compose()
          {
              return this;
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> Do(Action<T> action)
          {
              var graphWithoutSeedIgnoringRelay = WithoutSeedIgnoringRelay(this);
  
              var container = FindContainer(graphWithoutSeedIgnoringRelay);
              var autoPropertiesNode = FindAutoPropertiesNode(container);
              if (autoPropertiesNode != null)
                  container = autoPropertiesNode;
  
              var graphWithDoNode = WithDoNode(
                  action,
                  graphWithoutSeedIgnoringRelay,
                  container);
  
              var filter = FindContainer(graphWithDoNode);
              return (NodeComposer<T>)graphWithDoNode.ReplaceNodes(
                  with: n => n.Compose(n.Concat(new[] { new SeedIgnoringRelay() })),
                  when: filter.Equals);
          }
  
          private static NodeComposer<T> WithDoNode(
              Action<T> action,
              ISpecimenBuilderNode graph,
              ISpecimenBuilderNode container)
          {
              return (NodeComposer<T>)graph.ReplaceNodes(
                  with: n => n.Compose(
                      new[]
                      {
                          new Postprocessor(
                              CompositeSpecimenBuilder.ComposeIfMultiple(n),
                              new ActionSpecimenCommand<T>(action))
                      }),
                  when: container.Equals);
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> OmitAutoProperties()
          {
              var autoPropertiesNode = FindAutoPropertiesNode(this);
  
              if (autoPropertiesNode == null)
                  return this;
  
              // We disable postprocessor rather than delete it.
              // The reason is that it's the only holder of which properties/fields should not be populated.
              // If user later decide to enable properties population, we'll need this information
              // (e.g. if user does smth like fixture.Build().Without(x => x.P).OmitAutoProperties().WithAutoProperties()
              // the information from "Without" should not be missed)
              return (NodeComposer<T>)this.ReplaceNodes(
                  with: n => new Postprocessor(
                      autoPropertiesNode.Builder,
                      autoPropertiesNode.Command,
                      new FalseRequestSpecification()),
                  when: autoPropertiesNode.Equals);
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> With<TProperty>(
              Expression<Func<T, TProperty>> propertyPicker)
          {
              return this.WithCommand(
                  propertyPicker,
                  new BindingCommand<T, TProperty>(propertyPicker));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> With<TProperty>(
              Expression<Func<T, TProperty>> propertyPicker, TProperty value)
          {
              return this.WithCommand(
                  propertyPicker,
                  new BindingCommand<T, TProperty>(propertyPicker, value));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> With<TProperty>(Expression<Func<T, TProperty>> propertyPicker, Func<TProperty> valueFactory)
          {
              return this.WithCommand(
                  propertyPicker,
                  new BindingCommand<T, TProperty>(propertyPicker, _ => valueFactory.Invoke()));
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> With<TProperty, TInput>(Expression<Func<T, TProperty>> propertyPicker, Func<TInput, TProperty> valueFactory)
          {
              return this.WithCommand(
                  propertyPicker,
                  new BindingCommand<T, TProperty>(propertyPicker, context =>
                  {
                      var arg = (TInput)context.Resolve(typeof(TInput));
                      return valueFactory.Invoke(arg);
                  }));
          }
  
          private IPostprocessComposer<T> WithCommand<TProperty>(Expression<Func<T, TProperty>> propertyPicker, ISpecimenCommand command)
          {
              ExpressionReflector.VerifyIsNonNestedWritableMemberExpression(propertyPicker);
  
              var graphWithAutoPropertiesNode = this.GetGraphWithAutoPropertiesNode();
              var graphWithoutSeedIgnoringRelay = WithoutSeedIgnoringRelay(graphWithAutoPropertiesNode);
  
              var container = FindContainer(graphWithoutSeedIgnoringRelay);
  
              var graphWithProperty = graphWithoutSeedIgnoringRelay.ReplaceNodes(
                  with: n => n.Compose(
                      new ISpecimenBuilder[]
                      {
                          new Postprocessor(
                              CompositeSpecimenBuilder.ComposeIfMultiple(n),
                              command,
                              CreateSpecification()),
                          new SeedIgnoringRelay()
                      }),
                  when: container.Equals);
  
              var member = propertyPicker.GetWritableMember().Member;
              return (NodeComposer<T>)ExcludeMemberFromAutoProperties(member, graphWithProperty);
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> WithAutoProperties()
          {
              var g = this.GetGraphWithAutoPropertiesNode();
              var autoProperties = FindAutoPropertiesNode(g);
  
              return (NodeComposer<T>)g.ReplaceNodes(
                  with: n => new Postprocessor(
                      autoProperties.Builder,
                      autoProperties.Command,
                      new TrueRequestSpecification()),
                  when: autoProperties.Equals);
          }
  
          /// <inheritdoc />
          public IPostprocessComposer<T> Without<TProperty>(Expression<Func<T, TProperty>> propertyPicker)
          {
              if (propertyPicker == null) throw new ArgumentNullException(nameof(propertyPicker));
  
              ExpressionReflector.VerifyIsNonNestedWritableMemberExpression(propertyPicker);
  
              var member = propertyPicker.GetWritableMember().Member;
              var graphWithAutoPropertiesNode = this.GetGraphWithAutoPropertiesNode();
  
              return (NodeComposer<T>)ExcludeMemberFromAutoProperties(member, graphWithAutoPropertiesNode);
          }
  
          /// <summary>
          /// Controls whether auto-properties will be enabled or not.
          /// </summary>
          /// <param name="enable">
          /// Set to <see langword="true"/> to enable auto-properties.
          /// </param>
          /// <returns>
          /// A new instance of <see cref="NodeComposer{T}" /> where
          /// auto-properties are either enabled or disabled according to
          /// <paramref name="enable" />.
          /// </returns>
          public NodeComposer<T> WithAutoProperties(bool enable)
          {
              if (!enable)
                  return (NodeComposer<T>)this.OmitAutoProperties();
  
              return (NodeComposer<T>)this.WithAutoProperties();
          }
  
          private NodeComposer<T> WithFactory(ISpecimenBuilder factory)
          {
              return (NodeComposer<T>)this.ReplaceNodes(
                  with: n => n.Compose(new[] { factory }),
                  when: n => n is NoSpecimenOutputGuard);
          }
  
          private static IRequestSpecification CreateSpecification()
          {
              return new OrRequestSpecification(
                  new SeedRequestSpecification(typeof(T)),
                  new ExactTypeSpecification(typeof(T)));
          }
  
          /// <inheritdoc />
          public ISpecimenBuilderNode Compose(IEnumerable<ISpecimenBuilder> builders)
          {
              if (builders == null) throw new ArgumentNullException(nameof(builders));
  
              var composedBuilder =
                  CompositeSpecimenBuilder.ComposeIfMultiple(builders);
              return new NodeComposer<T>(composedBuilder);
          }
  
          /// <inheritdoc />
          public object Create(object request, ISpecimenContext context)
          {
              return this.Builder.Create(request, context);
          }
  
          /// <inheritdoc />
          public IEnumerator<ISpecimenBuilder> GetEnumerator()
          {
              yield return this.Builder;
          }
  
          /// <inheritdoc />
          System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => this.GetEnumerator();
  
          /// <summary>
          /// Looks for the AutoProperties postprocessor in the current graph.
          /// If postprocessor is missing, it's created in inactive state.
          /// </summary>
          private NodeComposer<T> GetGraphWithAutoPropertiesNode()
          {
              var existingNode = FindAutoPropertiesNode(this);
              if (existingNode != null) return this;
  
              var g = WithoutSeedIgnoringRelay(this);
              var filter = FindContainer(g);
  
              // Create AutoProperties node in inactive state
              return (NodeComposer<T>)g.ReplaceNodes(
                  with: n => n.Compose(
                      new ISpecimenBuilder[]
                      {
                          new Postprocessor(
                              CompositeSpecimenBuilder.ComposeIfMultiple(n),
                              new AutoPropertiesCommand(typeof(T)),
                              new FalseRequestSpecification()),
                          new SeedIgnoringRelay()
                      }),
                  when: filter.Equals);
          }
  
          private static Postprocessor FindAutoPropertiesNode(ISpecimenBuilderNode graph)
          {
              return (Postprocessor)graph
                  .FindFirstNodeOrDefault(n => n is Postprocessor postprocessor &&
                                               postprocessor.Command is AutoPropertiesCommand);
          }
  
          /// <summary>
          /// Adjusts the AutoProperties postprocessor and changes rule to avoid the specified member population.
          /// If AutoProperties node is missing, nothing is done.
          /// </summary>
          private static ISpecimenBuilderNode ExcludeMemberFromAutoProperties(MemberInfo member,
              ISpecimenBuilderNode graph)
          {
              var autoPropertiesNode = FindAutoPropertiesNode(graph);
              if (autoPropertiesNode == null) return graph;
  
              var currentSpecification = ((AutoPropertiesCommand)autoPropertiesNode.Command).Specification;
              var newRule = new InverseRequestSpecification(
                  new EqualRequestSpecification(
                      member,
                      new MemberInfoEqualityComparer()));
  
              // Try to make specification list flat if possible
              IRequestSpecification specification;
              if (currentSpecification is TrueRequestSpecification)
              {
                  specification = newRule;
              }
              else if (currentSpecification is AndRequestSpecification andSpec)
              {
                  specification = new AndRequestSpecification(andSpec.Specifications.Concat(new[] { newRule }));
              }
              else
              {
                  specification = new AndRequestSpecification(currentSpecification, newRule);
              }
  
              return graph.ReplaceNodes(
                  with: _ => new Postprocessor(
                      autoPropertiesNode.Builder,
                      new AutoPropertiesCommand(typeof(T), specification),
                      autoPropertiesNode.Specification),
                  when: autoPropertiesNode.Equals);
          }
  
          private static ISpecimenBuilderNode WithoutSeedIgnoringRelay(ISpecimenBuilderNode graph)
          {
              var g = graph.ReplaceNodes(
                  with: n => CompositeSpecimenBuilder.UnwrapIfSingle(
                      n.Compose(n.Where(b => !(b is SeedIgnoringRelay)))),
                  when: n => n.OfType<SeedIgnoringRelay>().Any());
              return g;
          }
  
          private static ISpecimenBuilderNode FindContainer(ISpecimenBuilderNode graph)
          {
              return graph.FindFirstNode(n => n is FilteringSpecimenBuilder);
          }
      }
  }
  