package net.bytebuddy.implementation;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.bind.annotation.Morph;
import net.bytebuddy.test.utility.CallTraceable;
import net.bytebuddy.test.utility.JavaVersionRule;
import org.hamcrest.CoreMatchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;

import java.io.Serializable;

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

public class MethodDelegationMorphTest {

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

    private static final int BAZ = 42;

    private static final String DEFAULT_INTERFACE = "net.bytebuddy.test.precompiled.v8.MorphDefaultInterface";

    private static final String DEFAULT_INTERFACE_TARGET_EXPLICIT = "net.bytebuddy.test.precompiled.v8.MorphDefaultDelegationTargetExplicit";

    private static final String DEFAULT_INTERFACE_TARGET_IMPLICIT = "net.bytebuddy.test.precompiled.v8.MorphDefaultDelegationTargetImplicit";

    @Rule
    public MethodRule javaVersionRule = new JavaVersionRule();

    @Test
    public void testMorph() throws Exception {
        DynamicType.Loaded<Foo> loaded = new ByteBuddy()
                .subclass(Foo.class)
                .method(isDeclaredBy(Foo.class))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(new SimpleMorph(QUX)))
                .make()
                .load(Foo.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        Foo instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        assertThat(instance.foo(FOO), is(QUX + BAR));
    }

    @Test
    public void testMorphVoid() throws Exception {
        SimpleMorph simpleMorph = new SimpleMorph(QUX);
        DynamicType.Loaded<Bar> loaded = new ByteBuddy()
                .subclass(Bar.class)
                .method(isDeclaredBy(Bar.class))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(simpleMorph))
                .make()
                .load(Bar.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        Bar instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        instance.foo();
        instance.assertOnlyCall(FOO);
        simpleMorph.assertOnlyCall(BAR);
    }

    @Test
    public void testMorphPrimitive() throws Exception {
        DynamicType.Loaded<Qux> loaded = new ByteBuddy()
                .subclass(Qux.class)
                .method(isDeclaredBy(Qux.class))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(new PrimitiveMorph(BAZ)))
                .make()
                .load(Qux.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        Qux instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        assertThat(instance.foo(0), is(BAZ * 2L));
    }

    @Test
    public void testMorphSerializable() throws Exception {
        DynamicType.Loaded<Foo> loaded = new ByteBuddy()
                .subclass(Foo.class)
                .method(isDeclaredBy(Foo.class))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(SimpleMorphSerializable.class))
                .make()
                .load(Foo.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        Foo instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        assertThat(instance.foo(FOO), is(QUX + BAR));
    }

    @Test(expected = IllegalStateException.class)
    public void testMorphIllegal() throws Exception {
        new ByteBuddy()
                .subclass(Foo.class)
                .method(isDeclaredBy(Foo.class))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(SimpleMorphIllegal.class))
                .make();
    }

    @Test(expected = ClassCastException.class)
    public void testMorphToIncompatibleTypeThrowsException() throws Exception {
        DynamicType.Loaded<Foo> loaded = new ByteBuddy()
                .subclass(Foo.class)
                .method(isDeclaredBy(Foo.class))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(new SimpleMorph(new Object())))
                .make()
                .load(Foo.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        Foo instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        instance.foo(QUX);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testMorphTypeDoesNotDeclareCorrectMethodThrowsException() throws Exception {
        Morph.Binder.install(Serializable.class);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testMorphTypeDoesInheritFromOtherTypeThrowsException() throws Exception {
        Morph.Binder.install(InheritingMorphingType.class);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testMorphTypeIsNotPublicThrowsException() throws Exception {
        Morph.Binder.install(PackagePrivateMorphing.class);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testPipeTypeDoesNotDeclareCorrectMethodSignatureThrowsException() throws Exception {
        Morph.Binder.install(WrongParametersMorphing.class);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testPipeTypeIsNotInterfaceThrowsException() throws Exception {
        MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(Object.class)).to(new SimpleMorph(QUX));
    }

    @Test
    @JavaVersionRule.Enforce(8)
    public void testDefaultMethodExplicit() throws Exception {
        DynamicType.Loaded<Object> loaded = new ByteBuddy()
                .subclass(Object.class)
                .implement(Class.forName(DEFAULT_INTERFACE))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(Class.forName(DEFAULT_INTERFACE_TARGET_EXPLICIT)))
                .make()
                .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        Object instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        assertThat(instance.getClass().getDeclaredMethod(FOO, String.class)
                .invoke(instance, QUX), is((Object) (FOO + BAR)));
    }

    @Test
    @JavaVersionRule.Enforce(8)
    public void testDefaultMethodImplicit() throws Exception {
        DynamicType.Loaded<Object> loaded = new ByteBuddy()
                .subclass(Object.class)
                .implement(Class.forName(DEFAULT_INTERFACE))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(Morphing.class))
                        .to(Class.forName(DEFAULT_INTERFACE_TARGET_IMPLICIT)))
                .make()
                .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);
        Object instance = loaded.getLoaded().getDeclaredConstructor().newInstance();
        assertThat(instance.getClass().getDeclaredMethod(FOO, String.class)
                .invoke(instance, QUX), is((Object) (FOO + BAR)));
    }

    public interface Morphing<T> {

        T morph(Object... arguments);
    }

    public interface InheritingMorphingType<T> extends Morphing<T> {
        /* empty */
    }

    @SuppressWarnings("unused")
    private interface PackagePrivateMorphing<T> {

        T morph(Object... arguments);
    }

    @SuppressWarnings("unused")
    private interface WrongParametersMorphing<T> {

        T morph(Object arguments);
    }

    public static class Foo {

        public String foo(String foo) {
            return foo + BAR;
        }
    }

    public static class Bar extends CallTraceable {

        public void foo() {
            register(FOO);
        }
    }

    public static class Qux {

        public long foo(int argument) {
            return argument * 2L;
        }
    }

    public static class SimpleMorph extends CallTraceable {

        private final Object[] arguments;

        public SimpleMorph(Object... arguments) {
            this.arguments = arguments;
        }

        public String intercept(@Morph Morphing<String> morphing) {
            register(BAR);
            assertThat(morphing, CoreMatchers.not(instanceOf(Serializable.class)));
            return morphing.morph(arguments);
        }
    }

    public static class PrimitiveMorph {

        private final int argument;

        public PrimitiveMorph(int argument) {
            this.argument = argument;
        }

        public Long intercept(@Morph Morphing<Long> morphing) {
            assertThat(morphing, CoreMatchers.not(instanceOf(Serializable.class)));
            return morphing.morph(argument);
        }
    }

    public static class SimpleMorphSerializable {

        public static String intercept(@Morph(serializableProxy = true) Morphing<String> morphing) {
            assertThat(morphing, instanceOf(Serializable.class));
            return morphing.morph(QUX);
        }
    }

    @SuppressWarnings("unused")
    public static class SimpleMorphIllegal {

        public static String intercept(@Morph Void morphing) {
            return null;
        }
    }
}
