/*
 * Copyright DataStax, Inc.
 *
 * 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.datastax.oss.driver.internal.core.config.typesafe;

import static com.datastax.oss.driver.Assertions.assertThat;
import static com.datastax.oss.driver.Assertions.assertThatStage;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.datastax.oss.driver.api.core.DefaultConsistencyLevel;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.config.DriverConfig;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent;
import com.datastax.oss.driver.internal.core.config.MockOptions;
import com.datastax.oss.driver.internal.core.context.EventBus;
import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
import com.datastax.oss.driver.internal.core.context.NettyOptions;
import com.datastax.oss.driver.internal.core.util.concurrent.ScheduledTaskCapturingEventLoop;
import com.datastax.oss.driver.internal.core.util.concurrent.ScheduledTaskCapturingEventLoop.CapturedTask;
import com.typesafe.config.ConfigFactory;
import io.netty.channel.EventLoopGroup;
import java.io.File;
import java.time.Duration;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class DefaultDriverConfigLoaderTest {

  @Mock private InternalDriverContext context;
  @Mock private NettyOptions nettyOptions;
  @Mock private EventLoopGroup adminEventExecutorGroup;
  @Mock private DriverConfig config;
  @Mock private DriverExecutionProfile defaultProfile;
  private ScheduledTaskCapturingEventLoop adminExecutor;
  private EventBus eventBus;
  private AtomicReference<String> configSource;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);

    when(context.getSessionName()).thenReturn("test");
    when(context.getNettyOptions()).thenReturn(nettyOptions);
    when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventExecutorGroup);

    adminExecutor = new ScheduledTaskCapturingEventLoop(adminEventExecutorGroup);
    when(adminEventExecutorGroup.next()).thenReturn(adminExecutor);

    eventBus = spy(new EventBus("test"));
    when(context.getEventBus()).thenReturn(eventBus);

    // The already loaded config in the context.
    // In real life, it's the object managed by the loader, but in this test it's simpler to mock
    // it.
    when(context.getConfig()).thenReturn(config);
    when(config.getDefaultProfile()).thenReturn(defaultProfile);
    when(defaultProfile.getDuration(DefaultDriverOption.CONFIG_RELOAD_INTERVAL))
        .thenReturn(Duration.ofSeconds(12));

    configSource = new AtomicReference<>("int1 = 42");
  }

  @Test
  public void should_build_initial_config() {
    DefaultDriverConfigLoader loader =
        new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get()));
    DriverConfig initialConfig = loader.getInitialConfig();
    assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42);
  }

  @Test
  public void should_schedule_reloading_task() {
    DefaultDriverConfigLoader loader =
        new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get()));

    loader.onDriverInit(context);
    adminExecutor.waitForNonScheduledTasks();

    CapturedTask<?> task = adminExecutor.nextTask();
    assertThat(task.getInitialDelay(TimeUnit.SECONDS)).isEqualTo(12);
    assertThat(task.getPeriod(TimeUnit.SECONDS)).isEqualTo(12);
  }

  @Test
  public void should_detect_config_change_from_periodic_reload() {
    DefaultDriverConfigLoader loader =
        new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get()));
    DriverConfig initialConfig = loader.getInitialConfig();
    assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42);

    loader.onDriverInit(context);
    adminExecutor.waitForNonScheduledTasks();

    CapturedTask<?> task = adminExecutor.nextTask();

    configSource.set("int1 = 43");

    task.run();

    assertThat(initialConfig).hasIntOption(MockOptions.INT1, 43);
    verify(eventBus).fire(ConfigChangeEvent.INSTANCE);
  }

  @Test
  public void should_detect_config_change_from_manual_reload() {
    DefaultDriverConfigLoader loader =
        new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get()));
    DriverConfig initialConfig = loader.getInitialConfig();
    assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42);

    loader.onDriverInit(context);
    adminExecutor.waitForNonScheduledTasks();

    configSource.set("int1 = 43");

    CompletionStage<Boolean> reloaded = loader.reload();
    adminExecutor.waitForNonScheduledTasks();

    assertThat(initialConfig).hasIntOption(MockOptions.INT1, 43);
    verify(eventBus).fire(ConfigChangeEvent.INSTANCE);
    assertThatStage(reloaded).isSuccess(changed -> assertThat(changed).isTrue());
  }

  @Test
  public void should_not_notify_from_periodic_reload_if_config_has_not_changed() {
    DefaultDriverConfigLoader loader =
        new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get()));
    DriverConfig initialConfig = loader.getInitialConfig();
    assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42);

    loader.onDriverInit(context);
    adminExecutor.waitForNonScheduledTasks();

    CapturedTask<?> task = adminExecutor.nextTask();

    // no change to the config source

    task.run();

    verify(eventBus, never()).fire(ConfigChangeEvent.INSTANCE);
  }

  @Test
  public void should_not_notify_from_manual_reload_if_config_has_not_changed() {
    DefaultDriverConfigLoader loader =
        new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get()));
    DriverConfig initialConfig = loader.getInitialConfig();
    assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42);

    loader.onDriverInit(context);
    adminExecutor.waitForNonScheduledTasks();

    CompletionStage<Boolean> reloaded = loader.reload();
    adminExecutor.waitForNonScheduledTasks();

    verify(eventBus, never()).fire(ConfigChangeEvent.INSTANCE);
    assertThatStage(reloaded).isSuccess(changed -> assertThat(changed).isFalse());
  }

  @Test
  public void should_load_from_other_classpath_resource() {
    DriverConfigLoader loader = DriverConfigLoader.fromClasspath("config/customApplication");
    DriverExecutionProfile config = loader.getInitialConfig().getDefaultProfile();
    // From customApplication.conf:
    assertThat(config.getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
        .isEqualTo(Duration.ofSeconds(5));
    // From customApplication.json:
    assertThat(config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(2000);
    // From customApplication.properties:
    assertThat(config.getString(DefaultDriverOption.REQUEST_CONSISTENCY))
        .isEqualTo(DefaultConsistencyLevel.ONE.name());
    // From reference.conf:
    assertThat(config.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY))
        .isEqualTo(DefaultConsistencyLevel.SERIAL.name());
  }

  @Test
  public void should_load_from_file() {
    File file = new File("src/test/resources/config/customApplication.conf");
    assertThat(file).exists();
    DriverConfigLoader loader = DriverConfigLoader.fromFile(file);
    DriverExecutionProfile config = loader.getInitialConfig().getDefaultProfile();
    // From customApplication.conf:
    assertThat(config.getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
        .isEqualTo(Duration.ofSeconds(5));
    // From reference.conf:
    assertThat(config.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY))
        .isEqualTo(DefaultConsistencyLevel.SERIAL.name());
  }

  @Test
  public void should_load_from_file_with_system_property() {
    File file = new File("src/test/resources/config/customApplication.conf");
    assertThat(file).exists();
    System.setProperty("config.file", file.getAbsolutePath());
    try {
      DriverConfigLoader loader = new DefaultDriverConfigLoader();
      DriverExecutionProfile config = loader.getInitialConfig().getDefaultProfile();
      // From customApplication.conf:
      assertThat(config.getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
          .isEqualTo(Duration.ofSeconds(5));
      // From reference.conf:
      assertThat(config.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY))
          .isEqualTo(DefaultConsistencyLevel.SERIAL.name());
    } finally {
      System.clearProperty("config.file");
    }
  }

  @Test
  public void should_return_failed_future_if_reloading_not_supported() {
    DefaultDriverConfigLoader loader =
        new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get()), false);
    assertThat(loader.supportsReloading()).isFalse();
    CompletionStage<Boolean> stage = loader.reload();
    assertThatStage(stage)
        .isFailed(
            t ->
                assertThat(t)
                    .isInstanceOf(UnsupportedOperationException.class)
                    .hasMessage(
                        "This instance of DefaultDriverConfigLoader does not support reloading"));
  }

  /** Test for JAVA-2846. */
  @Test
  public void should_load_setting_from_system_property_when_application_conf_is_also_provided() {
    System.setProperty("datastax-java-driver.basic.request.timeout", "1 millisecond");
    try {
      assertThat(
              new DefaultDriverConfigLoader()
                  .getInitialConfig()
                  .getDefaultProfile()
                  .getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
          .isEqualTo(Duration.ofMillis(1));
    } finally {
      System.clearProperty("datastax-java-driver.basic.request.timeout");
    }
  }

  /** Test for JAVA-2846. */
  @Test
  public void
      should_load_and_resolve_setting_from_system_property_when_application_conf_is_also_provided() {
    System.setProperty(
        "datastax-java-driver.advanced.connection.init-query-timeout", "1234 milliseconds");
    try {
      assertThat(
              new DefaultDriverConfigLoader()
                  .getInitialConfig()
                  .getDefaultProfile()
                  .getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
          .isEqualTo(Duration.ofMillis(1234));
    } finally {
      System.clearProperty("datastax-java-driver.advanced.connection.init-query-timeout");
    }
  }

  /** Test for JAVA-2846. */
  @Test
  public void
      should_load_setting_from_system_property_when_application_conf_is_also_provided_for_custom_classloader() {
    System.setProperty("datastax-java-driver.basic.request.timeout", "1 millisecond");
    try {
      assertThat(
              new DefaultDriverConfigLoader(Thread.currentThread().getContextClassLoader())
                  .getInitialConfig()
                  .getDefaultProfile()
                  .getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
          .isEqualTo(Duration.ofMillis(1));
    } finally {
      System.clearProperty("datastax-java-driver.basic.request.timeout");
    }
  }

  @Test
  public void should_create_from_string() {
    DriverExecutionProfile config =
        DriverConfigLoader.fromString(
                "datastax-java-driver.basic { session-name = my-app\nrequest.timeout = 1 millisecond }")
            .getInitialConfig()
            .getDefaultProfile();

    assertThat(config.getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("my-app");
    assertThat(config.getDuration(DefaultDriverOption.REQUEST_TIMEOUT))
        .isEqualTo(Duration.ofMillis(1));
    // Any option not in the string should be pulled from reference.conf
    assertThat(config.getString(DefaultDriverOption.REQUEST_CONSISTENCY)).isEqualTo("LOCAL_ONE");
  }
}
