/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.turbine.binder;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.turbine.testing.TestClassPaths.TURBINE_BOOTCLASSPATH;
import static com.google.turbine.testing.TestResources.getResourceBytes;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Locale.ENGLISH;
import static java.util.Objects.requireNonNull;
import static org.junit.Assert.assertThrows;

import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.io.MoreFiles;
import com.google.turbine.binder.bound.EnumConstantValue;
import com.google.turbine.binder.bound.TypeBoundClass;
import com.google.turbine.binder.bytecode.BytecodeBoundClass;
import com.google.turbine.binder.env.Env;
import com.google.turbine.binder.lookup.LookupKey;
import com.google.turbine.binder.lookup.LookupResult;
import com.google.turbine.binder.lookup.PackageScope;
import com.google.turbine.binder.lookup.Scope;
import com.google.turbine.binder.sym.ClassSymbol;
import com.google.turbine.binder.sym.FieldSymbol;
import com.google.turbine.model.TurbineFlag;
import com.google.turbine.model.TurbineTyKind;
import com.google.turbine.tree.Tree.Ident;
import com.google.turbine.type.AnnoInfo;
import com.google.turbine.type.Type.ClassTy;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class ClassPathBinderTest {

  @Parameterized.Parameters
  public static ImmutableCollection<Object[]> parameters() {
    Object[] testCases = {
      TURBINE_BOOTCLASSPATH,
      FileManagerClassBinder.adapt(
          ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, ENGLISH, UTF_8),
          StandardLocation.PLATFORM_CLASS_PATH),
    };
    return Arrays.stream(testCases).map(x -> new Object[] {x}).collect(toImmutableList());
  }

  private final ClassPath classPath;

  public ClassPathBinderTest(ClassPath classPath) {
    this.classPath = classPath;
  }

  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();

  private static Ident ident(String string) {
    return new Ident(/* position= */ -1, string);
  }

  @Test
  public void classPathLookup() {

    Scope javaLang = classPath.index().lookupPackage(ImmutableList.of("java", "lang"));

    final String string = "String";
    LookupResult result = javaLang.lookup(new LookupKey(ImmutableList.of(ident(string))));
    assertThat(result.remaining()).isEmpty();
    assertThat(result.sym()).isEqualTo(new ClassSymbol("java/lang/String"));

    result = javaLang.lookup(new LookupKey(ImmutableList.of(ident("Object"))));
    assertThat(result.remaining()).isEmpty();
    assertThat(result.sym()).isEqualTo(new ClassSymbol("java/lang/Object"));
  }

  @Test
  public void packageScope() {

    PackageScope result = classPath.index().lookupPackage(ImmutableList.of("java", "nosuch"));
    assertThat(result).isNull();

    result = classPath.index().lookupPackage(ImmutableList.of("java", "lang"));
    assertThat(result.classes()).contains(new ClassSymbol("java/lang/String"));

    assertThat(result.lookup(new LookupKey(ImmutableList.of(ident("NoSuch"))))).isNull();
  }

  @Test
  public void scope() {
    Scope scope = classPath.index().scope();
    LookupResult result;

    result =
        scope.lookup(
            new LookupKey(
                ImmutableList.of(ident("java"), ident("util"), ident("Map"), ident("Entry"))));
    assertThat(result.sym()).isEqualTo(new ClassSymbol("java/util/Map"));
    assertThat(result.remaining().stream().map(Ident::value)).containsExactly("Entry");

    result =
        scope.lookup(new LookupKey(ImmutableList.of(ident("java"), ident("util"), ident("Map"))));
    assertThat(result.sym()).isEqualTo(new ClassSymbol("java/util/Map"));
    assertThat(result.remaining()).isEmpty();

    result =
        scope.lookup(
            new LookupKey(ImmutableList.of(ident("java"), ident("util"), ident("NoSuch"))));
    assertThat(result).isNull();
  }

  @Test
  public void classPathClasses() {
    Env<ClassSymbol, BytecodeBoundClass> env = classPath.env();

    TypeBoundClass c = env.get(new ClassSymbol("java/util/Map$Entry"));
    assertThat(c.owner()).isEqualTo(new ClassSymbol("java/util/Map"));
    assertThat(c.kind()).isEqualTo(TurbineTyKind.INTERFACE);

    assertThat(env.get(new ClassSymbol("javax/lang/model/SourceVersion")).kind())
        .isEqualTo(TurbineTyKind.ENUM);
    assertThat(env.get(new ClassSymbol("java/lang/String")).kind()).isEqualTo(TurbineTyKind.CLASS);
    assertThat(env.get(new ClassSymbol("java/lang/Override")).kind())
        .isEqualTo(TurbineTyKind.ANNOTATION);

    c = env.get(new ClassSymbol("java/util/ArrayList"));
    assertThat((c.access() & TurbineFlag.ACC_PUBLIC)).isEqualTo(TurbineFlag.ACC_PUBLIC);
    assertThat(c.superclass()).isEqualTo(new ClassSymbol("java/util/AbstractList"));
    assertThat(c.interfaces()).contains(new ClassSymbol("java/util/List"));
    assertThat(c.owner()).isNull();
  }

  @Test
  public void interfaces() {
    Env<ClassSymbol, BytecodeBoundClass> env = classPath.env();

    TypeBoundClass c = env.get(new ClassSymbol("java/lang/annotation/Retention"));
    assertThat(c.interfaceTypes()).hasSize(1);
    assertThat(((ClassTy) getOnlyElement(c.interfaceTypes())).sym())
        .isEqualTo(new ClassSymbol("java/lang/annotation/Annotation"));

    c = env.get(new ClassSymbol("java/util/ArrayList"));
    ClassTy listInterface =
        (ClassTy)
            c.interfaceTypes().stream()
                .filter(i -> ((ClassTy) i).sym().equals(new ClassSymbol("java/util/List")))
                .collect(onlyElement());
    assertThat(getLast(listInterface.classes()).targs()).hasSize(1);
  }

  @Test
  public void annotations() {
    Env<ClassSymbol, BytecodeBoundClass> env = classPath.env();
    TypeBoundClass c = env.get(new ClassSymbol("java/lang/annotation/Retention"));

    AnnoInfo anno =
        c.annotations().stream()
            .filter(a -> a.sym().equals(new ClassSymbol("java/lang/annotation/Retention")))
            .collect(onlyElement());
    assertThat(anno.values().keySet()).containsExactly("value");
    // requireNonNull is safe because we checked that the keySet contains `"value"`.
    assertThat(((EnumConstantValue) requireNonNull(anno.values().get("value"))).sym())
        .isEqualTo(
            new FieldSymbol(new ClassSymbol("java/lang/annotation/RetentionPolicy"), "RUNTIME"));
  }

  @Test
  public void byteCodeBoundClassName() {
    Env<ClassSymbol, BytecodeBoundClass> env = classPath.env();
    BytecodeBoundClass c =
        new BytecodeBoundClass(
            new ClassSymbol("java/util/List"),
            () -> getResourceBytes(getClass(), "/java/util/ArrayList.class"),
            env,
            null);
    VerifyException e = assertThrows(VerifyException.class, () -> c.owner());
    assertThat(e)
        .hasMessageThat()
        .contains("expected class data for java/util/List, saw java/util/ArrayList instead");
  }

  @Test
  public void nonJarFile() throws Exception {
    Path lib = temporaryFolder.newFile("NOT_A_JAR").toPath();
    MoreFiles.asCharSink(lib, UTF_8).write("hello");

    IOException e =
        assertThrows(IOException.class, () -> ClassPathBinder.bindClasspath(ImmutableList.of(lib)));
    assertThat(e).hasMessageThat().contains("NOT_A_JAR");
  }

  @Test
  public void resources() throws Exception {
    Path path = temporaryFolder.newFile("tmp.jar").toPath();
    try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(path))) {
      jos.putNextEntry(new JarEntry("foo/bar/hello.txt"));
      jos.write("hello".getBytes(UTF_8));
      jos.putNextEntry(new JarEntry("foo/bar/Baz.class"));
      jos.write("goodbye".getBytes(UTF_8));
    }
    ClassPath classPath = ClassPathBinder.bindClasspath(ImmutableList.of(path));
    assertThat(new String(classPath.resource("foo/bar/hello.txt").get(), UTF_8)).isEqualTo("hello");
    assertThat(classPath.resource("foo/bar/Baz.class")).isNull();
  }

  @Test
  public void resourcesFileManager() throws Exception {
    Path path = temporaryFolder.newFile("tmp.jar").toPath();
    try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(path))) {
      jos.putNextEntry(new JarEntry("foo/bar/hello.txt"));
      jos.write("hello".getBytes(UTF_8));
      jos.putNextEntry(new JarEntry("foo/bar/Baz.class"));
      jos.write("goodbye".getBytes(UTF_8));
    }
    StandardJavaFileManager fileManager =
        ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, ENGLISH, UTF_8);
    fileManager.setLocation(StandardLocation.CLASS_PATH, ImmutableList.of(path.toFile()));
    ClassPath classPath = FileManagerClassBinder.adapt(fileManager, StandardLocation.CLASS_PATH);
    assertThat(new String(classPath.resource("foo/bar/hello.txt").get(), UTF_8)).isEqualTo("hello");
    assertThat(classPath.resource("foo/bar/NoSuch.class")).isNull();
  }
}
