package net.bytebuddy.dynamic.scaffold;

import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.attribute.AnnotationValueFilter;
import net.bytebuddy.implementation.attribute.FieldAttributeAppender;
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 org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;

import java.lang.annotation.RetentionPolicy;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class TypeWriterFieldPoolRecordTest {

    private static final int MODIFIER = 42;

    private static final String FOO = "foo", BAR = "bar", QUX = "qux", BAZ = "baz";

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

    @Mock
    private FieldAttributeAppender fieldAttributeAppender;

    @Mock
    private AnnotationValueFilter valueFilter;

    @Mock
    private AnnotationValueFilter.Factory annotationValueFilterFactory;

    @Mock
    private ClassVisitor classVisitor;

    @Mock
    private FieldVisitor fieldVisitor;

    @Mock
    private AnnotationVisitor annotationVisitor;

    @Mock
    private FieldDescription fieldDescription;

    @Mock
    private AnnotationDescription annotationDescription;

    @Mock
    private TypeDescription annotationType;

    @Mock
    private Object defaultValue;

    @Before
    @SuppressWarnings("unchecked")
    public void setUp() throws Exception {
        when(fieldDescription.getActualModifiers()).thenReturn(MODIFIER);
        when(fieldDescription.getInternalName()).thenReturn(FOO);
        when(fieldDescription.getDescriptor()).thenReturn(BAR);
        when(fieldDescription.getGenericSignature()).thenReturn(QUX);
        when(fieldDescription.getDeclaredAnnotations()).thenReturn(new AnnotationList.Explicit(annotationDescription));
        when(fieldDescription.getType()).thenReturn(TypeDescription.Generic.OfNonGenericType.ForLoadedType.of(Object.class));
        when(classVisitor.visitField(MODIFIER, FOO, BAR, QUX, defaultValue)).thenReturn(fieldVisitor);
        when(classVisitor.visitField(MODIFIER, FOO, BAR, QUX, FieldDescription.NO_DEFAULT_VALUE)).thenReturn(fieldVisitor);
        when(annotationValueFilterFactory.on(fieldDescription)).thenReturn(valueFilter);
        when(fieldVisitor.visitAnnotation(any(String.class), anyBoolean())).thenReturn(annotationVisitor);
        when(annotationDescription.getAnnotationType()).thenReturn(annotationType);
        when(annotationType.getDescriptor()).thenReturn(BAZ);
        when(annotationType.getDeclaredMethods()).thenReturn(new MethodList.Empty<MethodDescription.InDefinedShape>());
        when(annotationDescription.getRetention()).thenReturn(RetentionPolicy.RUNTIME);
    }

    @Test
    public void testExplicitFieldEntryProperties() throws Exception {
        TypeWriter.FieldPool.Record record = new TypeWriter.FieldPool.Record.ForExplicitField(fieldAttributeAppender, defaultValue, fieldDescription);
        assertThat(record.getFieldAppender(), is(fieldAttributeAppender));
        assertThat(record.resolveDefault(FieldDescription.NO_DEFAULT_VALUE), is(defaultValue));
        assertThat(record.isImplicit(), is(false));
    }

    @Test
    public void testExplicitFieldEntryWritesField() throws Exception {
        TypeWriter.FieldPool.Record record = new TypeWriter.FieldPool.Record.ForExplicitField(fieldAttributeAppender, defaultValue, fieldDescription);
        record.apply(classVisitor, annotationValueFilterFactory);
        verify(classVisitor).visitField(MODIFIER, FOO, BAR, QUX, defaultValue);
        verifyNoMoreInteractions(classVisitor);
        verify(fieldAttributeAppender).apply(fieldVisitor, fieldDescription, valueFilter);
        verifyNoMoreInteractions(fieldAttributeAppender);
        verify(fieldVisitor).visitEnd();
        verifyNoMoreInteractions(fieldVisitor);
    }

    @Test
    public void testExplicitFieldEntryWritesFieldPartialApplication() throws Exception {
        TypeWriter.FieldPool.Record record = new TypeWriter.FieldPool.Record.ForExplicitField(fieldAttributeAppender, defaultValue, fieldDescription);
        record.apply(fieldVisitor, annotationValueFilterFactory);
        verify(fieldAttributeAppender).apply(fieldVisitor, fieldDescription, valueFilter);
        verifyNoMoreInteractions(fieldAttributeAppender);
        verifyNoMoreInteractions(fieldVisitor);
    }

    @Test
    public void testImplicitFieldEntryProperties() throws Exception {
        TypeWriter.FieldPool.Record record = new TypeWriter.FieldPool.Record.ForImplicitField(fieldDescription);
        assertThat(record.isImplicit(), is(true));
    }

    @Test
    public void testImplicitFieldEntryWritesField() throws Exception {
        TypeWriter.FieldPool.Record record = new TypeWriter.FieldPool.Record.ForImplicitField(fieldDescription);
        record.apply(classVisitor, annotationValueFilterFactory);
        verify(classVisitor).visitField(MODIFIER, FOO, BAR, QUX, FieldDescription.NO_DEFAULT_VALUE);
        verifyNoMoreInteractions(classVisitor);
        verify(fieldVisitor).visitAnnotation(BAZ, true);
        verify(fieldVisitor).visitEnd();
        verifyNoMoreInteractions(fieldVisitor);
        verify(annotationVisitor).visitEnd();
        verifyNoMoreInteractions(annotationVisitor);
    }

    @Test(expected = IllegalStateException.class)
    public void testImplicitFieldWritesFieldPartialApplication() throws Exception {
        new TypeWriter.FieldPool.Record.ForImplicitField(fieldDescription).apply(fieldVisitor, annotationValueFilterFactory);
    }

    @Test(expected = IllegalStateException.class)
    public void testImplicitFieldEntryAppliedToField() throws Exception {
        new TypeWriter.FieldPool.Record.ForImplicitField(fieldDescription).apply(fieldVisitor, annotationValueFilterFactory);
    }
}
