package net.bytebuddy.dynamic.scaffold.inline;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.LoadedTypeInitializer;
import net.bytebuddy.implementation.attribute.AnnotationRetention;
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
import net.bytebuddy.matcher.LatentMatcher;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.OpenedClassReader;
import org.junit.Test;
import org.objectweb.asm.MethodVisitor;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Map;

import static net.bytebuddy.matcher.ElementMatchers.*;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;

public class DecoratingDynamicTypeBuilderTest {

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

    @Test
    public void testDecoration() throws Exception {
        Object instance = new ByteBuddy()
                .decorate(Foo.class)
                .annotateType(AnnotationDescription.Builder.ofType(Qux.class).build())
                .ignoreAlso(new LatentMatcher.Resolved<MethodDescription>(none()))
                .visit(new AsmVisitorWrapper.ForDeclaredMethods().method(named(FOO), new AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper() {
                    public MethodVisitor wrap(TypeDescription instrumentedType,
                                              MethodDescription instrumentedMethod,
                                              MethodVisitor methodVisitor,
                                              Implementation.Context implementationContext,
                                              TypePool typePool,
                                              int writerFlags,
                                              int readerFlags) {
                        return new MethodVisitor(OpenedClassReader.ASM_API, methodVisitor) {
                            public void visitLdcInsn(Object value) {
                                if (FOO.equals(value)) {
                                    value = BAR;
                                }
                                super.visitLdcInsn(value);
                            }
                        };
                    }
                }))
                .make()
                .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.CHILD_FIRST)
                .getLoaded()
                .getConstructor()
                .newInstance();
        assertThat(instance.getClass().getMethod(FOO).invoke(instance), is((Object) BAR));
        assertThat(instance.getClass().isAnnotationPresent(Bar.class), is(true));
        assertThat(instance.getClass().isAnnotationPresent(Qux.class), is(true));
    }

    @Test
    public void testDecorationNonVirtualMember() throws Exception {
        Object instance = new ByteBuddy()
                .decorate(Foo.class)
                .annotateType(AnnotationDescription.Builder.ofType(Qux.class).build())
                .ignoreAlso(new LatentMatcher.Resolved<MethodDescription>(none()))
                .visit(new AsmVisitorWrapper.ForDeclaredMethods().method(named(BAR), new AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper() {
                    public MethodVisitor wrap(TypeDescription instrumentedType,
                                              MethodDescription instrumentedMethod,
                                              MethodVisitor methodVisitor,
                                              Implementation.Context implementationContext,
                                              TypePool typePool,
                                              int writerFlags,
                                              int readerFlags) {
                        return new MethodVisitor(OpenedClassReader.ASM_API, methodVisitor) {
                            @Override
                            public void visitLdcInsn(Object value) {
                                if (FOO.equals(value)) {
                                    value = BAR;
                                }
                                super.visitLdcInsn(value);
                            }
                        };
                    }
                }))
                .make()
                .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.CHILD_FIRST)
                .getLoaded()
                .getConstructor()
                .newInstance();
        assertThat(instance.getClass().getMethod(BAR).invoke(null), is((Object) BAR));
        assertThat(instance.getClass().isAnnotationPresent(Bar.class), is(true));
        assertThat(instance.getClass().isAnnotationPresent(Qux.class), is(true));
    }

    @Test
    public void testDecorationWithoutAnnotationRetention() throws Exception {
        Object instance = new ByteBuddy()
                .with(AnnotationRetention.DISABLED)
                .decorate(Foo.class)
                .annotateType(AnnotationDescription.Builder.ofType(Qux.class).build())
                .ignoreAlso(new LatentMatcher.Resolved<MethodDescription>(none()))
                .visit(new AsmVisitorWrapper.ForDeclaredMethods()
                        .method(named(FOO), new AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper() {
                            public MethodVisitor wrap(TypeDescription instrumentedType,
                                                      MethodDescription instrumentedMethod,
                                                      MethodVisitor methodVisitor,
                                                      Implementation.Context implementationContext,
                                                      TypePool typePool,
                                                      int writerFlags,
                                                      int readerFlags) {
                                return new MethodVisitor(OpenedClassReader.ASM_API, methodVisitor) {
                                    @Override
                                    public void visitLdcInsn(Object value) {
                                        if (FOO.equals(value)) {
                                            value = BAR;
                                        }
                                        super.visitLdcInsn(value);
                                    }
                                };
                            }
                        }))
                .make()
                .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.CHILD_FIRST)
                .getLoaded()
                .getConstructor()
                .newInstance();
        assertThat(instance.getClass().getMethod(FOO).invoke(instance), is((Object) BAR));
        assertThat(instance.getClass().isAnnotationPresent(Bar.class), is(true));
        assertThat(instance.getClass().isAnnotationPresent(Qux.class), is(true));
    }

    @Test
    public void testAuxiliaryTypes() throws Exception {
        Map<TypeDescription, byte[]> auxiliaryTypes = new ByteBuddy()
                .decorate(Foo.class)
                .require(TypeDescription.VOID, new byte[]{1, 2, 3})
                .make()
                .getAuxiliaryTypes();
        assertThat(auxiliaryTypes.size(), is(1));
        assertThat(auxiliaryTypes.get(TypeDescription.VOID).length, is(3));
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDecorationChangeName() throws Exception {
        new ByteBuddy().decorate(Foo.class).name(FOO);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDecorationChangeModifiers() throws Exception {
        new ByteBuddy().decorate(Foo.class).modifiers(0);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDecorationChangeModifiersMerge() throws Exception {
        new ByteBuddy().decorate(Foo.class).merge();
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDecorationChangeInterface() throws Exception {
        new ByteBuddy().decorate(Foo.class).implement(Runnable.class);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDecorationChange() throws Exception {
        new ByteBuddy().decorate(Foo.class).implement(Runnable.class);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInnerClassChangeForTopLevel() throws Exception {
        new ByteBuddy().decorate(Foo.class).topLevelType();
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInnerClassChangeForType() throws Exception {
        new ByteBuddy().decorate(Foo.class).innerTypeOf(Object.class);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInnerClassChangeForMethod() throws Exception {
        new ByteBuddy().decorate(Foo.class).innerTypeOf(Object.class.getMethod("toString"));
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInnerClassChangeForConstructor() throws Exception {
        new ByteBuddy().decorate(Foo.class).innerTypeOf(Object.class.getConstructor());
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testNestHost() throws Exception {
        new ByteBuddy().decorate(Foo.class).nestHost(Object.class);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testNestMember() throws Exception {
        new ByteBuddy().decorate(Foo.class).nestMembers(Object.class);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDefineField() throws Exception {
        new ByteBuddy().decorate(Foo.class).defineField(FOO, Void.class);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInterceptField() throws Exception {
        new ByteBuddy().decorate(Foo.class).field(any());
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDefineMethod() throws Exception {
        new ByteBuddy().decorate(Foo.class).defineMethod(FOO, void.class);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testDefineConstructor() throws Exception {
        new ByteBuddy().decorate(Foo.class).defineConstructor();
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInterceptInvokable() throws Exception {
        new ByteBuddy().decorate(Foo.class).invokable(any());
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testTypeVariable() throws Exception {
        new ByteBuddy().decorate(Foo.class).typeVariable(FOO);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInitializer() throws Exception {
        new ByteBuddy().decorate(Foo.class).initializer(mock(ByteCodeAppender.class));
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testLoadedInitializer() throws Exception {
        new ByteBuddy().decorate(Foo.class).initializer(mock(LoadedTypeInitializer.class));
    }

    @Bar
    public static class Foo {

        public String foo() {
            return FOO;
        }

        public static String bar() {
            return FOO;
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Bar {
        /* empty */
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Qux {
        /* empty */
    }
}