package net.bytebuddy.build.gradle;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.build.EntryPoint;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.scaffold.inline.MethodNameTransformer;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.test.*;
import net.bytebuddy.test.utility.MockitoRule;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.tasks.compile.AbstractCompile;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;

import static junit.framework.TestCase.fail;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TransformationActionTest {

    private static final String FOO = "foo", BAR = "bar", QUX = "qux", TEMP = ".tmp";

    @Rule
    public TestRule mockitoRule = new MockitoRule(this);

    @Mock
    private Project project;

    @Mock
    private Logger logger;

    @Mock
    private ByteBuddyExtension byteBuddyExtension;

    @Mock
    private AbstractCompile parent;

    @Mock
    private Task task;

    @Mock
    private Transformation transformation;

    @Mock
    private Initialization initialization;

    @Mock
    private FileCollection fileCollection;

    private File target;

    private TransformationAction transformationAction;

    @Before
    @SuppressWarnings("unchecked")
    public void setUp() throws Exception {
        target = File.createTempFile(FOO, TEMP);
        assertThat(target.delete(), is(true));
        assertThat(target.mkdir(), is(true));
        when(project.getLogger()).thenReturn(logger);
        when(byteBuddyExtension.getTransformations()).thenReturn(Collections.singletonList(transformation));
        when(byteBuddyExtension.getInitialization()).thenReturn(initialization);
        when(parent.getDestinationDir()).thenReturn(target);
        when(transformation.getClassPath(any(File.class), any(Iterable.class))).thenReturn((Iterable) Collections.emptySet());
        when(parent.getClasspath()).thenReturn(fileCollection);
        when(fileCollection.iterator()).then(new Answer<Iterator<File>>() {
            @Override
            public Iterator<File> answer(InvocationOnMock invocationOnMock) throws Throwable {
                return Collections.singleton(target).iterator();
            }
        });
        when(byteBuddyExtension.getMethodNameTransformer()).thenReturn(MethodNameTransformer.Suffixing.withRandomSuffix());
        transformationAction = new TransformationAction(project, byteBuddyExtension, parent);
    }

    @After
    public void tearDown() throws Exception {
        assertThat(target.delete(), is(true));
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testSimpleTransformation() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            when(transformation.getPlugin()).thenReturn(SimplePlugin.class.getName());
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(EntryPoint.Default.REBASE);
            transformationAction.execute(task);
            ClassLoader classLoader = new URLClassLoader(new URL[]{target.toURI().toURL()});
            assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
            assertMethod(classLoader.loadClass("foo.Bar"), BAR, BAR);
            assertMethod(classLoader.loadClass("foo.Qux"), FOO, FOO);
            assertMethod(classLoader.loadClass("foo.Qux"), BAR, BAR);
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testLiveInitializer() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            when(transformation.getPlugin()).thenReturn(LiveInitializerPlugin.class.getName());
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(EntryPoint.Default.REBASE);
            transformationAction.execute(task);
            ClassLoader classLoader = new URLClassLoader(new URL[]{target.toURI().toURL()});
            try {
                assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
                fail();
            } catch (InvocationTargetException exception) {
                assertThat(exception.getCause(), instanceOf(NullPointerException.class));
            }
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testLiveInitializerAllowed() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            when(transformation.getPlugin()).thenReturn(LiveInitializerPlugin.class.getName());
            when(byteBuddyExtension.isFailOnLiveInitializer()).thenReturn(false);
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(EntryPoint.Default.REBASE);
            transformationAction.execute(task);
            ClassLoader classLoader = new URLClassLoader(new URL[]{target.toURI().toURL()});
            try {
                assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
                fail();
            } catch (InvocationTargetException exception) {
                assertThat(exception.getCause(), instanceOf(NullPointerException.class));
            }
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test(expected = GradleException.class)
    @SuppressWarnings("unchecked")
    public void testIllegalTransformer() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            when(transformation.getPlugin()).thenReturn(IllegalTransformPlugin.class.getName());
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(EntryPoint.Default.REBASE);
            transformationAction.execute(task);
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test(expected = GradleException.class)
    @SuppressWarnings("unchecked")
    public void testIllegalTransformation() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            when(transformation.getPlugin()).thenReturn(IllegalPlugin.class.getName());
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(EntryPoint.Default.REBASE);
            transformationAction.execute(task);
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testSimpleEntry() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            when(transformation.getPlugin()).thenReturn(SimplePlugin.class.getName());
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(new SimpleEntryPoint());
            transformationAction.execute(task);
            ClassLoader classLoader = new URLClassLoader(new URL[]{target.toURI().toURL()});
            assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
            assertMethod(classLoader.loadClass("foo.Bar"), BAR, BAR);
            assertMethod(classLoader.loadClass("foo.Qux"), FOO, FOO);
            assertMethod(classLoader.loadClass("foo.Qux"), BAR, BAR);
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test(expected = GradleException.class)
    @SuppressWarnings("unchecked")
    public void testIllegalByteBuddy() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            when(transformation.getPlugin()).thenReturn(SimplePlugin.class.getName());
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(new IllegalEntryPoint());
            transformationAction.execute(task);
            ClassLoader classLoader = new URLClassLoader(new URL[]{target.toURI().toURL()});
            assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
            assertMethod(classLoader.loadClass("foo.Bar"), BAR, BAR);
            assertMethod(classLoader.loadClass("foo.Qux"), FOO, FOO);
            assertMethod(classLoader.loadClass("foo.Qux"), BAR, BAR);
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test(expected = GradleException.class)
    @SuppressWarnings("unchecked")
    public void testIllegalTransform() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            when(transformation.getPlugin()).thenReturn(SimplePlugin.class.getName());
            when(initialization.getEntryPoint(any(ClassLoaderResolver.class), any(File.class), any(Iterable.class))).thenReturn(new IllegalTransformEntryPoint());
            transformationAction.execute(task);
            ClassLoader classLoader = new URLClassLoader(new URL[]{target.toURI().toURL()});
            assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
            assertMethod(classLoader.loadClass("foo.Bar"), BAR, BAR);
            assertMethod(classLoader.loadClass("foo.Qux"), FOO, FOO);
            assertMethod(classLoader.loadClass("foo.Qux"), BAR, BAR);
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(target, FOO).delete(), is(true));
        }
    }

    @Test(expected = GradleException.class)
    public void testNoDirectory() throws Exception {
        when(parent.getDestinationDir()).thenReturn(mock(File.class));
        transformationAction.execute(task);
    }

    private void assertMethod(Class<?> type, String name, Object expected) throws Exception {
        assertThat(type.getDeclaredMethod(name).invoke(type.getDeclaredConstructor().newInstance()), is(expected));
    }

    private Collection<File> addClass(String name) throws IOException {
        return new ByteBuddy()
                .subclass(Object.class)
                .name(name)
                .defineMethod(FOO, String.class, Visibility.PUBLIC).intercept(FixedValue.value(FOO))
                .defineMethod(BAR, String.class, Visibility.PUBLIC).intercept(FixedValue.value(BAR))
                .make()
                .saveIn(target)
                .values();
    }
}
