/*
 * Copyright (C) 2017 The Dagger Authors.
 *
 * 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 dagger.internal.codegen;

import static com.google.testing.compile.CompilationSubject.assertThat;
import static dagger.internal.codegen.Compilers.compilerWithOptions;

import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import java.util.Collection;
import javax.tools.JavaFileObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class ElidedFactoriesTest {
  @Parameters(name = "{0}")
  public static Collection<Object[]> parameters() {
    return CompilerMode.TEST_PARAMETERS;
  }

  private final CompilerMode compilerMode;

  public ElidedFactoriesTest(CompilerMode compilerMode) {
    this.compilerMode = compilerMode;
  }

  @Test
  public void simpleComponent() {
    JavaFileObject injectedType =
        JavaFileObjects.forSourceLines(
            "test.InjectedType",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "final class InjectedType {",
            "  @Inject InjectedType() {}",
            "}");

    JavaFileObject dependsOnInjected =
        JavaFileObjects.forSourceLines(
            "test.InjectedType",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "",
            "final class DependsOnInjected {",
            "  @Inject DependsOnInjected(InjectedType injected) {}",
            "}");
    JavaFileObject componentFile =
        JavaFileObjects.forSourceLines(
            "test.SimpleComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "",
            "@Component",
            "interface SimpleComponent {",
            "  DependsOnInjected dependsOnInjected();",
            "}");
    JavaFileObject generatedComponent =
        JavaFileObjects.forSourceLines(
            "test.DaggerSimpleComponent",
            "package test;",
            "",
            GeneratedLines.generatedImports(),
            "",
            GeneratedLines.generatedAnnotations(),
            "final class DaggerSimpleComponent implements SimpleComponent {",
            "  private DaggerSimpleComponent() {}",
            "",
            "  public static Builder builder() {",
            "    return new Builder();",
            "  }",
            "",
            "  public static SimpleComponent create() {",
            "    return new Builder().build();",
            "  }",
            "",
            "  @Override",
            "  public DependsOnInjected dependsOnInjected() {",
            "    return new DependsOnInjected(new InjectedType());",
            "  }",
            "",
            "  static final class Builder {",
            "    private Builder() {",
            "    }",
            "",
            "    public SimpleComponent build() {",
            "      return new DaggerSimpleComponent();",
            "    }",
            "  }",
            "}");

    Compilation compilation =
        compilerWithOptions(compilerMode.javacopts())
            .compile(injectedType, dependsOnInjected, componentFile);
    assertThat(compilation).succeeded();
    assertThat(compilation)
        .generatedSourceFile("test.DaggerSimpleComponent")
        .hasSourceEquivalentTo(generatedComponent);
  }

  @Test
  public void simpleComponent_injectsProviderOf_dependsOnScoped() {
    JavaFileObject scopedType =
        JavaFileObjects.forSourceLines(
            "test.ScopedType",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "import javax.inject.Singleton;",
            "",
            "@Singleton",
            "final class ScopedType {",
            "  @Inject ScopedType() {}",
            "}");

    JavaFileObject dependsOnScoped =
        JavaFileObjects.forSourceLines(
            "test.ScopedType",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "import javax.inject.Provider;",
            "",
            "final class DependsOnScoped {",
            "  @Inject DependsOnScoped(ScopedType scoped) {}",
            "}");

    JavaFileObject needsProvider =
        JavaFileObjects.forSourceLines(
            "test.NeedsProvider",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "import javax.inject.Provider;",
            "",
            "class NeedsProvider {",
            "  @Inject NeedsProvider(Provider<DependsOnScoped> provider) {}",
            "}");
    JavaFileObject componentFile =
        JavaFileObjects.forSourceLines(
            "test.SimpleComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "import javax.inject.Singleton;",
            "",
            "@Singleton",
            "@Component",
            "interface SimpleComponent {",
            "  NeedsProvider needsProvider();",
            "}");
    JavaFileObject generatedComponent;
    switch (compilerMode) {
      case FAST_INIT_MODE:
        generatedComponent =
            JavaFileObjects.forSourceLines(
                "test.DaggerSimpleComponent",
                "package test;",
                "",
                GeneratedLines.generatedImports(
                    "import dagger.internal.DoubleCheck;",
                    "import dagger.internal.MemoizedSentinel;",
                    "import javax.inject.Provider;"),
                "",
                GeneratedLines.generatedAnnotations(),
                "final class DaggerSimpleComponent implements SimpleComponent {",
                "  private volatile Object scopedType = new MemoizedSentinel();",
                "  private volatile Provider<DependsOnScoped> dependsOnScopedProvider;",
                "",
                "  private DaggerSimpleComponent() {}",
                "",
                "  public static Builder builder() {",
                "    return new Builder();",
                "  }",
                "",
                "  public static SimpleComponent create() {",
                "    return new Builder().build();",
                "  }",
                "",
                "  private ScopedType scopedType() {",
                "    Object local = scopedType;",
                "    if (local instanceof MemoizedSentinel) {",
                "      synchronized (local) {",
                "        local = scopedType;",
                "        if (local instanceof MemoizedSentinel) {",
                "          local = new ScopedType();",
                "          scopedType = DoubleCheck.reentrantCheck(scopedType, local);",
                "        }",
                "      }",
                "    }",
                "    return (ScopedType) local;",
                "  }",
                "",
                "  private DependsOnScoped dependsOnScoped() {",
                "    return new DependsOnScoped(scopedType());",
                "  }",
                "",
                "  private Provider<DependsOnScoped> dependsOnScopedProvider() {",
                "    Object local = dependsOnScopedProvider;",
                "    if (local == null) {",
                "      local = new SwitchingProvider<>(0);",
                "      dependsOnScopedProvider = (Provider<DependsOnScoped>) local;",
                "    }",
                "    return (Provider<DependsOnScoped>) local;",
                "  }",
                "",
                "  @Override",
                "  public NeedsProvider needsProvider() {",
                "    return new NeedsProvider(dependsOnScopedProvider());",
                "  }",
                "",
                "  static final class Builder {",
                "    private Builder() {}",
                "",
                "    public SimpleComponent build() {",
                "      return new DaggerSimpleComponent();",
                "    }",
                "  }",
                "  private final class SwitchingProvider<T> implements Provider<T> {",
                "    private final int id;",
                "",
                "    SwitchingProvider(int id) {",
                "      this.id = id;",
                "    }",
                "",
                "    @SuppressWarnings(\"unchecked\")",
                "    @Override",
                "    public T get() {",
                "      switch (id) {",
                "        case 0: return (T) DaggerSimpleComponent.this.dependsOnScoped();",
                "        default: throw new AssertionError(id);",
                "      }",
                "    }",
                "  }",
                "}");
        break;
      default:
        generatedComponent =
            JavaFileObjects.forSourceLines(
                "test.DaggerSimpleComponent",
                "package test;",
                "",
                GeneratedLines.generatedImports(
                    "import dagger.internal.DoubleCheck;",
                    "import javax.inject.Provider;"),
                "",
                GeneratedLines.generatedAnnotations(),
                "final class DaggerSimpleComponent implements SimpleComponent {",
                "  private Provider<ScopedType> scopedTypeProvider;",
                "  private Provider<DependsOnScoped> dependsOnScopedProvider;",
                "",
                "  private DaggerSimpleComponent() {",
                "    initialize();",
                "  }",
                "",
                "  public static Builder builder() {",
                "    return new Builder();",
                "  }",
                "",
                "  public static SimpleComponent create() {",
                "    return new Builder().build();",
                "  }",
                "",
                "  @SuppressWarnings(\"unchecked\")",
                "  private void initialize() {",
                "    this.scopedTypeProvider = DoubleCheck.provider(ScopedType_Factory.create());",
                "    this.dependsOnScopedProvider = ",
                "        DependsOnScoped_Factory.create(scopedTypeProvider);",
                "  }",
                "",
                "  @Override",
                "  public NeedsProvider needsProvider() {",
                "    return new NeedsProvider(dependsOnScopedProvider);",
                "  }",
                "",
                "  static final class Builder {",
                "    private Builder() {",
                "    }",
                "",
                "    public SimpleComponent build() {",
                "      return new DaggerSimpleComponent();",
                "    }",
                "  }",
                "}");
    }
    Compilation compilation =
        compilerWithOptions(compilerMode.javacopts())
            .compile(scopedType, dependsOnScoped, componentFile, needsProvider);
    assertThat(compilation).succeeded();
    assertThat(compilation)
        .generatedSourceFile("test.DaggerSimpleComponent")
        .hasSourceEquivalentTo(generatedComponent);
  }

  @Test
  public void scopedBinding_onlyUsedInSubcomponent() {
    JavaFileObject scopedType =
        JavaFileObjects.forSourceLines(
            "test.ScopedType",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "import javax.inject.Singleton;",
            "",
            "@Singleton",
            "final class ScopedType {",
            "  @Inject ScopedType() {}",
            "}");

    JavaFileObject dependsOnScoped =
        JavaFileObjects.forSourceLines(
            "test.ScopedType",
            "package test;",
            "",
            "import javax.inject.Inject;",
            "import javax.inject.Provider;",
            "",
            "final class DependsOnScoped {",
            "  @Inject DependsOnScoped(ScopedType scoped) {}",
            "}");
    JavaFileObject componentFile =
        JavaFileObjects.forSourceLines(
            "test.SimpleComponent",
            "package test;",
            "",
            "import dagger.Component;",
            "import javax.inject.Singleton;",
            "",
            "@Singleton",
            "@Component",
            "interface SimpleComponent {",
            "  Sub sub();",
            "}");
    JavaFileObject subcomponentFile =
        JavaFileObjects.forSourceLines(
            "test.SimpleComponent",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface Sub {",
            "  DependsOnScoped dependsOnScoped();",
            "}");

    JavaFileObject generatedComponent;
    switch (compilerMode) {
      case FAST_INIT_MODE:
        generatedComponent =
            JavaFileObjects.forSourceLines(
                "test.DaggerSimpleComponent",
                "package test;",
                "",
                GeneratedLines.generatedImports(
                    "import dagger.internal.DoubleCheck;",
                    "import dagger.internal.MemoizedSentinel;"),
                "",
                GeneratedLines.generatedAnnotations(),
                "final class DaggerSimpleComponent implements SimpleComponent {",
                "  private volatile Object scopedType = new MemoizedSentinel();",
                "",
                "  private DaggerSimpleComponent() {}",
                "",
                "  public static Builder builder() {",
                "    return new Builder();",
                "  }",
                "",
                "  public static SimpleComponent create() {",
                "    return new Builder().build();",
                "  }",
                "",
                "  private ScopedType scopedType() {",
                "    Object local = scopedType;",
                "    if (local instanceof MemoizedSentinel) {",
                "      synchronized (local) {",
                "        local = scopedType;",
                "        if (local instanceof MemoizedSentinel) {",
                "          local = new ScopedType();",
                "          scopedType = DoubleCheck.reentrantCheck(scopedType, local);",
                "        }",
                "      }",
                "    }",
                "    return (ScopedType) local;",
                "  }",
                "",
                "  @Override",
                "  public Sub sub() {",
                "    return new SubImpl();",
                "  }",
                "",
                "  static final class Builder {",
                "    private Builder() {}",
                "",
                "    public SimpleComponent build() {",
                "      return new DaggerSimpleComponent();",
                "    }",
                "  }",
                "",
                "  private final class SubImpl implements Sub {",
                "    private SubImpl() {}",
                "",
                "    @Override",
                "    public DependsOnScoped dependsOnScoped() {",
                "      return new DependsOnScoped(DaggerSimpleComponent.this.scopedType());",
                "    }",
                "  }",
                "}");
        break;
      default:
        generatedComponent =
            JavaFileObjects.forSourceLines(
                "test.DaggerSimpleComponent",
                "package test;",
                "",
                GeneratedLines.generatedImports(
                    "import dagger.internal.DoubleCheck;",
                    "import javax.inject.Provider;"),
                "",
                GeneratedLines.generatedAnnotations(),
                "final class DaggerSimpleComponent implements SimpleComponent {",
                "  private Provider<ScopedType> scopedTypeProvider;",
                "",
                "  private DaggerSimpleComponent() {",
                "    initialize();",
                "  }",
                "",
                "  public static Builder builder() {",
                "    return new Builder();",
                "  }",
                "",
                "  public static SimpleComponent create() {",
                "    return new Builder().build();",
                "  }",
                "",
                "  @SuppressWarnings(\"unchecked\")",
                "  private void initialize() {",
                "    this.scopedTypeProvider = DoubleCheck.provider(ScopedType_Factory.create());",
                "  }",
                "",
                "  @Override",
                "  public Sub sub() {",
                "    return new SubImpl();",
                "  }",
                "",
                "  static final class Builder {",
                "    private Builder() {}",
                "",
                "    public SimpleComponent build() {",
                "      return new DaggerSimpleComponent();",
                "    }",
                "  }",
                "",
                "  private final class SubImpl implements Sub {",
                "    private SubImpl() {}",
                "",
                "    @Override",
                "    public DependsOnScoped dependsOnScoped() {",
                "      return new DependsOnScoped(",
                "          DaggerSimpleComponent.this.scopedTypeProvider.get());",
                "    }",
                "  }",
                "}");
    }
    Compilation compilation =
        compilerWithOptions(compilerMode.javacopts())
            .compile(scopedType, dependsOnScoped, componentFile, subcomponentFile);
    assertThat(compilation).succeeded();
    assertThat(compilation)
        .generatedSourceFile("test.DaggerSimpleComponent")
        .hasSourceEquivalentTo(generatedComponent);
  }
}
