/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.core.config;

import static com.datastax.oss.simulacron.common.codec.ConsistencyLevel.QUORUM;
import static com.datastax.oss.simulacron.common.stubbing.PrimeDsl.unavailable;
import static com.datastax.oss.simulacron.common.stubbing.PrimeDsl.when;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;

import com.datastax.oss.driver.api.core.AllNodesFailedException;
import com.datastax.oss.driver.api.core.ConsistencyLevel;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import com.datastax.oss.driver.api.core.config.OptionsMap;
import com.datastax.oss.driver.api.core.config.TypedDriverOption;
import com.datastax.oss.driver.api.core.context.DriverContext;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.metadata.Node;
import com.datastax.oss.driver.api.core.retry.RetryDecision;
import com.datastax.oss.driver.api.core.retry.RetryPolicy;
import com.datastax.oss.driver.api.core.servererrors.CoordinatorException;
import com.datastax.oss.driver.api.core.servererrors.UnavailableException;
import com.datastax.oss.driver.api.core.servererrors.WriteType;
import com.datastax.oss.driver.api.core.session.Request;
import com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule;
import com.datastax.oss.driver.categories.ParallelizableTests;
import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent;
import com.datastax.oss.simulacron.common.cluster.ClusterSpec;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(ParallelizableTests.class)
public class MapBasedConfigLoaderIT {

  @ClassRule
  public static final SimulacronRule SIMULACRON_RULE =
      new SimulacronRule(ClusterSpec.builder().withNodes(1));

  @Before
  public void setup() {
    SIMULACRON_RULE.cluster().clearPrimes(true);
  }

  /**
   * Checks that runtime changes to the pool size are reflected in the driver. This is a special
   * case because unlike other options, the driver does not re-read the option at regular intervals;
   * instead, it relies on the {@link ConfigChangeEvent} being fired.
   */
  @Test
  public void should_resize_pool_when_config_changes() {
    OptionsMap optionsMap = OptionsMap.driverDefaults();

    try (CqlSession session =
        CqlSession.builder()
            .addContactEndPoints(SIMULACRON_RULE.getContactPoints())
            .withLocalDatacenter("dc1")
            .withConfigLoader(DriverConfigLoader.fromMap(optionsMap))
            .build()) {

      Node node = session.getMetadata().getNodes().values().iterator().next();
      assertThat(node.getOpenConnections()).isEqualTo(2); // control connection + pool (default 1)

      optionsMap.put(TypedDriverOption.CONNECTION_POOL_LOCAL_SIZE, 2);

      await()
          .pollInterval(500, TimeUnit.MILLISECONDS)
          .atMost(60, TimeUnit.SECONDS)
          .until(() -> node.getOpenConnections() == 3);
    }
  }

  /** Checks that profiles that have specific policy options will get their own policy instance. */
  @Test
  public void should_create_policies_per_profile() {
    // Given
    // a query that throws UNAVAILABLE
    String mockQuery = "mock query";
    SIMULACRON_RULE.cluster().prime(when(mockQuery).then(unavailable(QUORUM, 3, 2)));

    // a default profile that uses the default retry policy, and an alternate profile that uses a
    // policy that ignores all errors
    OptionsMap optionsMap = OptionsMap.driverDefaults();
    String alternateProfile = "profile1";
    optionsMap.put(
        alternateProfile, TypedDriverOption.RETRY_POLICY_CLASS, IgnoreAllPolicy.class.getName());

    try (CqlSession session =
        CqlSession.builder()
            .addContactEndPoints(SIMULACRON_RULE.getContactPoints())
            .withLocalDatacenter("dc1")
            .withConfigLoader(DriverConfigLoader.fromMap(optionsMap))
            .build()) {

      // When
      // executing the query for the default profile
      SimpleStatement defaultProfileStatement = SimpleStatement.newInstance(mockQuery);
      assertThatThrownBy(() -> session.execute(defaultProfileStatement))
          .satisfies(
              t -> {
                // Then
                // the UNAVAILABLE error is surfaced
                assertThat(t).isInstanceOf(AllNodesFailedException.class);
                AllNodesFailedException anfe = (AllNodesFailedException) t;
                assertThat(anfe.getAllErrors()).hasSize(1);
                List<Throwable> nodeErrors = anfe.getAllErrors().values().iterator().next();
                assertThat(nodeErrors).hasSize(1);
                assertThat(nodeErrors.get(0)).isInstanceOf(UnavailableException.class);
              });

      // When
      // executing the query for the alternate profile
      SimpleStatement alternateProfileStatement =
          SimpleStatement.newInstance(mockQuery).setExecutionProfileName(alternateProfile);
      ResultSet rs = session.execute(alternateProfileStatement);

      // Then
      // the error is ignored
      assertThat(rs.one()).isNull();
    }
  }

  public static class IgnoreAllPolicy implements RetryPolicy {

    public IgnoreAllPolicy(
        @SuppressWarnings("unused") DriverContext context,
        @SuppressWarnings("unused") String profile) {
      // nothing to do
    }

    @Override
    @Deprecated
    public RetryDecision onReadTimeout(
        @NonNull Request request,
        @NonNull ConsistencyLevel cl,
        int blockFor,
        int received,
        boolean dataPresent,
        int retryCount) {
      return RetryDecision.IGNORE;
    }

    @Override
    @Deprecated
    public RetryDecision onWriteTimeout(
        @NonNull Request request,
        @NonNull ConsistencyLevel cl,
        @NonNull WriteType writeType,
        int blockFor,
        int received,
        int retryCount) {
      return RetryDecision.IGNORE;
    }

    @Override
    @Deprecated
    public RetryDecision onUnavailable(
        @NonNull Request request,
        @NonNull ConsistencyLevel cl,
        int required,
        int alive,
        int retryCount) {
      return RetryDecision.IGNORE;
    }

    @Override
    @Deprecated
    public RetryDecision onRequestAborted(
        @NonNull Request request, @NonNull Throwable error, int retryCount) {
      return RetryDecision.IGNORE;
    }

    @Override
    @Deprecated
    public RetryDecision onErrorResponse(
        @NonNull Request request, @NonNull CoordinatorException error, int retryCount) {
      return RetryDecision.IGNORE;
    }

    @Override
    public void close() {
      // nothing to do
    }
  }
}
