package net.bytebuddy.build.maven;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.test.utility.MockitoRule;
import org.apache.maven.plugin.Mojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.testing.MojoRule;
import org.apache.maven.plugin.testing.SilentLog;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.CollectResult;
import org.eclipse.aether.graph.DependencyNode;
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.Mockito;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import static junit.framework.TestCase.fail;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

public class ByteBuddyMojoTest {

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

    @Rule
    public MojoRule mojoRule = new MojoRule();

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

    @Mock
    private RepositorySystem repositorySystem;

    @Mock
    private DependencyNode root;

    private File project;

    @Before
    public void setUp() throws Exception {
        when(repositorySystem.collectDependencies(Mockito.<RepositorySystemSession>any(), Mockito.<CollectRequest>any()))
                .thenReturn(new CollectResult(new CollectRequest()).setRoot(root));
        project = File.createTempFile(FOO, TEMP);
        assertThat(project.delete(), is(true));
        assertThat(project.mkdir(), is(true));
    }

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

    @Test
    public void testEmptyTransformation() throws Exception {
        execute("transform", "empty");
    }

    @Test
    public void testSimpleTransformation() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            execute("transform", "simple");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.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(project, FOO).delete(), is(true));
        }
    }

    @Test
    public void testSimpleTransformationWithSuffix() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            execute("transform", "suffix");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.toURI().toURL()});
            assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
            assertMethod(classLoader.loadClass("foo.Bar"), BAR, BAR);
            assertThat(classLoader.loadClass("foo.Bar").getDeclaredMethod(FOO + "$" + QUX), notNullValue(Method.class));
            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(project, FOO).delete(), is(true));
        }
    }

    @Test(expected = MojoExecutionException.class)
    public void testLiveInitializer() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            execute("transform", "live");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.toURI().toURL()});
            assertMethod(classLoader.loadClass("foo.Bar"), FOO, QUX);
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(project, FOO).delete(), is(true));
        }
    }

    @Test
    public void testLiveInitializerAllowed() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            execute("transform", "live.allowed");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.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(project, FOO).delete(), is(true));
        }
    }

    @Test(expected = MojoExecutionException.class)
    public void testIllegalTransformer() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            execute("transform", "illegal");
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(project, FOO).delete(), is(true));
        }
    }

    @Test(expected = MojoExecutionException.class)
    public void testIllegalTransformation() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        try {
            execute("transform", "illegal.apply");
        } finally {
            for (File file : files) {
                assertThat(file.delete(), is(true));
            }
            assertThat(new File(project, FOO).delete(), is(true));
        }
    }

    @Test
    public void testTestTransformation() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            execute("transform-test", "simple");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.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(project, FOO).delete(), is(true));
        }
    }

    @Test
    public void testSimpleEntry() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            execute("transform", "entry");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.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(project, FOO).delete(), is(true));
        }
    }

    @Test(expected = MojoExecutionException.class)
    public void testIllegalByteBuddy() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            execute("transform", "entry.illegal");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.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(project, FOO).delete(), is(true));
        }
    }

    @Test(expected = MojoExecutionException.class)
    public void testIllegalTransform() throws Exception {
        Set<File> files = new HashSet<File>();
        files.addAll(addClass("foo.Bar"));
        files.addAll(addClass("foo.Qux"));
        try {
            execute("transform", "entry.illegal.transform");
            ClassLoader classLoader = new URLClassLoader(new URL[]{project.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(project, FOO).delete(), is(true));
        }
    }

    private void execute(String goal, String target) throws Exception {
        Mojo mojo = mojoRule.lookupMojo(goal, new File("src/test/resources/net/bytebuddy/test/" + target + ".pom.xml"));
        if (goal.equals("transform")) {
            mojoRule.setVariableValueToObject(mojo, "outputDirectory", project.getAbsolutePath());
            mojoRule.setVariableValueToObject(mojo, "compileClasspathElements", Collections.emptyList());
        } else if (goal.equals("transform-test")) {
            mojoRule.setVariableValueToObject(mojo, "testOutputDirectory", project.getAbsolutePath());
            mojoRule.setVariableValueToObject(mojo, "testClasspathElements", Collections.emptyList());
        } else {
            throw new AssertionError("Unknown goal: " + goal);
        }
        mojoRule.setVariableValueToObject(mojo, "repositorySystem", repositorySystem);
        mojoRule.setVariableValueToObject(mojo, "groupId", FOO);
        mojoRule.setVariableValueToObject(mojo, "artifactId", BAR);
        mojoRule.setVariableValueToObject(mojo, "version", QUX);
        mojo.setLog(new SilentLog());
        mojo.execute();
    }

    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(project)
                .values();
    }

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