/*
 * Copyright 2014 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.acai;

import com.google.inject.AbstractModule;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.inject.Inject;

import static com.google.common.truth.Truth.assertThat;

@RunWith(MockitoJUnitRunner.class)
public class TestScopeTest {
  @Rule public ExpectedException thrown = ExpectedException.none();
  @Mock private Statement statement;
  @Mock private FrameworkMethod frameworkMethod;

  @Test
  public void sameInstanceInjectedWithinTest() throws Throwable {
    FakeTestClass test = new FakeTestClass();

    new Acai(EmptyTestModule.class).apply(statement, frameworkMethod, test).evaluate();

    assertThat(test.instanceOne).isNotNull();
    assertThat(test.instanceTwo).isSameAs(test.instanceOne);
  }

  @Test
  public void differentInstanceInjectedAcrossTests() throws Throwable {
    FakeTestClass testOne = new FakeTestClass();
    FakeTestClass testTwo = new FakeTestClass();

    new Acai(EmptyTestModule.class).apply(statement, frameworkMethod, testOne).evaluate();
    new Acai(EmptyTestModule.class).apply(statement, frameworkMethod, testTwo).evaluate();

    assertThat(testOne.instanceOne).isNotSameAs(testTwo.instanceOne);
  }

  @Test
  public void servicesInstantiatedOutsideTestScope() throws Throwable {
    FakeTestClass test = new FakeTestClass();

    thrown.expect(ProvisionException.class);
    thrown.expectMessage("@TestScoped binding outside test");
    new Acai(InvalidTestModule.class).apply(statement, frameworkMethod, test).evaluate();
  }

  @Test
  public void canCallProviderInServiceSetupAndTeardown() throws Throwable {
    FakeTestClass test = new FakeTestClass();

    new Acai(ValidTestModule.class).apply(statement, frameworkMethod, test).evaluate();

    assertThat(ValidTestingService.valueFromBeforeTest).isNotNull();
    assertThat(ValidTestingService.valueFromAfterTest)
        .isSameAs(ValidTestingService.valueFromBeforeTest);
  }

  private static class EmptyTestModule extends AbstractModule {
    @Override protected void configure() {
      // No-op.
    }
  }

  private static class InvalidTestModule extends TestingServiceModule {
    @Override protected void configureTestingServices() {
      bindTestingService(InvalidTestingService.class);
    }
  }

  private static class ValidTestModule extends TestingServiceModule {
    @Override protected void configureTestingServices() {
      bindTestingService(ValidTestingService.class);
    }
  }

  private static class FakeTestClass {
    @Inject MyTestScopedClass instanceOne;
    @Inject MyTestScopedClass instanceTwo;
  }

  @TestScoped
  private static class MyTestScopedClass {
  }

  private static class InvalidTestingService implements TestingService {
    @Inject MyTestScopedClass testScoped;
  }

  private static class ValidTestingService implements TestingService {
    @Inject Provider<MyTestScopedClass> testScoped;
    static MyTestScopedClass valueFromBeforeTest;
    static MyTestScopedClass valueFromAfterTest;

    @BeforeTest void beforeTest() {
      valueFromBeforeTest = testScoped.get();
    }

    @AfterTest void afterTest() {
      valueFromAfterTest = testScoped.get();
    }
  }
}

@@ -636,16 +636,115 @@
 .acai;%0A%0A
+import static com.google.common.truth.Truth.assertThat;%0A%0Aimport com.google.common.base.Throwables;%0A
 import c
@@ -848,32 +848,33 @@
 isionException;%0A
+%0A
 import org.junit
@@ -1156,79 +1156,67 @@
 java
-x.inject.Inject;%0A%0Aimport static com.google.common.truth.Truth.assertTha
+.util.concurrent.CountDownLatch;%0A%0Aimport javax.inject.Injec
 t;%0A%0A
@@ -2927,24 +2927,1299 @@
 Test);%0A  %7D%0A%0A
+  @Test%0A  public void parallelTestsReceiveDifferentInstances() throws Throwable %7B%0A    final FakeTestClass testOne = new FakeTestClass();%0A    FakeTestClass testTwo = new FakeTestClass();%0A    final CountDownLatch testStarted = new CountDownLatch(1);%0A    final CountDownLatch endTest = new CountDownLatch(1);%0A%0A    Thread testOneThread = new Thread(new Runnable() %7B%0A      @Override public void run() %7B%0A        try %7B%0A          new Acai(EmptyTestModule.class).apply(new Statement() %7B%0A            @Override public void evaluate() throws Throwable %7B%0A              testStarted.countDown();%0A              endTest.await();%0A            %7D%0A          %7D, frameworkMethod, testOne).evaluate();%0A        %7D catch (Throwable throwable) %7B%0A          throw Throwables.propagate(throwable);%0A        %7D%0A      %7D%0A    %7D);%0A%0A    testOneThread.start();%0A%0A    // Wait for test one to be running.%0A    testStarted.await();%0A    new Acai(EmptyTestModule.class).apply(statement, frameworkMethod, testTwo).evaluate();%0A%0A    // Allow test one to complete now that testTwo has run in parallel.%0A    endTest.countDown();%0A    testOneThread.join();%0A%0A    assertThat(testOne.instanceOne).isNotNull();%0A    assertThat(testOne.instanceOne).isNotNull();%0A    assertThat(testOne.instanceOne).isNotSameAs(testTwo.instanceOne);%0A  %7D%0A%0A
   private st

Test that @TestScope works with parallel tests.