package net.bytebuddy.dynamic;

import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.modifier.ModifierContributor;
import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;

import java.util.Collections;

import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.none;
import static net.bytebuddy.test.utility.FieldByFieldComparison.matchesPrototype;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.when;

public class TransformerForFieldTest {

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

    private static final int MODIFIERS = 42, RANGE = 3, MASK = 1;

    @Rule
    public MethodRule mockitoRule = MockitoJUnit.rule().silent();

    @Mock
    private TypeDescription instrumentedType;

    @Mock
    private Transformer<FieldDescription.Token> tokenTransformer;

    @Mock
    private FieldDescription fieldDescription;

    @Mock
    private FieldDescription.InDefinedShape definedField;

    @Mock
    private FieldDescription.Token fieldToken;

    @Mock
    private TypeDescription.Generic fieldType, declaringType;

    @Mock
    private AnnotationDescription fieldAnnotation;

    @Mock
    private ModifierContributor.ForField modifierContributor;

    @Before
    @SuppressWarnings("unchecked")
    public void setUp() throws Exception {
        when(fieldType.accept(any(TypeDescription.Generic.Visitor.class))).thenReturn(fieldType);
        when(fieldDescription.asToken(matchesPrototype(none()))).thenReturn(fieldToken);
        when(fieldDescription.getDeclaringType()).thenReturn(declaringType);
        when(fieldDescription.asDefined()).thenReturn(definedField);
        when(fieldToken.getName()).thenReturn(FOO);
        when(fieldToken.getModifiers()).thenReturn(MODIFIERS);
        when(fieldToken.getType()).thenReturn(fieldType);
        when(fieldToken.getAnnotations()).thenReturn(new AnnotationList.Explicit(fieldAnnotation));
        when(modifierContributor.getMask()).thenReturn(MASK);
        when(modifierContributor.getRange()).thenReturn(RANGE);
    }

    @Test
    public void testSimpleTransformation() throws Exception {
        when(tokenTransformer.transform(instrumentedType, fieldToken)).thenReturn(fieldToken);
        FieldDescription transformed = new Transformer.ForField(tokenTransformer).transform(instrumentedType, fieldDescription);
        assertThat(transformed.getDeclaringType(), is((TypeDefinition) declaringType));
        assertThat(transformed.getInternalName(), is(FOO));
        assertThat(transformed.getModifiers(), is(MODIFIERS));
        assertThat(transformed.getDeclaredAnnotations(), is(Collections.singletonList(fieldAnnotation)));
        assertThat(transformed.getType(), is(fieldType));
        assertThat(transformed.asDefined(), is(definedField));
    }

    @Test
    public void testModifierTransformation() throws Exception {
        FieldDescription.Token transformed = new Transformer.ForField.FieldModifierTransformer(ModifierContributor.Resolver.of(modifierContributor))
                .transform(instrumentedType, fieldToken);
        assertThat(transformed.getName(), is(FOO));
        assertThat(transformed.getModifiers(), is((MODIFIERS & ~RANGE) | MASK));
        assertThat(transformed.getType(), is(fieldType));
    }

    @Test
    public void testNoChangesUnlessSpecified() throws Exception {
        TypeDescription typeDescription = TypeDescription.ForLoadedType.of(Bar.class);
        FieldDescription fieldDescription = typeDescription.getSuperClass().getDeclaredFields().filter(named(FOO)).getOnly();
        FieldDescription transformed = Transformer.ForField.withModifiers().transform(typeDescription, fieldDescription);
        assertThat(transformed, is(fieldDescription));
        assertThat(transformed.getModifiers(), is(fieldDescription.getModifiers()));
    }

    @Test
    public void testRetainsInstrumentedType() throws Exception {
        TypeDescription typeDescription = TypeDescription.ForLoadedType.of(Bar.class);
        FieldDescription fieldDescription = typeDescription.getSuperClass().getDeclaredFields().filter(named(BAR)).getOnly();
        FieldDescription transformed = Transformer.ForField.withModifiers().transform(typeDescription, fieldDescription);
        assertThat(transformed, is(fieldDescription));
        assertThat(transformed.getModifiers(), is(fieldDescription.getModifiers()));
        assertThat(transformed.getType().asErasure(), is(typeDescription));
        assertThat(transformed.getType().getSort(), is(TypeDefinition.Sort.PARAMETERIZED));
        assertThat(transformed.getType().getTypeArguments().size(), is(1));
        assertThat(transformed.getType().getTypeArguments().getOnly(), is(typeDescription.getSuperClass().getDeclaredFields().filter(named(FOO)).getOnly().getType()));
    }

    private static class Foo<T> {

        T foo;

        Bar<T> bar;
    }

    private static class Bar<S> extends Foo<S> {
        /* empty */
    }
}
