/*
 * Copyright 2014 Stefan Mandel, Urs Metz
 *
 * 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.
 */

package org.pitest.mutationtest.engine.gregor.mutators;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.pitest.mutationtest.engine.gregor.mutators.ArgumentPropagationMutator.ARGUMENT_PROPAGATION_MUTATOR;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;

import org.junit.Before;
import org.junit.Test;
import org.pitest.mutationtest.engine.Mutant;
import org.pitest.mutationtest.engine.gregor.MutatorTestBase;

public class ArgumentPropagationMutatorTest extends MutatorTestBase {

  @Before
  public void setupEngineToUseReplaceMethodWithArgumentOfSameTypeAsReturnValueMutator() {
    createTesteeWith(mutateOnlyCallMethod(), ARGUMENT_PROPAGATION_MUTATOR);
  }

  @Test
  public void shouldReplaceMethodCallWithStringArgument() throws Exception {
    final Mutant mutant = getFirstMutant(HasStringMethodCall.class);
    assertMutantCallableReturns(new HasStringMethodCall("example"), mutant,
        "example");
  }

  private static class HasStringMethodCall implements Callable<String> {
    private final String arg;

    public HasStringMethodCall(String arg) {
      this.arg = arg;
    }

    public String delegate(final String aString) {
      return "abc" + aString;
    }

    @Override
    public String call() throws Exception {
      return delegate(this.arg);
    }
  }

  @Test
  public void shouldReplaceMethodCallWithIntArgument() throws Exception {
    final Mutant mutant = getFirstMutant(HasIntMethodCall.class);
    assertMutantCallableReturns(new HasIntMethodCall(20), mutant, "20");
  }

  private static class HasIntMethodCall implements Callable<String> {
    private final int arg;

    public HasIntMethodCall(int arg) {
      this.arg = arg;
    }

    public int delegate(int aInt) {
      return 22 + aInt;
    }

    @Override
    public String call() throws Exception {
      return String.valueOf(delegate(this.arg));
    }
  }

  @Test
  public void shouldReplaceMethodCallWithLongArgument() throws Exception {
    final Mutant mutant = getFirstMutant(HasLongMethodCall.class);
    assertMutantCallableReturns(new HasLongMethodCall(20L), mutant, "20");
  }

  private static class HasLongMethodCall implements Callable<String> {
    private final long arg;

    public HasLongMethodCall(long arg) {
      this.arg = arg;
    }

    public long delegate(long argument) {
      return 22L + argument;
    }

    @Override
    public String call() throws Exception {
      return String.valueOf(delegate(this.arg));
    }
  }

  @Test
  public void shouldNotMutateMethodThatReturnsDifferentType() throws Exception {
    assertNoMutants(ReturnsDifferentType.class);
  }

  class ReturnsDifferentType implements Callable<String> {
    @Override
    public String call() {
      return addThreeAndConvertToString(3);
    }

    private String addThreeAndConvertToString(int argument) {
      return String.valueOf(3 + argument);
    }
  }

  @Test
  public void continuesUntilMatchingArgumentTypeIsFound() throws Exception {
    final Mutant mutant = getFirstMutant(OnlyFirstArgumentHasMatchingType.class);
    assertMutantCallableReturns(new OnlyFirstArgumentHasMatchingType("abc",
        new Object(), 3), mutant, "abc");
  }

  private class OnlyFirstArgumentHasMatchingType implements Callable<String> {
    private final String aString;
    private final Object anObject;
    private final long   aLong;

    public OnlyFirstArgumentHasMatchingType(String aString, Object anObject,
        long aLong) {
      this.aString = aString;
      this.anObject = anObject;
      this.aLong = aLong;
    }

    @Override
    public String call() throws Exception {
      return aMethod(this.aString, this.anObject, this.aLong);
    }

    private String aMethod(String aString, Object anObject, long aLong) {
      return String.valueOf(anObject) + aString + String.valueOf(aLong);
    }
  }

  @Test
  public void usesLastArgumentOfMatchingTypeToReplaceMethod() throws Exception {
    final Mutant mutant = getFirstMutant(HasSeveralArgumentWithMatchingType.class);
    assertMutantCallableReturns(new HasSeveralArgumentWithMatchingType(11, 22),
        mutant, "22");
  }

  private class HasSeveralArgumentWithMatchingType implements Callable<String> {
    private final int int1;
    private final int int2;

    public HasSeveralArgumentWithMatchingType(int i, int j) {
      this.int1 = i;
      this.int2 = j;
    }

    @Override
    public String call() throws Exception {
      final String anInt = "3";
      return String.valueOf(aMethod(this.int1, anInt, this.int2));
    }

    private int aMethod(int int1, String aString, int int2) {
      return int1 + int2;
    }
  }

  @Test
  public void alsoReplaceCallToMethodWhenReturnValueIsNotUsed()
      throws Exception {
    final Mutant mutant = getFirstMutant(ReturnValueNotUsed.class);
    assertMutantCallableReturns(new ReturnValueNotUsed(), mutant, false);
  }

  private class ReturnValueNotUsed implements Callable<Boolean> {
    private final List<String> aList = asList("xyz");

