package net.bytebuddy.asm;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.util.Arrays;
import java.util.Collection;

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

@RunWith(Parameterized.class)
public class AdviceFrameTest {

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

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
                {FrameAdvice.class, 2},
                {FrameAdviceWithoutThrowable.class, 2},
                {FrameAdviceWithSuppression.class, 2},
                {FrameAdviceEntryOnly.class, 1},
                {FrameAdviceEntryOnlyWithSuppression.class, 1},
                {FrameAdviceExitOnly.class, 1},
                {FrameAdviceExitOnlyWithSuppression.class, 1},
                {FrameAdviceExitOnlyWithSuppressionAndNonExceptionHandling.class, 1},
                {FrameReturnAdvice.class, 2},
                {FrameAdviceRetainedArguments.class, 2},
                {FrameAdviceWithoutThrowableRetainedArguments.class, 2},
                {FrameAdviceWithSuppressionRetainedArguments.class, 2},
                {FrameAdviceExitOnlyRetainedArguments.class, 1},
                {FrameAdviceExitOnlyWithSuppressionRetainedArguments.class, 1},
                {FrameAdviceExitOnlyWithSuppressionAndNonExceptionHandlingRetainedArguments.class, 1},
                {FrameReturnAdviceRetainedArguments.class, 2}
        });
    }

    private final Class<?> advice;

    private final int count;

    public AdviceFrameTest(Class<?> advice, int count) {
        this.advice = advice;
        this.count = count;
    }

    @Test
    public void testFrameAdvice() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(FOO)))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(FOO, String.class).invoke(type.getDeclaredConstructor().newInstance(), FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @Test
    public void testFrameAdviceStaticMethod() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(BAR)))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(BAR, String.class).invoke(null, FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @Test
    public void testFrameAdviceExpanded() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(FOO)).readerFlags(ClassReader.EXPAND_FRAMES))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(FOO, String.class).invoke(type.getDeclaredConstructor().newInstance(), FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @Test
    public void testFrameAdviceStaticMethodExpanded() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(BAR)).readerFlags(ClassReader.EXPAND_FRAMES))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(BAR, String.class).invoke(null, FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @Test
    public void testFrameAdviceComputedMaxima() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(FOO)).writerFlags(ClassWriter.COMPUTE_MAXS))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(FOO, String.class).invoke(type.getDeclaredConstructor().newInstance(), FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @Test
    public void testFrameAdviceStaticMethodComputedMaxima() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(BAR)).writerFlags(ClassWriter.COMPUTE_MAXS))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(BAR, String.class).invoke(null, FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @Test
    public void testFrameAdviceComputedFrames() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(FOO)).writerFlags(ClassWriter.COMPUTE_FRAMES))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(FOO, String.class).invoke(type.getDeclaredConstructor().newInstance(), FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @Test
    public void testFrameAdviceStaticMethodComputedFrames() throws Exception {
        Class<?> type = new ByteBuddy()
                .redefine(FrameSample.class)
                .visit(Advice.to(advice).on(named(BAR)).writerFlags(ClassWriter.COMPUTE_FRAMES))
                .make()
                .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();
        assertThat(type.getDeclaredMethod(BAR, String.class).invoke(null, FOO), is((Object) FOO));
        assertThat(type.getField(COUNT).getInt(null), is((Object) count));
    }

    @SuppressWarnings("all")
    public static class FrameSample {

        public static int count;

        public String foo(String value) {
            int ignored = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            return value;
        }

        public static String bar(String value) {
            int ignored = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            return value;
        }
    }

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

        @Advice.OnMethodEnter
        @Advice.OnMethodExit(onThrowable = Exception.class)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodEnter
        @Advice.OnMethodExit
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodEnter(suppress = Exception.class)
        @Advice.OnMethodExit(suppress = Exception.class, onThrowable = Exception.class)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodEnter
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodEnter(suppress = Exception.class)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodExit(onThrowable = Exception.class)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodExit(suppress = Exception.class, onThrowable = Exception.class)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodExit(suppress = Exception.class, onThrowable = Exception.class)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

    @SuppressWarnings("all")
    public static class FrameReturnAdvice {

        @Advice.OnMethodEnter(suppress = RuntimeException.class)
        @Advice.OnMethodExit(suppress = RuntimeException.class)
        private static String advice() {
            try {
                int ignored = 0;
                if (ignored != 0) {
                    return BAR;
                }
            } catch (Exception e) {
                int ignored = 0;
                if (ignored != 0) {
                    return QUX;
                }
            }
            FrameSample.count++;
            return FOO;
        }
    }

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

        @Advice.OnMethodEnter
        @Advice.OnMethodExit(onThrowable = Exception.class, backupArguments = false)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodEnter
        @Advice.OnMethodExit(backupArguments = false)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodEnter(suppress = Exception.class)
        @Advice.OnMethodExit(suppress = Exception.class, onThrowable = Exception.class, backupArguments = false)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodExit(onThrowable = Exception.class, backupArguments = false)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodExit(suppress = Exception.class, onThrowable = Exception.class, backupArguments = false)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

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

        @Advice.OnMethodExit(suppress = Exception.class, onThrowable = Exception.class, backupArguments = false)
        private static String advice(@Advice.Unused int ignored, @Advice.Argument(0) String value) {
            int v0 = 0;
            {
                long v1 = 1L, v2 = 2L;
                if (ignored == 1) {
                    throw new AssertionError();
                } else if (ignored == 2) {
                    if (v1 + v2 == 0L) {
                        throw new AssertionError();
                    }
                }
            }
            long v3 = 3L, v4 = 4L, v5 = 5L, v6 = 6L;
            if (ignored == 3) {
                throw new AssertionError();
            } else if (ignored == 4) {
                if (v3 + v4 + v5 + v6 == 0L) {
                    throw new AssertionError();
                }
            }
            try {
                long v7 = 7L;
            } catch (Exception exception) {
                long v8 = 8L;
            }
            FrameSample.count++;
            return value;
        }
    }

    @SuppressWarnings("all")
    public static class FrameReturnAdviceRetainedArguments {

        @Advice.OnMethodEnter(suppress = RuntimeException.class)
        @Advice.OnMethodExit(suppress = RuntimeException.class, backupArguments = false)
        private static String advice() {
            try {
                int ignored = 0;
                if (ignored != 0) {
                    return BAR;
                }
            } catch (Exception e) {
                int ignored = 0;
                if (ignored != 0) {
                    return QUX;
                }
            }
            FrameSample.count++;
            return FOO;
        }
    }
}
