package de.renew.formalism.function;

import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import de.renew.unify.Impossible;
import de.renew.unify.StateRecorder;
import de.renew.unify.Tuple;
import de.renew.util.Value;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;


class BasicFunctionTest {
    @ParameterizedTest
    @MethodSource("provideNumberFunctions")
    void testFloatReturnFloat(BasicFunction function) {
        // Given
        float first = -7.4f;
        float second = 3.2f;

        // When / Then
        assertFunctionReturnValue(function, first, second, Float.class);
    }

    @ParameterizedTest
    @MethodSource("provideBooleanFunctions")
    void testFloatReturnBoolean(BasicFunction function) {
        // Given
        float first = -7.4f;
        float second = 3.2f;

        // When / Then
        assertFunctionReturnValue(function, first, second, Boolean.class);
    }

    @ParameterizedTest
    @MethodSource("provideNumberFunctions")
    void testIntReturnInt(BasicFunction function) {
        // Given
        int first = -7;
        int second = 7;

        // When / Then
        assertFunctionReturnValue(function, first, second, Integer.class);
    }

    @ParameterizedTest
    @MethodSource("provideBooleanFunctions")
    void testIntReturnBoolean(BasicFunction function) {
        // Given
        int first = -7;
        int second = 7;

        // When / Then
        assertFunctionReturnValue(function, first, second, Boolean.class);
    }

    @ParameterizedTest
    @MethodSource("provideBooleanFunctions")
    void testMixedBooleanFirstThrowImpossible(BasicFunction function) {
        // Given
        boolean first = true;
        int second = 7;

        // When / Then
        assertFunctionThrowsImpossible(function, first, second);
    }

    @ParameterizedTest
    @MethodSource("provideBooleanFunctions")
    void testMixedBooleanSecondThrowImpossible(BasicFunction function) {
        // Given
        int first = -7;
        boolean second = true;

        // When / Then
        assertFunctionThrowsImpossible(function, first, second);
    }

    public static Stream<BasicFunction> provideBooleanFunctions() {
        return Stream.of(
            BasicFunction.GREATER, BasicFunction.LESS, BasicFunction.EQUAL,
            BasicFunction.GREATEREQUAL, BasicFunction.LESSEQUAL, BasicFunction.NEQUAL);
    }

    public static Stream<BasicFunction> provideNumberFunctions() {
        return Stream.of(
            BasicFunction.TIMES, BasicFunction.DIVIDE, BasicFunction.MINUS, BasicFunction.MOD,
            BasicFunction.PLUS);
    }

    private static void assertFunctionReturnValue(
        BasicFunction function, Object first, Object second, Class<?> expectedReturnClass)
    {
        assertThatCode(() -> {
            Value result = (Value) function.function(
                new Tuple(
                    new Object[] { new Value(first), new Value(second) }, new StateRecorder()));

            // Then
            assertThat(result.value).isInstanceOf(expectedReturnClass);
        }).doesNotThrowAnyException();
    }

    private static void assertFunctionThrowsImpossible(
        BasicFunction function, Object first, Object second)
    {
        assertThatThrownBy(
            () -> function.function(
                new Tuple(
                    new Object[] { new Value(first), new Value(second) }, new StateRecorder())))
            .isInstanceOf(Impossible.class);

    }
}