    @Override
    public Boolean call() throws Exception {
      this.aList.set(0, "will not be present in list in mutated version");
      return this.aList
          .contains("will not be present in list in mutated version");
    }
  }

  @Test
  public void shouldReplaceMethodsReturningArraysMatchingArgumentType()
      throws Exception {
    final Mutant mutant = getFirstMutant(HasArrayMethod.class);
    final String[] expected = { "1", "2" };
    final String[] actual = mutateAndCall(new HasArrayMethod(), mutant);
    assertThat(actual).containsExactly(expected);
  }

  private static class HasArrayMethod implements Callable<String[]> {

    public String[] delegate(final String[] ss) {
      return new String[] {};
    }

    @Override
    public String[] call() throws Exception {
      final String[] s = { "1", "2" };
      return delegate(s);
    }
  }

  @Test
  public void shouldNotReplaceMethodsReturningArraysOfUnmatchedType()
      throws Exception {
    assertNoMutants(HasArrayMethodOfDifferentType.class);
  }

  private static class HasArrayMethodOfDifferentType implements
  Callable<String[]> {

    public String[] delegate(final Integer[] ss) {
      return new String[] {};
    }

    @Override
    public String[] call() throws Exception {
      final Integer[] s = { 1, 2 };
      return delegate(s);
    }
  }

  @Test
  public void willSubstituteCollectionsOfDifferentTypesDueToTypeErasure()
      throws Exception {
    final Mutant mutant = getFirstMutant(HasListMethod.class);
    final List<String> expected = Collections.emptyList();
    final List<String> actual = mutateAndCall(new HasListMethod(), mutant);
    assertThat(actual).isEqualTo(expected);
  }

  private static class HasListMethod implements Callable<List<String>> {

    public List<String> delegate(final List<Integer> is) {
      return Arrays.asList(new String[] { "foo", "bar" });
    }

    @Override
    public List<String> call() throws Exception {
      final List<Integer> s = Collections.emptyList();
      return delegate(s);
    }
  }

  @Test
  public void shouldReplaceInstanceMethodCallThatIsUsedAsArgumentForCallToOtherObject()
      throws Exception {
    final Mutant mutant = getFirstMutant(CallsOtherObjectWithResultOfInstanceMethod.class);
    final MyListener listener = new MyListener();
    assertMutantCallableReturns(new CallsOtherObjectWithResultOfInstanceMethod(
        "lowercase", listener), mutant, "lowercase");
  }

  private class CallsOtherObjectWithResultOfInstanceMethod implements
      Callable<String> {
    private final String     arg;
    private final MyListener listener;

    public CallsOtherObjectWithResultOfInstanceMethod(String arg,
        MyListener listener) {
      this.arg = arg;
      this.listener = listener;
    }

    private String delegate(String aString) {
      return aString.toUpperCase();
    }

    @Override
    public String call() throws Exception {
      this.listener.call(delegate(this.arg));
      return this.listener.getCalledWith();
    }
  }

  @Test
  public void shouldReplaceStaticMethodCallThatIsUsedAsArgumentForCallToOtherObject()
      throws Exception {
    final Mutant mutant = getFirstMutant(CallsOtherObjectWithResultOfStaticMethod.class);
    final MyListener listener = new MyListener();
    assertMutantCallableReturns(new CallsOtherObjectWithResultOfStaticMethod(
        "lowercase", listener), mutant, "lowercase");
  }

  private static class CallsOtherObjectWithResultOfStaticMethod implements
      Callable<String> {
    private final String     arg;
    private final MyListener listener;

    public CallsOtherObjectWithResultOfStaticMethod(String arg,
        MyListener listener) {
      this.arg = arg;
      this.listener = listener;
    }

    private static String delegate(int i, String aString, long l) {
      return aString.toUpperCase();
    }

    @Override
    public String call() throws Exception {
      this.listener.call(delegate(3, this.arg, 5L));
      return this.listener.getCalledWith();
    }
  }

  @Test
  public void shouldReplaceInstanceMethodCallWithSeveralArgumentsThatIsUsedAsArgumentForCallToOtherObject()
      throws Exception {
    final Mutant mutant = getFirstMutant(CallsOtherObjectWithResultOfInstanceMethodHavingSeveralArguments.class);
    final MyListener listener = new MyListener();
    assertMutantCallableReturns(
        new CallsOtherObjectWithResultOfInstanceMethodHavingSeveralArguments(
            "lowercase", listener), mutant, "lowercase");

  }

  private static class CallsOtherObjectWithResultOfInstanceMethodHavingSeveralArguments
  implements Callable<String> {
    private final String     arg;
    private final MyListener listener;

    public CallsOtherObjectWithResultOfInstanceMethodHavingSeveralArguments(
        String arg, MyListener listener) {
      this.arg = arg;
      this.listener = listener;
    }

    private String delegate(int i, double aDouble, Object object,
        String aString, long l) {
      return aString.toUpperCase();
    }

    @Override
    public String call() throws Exception {
      this.listener.call(delegate(3, 4.2D, new Object(), this.arg, 5L));
      return this.listener.getCalledWith();
    }
  }

  private static class MyListener {
    private String calledWith = "not called";

    public void call(String text) {
      this.calledWith = text;
    }

    public String getCalledWith() {
      return this.calledWith;
    }
  }
}