/*
 * Copyright 2017 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.cloud.spanner;

import static com.google.common.base.Preconditions.checkState;

import com.google.cloud.RetryOption;
import com.google.cloud.spanner.testing.RemoteSpannerHelper;
import com.google.common.collect.Iterators;
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.rules.ExternalResource;
import org.threeten.bp.Duration;

/**
 * JUnit 4 test rule that provides access to a Cloud Spanner instance to use for tests, and allows
 * uniquely named (per {@code IntegrationTestEnv} instance) test databases to be created within that
 * instance. An existing instance can be used by naming it in the {@link #TEST_INSTANCE_PROPERTY}
 * property; if the property is not set, an instance will be created and destroyed by the rule.
 *
 * <p>This class is normally used as a {@code @ClassRule}.
 */
public class IntegrationTestEnv extends ExternalResource {

  /** Names a property that provides the class name of the {@link TestEnvConfig} to use. */
  public static final String TEST_ENV_CONFIG_CLASS_NAME = "spanner.testenv.config.class";

  /**
   * Names a property that, if set, identifies an existing Cloud Spanner instance to use for tests.
   */
  public static final String TEST_INSTANCE_PROPERTY = "spanner.testenv.instance";

  private static final Logger logger = Logger.getLogger(IntegrationTestEnv.class.getName());

  private TestEnvConfig config;
  private InstanceAdminClient instanceAdminClient;
  private boolean isOwnedInstance;
  private RemoteSpannerHelper testHelper;

  public RemoteSpannerHelper getTestHelper() {
    checkInitialized();
    return testHelper;
  }

  @SuppressWarnings("unchecked")
  protected void initializeConfig()
      throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    String configClassName = System.getProperty(TEST_ENV_CONFIG_CLASS_NAME, null);
    if (configClassName == null) {
      throw new NullPointerException("Property " + TEST_ENV_CONFIG_CLASS_NAME + " needs to be set");
    }
    Class<? extends TestEnvConfig> configClass;
    configClass = (Class<? extends TestEnvConfig>) Class.forName(configClassName);
    config = configClass.newInstance();
  }

  @Override
  protected void before() throws Throwable {
    this.initializeConfig();
    this.config.setUp();

    SpannerOptions options = config.spannerOptions();
    String instanceProperty = System.getProperty(TEST_INSTANCE_PROPERTY, "");
    InstanceId instanceId;
    if (!instanceProperty.isEmpty()) {
      instanceId = InstanceId.of(instanceProperty);
      isOwnedInstance = false;
      logger.log(Level.INFO, "Using existing test instance: {0}", instanceId);
    } else {
      instanceId =
          InstanceId.of(config.spannerOptions().getProjectId(), "test-instance");
      isOwnedInstance = true;
    }
    testHelper = RemoteSpannerHelper.create(options, instanceId);
    instanceAdminClient = testHelper.getClient().getInstanceAdminClient();
    logger.log(Level.FINE, "Test env endpoint is {0}", options.getHost());
    if (isOwnedInstance) {
      initializeInstance(instanceId);
    }
  }

  @Override
  protected void after() {
    cleanUpInstance();
    this.config.tearDown();
  }

  private void initializeInstance(InstanceId instanceId) {
    InstanceConfig instanceConfig =
        Iterators.get(instanceAdminClient.listInstanceConfigs().iterateAll().iterator(), 0, null);
    checkState(instanceConfig != null, "No instance configs found");

    InstanceConfigId configId = instanceConfig.getId();
    logger.log(Level.FINE, "Creating instance using config {0}", configId);
    InstanceInfo instance =
        InstanceInfo.newBuilder(instanceId)
            .setNodeCount(1)
            .setDisplayName("Test instance")
            .setInstanceConfigId(configId)
            .build();
    Operation<Instance, CreateInstanceMetadata> op = instanceAdminClient.createInstance(instance);
    op = op.waitFor(RetryOption.initialRetryDelay(Duration.ofMillis(500L)));
    Instance createdInstance = op.getResult();
    logger.log(Level.INFO, "Created test instance: {0}", createdInstance.getId());
  }

  private void cleanUpInstance() {
    if (isOwnedInstance) {
      // Delete the instance, which implicitly drops all databases in it.
      try {
        logger.log(Level.FINE, "Deleting test instance {0}", testHelper.getInstanceId());
        instanceAdminClient.deleteInstance(testHelper.getInstanceId().getInstance());
        logger.log(Level.INFO, "Deleted test instance {0}", testHelper.getInstanceId());
      } catch (SpannerException e) {
        logger.log(Level.SEVERE, "Failed to delete test instance " + testHelper.getInstanceId(), e);
      }
    } else {
      testHelper.cleanUp();
    }
  }

  private void checkInitialized() {
    checkState(testHelper != null, "Setup has not completed successfully");
  }
}
