package de.renew.formalism.function;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import de.renew.util.Value;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @author Philipp Schult
 * @version 2023-06-09
 */
public class ExecutorTest {

    private static Class<?> _dummyClass;
    private static Constructor<?> _dummyClassStringConstructor;

    @BeforeAll
    public static void setUpAll() throws NoSuchMethodException {
        _dummyClass = DummyClass.class;
        _dummyClassStringConstructor = _dummyClass.getConstructor(String.class);
    }

    @Test
    public void testGetTypesForPrimitives() {
        //given
        int i = 0;
        float f = 1.1f;
        boolean b = false;
        Object[] input = { i, f, b };
        //when
        Class<?>[] expectedOutput = { Integer.class, Float.class, Boolean.class };
        Class<?>[] actualOutput = Executor.getTypes(input);
        //then
        assertArrayEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testGetTypesForValues() {
        //given
        int i = 0;
        float f = 1.1f;
        boolean b = false;
        Value v1 = new Value(i);
        Value v2 = new Value(f);
        Value v3 = new Value(b);
        Object[] input = { v1, v2, v3 };
        //when
        Class<?>[] expectedOutput = { int.class, float.class, boolean.class };
        Class<?>[] actualOutput = Executor.getTypes(input);
        //then
        assertArrayEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testGetTypesForNonPrimitives() {
        //given
        String s = "s";
        DummyClass d = new DummyClass();
        Object[] input = { s, d };
        //when
        Class<?>[] expectedOutput = { String.class, DummyClass.class };
        Class<?>[] actualOutput = Executor.getTypes(input);
        //then
        assertArrayEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorNoParametersWithoutUniqueness()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] noInput = { };
        //when
        Constructor<?> actualOutput = Executor.findBestConstructor(_dummyClass, noInput, false);
        Constructor<?> expectedOutput = _dummyClass.getConstructor(noInput);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorExactMatchWithoutUniquenessSingleParam()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] longInput = { long.class };
        //when
        Constructor<?> expectedOutput = _dummyClass.getConstructor(longInput);
        Constructor<?> actualOutput = Executor.findBestConstructor(_dummyClass, longInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorExactMatchWithoutUniquenessMultipleParams()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] longIntInput = { long.class, int.class };
        //when
        Constructor<?> expectedOutput = _dummyClass.getConstructor(longIntInput);
        Constructor<?> actualOutput =
            Executor.findBestConstructor(_dummyClass, longIntInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorExactMatchWithUniquenessOneParam()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] longInput = { long.class };
        //when
        Constructor<?> expectedOutput = _dummyClass.getConstructor(longInput);
        Constructor<?> actualOutput = Executor.findBestConstructor(_dummyClass, longInput, true);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorExactMatchWithUniquenessMultipleParams()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] longIntInput = { long.class, int.class };
        //when
        Constructor<?> expectedOutput = _dummyClass.getConstructor(longIntInput);
        Constructor<?> actualOutput = Executor.findBestConstructor(_dummyClass, longIntInput, true);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorNoExactMatchWithoutUniquenessOneParam()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] longInput = { long.class };
        Class<?>[] intInput = { int.class };
        //when
        Constructor<?> actualOutput = Executor.findBestConstructor(_dummyClass, intInput, false);
        Constructor<?> expectedOutput = _dummyClass.getConstructor(longInput);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorNoExactMatchWithoutUniquenessMultipleParams()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] intIntInput = { int.class, int.class };
        Class<?>[] longIntInput = { long.class, int.class };
        Class<?>[] intLongInput = { int.class, long.class };
        //when
        Constructor<?> actualOutputTwoInt =
            Executor.findBestConstructor(_dummyClass, intIntInput, false);
        Constructor<?> expectedOutputTwoInt1 = _dummyClass.getConstructor(longIntInput);
        Constructor<?> expectedOutputTwoInt2 = _dummyClass.getConstructor(intLongInput);

        //then
        //in this case, both results can happen, it seems to be random, so we check for both
        assertTrue(
            expectedOutputTwoInt1.equals(actualOutputTwoInt)
                || expectedOutputTwoInt2.equals(actualOutputTwoInt));
    }

    @Test
    public void testFindBestConstructorExactMatchNoParams() throws NoSuchMethodException {
        //given
        Class<?>[] noInput = { };
        //when
        Constructor<?> expectedOutput = _dummyClass.getConstructor(noInput);
        Constructor<?> actualOutput = Executor.findBestConstructor(_dummyClass, noInput, true);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestConstructorWithUniquenessFindsMultipleMatches()
        throws NoSuchMethodException
    {
        //given
        Class<?>[] intIntInput = { int.class, int.class };
        //when
        Constructor<?> output = Executor.findBestConstructor(_dummyClass, intIntInput, true);
        //then
        assertNull(output);
    }

    @Test
    public void testFindBestConstructorWithInvalidParameters() {
        //given
        Class<?>[] objectInput = { Object.class };
        //when/then
        assertThrows(
            NoSuchMethodException.class,
            () -> Executor.findBestConstructor(_dummyClass, objectInput, false));
    }

    @Test
    public void testFindBestMethodNoParameters() throws NoSuchMethodException {
        //similar to the above, but it also looks through base or inherited methods,
        //such as from Object class
        //given
        Class<?>[] noInput = { };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", noInput);
        Method actualOutput = Executor.findBestMethod(_dummyClass, "dummyMethod", noInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodExactMatchSingleParam() throws NoSuchMethodException {
        //given
        Class<?>[] longInput = { long.class };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", longInput);
        Method actualOutput = Executor.findBestMethod(_dummyClass, "dummyMethod", longInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodExactMatchMultipleParams() throws NoSuchMethodException {
        //given
        Class<?>[] longLongInput = { long.class, long.class };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", longLongInput);
        Method actualOutput =
            Executor.findBestMethod(_dummyClass, "dummyMethod", longLongInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodExactMatchUniqueSingleParam() throws NoSuchMethodException {
        //given
        Class<?>[] longInput = { long.class };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", longInput);
        Method actualOutput = Executor.findBestMethod(_dummyClass, "dummyMethod", longInput, true);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodExactMatchUniqueMultipleParams() throws NoSuchMethodException {
        //given
        Class<?>[] longLongInput = { long.class, long.class };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", longLongInput);
        Method actualOutput =
            Executor.findBestMethod(_dummyClass, "dummyMethod", longLongInput, true);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodUniqueFindsMultipleMatches() throws NoSuchMethodException {
        //given
        Class<?>[] input = { int.class, int.class, int.class };
        //when
        Method output = Executor.findBestMethod(_dummyClass, "dummyMethod", input, true);
        //then
        assertNull(output);
    }

    @Test
    public void testFindBestMethodExactMatchInherited() throws NoSuchMethodException {
        //given
        Class<?>[] intInput = { int.class };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", intInput);
        Method actualOutput = Executor.findBestMethod(_dummyClass, "dummyMethod", intInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodNotExactMatchOneCast() throws NoSuchMethodException {
        //given
        Class<?>[] longIntInput = { long.class, int.class };
        Class<?>[] longLongInput = { long.class, long.class };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", longLongInput);
        Method actualOutput =
            Executor.findBestMethod(_dummyClass, "dummyMethod", longIntInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodNotExactMatchTwoCasts() throws NoSuchMethodException {
        //given
        Class<?>[] intIntInput = { int.class, int.class };
        Class<?>[] longLongInput = { long.class, long.class };
        //when
        Method expectedOutput = _dummyClass.getMethod("dummyMethod", longLongInput);
        Method actualOutput =
            Executor.findBestMethod(_dummyClass, "dummyMethod", intIntInput, false);
        //then
        assertEquals(expectedOutput, actualOutput);
    }

    @Test
    public void testFindBestMethodNoMatchingParameters() {
        //given
        Class<?>[] stringInput = { String.class };
        //when/then
        assertThrows(
            NoSuchMethodException.class,
            () -> Executor.findBestMethod(_dummyClass, "dummyMethod", stringInput, false));
    }

    @Test
    public void testFindBestMethodNoMatchingMethodName() {
        //given
        Class<?>[] input = { };
        //when/then
        assertThrows(
            NoSuchMethodException.class,
            () -> Executor.findBestMethod(_dummyClass, "randomstring", input, false));
    }

    @Test
    public void testExecuteConstructorGivenConstructorValidInput() throws Exception {
        //given
        Object[] paramsValid = { "testinput" };
        //when
        DummyClass dc =
            (DummyClass) Executor.executeConstructor(_dummyClassStringConstructor, paramsValid);
        //then
        assertEquals("testinput", dc._s);
    }

    @Test
    public void testExecuteConstructorGivenConstructorGivenIllegalArgument() {
        //given
        Object[] paramsWrongType = { 1 };
        //when/then
        assertThrows(
            RuntimeException.class,
            () -> Executor.executeConstructor(_dummyClassStringConstructor, paramsWrongType));
    }

    @Test
    public void testExecuteConstructorGivenConstructorForAbstractClass()
        throws NoSuchMethodException
    {
        //given
        Object[] params = { "teststring" };
        Constructor<?> constructor = DummyClassAbstract.class.getConstructor(String.class);
        //when/then
        assertThrows(
            NoSuchMethodException.class, () -> Executor.executeConstructor(constructor, params));
    }

    @Test
    public void testExecuteConstructorGivenPrivateConstructor() throws NoSuchMethodException {
        //given
        Object[] params = { };
        Constructor<?> constructor = PrivateDummyClass.class.getDeclaredConstructor();
        //when/then
        assertThrows(
            NoSuchMethodException.class, () -> Executor.executeConstructor(constructor, params));
    }

    @Test
    public void testExecuteConstructorGivenConstructorThrowsInvocationTarget() {
        //given
        Object[] paramsInvocationTarget = { "invocationtarget" };
        //when/then
        assertThrows(
            InvocationTargetException.class, () -> Executor
                .executeConstructor(_dummyClassStringConstructor, paramsInvocationTarget));
    }

    @Test
    public void testExecuteConstructorGivenArrayClassOneDimensional() throws Exception {
        //given
        Class<?> inputClass = int[].class;
        Value v1 = new Value(6);
        Object[] params = { v1 };
        //when
        int[] actualResult = (int[]) Executor.executeConstructor(inputClass, params);
        int[] expectedResult = new int[6];
        //then
        assertEquals(expectedResult.length, actualResult.length);
    }

    @Test
    public void testExecuteConstructorGivenArrayClassMultidimensional() throws Exception {
        //given
        Class<?> inputClass = int[][].class;
        Value v1 = new Value(6);
        Value v2 = new Value(9);
        Object[] params = { v1, v2 };
        //when
        int[][] actualResult = (int[][]) Executor.executeConstructor(inputClass, params);
        int[][] expectedResult = new int[6][9];
        //then
        //check for all dimensions if their size is equal
        assertEquals(expectedResult.length, actualResult.length);
        for (int i = 0; i < expectedResult.length; i++) {
            assertEquals(expectedResult[i].length, actualResult[i].length);
        }
    }

    //for non-array classes, this method just uses the findBestConstructor and executeConstructor with a constructor.
    //we don't need to test all the cases again, because we already tested the behaviour of said methods above.
    @Test
    public void testExecuteConstructorGivenClass() throws Exception {
        //given
        Object[] params = { "teststring" };
        //when
        DummyClass actualResult = (DummyClass) Executor.executeConstructor(_dummyClass, params);
        DummyClass expectedResult = new DummyClass("teststring");
        //then
        assertEquals(expectedResult._s, actualResult._s);
    }

    @Test
    public void testExecuteMethodGivenMethodValidArguments()
        throws NoSuchMethodException, InvocationTargetException
    {
        //given
        Method method = _dummyClass.getMethod("storeString", String.class);
        DummyClass target = new DummyClass();
        Object[] params = { "teststring" };
        //when
        Executor.executeMethod(method, target, params);
        //then
        assertEquals("teststring", target._storedString);
    }

    @Test
    public void testExecuteMethodGivenMethodInvalidArguments() throws NoSuchMethodException {
        //given
        Method method = _dummyClass.getMethod("storeString", String.class);
        DummyClass target = new DummyClass();
        Object[] params = { 1 };
        //when/then
        assertThrows(RuntimeException.class, () -> Executor.executeMethod(method, target, params));
    }

    @Test
    public void testExecuteMethodGivenPrivateMethod() throws NoSuchMethodException {
        //given
        Method method = PrivateDummyClass.class.getDeclaredMethod("privateMethod");
        Object target = new PrivateDummyClass(1);
        Object[] params = { };
        //when/then
        assertThrows(
            NoSuchMethodException.class, () -> Executor.executeMethod(method, target, params));
    }

    //this method only uses findBestMethod and executeMethod on a method.
    //we tested both of those methods above, so we just need to test if this method functions correctly.
    @Test
    public void testExecuteMethodGivenClass()
        throws InvocationTargetException, NoSuchMethodException
    {
        //given
        DummyClass target = new DummyClass();
        String name = "storeString";
        Object[] params = { "teststring" };
        //when
        Executor.executeMethod(_dummyClass, target, name, params);
        //then
        assertEquals("teststring", target._storedString);
    }

    @Test
    public void testRenderMethodSignature() {
        //given
        String name = "dummyMethod";
        Class<?>[] params = { long.class };
        //when
        String actualResult = Executor.renderMethodSignature(_dummyClass, name, params);
        String expectedResult =
            "de.renew.formalism.function.ExecutorTest$DummyClass.dummyMethod(long)";
        //then
        assertEquals(expectedResult, actualResult);
    }

    @Test
    public void testRenderMethodSignatureNotGivenParameters() {
        //given
        String name = "dummyMethod";
        Class<?>[] params = null;
        //when
        String actualResult = Executor.renderMethodSignature(_dummyClass, name, params);
        String expectedResult =
            "de.renew.formalism.function.ExecutorTest$DummyClass.dummyMethod<unknown parameters>";
        //then
        assertEquals(expectedResult, actualResult);
    }

    @Test
    public void testRenderMethodSignatureGivenNullParameters() {
        //given
        String name = null;
        Class<?>[] params = { null, null };
        //when
        String actualResult = Executor.renderMethodSignature(null, name, params);
        String expectedResult = "null.null(null, null)";
        //then
        assertEquals(expectedResult, actualResult);
    }

    @Test
    public void testRenderArrayConstructor() {
        //given
        Class<?> inputClass = int[][].class;
        Object[] params = { 2, 4 };
        //when
        String actualResult = Executor.renderArrayConstructor(inputClass, params);
        String expectedResult = "int[][](2, 4)";
        //then
        assertEquals(expectedResult, actualResult);
    }

    @Test
    public void testRenderArrayConstructorGivenNoParameters() {
        //given
        Class<?> inputClass = int[][].class;
        Object[] params = null;
        //when
        String actualResult = Executor.renderArrayConstructor(inputClass, params);
        String expectedResult = "int[][]<unknown parameters>";
        //then
        assertEquals(expectedResult, actualResult);
    }

    @Test
    public void testRenderArrayConstructorGivenNullParameters() {
        //given
        Class<?> inputClass = null;
        Object[] params = { null, null };
        //when
        String actualResult = Executor.renderArrayConstructor(inputClass, params);
        String expectedResult = "null(null, null)";
        //then
        assertEquals(expectedResult, actualResult);
    }

    public static class DummyClass extends ParentDummyClass {
        //String stored by constructor
        public String _s;
        //String stored by method
        public String _storedString;

        public DummyClass() {}

        public DummyClass(long l) {}

        public DummyClass(int l, long l2) {}

        public DummyClass(long l, int i) {}

        //This constructor is for testing the executeConstructor method.
        //It either throws an Exception based on the input string, or stores the string.
        public DummyClass(String str) throws InvocationTargetException {
            if (str.equals("invocationtarget")) {
                throw new InvocationTargetException(new Exception());
            }
            _s = str;
        }

        //These methods are for the purposes of testing the findBestConstructor method.
        //they're not supposed to do anything.
        public void dummyMethod() {}

        public void dummyMethod(long l) {}

        public void dummyMethod(long l1, long l2) {}

        public void dummyMethod(int i, long l, int i2) {}

        public void dummyMethod(int i, int i2, long l) {}

        public void storeString(String s) {
            this._storedString = s;
        }
    }

    public static class ParentDummyClass {
        public void dummyMethod(int i) {}
    }

    public static abstract class DummyClassAbstract {
        public DummyClassAbstract(String s) {}

        public void dummyMethod(int i) {}
    }

    public static class PrivateDummyClass {
        private PrivateDummyClass() {}

        private void privateMethod() {}

        public PrivateDummyClass(int i) {}
    }
}
