package net.bytebuddy.implementation;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.test.utility.CallTraceable;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;

import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(Parameterized.class)
public class MethodDelegationConstructionTest<T extends CallTraceable> {

    private static final String FOO = "foo", BAR = "bar";

    private static final byte BYTE_MULTIPLICATOR = 3;

    private static final short SHORT_MULTIPLICATOR = 3;

    private static final char CHAR_MULTIPLICATOR = 3;

    private static final int INT_MULTIPLICATOR = 3;

    private static final long LONG_MULTIPLICATOR = 3L;

    private static final float FLOAT_MULTIPLICATOR = 3f;

    private static final double DOUBLE_MULTIPLICATOR = 3d;

    private static final boolean DEFAULT_BOOLEAN = false;

    private static final byte DEFAULT_BYTE = 1;

    private static final short DEFAULT_SHORT = 1;

    private static final char DEFAULT_CHAR = 1;

    private static final int DEFAULT_INT = 1;

    private static final long DEFAULT_LONG = 1L;

    private static final float DEFAULT_FLOAT = 1f;

    private static final double DEFAULT_DOUBLE = 1d;

    private final Class<T> sourceType;

    private final Class<?> targetType;

    private final Class<?>[] parameterTypes;

    private final Object[] arguments;

    private final Matcher<?> matcher;

