/*
 * 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.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.turbine.testing.TestClassPaths.TURBINE_BOOTCLASSPATH;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;

import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
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.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.IOError;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ClassPathBinderTest {

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

  @Test
  public void classPathLookup() throws IOException {

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

    LookupResult result = javaLang.lookup(new LookupKey(ImmutableList.of(new Ident(-1, "String"))));
    assertThat(result.remaining()).isEmpty();
    assertThat(result.sym()).isEqualTo(new ClassSymbol("java/lang/String"));

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

  @Test
  public void classPathClasses() throws IOException {
    Env<ClassSymbol, BytecodeBoundClass> env = TURBINE_BOOTCLASSPATH.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 = TURBINE_BOOTCLASSPATH.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 = TURBINE_BOOTCLASSPATH.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");
    assertThat(((EnumConstantValue) anno.values().get("value")).sym())
        .isEqualTo(
            new FieldSymbol(new ClassSymbol("java/lang/annotation/RetentionPolicy"), "RUNTIME"));
  }

  @Test
  public void byteCodeBoundClassName() {
    BytecodeBoundClass c =
        new BytecodeBoundClass(
            new ClassSymbol("java/util/List"),
            () -> {
              try {
                return ByteStreams.toByteArray(
                    getClass().getClassLoader().getResourceAsStream("java/util/ArrayList.class"));
              } catch (IOException e) {
                throw new IOError(e);
              }
            },
            null,
            null);
    try {
      c.owner();
      fail();
    } catch (VerifyException e) {
      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");

    try {
      ClassPathBinder.bindClasspath(ImmutableList.of(lib));
      fail();
    } catch (IOException e) {
      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();
  }
}