package com.insightfullogic.lambdabehave.expectations;

import com.insightfullogic.lambdabehave.Block;
import org.junit.Assert;
import org.mockito.Mockito;
import org.mockito.stubbing.Stubber;

import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

public final class Expect {

    public <T> BoundExpectation<T> that(final T value) {
        return new BoundExpectation<T>(value, true);
    }

    public <T> CollectionExpectation<T> that(final Collection<T> collection) {
        return new CollectionExpectation<>(collection, true);
    }

    public <T extends Comparable<T>> ComparableExpectation<T> that(final T comparable) {
        return new ComparableExpectation<T>(comparable, true);
    }

    public StringExpectation that(final String str) {
        return new StringExpectation(str, true);
    }

    public DoubleExpectation that(final Double value) {
        return new DoubleExpectation(value, true);
    }

    public <T> ArrayExpectation<T> that(final T[] array) {
        return new ArrayExpectation<>(array, true);
    }

    @SuppressWarnings("unchecked")
    public <T extends Throwable> BoundExpectation<T> exception(final Class<T> expectedException, final Block block) throws Exception {
        String expectedName = expectedException.getName();
        try {
            block.run();
        } catch (final Throwable throwable) {
            if (!expectedException.isInstance(throwable)) {
                String name = throwable.getClass().getName();
                failure("Expected exception: " + expectedName + ", but " + name + " was thrown");
            }
            return new BoundExpectation<>((T)throwable, true);
        }
        failure("Expected exception: " + expectedName + ", but no exception was thrown");
        return null;
    }

    // NB: no failure without a message because its a bad idea to not have test failure diagnostics
    public void failure(final String message) {
        Assert.fail(message);
    }

    @SuppressWarnings("unchecked")
    public <T> Stubber toAnswer(final Runnable method) {
        return Mockito.doAnswer(invocation -> {
            Object[] arguments = invocation.getArguments();
            method.run();
            return null;
        });
    }

    @SuppressWarnings("unchecked")
    public <T> Stubber toAnswer(final Consumer<T> method) {
        return doAnswer(arguments -> method.accept((T) arguments[0]), 1);
    }

    @SuppressWarnings("unchecked")
    public <F, S> Stubber toAnswer(final BiConsumer<F, S> method) {
        return doAnswer(arguments ->
            method.accept((F) arguments[0], (S) arguments[1]), 2);
    }

    @SuppressWarnings("unchecked")
    public <F, S, T> Stubber toAnswer(final TriConsumer<F, S, T> method) {
        return doAnswer(arguments ->
                method.accept((F) arguments[0], (S) arguments[1], (T) arguments[2]), 3);
    }

    private  Stubber doAnswer(final Consumer<Object[]> method, final int argumentCount) {
        return Mockito.doAnswer(invocation -> {
            Object[] arguments = invocation.getArguments();
            if (arguments.length >= argumentCount) {
                method.accept(arguments);
            } else {
                failure("Invocation requires at least " + argumentCount + " argument");
            }
            return null;
        });
    }

}