    public MethodDelegationConstructionTest(Class<T> sourceType,
                                            Class<?> targetType,
                                            Class<?>[] parameterTypes,
                                            Object[] arguments,
                                            Matcher<?> matcher) {
        this.sourceType = sourceType;
        this.targetType = targetType;
        this.parameterTypes = parameterTypes;
        this.arguments = arguments;
        this.matcher = matcher;
    }

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
                {BooleanSource.class, BooleanTarget.class, new Class<?>[]{boolean.class}, new Object[]{DEFAULT_BOOLEAN}, is(!DEFAULT_BOOLEAN)},
                {ByteSource.class, ByteTarget.class, new Class<?>[]{byte.class}, new Object[]{DEFAULT_BYTE}, is((byte) (DEFAULT_BYTE * BYTE_MULTIPLICATOR))},
                {ShortSource.class, ShortTarget.class, new Class<?>[]{short.class}, new Object[]{DEFAULT_SHORT}, is((short) (DEFAULT_SHORT * SHORT_MULTIPLICATOR))},
                {CharSource.class, CharTarget.class, new Class<?>[]{char.class}, new Object[]{DEFAULT_CHAR}, is((char) (DEFAULT_CHAR * CHAR_MULTIPLICATOR))},
                {IntSource.class, IntTarget.class, new Class<?>[]{int.class}, new Object[]{DEFAULT_INT}, is(DEFAULT_INT * INT_MULTIPLICATOR)},
                {LongSource.class, LongTarget.class, new Class<?>[]{long.class}, new Object[]{DEFAULT_LONG}, is(DEFAULT_LONG * LONG_MULTIPLICATOR)},
                {FloatSource.class, FloatTarget.class, new Class<?>[]{float.class}, new Object[]{DEFAULT_FLOAT}, is(DEFAULT_FLOAT * FLOAT_MULTIPLICATOR)},
                {DoubleSource.class, DoubleTarget.class, new Class<?>[]{double.class}, new Object[]{DEFAULT_DOUBLE}, is(DEFAULT_DOUBLE * DOUBLE_MULTIPLICATOR)},
                {VoidSource.class, VoidTarget.class, new Class<?>[0], new Object[0], nullValue()},
                {StringSource.class, StringTarget.class, new Class<?>[]{String.class}, new Object[]{FOO}, is(FOO + BAR)},
        });
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testConstruction() throws Exception {
        DynamicType.Loaded<T> loaded = new ByteBuddy()
                .subclass(sourceType)
                .method(isDeclaredBy(sourceType))
                .intercept(MethodDelegation.toConstructor(targetType))
                .make()
                .load(sourceType.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        assertThat(loaded.getLoadedAuxiliaryTypes().size(), is(0));
        assertThat(loaded.getLoaded().getDeclaredMethods().length, is(1));
        assertThat(loaded.getLoaded().getDeclaredFields().length, is(0));
        T instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        assertThat(instance.getClass(), not(CoreMatchers.<Class<?>>is(sourceType)));
        assertThat(instance, instanceOf(sourceType));
        Object value = loaded.getLoaded().getDeclaredMethod(FOO, parameterTypes).invoke(instance, arguments);
        assertThat(value, instanceOf(targetType));
        Field field = targetType.getDeclaredField("value");
        field.setAccessible(true);
        assertThat(field.get(value), (Matcher) matcher);
        instance.assertZeroCalls();
    }

    public static class BooleanSource extends CallTraceable {

        @SuppressWarnings("unused")
        public BooleanTarget foo(boolean b) {
            register(FOO);
            return null;
        }
    }

    public static class BooleanTarget {

        @SuppressWarnings("unused")
        private final boolean value;

        public BooleanTarget(boolean value) {
            this.value = !value;
        }
    }

    public static class ByteSource extends CallTraceable {

        @SuppressWarnings("unused")
        public ByteTarget foo(byte b) {
            register(FOO);
            return null;
        }
    }

    public static class ByteTarget {

        @SuppressWarnings("unused")
        private final byte value;

        public ByteTarget(byte b) {
            value = bar(b);
        }

        private static byte bar(byte b) {
            return (byte) (b * BYTE_MULTIPLICATOR);
        }
    }

    public static class ShortSource extends CallTraceable {

        @SuppressWarnings("unused")
        public ShortTarget foo(short s) {
            register(FOO);
            return null;
        }
    }

    public static class ShortTarget {

        @SuppressWarnings("unused")
        private final short value;

        public ShortTarget(short s) {
            this.value = bar(s);
        }

        private static short bar(short s) {
            return (short) (s * SHORT_MULTIPLICATOR);
        }
    }

    public static class CharSource extends CallTraceable {

        @SuppressWarnings("unused")
        public CharTarget foo(char s) {
            register(FOO);
            return null;
        }
    }

    public static class CharTarget {

        @SuppressWarnings("unused")
        private final char value;

        public CharTarget(char c) {
            this.value = bar(c);
        }

        private static char bar(char c) {
            return (char) (c * CHAR_MULTIPLICATOR);
        }
    }

    public static class IntSource extends CallTraceable {

        @SuppressWarnings("unused")
        public IntTarget foo(int i) {
            register(FOO);
            return null;
        }
    }

    public static class IntTarget {

        @SuppressWarnings("unused")
        private final int value;

        public IntTarget(int i) {
            this.value = bar(i);
        }

        private static int bar(int i) {
            return i * INT_MULTIPLICATOR;
        }
    }

    public static class LongSource extends CallTraceable {

        @SuppressWarnings("unused")
        public LongTarget foo(long l) {
            register(FOO);
            return null;
        }
    }

    public static class LongTarget {

        @SuppressWarnings("unused")
        private final long value;

        public LongTarget(long l) {
            this.value = bar(l);
        }

        private static long bar(long l) {
            return l * LONG_MULTIPLICATOR;
        }
    }

    public static class FloatSource extends CallTraceable {

        @SuppressWarnings("unused")
        public FloatTarget foo(float f) {
            register(FOO);
            return null;
        }
    }

    public static class FloatTarget {

        @SuppressWarnings("unused")
        private final float value;

        public FloatTarget(float f) {
            this.value = bar(f);
        }

        private static float bar(float f) {
            return f * FLOAT_MULTIPLICATOR;
        }
    }

    public static class DoubleSource extends CallTraceable {

        @SuppressWarnings("unused")
        public DoubleTarget foo(double d) {
            register(FOO);
            return null;
        }
    }

    public static class DoubleTarget {

        @SuppressWarnings("unused")

        private final double value;

        public DoubleTarget(double d) {
            this.value = bar(d);
        }

        public static double bar(double d) {
            return d * DOUBLE_MULTIPLICATOR;
        }
    }

    public static class VoidSource extends CallTraceable {

        public VoidTarget foo() {
            register(FOO);
            return null;
        }
    }

    public static class VoidTarget {

        @SuppressWarnings("unused")
        private final Void value = null;
    }

    public static class StringSource extends CallTraceable {

        public StringTarget foo(String s) {
            register(FOO);
            return null;
        }
    }

    public static class StringTarget {

        @SuppressWarnings("unused")
        private final String value;

        public StringTarget(String s) {
            this.value = bar(s);
        }

        public static String bar(String s) {
            return s + BAR;
        }
    }
}
