package net.bytebuddy.asm;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.StubMethod;
import org.junit.Test;

import static net.bytebuddy.matcher.ElementMatchers.named;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;

public class AdviceImplementationTest {

    private static final String FOO = "foo";

    @Test(expected = IllegalStateException.class)
    public void testAbstractMethod() throws Exception {
        new ByteBuddy(ClassFileVersion.of(Foo.class))
                .subclass(Foo.class)
                .method(named(FOO))
                .intercept(Advice.to(Foo.class))
                .make();
    }

    @Test
    public void testActualMethod() throws Exception {
        assertThat(new ByteBuddy(ClassFileVersion.of(Bar.class))
                .subclass(Bar.class)
                .method(named(FOO))
                .intercept(Advice.to(Bar.class))
                .make()
                .load(Bar.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded()
                .getDeclaredConstructor()
                .newInstance()
                .foo(), is((Object) FOO));
    }

    @Test
    public void testExplicitWrap() throws Exception {
        assertThat(new ByteBuddy(ClassFileVersion.of(Qux.class))
                .subclass(Qux.class)
                .method(named(FOO))
                .intercept(Advice.to(Qux.class).wrap(StubMethod.INSTANCE))
                .make()
                .load(Qux.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded()
                .getDeclaredConstructor()
                .newInstance()
                .foo(), is(FOO));
    }

    @Test
    public void testExplicitWrapMultiple() throws Exception {
        Class<?> type = new ByteBuddy(ClassFileVersion.of(Baz.class))
                .redefine(Baz.class)
                .method(named(FOO))
                .intercept(Advice.to(Baz.class).wrap(Advice.to(Baz.class).wrap(StubMethod.INSTANCE)))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        Object baz = type.getDeclaredConstructor().newInstance();
        assertThat(type.getDeclaredMethod(FOO).invoke(baz), nullValue(Object.class));
        assertThat(type.getDeclaredField(FOO).getInt(null), is(2));
    }

    public abstract static class Foo {

        public abstract String foo();

        @Advice.OnMethodExit
        public static void exit(@Advice.Return(readOnly = false) String returned) {
            returned = FOO;
        }
    }

    public static class Bar {

        public Object foo() {
            throw new RuntimeException();
        }

        @Advice.OnMethodExit(onThrowable = RuntimeException.class)
        public static void exit(@Advice.Thrown(readOnly = false) Throwable throwable, @Advice.Return(readOnly = false) Object returned) {
            if (!(throwable instanceof RuntimeException)) {
                throw new AssertionError();
            }
            throwable = null;
            returned = FOO;
        }
    }

    public static class Qux {

        public String foo() {
            throw new AssertionError();
        }

        @Advice.OnMethodExit
        public static void exit(@Advice.Return(readOnly = false) String returned) {
            if (returned != null) {
                throw new AssertionError();
            }
            returned = FOO;
        }
    }

    public static class Baz {

        public static int foo;

        public void foo() {
            /* empty */
        }

        @Advice.OnMethodExit
        public static void exit() {
            foo += 1;
        }
    }
}
