/*
 * 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.driver.core;

import static com.datastax.driver.core.Assertions.assertThat;
import static com.datastax.driver.core.CreateCCM.TestMode.PER_METHOD;
import static com.datastax.driver.core.TestUtils.waitForUp;

import com.google.common.collect.Maps;
import java.util.Map;
import org.testng.annotations.Test;

@CreateCCM(PER_METHOD)
public class MetadataTest extends CCMTestsSupport {

  /**
   * Validates that when the topology of the cluster changes that Cluster Metadata is properly
   * updated.
   *
   * <p>
   *
   * <p>This test does the following:
   *
   * <p>
   *
   * <ol>
   *   <li>Creates a 3 node cluster and capture token range data from the {@link Cluster}'s {@link
   *       Metadata}
   *   <li>Decommission node 3.
   *   <li>Validates that the token range data was updated to reflect node 3 leaving and the other
   *       nodes taking on its token range.
   *   <li>Adds a new node, node 4.
   *   <li>Validates that the token range data was updated to reflect node 4 joining the {@link
   *       Cluster} and the token ranges reflecting this.
   * </ol>
   *
   * @test_category metadata:token
   * @expected_result cluster metadata is properly updated in response to node remove and add
   *     events.
   * @jira_ticket JAVA-312
   * @since 2.0.10, 2.1.5
   */
  @Test(groups = "long")
  @CCMConfig(numberOfNodes = 3, dirtiesContext = true, createCluster = false)
  public void should_update_metadata_on_topology_change() {
    Cluster cluster = register(createClusterBuilderNoDebouncing().build());
    Session session = cluster.connect();

    String keyspace = "test";
    session.execute(
        "CREATE KEYSPACE "
            + keyspace
            + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}");
    Metadata metadata = cluster.getMetadata();

    // Capture all Token data.
    assertThat(metadata.getTokenRanges()).hasSize(3);
    Map<Host, Token> tokensForHost = getTokenForHosts(metadata);

    // Capture host3s token and range before we take it down.
    Host host3 = TestUtils.findHost(cluster, 3);
    Token host3Token = tokensForHost.get(host3);

    ccm().decommission(3);
    ccm().remove(3);

    // Ensure that the token ranges were updated, there should only be 2 ranges now.
    assertThat(metadata.getTokenRanges()).hasSize(2);

    // The token should not be present for any Host.
    assertThat(getTokenForHosts(metadata)).doesNotContainValue(host3Token);

    // The ring should be fully accounted for.
    assertThat(cluster).hasValidTokenRanges("test");
    assertThat(cluster).hasValidTokenRanges();

    // Add an additional node.
    ccm().add(4);
    ccm().start(4);
    waitForUp(TestUtils.IP_PREFIX + '4', cluster);

    // Ensure that the token ranges were updated, there should only be 3 ranges now.
    assertThat(metadata.getTokenRanges()).hasSize(3);

    Host host4 = TestUtils.findHost(cluster, 4);
    TokenRange host4Range = metadata.getTokenRanges(keyspace, host4).iterator().next();

    // Ensure no host token range intersects with node 4.
    for (Host host : metadata.getAllHosts()) {
      if (!host.equals(host4)) {
        TokenRange hostRange = metadata.getTokenRanges(keyspace, host).iterator().next();
        assertThat(host4Range).doesNotIntersect(hostRange);
      }
    }

    // The ring should be fully accounted for.
    assertThat(cluster).hasValidTokenRanges("test");
    assertThat(cluster).hasValidTokenRanges();
  }

  /** @return A mapping of Host -> Token for each Host in the given {@link Metadata} */
  private Map<Host, Token> getTokenForHosts(Metadata metadata) {
    Map<Host, Token> tokensByHost = Maps.newHashMap();
    for (Host host : metadata.getAllHosts()) {
      tokensByHost.put(host, host.getTokens().iterator().next());
    }
    return tokensByHost;
  }

  @Test(groups = "unit")
  public void handleId_should_lowercase_unquoted_alphanumeric_identifiers() {
    assertThat(Metadata.handleId("FooBar1")).isEqualTo("foobar1");
    assertThat(Metadata.handleId("Foo_Bar_1")).isEqualTo("foo_bar_1");
    assertThat(Metadata.handleId("foo_bar_1")).isEqualTo("foo_bar_1");
  }

  @Test(groups = "unit")
  public void handleId_should_unquote_and_preserve_case_of_quoted_identifiers() {
    assertThat(Metadata.handleId("\"FooBar1\"")).isEqualTo("FooBar1");
    assertThat(Metadata.handleId("\"Foo_Bar_1\"")).isEqualTo("Foo_Bar_1");
    assertThat(Metadata.handleId("\"Foo Bar 1\"")).isEqualTo("Foo Bar 1");
  }

  @Test(groups = "unit")
  public void handleId_should_unescape_duplicate_double_quotes_in_quoted_identifiers() {
    assertThat(Metadata.handleId("\"Foo\"\"Bar\"")).isEqualTo("Foo\"Bar");
  }

  @Test(groups = "unit")
  public void handleId_should_preserve_unquoted_non_alphanumeric_identifiers() {
    assertThat(Metadata.handleId("Foo Bar")).isEqualTo("Foo Bar");
  }

  @Test(groups = "unit")
  public void escapeId_should_not_quote_lowercase_identifiers() {
    String id = "this_does_not_need_quoting_0123456789abcdefghijklmnopqrstuvwxyz";
    assertThat(Metadata.quoteIfNecessary(id)).isEqualTo(id);
  }

  @Test(groups = "unit")
  public void escapeId_should_quote_non_lowercase_identifiers() {
    assertThat(Metadata.quoteIfNecessary("This_Needs_Quoting_1234"))
        .isEqualTo("\"This_Needs_Quoting_1234\"");
    assertThat(Metadata.quoteIfNecessary("This Needs Quoting 1234!!"))
        .isEqualTo("\"This Needs Quoting 1234!!\"");
  }

  @Test(groups = "unit")
  public void escapeId_should_quote_reserved_cql_keywords() {
    assertThat(Metadata.quoteIfNecessary("columnfamily")).isEqualTo("\"columnfamily\"");
  }

  /** @jira_ticket JAVA-2174 */
  @Test(groups = "unit")
  public void escapeId_should_quote_empty_keyword() {
    assertThat(Metadata.quoteIfNecessary("")).isEqualTo("\"\"");
  }

  @Test(groups = "unit")
  public void should_detect_reserved_keywords_in_upper_case() {
    assertThat(Metadata.isReservedCqlKeyword("COLUMNFAMILY")).isTrue();
    assertThat(Metadata.isReservedCqlKeyword("TEST_COLUMNFAMILY")).isFalse();
  }

  @Test(groups = "unit")
  public void should_detect_reserved_keywords_in_lower_case() {
    assertThat(Metadata.isReservedCqlKeyword("columnfamily")).isTrue();
    assertThat(Metadata.isReservedCqlKeyword("test_columnfamily")).isFalse();
  }

  @Test(groups = "unit")
  public void should_detect_reserved_keywords_in_mixed_case() {
    assertThat(Metadata.isReservedCqlKeyword("ColumnFamily")).isTrue();
    assertThat(Metadata.isReservedCqlKeyword("Test_ColumnFamily")).isFalse();
  }
}
