/*
 * Copyright 2021 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;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static org.junit.Assert.fail;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.math.BigInteger;
import java.time.LocalTime;
import java.util.AbstractSet;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class AutoBuilderTest {
  static class Simple {
    private final int anInt;
    private final String aString;

    Simple(int anInt, String aString) {
      this.anInt = anInt;
      this.aString = aString;
    }

    static Simple of(int anInt, String aString) {
      return new Simple(anInt, aString);
    }

    @Override
    public boolean equals(Object x) {
      if (x instanceof Simple) {
        Simple that = (Simple) x;
        return this.anInt == that.anInt && Objects.equals(this.aString, that.aString);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(anInt, aString);
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("anInt", anInt)
          .add("aString", aString)
          .toString();
    }

    static Builder builder() {
      return new AutoBuilder_AutoBuilderTest_Simple_Builder();
    }

    @AutoBuilder
    abstract static class Builder {
      abstract Builder setAnInt(int x);

      abstract Builder setAString(String x);

      abstract Simple build();
    }
  }

  @Test
  public void simple() {
    Simple x = Simple.builder().setAnInt(23).setAString("skidoo").build();
    assertThat(x).isEqualTo(new Simple(23, "skidoo"));
  }

  @AutoValue
  abstract static class SimpleAuto {
    abstract int getFoo();

    abstract String getBar();

    static Builder builder() {
      return new AutoBuilder_AutoBuilderTest_SimpleAuto_Builder();
    }

    // There's no particular reason to do this since @AutoValue.Builder works just as well, but
    // let's check anyway.
    @AutoBuilder(ofClass = AutoValue_AutoBuilderTest_SimpleAuto.class)
    abstract static class Builder {
      abstract Builder setFoo(int x);

      abstract Builder setBar(String x);

      abstract AutoValue_AutoBuilderTest_SimpleAuto build();
    }
  }

  @Test
  public void simpleAuto() {
    SimpleAuto x = SimpleAuto.builder().setFoo(23).setBar("skidoo").build();
    assertThat(x.getFoo()).isEqualTo(23);
    assertThat(x.getBar()).isEqualTo("skidoo");
  }

  enum Truthiness {FALSY, TRUTHY}

  @interface MyAnnotation {
    String value();
    int DEFAULT_ID = -1;
    int id() default DEFAULT_ID;
    Truthiness DEFAULT_TRUTHINESS = Truthiness.FALSY;
    Truthiness truthiness() default Truthiness.FALSY;
  }

  // This method has a parameter for `truthiness`, even though that has a default, but it has no
  // parameter for `id`, which also has a default.
  @AutoAnnotation
  static MyAnnotation myAnnotation(String value, Truthiness truthiness) {
    return new AutoAnnotation_AutoBuilderTest_myAnnotation(value, truthiness);
  }

  @AutoBuilder(callMethod = "myAnnotation")
  interface MyAnnotationBuilder {
    MyAnnotationBuilder value(String x);
    MyAnnotationBuilder truthiness(Truthiness x);
    MyAnnotation build();
  }

  static MyAnnotationBuilder myAnnotationBuilder() {
    return new AutoBuilder_AutoBuilderTest_MyAnnotationBuilder()
        .truthiness(MyAnnotation.DEFAULT_TRUTHINESS);
  }

  @Test
  public void simpleAutoAnnotation() {
    MyAnnotation annotation1 = myAnnotationBuilder().value("foo").build();
    assertThat(annotation1.value()).isEqualTo("foo");
    assertThat(annotation1.id()).isEqualTo(MyAnnotation.DEFAULT_ID);
    assertThat(annotation1.truthiness()).isEqualTo(MyAnnotation.DEFAULT_TRUTHINESS);
    MyAnnotation annotation2 =
        myAnnotationBuilder().value("bar").truthiness(Truthiness.TRUTHY).build();
    assertThat(annotation2.value()).isEqualTo("bar");
    assertThat(annotation2.id()).isEqualTo(MyAnnotation.DEFAULT_ID);
    assertThat(annotation2.truthiness()).isEqualTo(Truthiness.TRUTHY);
  }

  static class Overload {
    final int anInt;
    final String aString;
    final BigInteger aBigInteger;

    Overload(int anInt, String aString) {
      this(anInt, aString, BigInteger.ZERO);
    }

    Overload(int anInt, String aString, BigInteger aBigInteger) {
      this.anInt = anInt;
      this.aString = aString;
      this.aBigInteger = aBigInteger;
    }

    @Override
    public boolean equals(Object x) {
      if (x instanceof Overload) {
        Overload that = (Overload) x;
        return this.anInt == that.anInt
            && Objects.equals(this.aString, that.aString)
            && Objects.equals(this.aBigInteger, that.aBigInteger);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(anInt, aString, aBigInteger);
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("anInt", anInt)
          .add("aString", aString)
          .add("aBigInteger", aBigInteger)
          .toString();
    }

    static Builder1 builder1() {
      return new AutoBuilder_AutoBuilderTest_Overload_Builder1();
    }

    static Builder2 builder2() {
      return new AutoBuilder_AutoBuilderTest_Overload_Builder2();
    }

    @AutoBuilder
    interface Builder1 {
      Builder1 setAnInt(int x);

      Builder1 setAString(String x);

      Overload build();
    }

    @AutoBuilder
    interface Builder2 {
      Builder2 setAnInt(int x);

      Builder2 setAString(String x);

      Builder2 setABigInteger(BigInteger x);

      Overload build();
    }
  }

  @Test
  public void overloadedConstructor() {
    Overload actual1 = Overload.builder1().setAnInt(23).setAString("skidoo").build();
    Overload expected1 = new Overload(23, "skidoo");
    assertThat(actual1).isEqualTo(expected1);

    BigInteger big17 = BigInteger.valueOf(17);
    Overload actual2 =
        Overload.builder2().setAnInt(17).setAString("17").setABigInteger(big17).build();
    Overload expected2 = new Overload(17, "17", big17);
    assertThat(actual2).isEqualTo(expected2);
  }

  @AutoBuilder(callMethod = "of", ofClass = Simple.class)
  interface SimpleStaticBuilder {
    SimpleStaticBuilder anInt(int x);

    SimpleStaticBuilder aString(String x);

    Simple build();
  }

  static SimpleStaticBuilder simpleStaticBuilder() {
    return new AutoBuilder_AutoBuilderTest_SimpleStaticBuilder();
  }

  @Test
  public void staticMethod() {
    Simple actual = simpleStaticBuilder().anInt(17).aString("17").build();
    Simple expected = new Simple(17, "17");
    assertThat(actual).isEqualTo(expected);
  }

  // We can't be sure that the java.time package has parameter names, so we use this intermediary.
  // Otherwise we could just write @AutoBuilder(callMethod = "of", ofClass = LocalTime.class).
  // It's still interesting to test this as a realistic example.
  static LocalTime localTimeOf(int hour, int minute, int second, int nanoOfSecond) {
    return LocalTime.of(hour, minute, second, nanoOfSecond);
  }

  static LocalTimeBuilder localTimeBuilder() {
    return new AutoBuilder_AutoBuilderTest_LocalTimeBuilder().nanoOfSecond(0);
  }

  @AutoBuilder(callMethod = "localTimeOf")
  interface LocalTimeBuilder {
    LocalTimeBuilder hour(int hour);

    LocalTimeBuilder minute(int minute);

    LocalTimeBuilder second(int second);

    LocalTimeBuilder nanoOfSecond(int nanoOfSecond);

    LocalTime build();
  }

  @Test
  public void staticMethodOfContainingClass() {
    LocalTime actual = localTimeBuilder().hour(12).minute(34).second(56).build();
    LocalTime expected = LocalTime.of(12, 34, 56);
    assertThat(actual).isEqualTo(expected);
  }

  @Test
  public void missingRequiredProperty() {
    // This test is compiled at source level 7 by CompileWithEclipseTest, so we can't use
    // assertThrows with a lambda.
    try {
      localTimeBuilder().hour(12).minute(34).build();
      fail();
    } catch (IllegalStateException e) {
      assertThat(e).hasMessageThat().isEqualTo("Missing required properties: second");
    }
  }

  static void throwException() throws IOException {
    throw new IOException("oops");
  }

  static ThrowExceptionBuilder throwExceptionBuilder() {
    return new AutoBuilder_AutoBuilderTest_ThrowExceptionBuilder();
  }

  @AutoBuilder(callMethod = "throwException")
  interface ThrowExceptionBuilder {
    void build() throws IOException;
  }

  @Test
  public void emptyBuilderThrowsException() {
    try {
      throwExceptionBuilder().build();
      fail();
    } catch (IOException expected) {
      assertThat(expected).hasMessageThat().isEqualTo("oops");
    }
  }

  static class ListContainer {
    private final ImmutableList<String> list;

    ListContainer(ImmutableList<String> list) {
      this.list = checkNotNull(list);
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof ListContainer && list.equals(((ListContainer) o).list);
    }

    @Override
    public int hashCode() {
      return list.hashCode();
    }

    @Override
    public String toString() {
      return list.toString();
    }

    static Builder builder() {
      return new AutoBuilder_AutoBuilderTest_ListContainer_Builder();
    }

    @AutoBuilder
    interface Builder {
      Builder setList(Iterable<String> list);

      ImmutableList.Builder<String> listBuilder();

      ListContainer build();
    }
  }

  @Test
  public void propertyBuilder() {
    ListContainer expected = new ListContainer(ImmutableList.of("one", "two", "three"));
    ListContainer actual1 =
        ListContainer.builder().setList(ImmutableList.of("one", "two", "three")).build();
    assertThat(actual1).isEqualTo(expected);

    ListContainer.Builder builder2 = ListContainer.builder();
    builder2.listBuilder().add("one", "two", "three");
    assertThat(builder2.build()).isEqualTo(expected);

    ListContainer.Builder builder3 = ListContainer.builder().setList(ImmutableList.of("one"));
    builder3.listBuilder().add("two", "three");
    assertThat(builder3.build()).isEqualTo(expected);

    ListContainer.Builder builder4 = ListContainer.builder();
    builder4.listBuilder();
    try {
      builder4.setList(ImmutableList.of("one", "two", "three"));
      fail();
    } catch (IllegalStateException e) {
      assertThat(e).hasMessageThat().isEqualTo("Cannot set list after calling listBuilder()");
    }
  }

  static <K, V extends Number> Map<K, V> singletonMap(K key, V value) {
    return Collections.singletonMap(key, value);
  }

  static <K, V extends Number> SingletonMapBuilder<K, V> singletonMapBuilder() {
    return new AutoBuilder_AutoBuilderTest_SingletonMapBuilder<>();
  }

  @AutoBuilder(callMethod = "singletonMap")
  interface SingletonMapBuilder<K, V extends Number> {
    SingletonMapBuilder<K, V> key(K key);
    SingletonMapBuilder<K, V> value(V value);
    Map<K, V> build();
  }

  @Test
  public void genericStaticMethod() {
    ImmutableMap<String, Integer> expected = ImmutableMap.of("17", 17);
    SingletonMapBuilder<String, Integer> builder = singletonMapBuilder();
    Map<String, Integer> actual = builder.key("17").value(17).build();
    assertThat(actual).isEqualTo(expected);
  }

  static class SingletonSet<E> extends AbstractSet<E> {
    private final E element;

    SingletonSet(E element) {
      this.element = element;
    }

    @Override
    public int size() {
      return 1;
    }

    @Override
    public Iterator<E> iterator() {
      return new Iterator<E>() {
        private boolean first = true;

        @Override
        public boolean hasNext() {
          return first;
        }

        @Override
        public E next() {
          if (!first) {
            throw new NoSuchElementException();
          }
          first = false;
          return element;
        }
      };
    }
  }

  @AutoBuilder(ofClass = SingletonSet.class)
  interface SingletonSetBuilder<E> {
    SingletonSetBuilder<E> setElement(E element);
    SingletonSet<E> build();
  }

  static <E> SingletonSetBuilder<E> singletonSetBuilder() {
    return new AutoBuilder_AutoBuilderTest_SingletonSetBuilder<>();
  }

  @Test
  public void genericClass() {
    ImmutableSet<String> expected = ImmutableSet.of("foo");
    SingletonSetBuilder<String> builder = singletonSetBuilder();
    Set<String> actual = builder.setElement("foo").build();
    assertThat(actual).isEqualTo(expected);
  }

  static class TypedSingletonSet<E> extends SingletonSet<E> {
    private final Class<?> type;

    <T extends E> TypedSingletonSet(T element, Class<T> type) {
      super(element);
      this.type = type;
    }

    @Override
    public String toString() {
      return type.getName() + super.toString();
    }
  }

  @AutoBuilder(ofClass = TypedSingletonSet.class)
  interface TypedSingletonSetBuilder<E, T extends E> {
    TypedSingletonSetBuilder<E, T> setElement(T element);
    TypedSingletonSetBuilder<E, T> setType(Class<T> type);
    TypedSingletonSet<E> build();
  }

  static <E, T extends E> TypedSingletonSetBuilder<E, T> typedSingletonSetBuilder() {
    return new AutoBuilder_AutoBuilderTest_TypedSingletonSetBuilder<>();
  }

  @Test
  public void genericClassWithGenericConstructor() {
    TypedSingletonSetBuilder<CharSequence, String> builder = typedSingletonSetBuilder();
    TypedSingletonSet<CharSequence> set = builder.setElement("foo").setType(String.class).build();
    assertThat(set.toString()).isEqualTo("java.lang.String[foo]");
  }

  static <T> ImmutableList<T> pair(T first, T second) {
    return ImmutableList.of(first, second);
  }

  @AutoBuilder(callMethod = "pair")
  interface PairBuilder<T> {
    PairBuilder<T> setFirst(T x);
    T getFirst();
    PairBuilder<T> setSecond(T x);
    Optional<T> getSecond();
    ImmutableList<T> build();
  }

  static <T> PairBuilder<T> pairBuilder() {
    return new AutoBuilder_AutoBuilderTest_PairBuilder<>();
  }

  @Test
  public void genericGetters() {
    PairBuilder<Number> builder = pairBuilder();
    assertThat(builder.getSecond()).isEmpty();
    builder.setSecond(2);
    assertThat(builder.getSecond()).hasValue(2);
    try {
      builder.getFirst();
      fail();
    } catch (IllegalStateException expected) {
    }
    builder.setFirst(1.0);
    assertThat(builder.getFirst()).isEqualTo(1.0);
    assertThat(builder.build()).containsExactly(1.0, 2).inOrder();
  }
}
