/*
 * Copyright 2019 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.processing;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.turbine.testing.TestResources.getResourceBytes;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import com.google.common.testing.EqualsTester;
import com.google.turbine.binder.Binder;
import com.google.turbine.binder.ClassPathBinder;
import com.google.turbine.binder.bound.TypeBoundClass;
import com.google.turbine.binder.env.CompoundEnv;
import com.google.turbine.binder.env.Env;
import com.google.turbine.binder.env.SimpleEnv;
import com.google.turbine.binder.sym.ClassSymbol;
import com.google.turbine.diag.SourceFile;
import com.google.turbine.lower.IntegrationTestSupport.TestInput;
import com.google.turbine.parse.Parser;
import com.google.turbine.processing.TurbineElement.TurbineTypeElement;
import com.google.turbine.testing.TestClassPaths;
import com.google.turbine.tree.Tree.CompUnit;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
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 TurbineAnnotationProxyTest {

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

  @Retention(RetentionPolicy.RUNTIME)
  public @interface A {
    B b() default @B(-1);

    ElementType e() default ElementType.PACKAGE;

    int[] xs() default {};

    Class<?> c() default String.class;

    Class<?>[] cx() default {};
  }

  @Retention(RetentionPolicy.RUNTIME)
  @Inherited
  public @interface B {
    int value();
  }

  @Retention(RetentionPolicy.RUNTIME)
  public @interface C {}

  @Retention(RetentionPolicy.RUNTIME)
  public @interface RS {
    R[] value() default {};
  }

  @Repeatable(RS.class)
  @Retention(RetentionPolicy.RUNTIME)
  public @interface R {
    int value() default 1;
  }

  @A
  static class I {}

  @Test
  public void test() throws IOException {

    Path lib = temporaryFolder.newFile("lib.jar").toPath();
    try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(lib))) {
      addClass(jos, TurbineAnnotationProxyTest.class);
      addClass(jos, A.class);
      addClass(jos, B.class);
      addClass(jos, C.class);
      addClass(jos, R.class);
    }

    TestInput input =
        TestInput.parse(
            Joiner.on('\n')
                .join(
                    "=== Super.java ===",
                    "import " + B.class.getCanonicalName() + ";",
                    "import " + C.class.getCanonicalName() + ";",
                    "@B(42)",
                    "@C",
                    "class Super {}",
                    "=== Test.java ===",
                    "import " + A.class.getCanonicalName() + ";",
                    "import " + R.class.getCanonicalName() + ";",
                    "@A(xs = {1,2,3}, cx = {Integer.class, Long.class})",
                    "@R(1)",
                    "@R(2)",
                    "@R(3)",
                    "class Test extends Super {}",
                    ""));

    ImmutableList<CompUnit> units =
        input.sources.entrySet().stream()
            .map(e -> new SourceFile(e.getKey(), e.getValue()))
            .map(Parser::parse)
            .collect(toImmutableList());

    Binder.BindingResult bound =
        Binder.bind(
            units,
            ClassPathBinder.bindClasspath(ImmutableList.of(lib)),
            TestClassPaths.TURBINE_BOOTCLASSPATH,
            Optional.empty());

    Env<ClassSymbol, TypeBoundClass> env =
        CompoundEnv.<ClassSymbol, TypeBoundClass>of(bound.classPathEnv())
            .append(new SimpleEnv<>(bound.units()));
    ModelFactory factory = new ModelFactory(env, ClassLoader.getSystemClassLoader(), bound.tli());
    TurbineTypeElement te = factory.typeElement(new ClassSymbol("Test"));

    A a = te.getAnnotation(A.class);
    B b = te.getAnnotation(B.class);
    assertThat(te.getAnnotation(C.class)).isNull();

    assertThat(a.b().value()).isEqualTo(-1);
    assertThat(a.e()).isEqualTo(ElementType.PACKAGE);
    {
      MirroredTypeException e = assertThrows(MirroredTypeException.class, () -> a.c());
      assertThat(e.getTypeMirror().getKind()).isEqualTo(TypeKind.DECLARED);
      assertThat(getQualifiedName(e.getTypeMirror())).contains("java.lang.String");
    }
    {
      MirroredTypesException e = assertThrows(MirroredTypesException.class, () -> a.cx());
      assertThat(
              e.getTypeMirrors().stream().map(m -> getQualifiedName(m)).collect(toImmutableList()))
          .containsExactly("java.lang.Integer", "java.lang.Long");
    }
    assertThat(Ints.asList(a.xs())).containsExactly(1, 2, 3).inOrder();
    assertThat(a.annotationType()).isEqualTo(A.class);

    assertThat(b.value()).isEqualTo(42);

    RS container = te.getAnnotation(RS.class);
    assertThat(container.value()).hasLength(3);
    R[] rs = te.getAnnotationsByType(R.class);
    assertThat(rs).hasLength(3);
    assertThat(Arrays.toString(rs))
        .isEqualTo(
            String.format(
                "[@%s(1), @%s(2), @%s(3)]",
                R.class.getCanonicalName(),
                R.class.getCanonicalName(),
                R.class.getCanonicalName()));

    new EqualsTester()
        .addEqualityGroup(a, te.getAnnotation(A.class))
        .addEqualityGroup(b, te.getAnnotation(B.class))
        .addEqualityGroup(rs[0])
        .addEqualityGroup(rs[1])
        .addEqualityGroup(rs[2])
        .addEqualityGroup(container)
        .addEqualityGroup(I.class.getAnnotation(A.class))
        .addEqualityGroup("unrelated")
        .testEquals();
  }

  private static void addClass(JarOutputStream jos, Class<?> clazz) throws IOException {
    String entryPath = clazz.getName().replace('.', '/') + ".class";
    jos.putNextEntry(new JarEntry(entryPath));
    jos.write(getResourceBytes(clazz, "/" + entryPath));
  }

  private static String getQualifiedName(TypeMirror typeMirror) {
    return ((TypeElement) ((DeclaredType) typeMirror).asElement()).getQualifiedName().toString();
  }
}
