package net.bytebuddy.implementation.bind.annotation;

import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.method.ParameterList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.bind.MethodDelegationBinder;
import net.bytebuddy.implementation.bytecode.StackManipulation;
import net.bytebuddy.implementation.bytecode.StackSize;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class AllArgumentsBinderTest extends AbstractAnnotationBinderTest<AllArguments> {

    private static final String FOO = "foo";

    @Mock
    private TypeDescription.Generic firstSourceType, secondSourceType, genericInstrumentedType;

    @Mock
    private TypeDescription rawTargetType, rawComponentType;

    @Mock
    private TypeDescription.Generic targetType, componentType;

    public AllArgumentsBinderTest() {
        super(AllArguments.class);
    }

    @Before
    @Override
    @SuppressWarnings("unchecked")
    public void setUp() throws Exception {
        super.setUp();
        when(firstSourceType.getStackSize()).thenReturn(StackSize.SINGLE);
        when(secondSourceType.getStackSize()).thenReturn(StackSize.SINGLE);
        when(componentType.asErasure()).thenReturn(rawComponentType);
        when(targetType.getComponentType()).thenReturn(componentType);
        when(targetType.asErasure()).thenReturn(rawTargetType);
        when(firstSourceType.asGenericType()).thenReturn(firstSourceType);
        when(firstSourceType.accept(any(TypeDescription.Generic.Visitor.class))).thenReturn(firstSourceType);
        when(secondSourceType.asGenericType()).thenReturn(secondSourceType);
        when(secondSourceType.accept(any(TypeDescription.Generic.Visitor.class))).thenReturn(secondSourceType);
    }

    @Override
    protected TargetMethodAnnotationDrivenBinder.ParameterBinder<AllArguments> getSimpleBinder() {
        return AllArguments.Binder.INSTANCE;
    }

    @Test
    public void testLegalStrictBindingRuntimeType() throws Exception {
        when(target.getIndex()).thenReturn(1);
        testLegalStrictBinding(Assigner.Typing.STATIC);
    }

    @Test
    public void testLegalStrictBindingNoRuntimeType() throws Exception {
        when(target.getIndex()).thenReturn(1);
        RuntimeType runtimeType = mock(RuntimeType.class);
        doReturn(RuntimeType.class).when(runtimeType).annotationType();
        when(target.getDeclaredAnnotations()).thenReturn(new AnnotationList.ForLoadedAnnotations(runtimeType));
        testLegalStrictBinding(Assigner.Typing.DYNAMIC);
    }

    private void testLegalStrictBinding(Assigner.Typing typing) throws Exception {
        when(annotation.value()).thenReturn(AllArguments.Assignment.STRICT);
        when(stackManipulation.isValid()).thenReturn(true);
        when(source.getParameters()).thenReturn(new ParameterList.Explicit.ForTypes(source, firstSourceType, secondSourceType));
        when(source.isStatic()).thenReturn(false);
        when(targetType.isArray()).thenReturn(true);
        when(targetType.getComponentType()).thenReturn(componentType);
        when(componentType.getStackSize()).thenReturn(StackSize.SINGLE);
        when(target.getType()).thenReturn(targetType);
        MethodDelegationBinder.ParameterBinding<?> parameterBinding = AllArguments.Binder.INSTANCE
                .bind(annotationDescription, source, target, implementationTarget, assigner, typing);
        assertThat(parameterBinding.isValid(), is(true));
        verify(source, atLeast(1)).getParameters();
        verify(source, atLeast(1)).isStatic();
        verify(target, atLeast(1)).getType();
        verify(target, never()).getDeclaredAnnotations();
        verify(assigner).assign(firstSourceType, componentType, typing);
        verify(assigner).assign(secondSourceType, componentType, typing);
        verifyNoMoreInteractions(assigner);
    }

    @Test
    public void testIllegalBinding() throws Exception {
        when(target.getIndex()).thenReturn(1);
        when(annotation.value()).thenReturn(AllArguments.Assignment.STRICT);
        when(stackManipulation.isValid()).thenReturn(false);
        when(source.getParameters()).thenReturn(new ParameterList.Explicit.ForTypes(source, firstSourceType, secondSourceType));
        when(source.isStatic()).thenReturn(false);
        when(targetType.isArray()).thenReturn(true);
        when(targetType.getComponentType()).thenReturn(componentType);
        when(componentType.getStackSize()).thenReturn(StackSize.SINGLE);
        when(target.getType()).thenReturn(targetType);
        when(target.getDeclaredAnnotations()).thenReturn(new AnnotationList.Empty());
        MethodDelegationBinder.ParameterBinding<?> parameterBinding = AllArguments.Binder.INSTANCE
                .bind(annotationDescription, source, target, implementationTarget, assigner, Assigner.Typing.STATIC);
        assertThat(parameterBinding.isValid(), is(false));
        verify(source, atLeast(1)).getParameters();
        verify(source, atLeast(1)).isStatic();
        verify(target, atLeast(1)).getType();
        verify(target, never()).getDeclaredAnnotations();
        verify(assigner).assign(firstSourceType, componentType, Assigner.Typing.STATIC);
        verifyNoMoreInteractions(assigner);
    }

    @Test
    public void testLegalSlackBinding() throws Exception {
        when(target.getIndex()).thenReturn(1);
        when(annotation.value()).thenReturn(AllArguments.Assignment.SLACK);
        when(stackManipulation.isValid()).thenReturn(false);
        when(source.getParameters()).thenReturn(new ParameterList.Explicit.ForTypes(source, firstSourceType, secondSourceType));
        when(source.isStatic()).thenReturn(false);
        when(targetType.isArray()).thenReturn(true);
        when(targetType.getComponentType()).thenReturn(componentType);
        when(componentType.getStackSize()).thenReturn(StackSize.SINGLE);
        when(target.getType()).thenReturn(targetType);
        when(target.getDeclaredAnnotations()).thenReturn(new AnnotationList.Empty());
        when(rawComponentType.getInternalName()).thenReturn(FOO);
        MethodDelegationBinder.ParameterBinding<?> parameterBinding = AllArguments.Binder.INSTANCE
                .bind(annotationDescription, source, target, implementationTarget, assigner, Assigner.Typing.STATIC);
        MethodVisitor methodVisitor = mock(MethodVisitor.class);
        Implementation.Context implementationContext = mock(Implementation.Context.class);
        StackManipulation.Size size = parameterBinding.apply(methodVisitor, implementationContext);
        assertThat(size.getSizeImpact(), is(1));
        assertThat(size.getMaximalSize(), is(1));
        verify(methodVisitor).visitInsn(Opcodes.ICONST_0);
        verify(methodVisitor).visitTypeInsn(Opcodes.ANEWARRAY, FOO);
        verifyNoMoreInteractions(methodVisitor);
        verifyNoMoreInteractions(implementationContext);
        assertThat(parameterBinding.isValid(), is(true));
        verify(source, atLeast(1)).getParameters();
        verify(source, atLeast(1)).isStatic();
        verify(target, atLeast(1)).getType();
        verify(target, never()).getDeclaredAnnotations();
        verify(assigner).assign(firstSourceType, componentType, Assigner.Typing.STATIC);
        verify(assigner).assign(secondSourceType, componentType, Assigner.Typing.STATIC);
        verifyNoMoreInteractions(assigner);
    }

    @Test(expected = IllegalStateException.class)
    public void testNonArrayTypeBinding() throws Exception {
        when(target.getIndex()).thenReturn(0);
        TypeDescription.Generic targetType = mock(TypeDescription.Generic.class);
        TypeDescription rawTargetType = mock(TypeDescription.class);
        when(targetType.asErasure()).thenReturn(rawTargetType);
        when(targetType.isArray()).thenReturn(false);
        when(target.getType()).thenReturn(targetType);
        AllArguments.Binder.INSTANCE.bind(annotationDescription, source, target, implementationTarget, assigner, Assigner.Typing.STATIC);
    }
}
