content: /*
   * 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.metadata;
  
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assertions.fail;
  
  import com.datastax.dse.driver.internal.core.metadata.schema.DefaultDseKeyspaceMetadata;
  import com.datastax.dse.driver.internal.core.metadata.schema.DefaultDseTableMetadata;
  import com.datastax.oss.driver.api.core.CqlSession;
  import com.datastax.oss.driver.api.core.Version;
  import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
  import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;
  import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;
  import com.datastax.oss.driver.api.testinfra.ccm.CcmRule;
  import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer;
  import com.datastax.oss.driver.api.testinfra.requirement.BackendType;
  import com.datastax.oss.driver.api.testinfra.session.SessionRule;
  import com.datastax.oss.driver.api.testinfra.session.SessionUtils;
  import com.datastax.oss.driver.categories.ParallelizableTests;
  import com.datastax.oss.driver.internal.SerializationHelper;
  import com.datastax.oss.driver.internal.core.metadata.schema.DefaultKeyspaceMetadata;
  import com.datastax.oss.driver.internal.core.metadata.schema.DefaultTableMetadata;
  import com.datastax.oss.driver.shaded.guava.common.base.Charsets;
  import com.datastax.oss.driver.shaded.guava.common.base.Splitter;
  import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
  import com.google.common.io.Files;
  import java.io.File;
  import java.io.IOException;
  import java.net.URL;
  import java.time.Duration;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.regex.Pattern;
  import org.junit.BeforeClass;
  import org.junit.ClassRule;
  import org.junit.Test;
  import org.junit.experimental.categories.Category;
  import org.junit.rules.RuleChain;
  import org.junit.rules.TestRule;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  @Category(ParallelizableTests.class)
  public class DescribeIT {
  
    private static final Logger LOG = LoggerFactory.getLogger(DescribeIT.class);
  
    private static final CcmRule CCM_RULE = CcmRule.getInstance();
  
    private static final SessionRule<CqlSession> SESSION_RULE =
        SessionRule.builder(CCM_RULE)
            .withConfigLoader(
                SessionUtils.configLoaderBuilder()
                    .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(30))
                    // disable debouncer to speed up test.
                    .withDuration(DefaultDriverOption.METADATA_SCHEMA_WINDOW, Duration.ofSeconds(0))
                    .build())
            .build();
  
    @ClassRule
    public static final TestRule CHAIN = RuleChain.outerRule(CCM_RULE).around(SESSION_RULE);
  
    private static final Splitter STATEMENT_SPLITTER =
        // Use a regex to ignore semicolons in function scripts
        Splitter.on(Pattern.compile(";\n")).omitEmptyStrings();
  
    private static Version serverVersion;
  
    private static final Map<BackendType, String> scriptFileForBackend =
        ImmutableMap.<BackendType, String>builder()
            .put(BackendType.CASSANDRA, "DescribeIT/oss")
            .put(BackendType.DSE, "DescribeIT/dse")
            .put(BackendType.HCD, "DescribeIT/hcd")
            .build();
  
    private static File scriptFile;
    private static String scriptContents;
  
    @BeforeClass
    public static void setup() {
      serverVersion =
          CCM_RULE.isDistributionOf(BackendType.CASSANDRA)
              ? CCM_RULE.getCassandraVersion().nextStable()
              : CCM_RULE.getDistributionVersion().nextStable();
  
      scriptFile = getScriptFile();
      assertThat(scriptFile).exists();
      assertThat(scriptFile).isFile();
      assertThat(scriptFile).canRead();
      scriptContents = getScriptContents();
  
      setupDatabase();
    }
  
    @Test
    public void describe_output_should_match_creation_script() throws Exception {
  
      CqlSession session = SESSION_RULE.session();
  
      KeyspaceMetadata keyspaceMetadata =
          session.getMetadata().getKeyspace(SESSION_RULE.keyspace()).orElseThrow(AssertionError::new);
      String describeOutput = keyspaceMetadata.describeWithChildren(true).trim();
  
      assertThat(describeOutput)
          .as(
              "Describe output doesn't match create statements, "
                  + "maybe you need to add a new script in integration-tests/src/test/resources. "
                  + "Server version = %s %s, used script = %s",
              CCM_RULE.getDistribution(), serverVersion, scriptFile)
          .isEqualTo(scriptContents);
    }
  
    private boolean atLeastVersion(Version dseVersion, Version ossVersion) {
      Version comparison = CCM_RULE.isDistributionOf(BackendType.DSE) ? dseVersion : ossVersion;
      return serverVersion.compareTo(comparison) >= 0;
    }
  
    @Test
    public void keyspace_metadata_should_be_serializable() throws Exception {
  
      CqlSession session = SESSION_RULE.session();
  
      Optional<KeyspaceMetadata> ksOption =
          session.getMetadata().getKeyspace(session.getKeyspace().get());
      assertThat(ksOption).isPresent();
      KeyspaceMetadata ks = ksOption.get();
      assertThat(ks).isInstanceOfAny(DefaultKeyspaceMetadata.class, DefaultDseKeyspaceMetadata.class);
  
      /* Validate that the keyspace metadata is fully populated */
      assertThat(ks.getUserDefinedTypes()).isNotEmpty();
      assertThat(ks.getTables()).isNotEmpty();
      if (atLeastVersion(Version.V5_0_0, Version.V3_0_0)) {
        assertThat(ks.getViews()).isNotEmpty();
      }
      if (atLeastVersion(Version.V5_0_0, Version.V2_2_0)) {
        assertThat(ks.getFunctions()).isNotEmpty();
        assertThat(ks.getAggregates()).isNotEmpty();
      }
  
      /* A table with an explicit compound primary key + specified clustering column */
      Optional<TableMetadata> tableOption = ks.getTable("rank_by_year_and_name");
      assertThat(tableOption).isPresent();
      TableMetadata table = tableOption.get();
      assertThat(table).isInstanceOfAny(DefaultTableMetadata.class, DefaultDseTableMetadata.class);
  
      /* Validate that the table metadata is fully populated */
      assertThat(table.getPartitionKey()).isNotEmpty();
      assertThat(table.getClusteringColumns()).isNotEmpty();
      assertThat(table.getColumns()).isNotEmpty();
      assertThat(table.getOptions()).isNotEmpty();
      assertThat(table.getIndexes()).isNotEmpty();
  
      KeyspaceMetadata deserialized = SerializationHelper.serializeAndDeserialize(ks);
      assertThat(deserialized).isEqualTo(ks);
    }
  
    /**
     * Find a creation script in our test resources that matches the current server version. If we
     * don't have an exact match, use the closest version below it.
     */
    private static File getScriptFile() {
      URL logbackTestUrl = DescribeIT.class.getResource("/logback-test.xml");
      if (logbackTestUrl == null || logbackTestUrl.getFile().isEmpty()) {
        fail(
            "Expected to use logback-test.xml to determine location of "
                + "target/test-classes, but got URL %s",
            logbackTestUrl);
      }
      File resourcesDir = new File(logbackTestUrl.getFile()).getParentFile();
      File scriptsDir = new File(resourcesDir, scriptFileForBackend.get(CCM_RULE.getDistribution()));
      LOG.debug("Looking for a matching script in directory {}", scriptsDir);
  
      File[] candidates = scriptsDir.listFiles();
      assertThat(candidates).isNotNull();
  
      File bestFile = null;
      Version bestVersion = null;
      for (File candidate : candidates) {
        String fileName = candidate.getName();
        String candidateVersionString = fileName.substring(0, fileName.lastIndexOf('.'));
        Version candidateVersion = Version.parse(candidateVersionString);
        LOG.debug("Considering {}, which resolves to version {}", fileName, candidateVersion);
        if (candidateVersion.compareTo(serverVersion) > 0) {
          LOG.debug("too high, discarding");
        } else if (bestVersion != null && bestVersion.compareTo(candidateVersion) >= 0) {
          LOG.debug("not higher than {}, discarding", bestVersion);
        } else {
          LOG.debug("best so far");
          bestVersion = candidateVersion;
          bestFile = candidate;
        }
      }
      assertThat(bestFile)
          .as("Could not find create script with version <= %s in %s", serverVersion, scriptsDir)
          .isNotNull();
  
      LOG.info("Using {} to test against {} {}", bestFile, CCM_RULE.getDistribution(), serverVersion);
      return bestFile;
    }
  
    private static String getScriptContents() {
  
      try {
  
        return Files.asCharSource(scriptFile, Charsets.UTF_8)
            .read()
            .trim()
            .replaceAll("ks_0", SESSION_RULE.keyspace().asCql(true));
      } catch (IOException ioe) {
        fail("Exception reading script file " + scriptFile, ioe);
        return null;
      }
    }
  
    private static void setupDatabase() {
      List<String> statements = STATEMENT_SPLITTER.splitToList(scriptContents);
      SchemaChangeSynchronizer.withLock(
          () -> {
            // Skip the first statement (CREATE KEYSPACE), we already have a keyspace
            for (int i = 1; i < statements.size(); i++) {
              String statement = statements.get(i);
              try {
                SESSION_RULE.session().execute(statement);
              } catch (Exception e) {
                fail("Error executing statement %s (%s)", statement, e);
              }
            }
          });
    }
  }
  