/*
 * Copyright (C) 2015 Marcin Zajączkowski.
 *
 * Licensed under the Apache License, Version 2.0.
 */
package info.solidsoft.mockito.java8;

import org.mockito.ArgumentMatcher;
import org.mockito.Incubating;
import org.mockito.Mockito;

import java.util.function.Consumer;

/**
 * Allows creating inlined ArgumentCaptor with a lambda expression.
 * <p>
 * With Java 8 and lambda expressions ArgumentCaptor can be inlined:
 *
 * <pre class="code"><code class="java">
 * {@literal @}Test
 * public void shouldAllowToUseAssertionInLambda() {
 *   //when
 *   ts.findNumberOfShipsInRangeByCriteria(searchCriteria);
 *   //then
 *   verify(ts).findNumberOfShipsInRangeByCriteria(assertArg(sc -> assertThat(sc.getMinimumRange()).isLessThan(2000)));
 * }
 * </code></pre>
 *
 * in comparison to 3 lines in the classic way:
 *
 * <pre class="code"><code class="java">
 * {@literal @}Test
 * public void shouldAllowToUseArgumentCaptorInClassicWay() {  //old way
 *     //when
 *     ts.findNumberOfShipsInRangeByCriteria(searchCriteria);
 *     //then
 *     ArgumentCaptor<ShipSearchCriteria> captor = ArgumentCaptor.forClass(ShipSearchCriteria.class);
 *     verify(ts).findNumberOfShipsInRangeByCriteria(captor.capture());
 *     assertThat(captor.getValue().getMinimumRange()).isLessThan(2000);
 * }
 *
 * AssertJ assertions (assertThat()) used in lambda generate meaningful error messages in face of failure, but any other assertion can be
 * used if needed/preferred.
 *
 * @param <T> type of argument
 *
 * @author Marcin Zajączkowski
 */
@SuppressWarnings("WeakerAccess")
public class AssertionMatcher<T> implements ArgumentMatcher<T> {

    private static final LambdaAwareHandyReturnValues handyReturnValues = new LambdaAwareHandyReturnValues();

    private final Consumer<T> consumer;
    private String errorMessage;

    private AssertionMatcher(Consumer<T> consumer) {
        this.consumer = consumer;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean matches(T argument) {
        try {
            consumer.accept(argument);
            return true;
        } catch (AssertionError e) {
            errorMessage = e.getMessage();
            return false;
        }
    }

    @Override
    public String toString() {
        return "AssertionMatcher reported: " + errorMessage;
    }

    public static <T> T assertArg(Consumer<T> consumer) {
        argThat(consumer);
        return handyReturnValues.returnForConsumerLambda(consumer);
    }

    /**
     * A variant of assertArg(Consumer) for lambdas declaring checked exceptions.
     */
    @Incubating
    public static <T> T assertArgThrowing(ThrowingConsumer<T> throwingConsumer) {
        argThat(throwingConsumer.uncheck());
        return handyReturnValues.returnForConsumerLambdaChecked(throwingConsumer);
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private static <T> void argThat(Consumer<T> consumer) {
        Mockito.argThat(new AssertionMatcher<>(consumer));
    }
}