/*
 * Copyright 2016 Google LLC
 *
 * 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.auto.value.extension.memoized;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import com.google.auto.value.AutoValue;
import com.google.auto.value.AutoValue.CopyAnnotations;
import com.google.auto.value.extension.memoized.MemoizedTest.HashCodeEqualsOptimization.EqualsCounter;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.ImmutableTypeParameter;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class MemoizedTest {

  private Value value;
  private ListValue<Integer, String> listValue;

  @AutoValue
  abstract static class ValueWithKeywordName {
    abstract boolean getNative();

    abstract boolean getNative0();

    @Memoized
    boolean getMemoizedNative() {
      return getNative();
    }

    @Memoized
    boolean getMemoizedNative0() {
      return getNative0();
    }
  }

  @AutoValue
  @CopyAnnotations
  @javax.annotation.Nullable
  abstract static class ValueWithCopyAnnotations {
    abstract boolean getNative();

    @Memoized
    boolean getMemoizedNative() {
      return getNative();
    }
  }

  @AutoValue
  @javax.annotation.Nullable
  abstract static class ValueWithoutCopyAnnotations {
    abstract boolean getNative();

    @Memoized
    boolean getMemoizedNative() {
      return getNative();
    }
  }

  @AutoValue
  abstract static class Value {
    private int primitiveCount;
    private int notNullableCount;
    private int nullableCount;
    private int returnsNullCount;
    private int nullableWithTypeAnnotationCount;
    private int returnsNullWithTypeAnnotationCount;
    private int notNullableButReturnsNullCount;
    private int throwsExceptionCount;

    @javax.annotation.Nullable
    abstract String string();

    abstract @org.checkerframework.checker.nullness.qual.Nullable String stringWithTypeAnnotation();

    abstract HashCodeAndToStringCounter counter();

    @Memoized
    int primitive() {
      return ++primitiveCount;
    }

    @Memoized
    String notNullable() {
      notNullableCount++;
      return "derived " + string() + " " + notNullableCount;
    }

    @Memoized
    @javax.annotation.Nullable
    String nullable() {
      nullableCount++;
      return "nullable derived " + string() + " " + nullableCount;
    }

    @Memoized
    @javax.annotation.Nullable
    String returnsNull() {
      returnsNullCount++;
      return null;
    }

    @Memoized
    @org.checkerframework.checker.nullness.qual.Nullable
    String nullableWithTypeAnnotation() {
      nullableWithTypeAnnotationCount++;
      return "nullable derived " + stringWithTypeAnnotation() + " "
          + nullableWithTypeAnnotationCount;
    }

    @Memoized
    @org.checkerframework.checker.nullness.qual.Nullable
    String returnsNullWithTypeAnnotation() {
      returnsNullWithTypeAnnotationCount++;
      return null;
    }

    @Memoized
    String notNullableButReturnsNull() {
      notNullableButReturnsNullCount++;
      return null;
    }

    @Memoized
    String throwsException() throws SomeCheckedException {
      throwsExceptionCount++;
      throw new SomeCheckedException();
    }

    @Override
    @Memoized
    public abstract int hashCode();

    @Override
    @Memoized
    public abstract String toString();
  }

  static final class SomeCheckedException extends Exception {}

  @AutoValue
  abstract static class ListValue<T extends Number, K> {

    abstract T value();

    abstract K otherValue();

    @Memoized
    ImmutableList<T> myTypedList() {
      return ImmutableList.of(value());
    }
  }

  static class HashCodeAndToStringCounter {
    int hashCodeCount;
    int toStringCount;

    @Override
    public int hashCode() {
      return ++hashCodeCount;
    }

    @Override
    public String toString() {
      return "a string" + ++toStringCount;
    }
  }

  @AutoValue
  abstract static class HashCodeEqualsOptimization {
    int overrideHashCode;
    int hashCodeCount;

    abstract EqualsCounter equalsCounter();

    @Memoized
    @Override
    public int hashCode() {
      hashCodeCount++;
      return overrideHashCode;
    }

    static class EqualsCounter {
      int equalsCount;

      @Override
      public int hashCode() {
        return 0;
      }

      @Override
      public boolean equals(Object obj) {
        equalsCount++;
        return true;
      }
    }
  }

  @AutoValue
  abstract static class HashCodeEqualsOptimizationOffWhenEqualsIsFinal {
    int hashCodeCount;

    @Override
    @Memoized
    public int hashCode() {
      hashCodeCount++;
      return 1;
    }

    @Override
    public final boolean equals(Object that) {
      return that instanceof HashCodeEqualsOptimizationOffWhenEqualsIsFinal;
    }
  }

  @Before
  public void setUp() {
    value = new AutoValue_MemoizedTest_Value(
        "string", "stringWithTypeAnnotation", new HashCodeAndToStringCounter());
    listValue = new AutoValue_MemoizedTest_ListValue<Integer, String>(0, "hello");
  }

  @Test
  public void listValueList() {
    assertThat(listValue.myTypedList()).containsExactly(listValue.value());
  }

  @Test
  public void listValueString() {
    assertThat(listValue.otherValue()).isEqualTo("hello");
  }

  @Test
  public void primitive() {
    assertThat(value.primitive()).isEqualTo(1);
    assertThat(value.primitive()).isEqualTo(1);
    assertThat(value.primitiveCount).isEqualTo(1);
  }

  @Test
  public void notNullable() {
    assertThat(value.notNullable()).isEqualTo("derived string 1");
    assertThat(value.notNullable()).isSameInstanceAs(value.notNullable());
    assertThat(value.notNullableCount).isEqualTo(1);
  }

  @Test
  public void nullable() {
    assertThat(value.nullable()).isEqualTo("nullable derived string 1");
    assertThat(value.nullable()).isSameInstanceAs(value.nullable());
    assertThat(value.nullableCount).isEqualTo(1);
  }

  @Test
  public void nullableWithTypeAnnotation() {
    assertThat(value.nullableWithTypeAnnotation())
        .isEqualTo("nullable derived stringWithTypeAnnotation 1");
    assertThat(value.nullableWithTypeAnnotation())
        .isSameInstanceAs(value.nullableWithTypeAnnotation());
    assertThat(value.nullableWithTypeAnnotationCount).isEqualTo(1);
  }

  @Test
  public void returnsNull() {
    assertThat(value.returnsNull()).isNull();
    assertThat(value.returnsNull()).isNull();
    assertThat(value.returnsNullCount).isEqualTo(1);
  }

  @Test
  public void returnsNullWithTypeAnnotation() {
    assertThat(value.returnsNullWithTypeAnnotation()).isNull();
    assertThat(value.returnsNullWithTypeAnnotation()).isNull();
    assertThat(value.returnsNullWithTypeAnnotationCount).isEqualTo(1);
  }

  @Test
  public void notNullableButReturnsNull() {
    try {
      value.notNullableButReturnsNull();
      fail();
    } catch (NullPointerException expected) {
      assertThat(expected)
          .hasMessageThat()
          .isEqualTo("notNullableButReturnsNull() cannot return null");
    }
    assertThat(value.notNullableButReturnsNullCount).isEqualTo(1);
  }

  @Test
  public void methodThrows() {
    // The exception is thrown.
    try {
      value.throwsException();
      fail();
    } catch (SomeCheckedException expected1) {
      // The exception is not memoized.
      try {
        value.throwsException();
        fail();
      } catch (SomeCheckedException expected2) {
        assertThat(expected2).isNotSameInstanceAs(expected1);
      }
    }
    assertThat(value.throwsExceptionCount).isEqualTo(2);
  }

  @Test
  public void testHashCode() {
    assertThat(value.hashCode()).isEqualTo(value.hashCode());
    assertThat(value.counter().hashCodeCount).isEqualTo(1);
  }

  @Test
  public void testToString() {
    assertThat(value.toString()).isEqualTo(value.toString());
    assertThat(value.counter().toStringCount).isEqualTo(1);
  }

  @Test
  public void keywords() {
    ValueWithKeywordName value = new AutoValue_MemoizedTest_ValueWithKeywordName(true, false);
    assertThat(value.getNative()).isTrue();
    assertThat(value.getMemoizedNative()).isTrue();
    assertThat(value.getNative0()).isFalse();
    assertThat(value.getMemoizedNative0()).isFalse();
  }

  @Test
  public void copyAnnotations() {
    ValueWithCopyAnnotations valueWithCopyAnnotations =
        new AutoValue_MemoizedTest_ValueWithCopyAnnotations(true);
    ValueWithoutCopyAnnotations valueWithoutCopyAnnotations =
        new AutoValue_MemoizedTest_ValueWithoutCopyAnnotations(true);

    assertThat(
            valueWithCopyAnnotations
                .getClass()
                .isAnnotationPresent(javax.annotation.Nullable.class))
        .isTrue();
    assertThat(
            valueWithoutCopyAnnotations
                .getClass()
                .isAnnotationPresent(javax.annotation.Nullable.class))
        .isFalse();
  }

  @Test
  public void nullableHasAnnotation() throws ReflectiveOperationException {
    Method nullable = AutoValue_MemoizedTest_Value.class.getDeclaredMethod("nullable");
    assertThat(nullable.isAnnotationPresent(javax.annotation.Nullable.class)).isTrue();
  }

  @Test
  public void nullableWithTypeAnnotationHasAnnotation() throws ReflectiveOperationException {
    Method nullable =
        AutoValue_MemoizedTest_Value.class.getDeclaredMethod("nullableWithTypeAnnotation");
    AnnotatedType returnType = nullable.getAnnotatedReturnType();
    assertThat(returnType.isAnnotationPresent(
                   org.checkerframework.checker.nullness.qual.Nullable.class))
        .isTrue();
  }

  @Test
  public void nullableConstructorParameter() throws ReflectiveOperationException {
    // Constructor parameters are potentially:
    // [0] @javax.annotation.Nullable String string,
    // [1] @org.checkerframework.checker.nullness.qual.Nullable String stringWithTypeAnnotation,
    // [2] HashCodeAndToStringCounter counter
    // We don't currently copy @javax.annotation.Nullable because it is not a TYPE_USE annotation.
    Constructor<?> constructor = AutoValue_MemoizedTest_Value.class.getDeclaredConstructor(
        String.class, String.class, HashCodeAndToStringCounter.class);
    AnnotatedType paramType = constructor.getAnnotatedParameterTypes()[1];
    assertThat(paramType.isAnnotationPresent(
                   org.checkerframework.checker.nullness.qual.Nullable.class))
        .isTrue();
  }

  @Test
  public void hashCodeEqualsOptimization() {
    HashCodeEqualsOptimization first =
        new AutoValue_MemoizedTest_HashCodeEqualsOptimization(new EqualsCounter());
    HashCodeEqualsOptimization second =
        new AutoValue_MemoizedTest_HashCodeEqualsOptimization(new EqualsCounter());

    first.overrideHashCode = 2;
    second.overrideHashCode = 2;
    assertThat(first.equals(second)).isTrue();
    assertThat(first.equalsCounter().equalsCount).isEqualTo(1);

    HashCodeEqualsOptimization otherwiseEqualsButDifferentHashCode =
        new AutoValue_MemoizedTest_HashCodeEqualsOptimization(new EqualsCounter());
    otherwiseEqualsButDifferentHashCode.overrideHashCode = 4;

    assertThat(otherwiseEqualsButDifferentHashCode.equals(first)).isFalse();
    assertThat(otherwiseEqualsButDifferentHashCode.equalsCounter().equalsCount).isEqualTo(0);
  }

  @Test
  public void hashCodeEqualsOptimization_otherTypes() {
    HashCodeEqualsOptimization optimizedEquals =
        new AutoValue_MemoizedTest_HashCodeEqualsOptimization(new EqualsCounter());

    assertThat(optimizedEquals.equals(new Object())).isFalse();
    assertThat(optimizedEquals.equals(null)).isFalse();

    assertThat(optimizedEquals.equalsCounter().equalsCount).isEqualTo(0);
    assertThat(optimizedEquals.hashCodeCount).isEqualTo(0);
  }

  @Test
  public void hashCodeEqualsOptimization_hashCodeIgnoredForSameInstance() {
    HashCodeEqualsOptimization optimizedEquals =
        new AutoValue_MemoizedTest_HashCodeEqualsOptimization(new EqualsCounter());

    assertThat(optimizedEquals.equals(optimizedEquals)).isTrue();

    assertThat(optimizedEquals.equalsCounter().equalsCount).isEqualTo(0);
    assertThat(optimizedEquals.hashCodeCount).isEqualTo(0);
  }

  @Test
  public void hashCodeEqualsOptimization_offWhenEqualsIsFinal() {
    HashCodeEqualsOptimizationOffWhenEqualsIsFinal memoizedHashCodeAndFinalEqualsMethod =
        new AutoValue_MemoizedTest_HashCodeEqualsOptimizationOffWhenEqualsIsFinal();
    HashCodeEqualsOptimizationOffWhenEqualsIsFinal second =
        new AutoValue_MemoizedTest_HashCodeEqualsOptimizationOffWhenEqualsIsFinal();

    assertThat(memoizedHashCodeAndFinalEqualsMethod.equals(second)).isTrue();
    assertThat(memoizedHashCodeAndFinalEqualsMethod.hashCodeCount).isEqualTo(0);

    memoizedHashCodeAndFinalEqualsMethod.hashCode();
    memoizedHashCodeAndFinalEqualsMethod.hashCode();
    assertThat(memoizedHashCodeAndFinalEqualsMethod.hashCodeCount).isEqualTo(1);
  }

  interface TypeEdgeIterable<InputT, ResultT> {}

  interface ResourceUri {}

  interface TypePath<InputT, ResultT> {}

  abstract static class AbstractTypePath<InputT, ResultT> {
    abstract TypeEdgeIterable<InputT, ResultT> edges();
  }

  @AutoValue
  abstract static class ResourceUriPath<InputT> extends AbstractTypePath<InputT, ResourceUri> {
    static <InputT> ResourceUriPath<InputT> create(
        TypeEdgeIterable<InputT, ResourceUri> edges) {
      return new AutoValue_MemoizedTest_ResourceUriPath<>(edges);
    }

    @Memoized
    TypePath<InputT, String> toServiceName() {
      return new TypePath<InputT, String>() {};
    }
  }

  @Test
  public void methodTypeFromTypeVariableSubsitution() {
    ResourceUriPath<String> path =
        ResourceUriPath.create(new TypeEdgeIterable<String, ResourceUri>() {});
    assertThat(path.edges()).isNotNull();
  }

  @Immutable
  @AutoValue
  abstract static class Unchanging<@ImmutableTypeParameter T> {
    abstract T value();

    @Override
    @Memoized
    public abstract int hashCode();

    static <@ImmutableTypeParameter T> Unchanging<T> of(T value) {
      return new AutoValue_MemoizedTest_Unchanging<T>(value);
    }
  }

  @Test
  public void copiedTypeAnnotations() {
    for (Class<?> c = Unchanging.of("foo").getClass(); c != Object.class; c = c.getSuperclass()) {
      assertThat(c.getTypeParameters()).hasLength(1);
      assertThat(c.getTypeParameters()[0].isAnnotationPresent(ImmutableTypeParameter.class))
          .isTrue();
    }
  }
}
