/*
 * Copyright (C) 2012 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.crsh.lang.impl.java;

import org.crsh.AbstractTestCase;
import org.crsh.util.Utils;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.ByteArrayAsset;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;

import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.Callable;

/** @author Julien Viet */
public class CompilerTestCase extends AbstractTestCase {

  public void testCompile() throws Exception {
    Compiler compiler = new Compiler();
    List<JavaClassFileObject> files = compiler.compile("A",
        "public class A implements java.util.concurrent.Callable<String> {\n" +
        "public String call() {\n" +
        "return \"hello\";\n" +
        "}\n" +
        "}");
    assertEquals(1, files.size());
    LoadingClassLoader loader = new LoadingClassLoader(Thread.currentThread().getContextClassLoader(), files);
    Class<?> A = loader.findClass("A");
    Callable<String> asCallable = (Callable<String>)A.newInstance();
    String ret = asCallable.call();
    assertEquals("hello", ret);
  }

  public void testImportFromFolder() throws Exception {
    doTestImport(new ClassLoaderFactory() {
      @Override
      public ClassLoader getClassLoader(JavaArchive jar) throws Exception {
        File tmp = File.createTempFile("root", "tmp");
        assertTrue(tmp.delete());
        assertTrue(tmp.mkdir());
        tmp.deleteOnExit();
        jar.as(ExplodedExporter.class).exportExploded(tmp, "root");
        URL url = new File(tmp, "root").toURI().toURL();
        return new URLClassLoader(new URL[]{url});
      }
    });
  }

  public void testImportFromJar() throws Exception {
    doTestImport(new ClassLoaderFactory() {
      @Override
      public ClassLoader getClassLoader(JavaArchive jar) throws Exception {
        File tmp = File.createTempFile("crash", ".jar");
        assertTrue(tmp.delete());
        jar.as(ZipExporter.class).exportTo(tmp);
        return new URLClassLoader(new URL[]{tmp.toURI().toURL()});
      }
    });
  }

  public void ignore_testImportFromNestedJar() throws Exception {
    doTestImport(new ClassLoaderFactory() {
      @Override
      public ClassLoader getClassLoader(final JavaArchive jar) throws Exception {
        WebArchive war = ShrinkWrap.create(WebArchive.class);
        war.setManifest(Thread.currentThread().getContextClassLoader().getResource("META-INF/MANIFEST.MF"));
        war.addAsLibrary(jar);
        final File tmp = File.createTempFile("crash", ".war");
        assertTrue(tmp.delete());
        war.as(ZipExporter.class).exportTo(tmp);
        final byte[] bytes = Utils.readAsBytes(jar.get("foo/A.class").getAsset().openStream());
        return new ClassLoader(Thread.currentThread().getContextClassLoader()) {
          Class<?> aClass = null;
          @Override
          protected Class<?> findClass(String name) throws ClassNotFoundException {
            if (name.equals("foo.A")) {
              if (aClass == null) {
                // Normally we should use the bytes from the nested crash.jar but it's too difficult
                // so we just use what we compiled before as it is the same bytecode and already
                // available here

                aClass = defineClass(name, bytes, 0, bytes.length);
              }
              return aClass;
            } else {
              return super.loadClass(name);
            }
          }
          @Override
          protected Enumeration<URL> findResources(String name) throws IOException {
            if ("META-INF/MANIFEST.MF".equals(name)) {
              URL u1 = new URL("jar:" + tmp.toURI().toURL() + "!/META-INF/MANIFEST.MF");
              URL u2 = new URL("jar:" + ("jar:" + tmp.toURI().toURL() + "!/WEB-INF/lib/crash.jar") + "!/META-INF/MANIFEST.MF");
              return Collections.enumeration(Arrays.asList(u1, u2));
            } else if ("foo".equals(name)) {
              String u = "jar:" + ("jar:" + tmp.toURI().toURL() + "!/WEB-INF/lib/crash.jar") + "!/foo/";
              return Collections.enumeration(Collections.singleton(new URL(u)));
            } else {
              return super.findResources(name);
            }
          }
        };
      }
    });
  }

  private interface ClassLoaderFactory {

    ClassLoader getClassLoader(JavaArchive jar) throws Exception;

  }

  private void doTestImport(ClassLoaderFactory factory) throws Exception {

    Compiler compiler = new Compiler();
    List<JavaClassFileObject> files = compiler.compile("foo.A", "package foo;\n public class A {}");
    assertEquals(1, files.size());
    JavaClassFileObject aFile = files.get(0);
    JavaArchive jar = ShrinkWrap.create(JavaArchive.class, "crash.jar");
    jar.add(new ByteArrayAsset(aFile.getBytes()), "foo/A.class");
    jar.setManifest(Thread.currentThread().getContextClassLoader().getResource("META-INF/MANIFEST.MF"));
    ClassLoader cl = factory.getClassLoader(jar);

    //
    compiler = new Compiler(cl);
    files = compiler.compile("B",
        "import foo.A;\n" +
            "public class B implements java.util.concurrent.Callable<A> {\n" +
            "public A call() {\n" +
            "return new A();\n" +
            "}\n" +
            "}");
    assertEquals(1, files.size());
    LoadingClassLoader loader = new LoadingClassLoader(cl, files);
    Class<?> B = loader.findClass("B");
    Callable<?> asCallable = (Callable<?>)B.newInstance();
    Object ret = asCallable.call();
    assertNotNull(ret);
    Class<?> A = ret.getClass();
    assertEquals("foo.A", A.getName());
    assertEquals(cl, A.getClassLoader());
  }

  public void testCompilationFailure() throws Exception {
    Compiler compiler = new Compiler();
    try {
      compiler.compile("foo.A", "package foo;\n public class A {");
    }
    catch (CompilationFailureException e) {
      List<Diagnostic<? extends JavaFileObject>> errors = e.getErrors();
      assertEquals(1, errors.size());
      Diagnostic<? extends JavaFileObject> error = errors.get(0);
      assertEndsWith("/foo/A.java", error.getSource().getName());
    }
  }
}
