package net.bytebuddy.asm;

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

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

public class AdviceExitValueTest {

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

    private static final String EXIT = "exit";

    private static final int VALUE = 42;

    @Test
    public void testAdviceWithEnterValue() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(Sample.class)
                .visit(Advice.to(ExitValueAdvice.class).on(named(FOO)))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(FOO).invoke(type.getDeclaredConstructor().newInstance()), is((Object) FOO));
        assertThat(type.getDeclaredField(EXIT).get(null), is((Object) 1));
    }

    @Test
    public void testEnterValueSubstitution() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(Sample.class)
                .visit(Advice.to(ExitSubstitutionAdvice.class).on(named(FOO)))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(FOO).invoke(type.getDeclaredConstructor().newInstance()), is((Object) FOO));
    }

    @Test(expected = IllegalStateException.class)
    public void testIllegalEnterValueSubstitution() throws Exception {
        new ByteBuddy()
                .redefine(Sample.class)
                .visit(Advice.to(IllegalExitSubstitutionAdvice.class).on(named(BAR)))
                .make();
    }

    @Test(expected = IllegalStateException.class)
    public void testAdviceWithNonAssignableEnterValueWritable() throws Exception {
        new ByteBuddy()
                .redefine(Sample.class)
                .visit(Advice.to(NonAssignableExitWriteAdvice.class).on(named(FOO)))
                .make();
    }

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

        public static int exit;

        public String foo() {
            return FOO;
        }

        public String bar(String argument) {
            return argument;
        }
    }

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

        @Advice.OnMethodExit
        private static int exit(@Advice.Exit int value) {
            if (value != 0) {
                throw new AssertionError();
            }
            Sample.exit++;
            return VALUE;
        }
    }

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

        @Advice.OnMethodExit
        @SuppressWarnings("all")
        private static String exit(@Advice.Exit(readOnly = false) String value) {
            value = BAR;
            if (!value.equals(BAR)) {
                throw new AssertionError();
            }
            return FOO;
        }
    }

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

        @Advice.OnMethodExit
        @SuppressWarnings("all")
        private static String exit(@Advice.Exit String value) {
            value = BAR;
            throw new AssertionError();
        }
    }

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

        @Advice.OnMethodExit
        private static void exit(@Advice.Exit(readOnly = false) Object value) {
            throw new AssertionError();
        }
    }

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

        @Advice.OnMethodExit
        private static String exit(@Advice.Exit(readOnly = false) Object value) {
            throw new AssertionError();
        }
    }

}