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

import static org.assertj.core.api.Assertions.assertThat;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.cql.AsyncResultSet;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException;
import com.datastax.oss.driver.api.testinfra.ccm.CcmRule;
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.core.util.concurrent.CompletableFutures;
import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.RuleChain;
import org.junit.rules.TestName;
import org.junit.rules.TestRule;

@Category(ParallelizableTests.class)
public class SimpleStatementCcmIT {

  private static final CcmRule CCM_RULE = CcmRule.getInstance();

  private static final SessionRule<CqlSession> SESSION_RULE =
      SessionRule.builder(CCM_RULE)
          .withConfigLoader(
              SessionUtils.configLoaderBuilder()
                  .withInt(DefaultDriverOption.REQUEST_PAGE_SIZE, 20)
                  .build())
          .build();

  @ClassRule
  public static final TestRule CHAIN = RuleChain.outerRule(CCM_RULE).around(SESSION_RULE);

  @Rule public TestName name = new TestName();

  private static final String KEY = "test";

  @BeforeClass
  public static void setupSchema() {
    // table where every column forms the primary key.
    SESSION_RULE
        .session()
        .execute(
            SimpleStatement.builder(
                    "CREATE TABLE IF NOT EXISTS test (k text, v int, PRIMARY KEY(k, v))")
                .setExecutionProfile(SESSION_RULE.slowProfile())
                .build());
    for (int i = 0; i < 100; i++) {
      SESSION_RULE
          .session()
          .execute(
              SimpleStatement.builder("INSERT INTO test (k, v) VALUES (?, ?)")
                  .addPositionalValues(KEY, i)
                  .build());
    }

    // table with simple primary key, single cell.
    SESSION_RULE
        .session()
        .execute(
            SimpleStatement.builder("CREATE TABLE IF NOT EXISTS test2 (k text primary key, v int)")
                .setExecutionProfile(SESSION_RULE.slowProfile())
                .build());
  }

  @Test
  public void should_use_paging_state_when_copied() {
    Statement<?> st =
        SimpleStatement.builder(String.format("SELECT v FROM test WHERE k='%s'", KEY)).build();
    ResultSet result = SESSION_RULE.session().execute(st);

    // given a query created from a copy of a previous query with paging state from previous queries
    // response.
    st = st.copy(result.getExecutionInfo().getPagingState());

    // when executing that query.
    result = SESSION_RULE.session().execute(st);

    // then the response should start on the page boundary.
    assertThat(result.iterator().next().getInt("v")).isEqualTo(20);
  }

  @Test
  public void should_use_paging_state_when_provided_to_new_statement() {
    Statement<?> st =
        SimpleStatement.builder(String.format("SELECT v FROM test WHERE k='%s'", KEY)).build();
    ResultSet result = SESSION_RULE.session().execute(st);

    // given a query created from a copy of a previous query with paging state from previous queries
    // response.
    st =
        SimpleStatement.builder(String.format("SELECT v FROM test where k='%s'", KEY))
            .setPagingState(result.getExecutionInfo().getPagingState())
            .build();

    // when executing that query.
    result = SESSION_RULE.session().execute(st);

    // then the response should start on the page boundary.
    assertThat(result.iterator().next().getInt("v")).isEqualTo(20);
  }

  @Test
  @Ignore
  public void should_fail_if_using_paging_state_from_different_query() {
    Statement<?> st =
        SimpleStatement.builder("SELECT v FROM test WHERE k=:k").addNamedValue("k", KEY).build();
    ResultSet result = SESSION_RULE.session().execute(st);

    // TODO Expect PagingStateException

    // given a new different query and providing the paging state from the previous query
    // then an exception should be thrown indicating incompatible paging state
    SimpleStatement.builder("SELECT v FROM test")
        .setPagingState(result.getExecutionInfo().getPagingState())
        .build();
  }

  @Test
  public void should_use_timestamp_when_set() {
    // given inserting data with a timestamp 40 days in the past.
    long timestamp = System.currentTimeMillis() - TimeUnit.MILLISECONDS.convert(40, TimeUnit.DAYS);
    SimpleStatement insert =
        SimpleStatement.builder("INSERT INTO test2 (k, v) values (?, ?)")
            .addPositionalValues(name.getMethodName(), 0)
            .setQueryTimestamp(timestamp)
            .build();

    SESSION_RULE.session().execute(insert);

    // when retrieving writetime of cell from that insert.
    SimpleStatement select =
        SimpleStatement.builder("SELECT writetime(v) as wv from test2 where k = ?")
            .addPositionalValue(name.getMethodName())
            .build();

    ResultSet result = SESSION_RULE.session().execute(select);
    List<Row> rows = result.all();
    assertThat(rows).hasSize(1);

    // then the writetime should equal the timestamp provided.
    Row row = rows.iterator().next();
    assertThat(row.getLong("wv")).isEqualTo(timestamp);
  }

  @Test
  @Ignore
  public void should_use_tracing_when_set() {
    // TODO currently there's no way to validate tracing was set since trace id is not set
    // also write test to verify it is not set.
    SESSION_RULE
        .session()
        .execute(SimpleStatement.builder("select * from test").setTracing().build());
  }

  @Test
  public void should_use_positional_values() {
    // given a statement with positional values
    SimpleStatement insert =
        SimpleStatement.builder("INSERT into test2 (k, v) values (?, ?)")
            .addPositionalValue(name.getMethodName())
            .addPositionalValue(4)
            .build();

    // when executing that statement
    SESSION_RULE.session().execute(insert);

    // then we should be able to retrieve the data as inserted.
    SimpleStatement select =
        SimpleStatement.builder("select k,v from test2 where k=?")
            .addPositionalValue(name.getMethodName())
            .build();

    ResultSet result = SESSION_RULE.session().execute(select);
    List<Row> rows = result.all();
    assertThat(rows).hasSize(1);

    Row row = rows.iterator().next();
    assertThat(row.getString("k")).isEqualTo(name.getMethodName());
    assertThat(row.getInt("v")).isEqualTo(4);
  }

  @Test
  public void should_allow_nulls_in_positional_values() {
    // given a statement with positional values
    SimpleStatement insert =
        SimpleStatement.builder("INSERT into test2 (k, v) values (?, ?)")
            .addPositionalValue(name.getMethodName())
            .addPositionalValue(null)
            .build();

    // when executing that statement
    SESSION_RULE.session().execute(insert);

    // then we should be able to retrieve the data as inserted.
    SimpleStatement select =
        SimpleStatement.builder("select k,v from test2 where k=?")
            .addPositionalValue(name.getMethodName())
            .build();

    ResultSet result = SESSION_RULE.session().execute(select);
    List<Row> rows = result.all();
    assertThat(rows).hasSize(1);

    Row row = rows.iterator().next();
    assertThat(row.getString("k")).isEqualTo(name.getMethodName());
    assertThat(row.getObject("v")).isNull();
  }

  @Test(expected = InvalidQueryException.class)
  public void should_fail_when_too_many_positional_values_provided() {
    // given a statement with more bound values than anticipated (3 given vs. 2 expected)
    SimpleStatement insert =
        SimpleStatement.builder("INSERT into test (k, v) values (?, ?)")
            .addPositionalValues(KEY, 0, 7)
            .build();

    // when executing that statement
    SESSION_RULE.session().execute(insert);

    // then the server will throw an InvalidQueryException which is thrown up to the client.
  }

  @Test(expected = InvalidQueryException.class)
  public void should_fail_when_not_enough_positional_values_provided() {
    // given a statement with not enough bound values (1 given vs. 2 expected)
    SimpleStatement insert =
        SimpleStatement.builder("SELECT * from test where k = ? and v = ?")
            .addPositionalValue(KEY)
            .build();

    // when executing that statement
    SESSION_RULE.session().execute(insert);

    // then the server will throw an InvalidQueryException which is thrown up to the client.
  }

  @Test
  public void should_use_named_values() {
    // given a statement with named values
    SimpleStatement insert =
        SimpleStatement.builder("INSERT into test2 (k, v) values (:k, :v)")
            .addNamedValue("k", name.getMethodName())
            .addNamedValue("v", 7)
            .build();

    // when executing that statement
    SESSION_RULE.session().execute(insert);

    // then we should be able to retrieve the data as inserted.
    SimpleStatement select =
        SimpleStatement.builder("select k,v from test2 where k=:k")
            .addNamedValue("k", name.getMethodName())
            .build();

    ResultSet result = SESSION_RULE.session().execute(select);
    List<Row> rows = result.all();
    assertThat(rows).hasSize(1);

    Row row = rows.iterator().next();
    assertThat(row.getString("k")).isEqualTo(name.getMethodName());
    assertThat(row.getInt("v")).isEqualTo(7);
  }

  @Test
  public void should_allow_nulls_in_named_values() {
    // given a statement with named values
    SimpleStatement insert =
        SimpleStatement.builder("INSERT into test2 (k, v) values (:k, :v)")
            .addNamedValue("k", name.getMethodName())
            .addNamedValue("v", null)
            .build();

    // when executing that statement
    SESSION_RULE.session().execute(insert);

    // then we should be able to retrieve the data as inserted.
    SimpleStatement select =
        SimpleStatement.builder("select k,v from test2 where k=:k")
            .addNamedValue("k", name.getMethodName())
            .build();

    ResultSet result = SESSION_RULE.session().execute(select);
    List<Row> rows = result.all();
    assertThat(rows).hasSize(1);

    Row row = rows.iterator().next();
    assertThat(row.getString("k")).isEqualTo(name.getMethodName());
    assertThat(row.getObject("v")).isNull();
  }

  @Test(expected = InvalidQueryException.class)
  public void should_fail_when_named_value_missing() {
    // given a statement with a missing named value (:k)
    SimpleStatement insert =
        SimpleStatement.builder("SELECT * from test where k = :k and v = :v")
            .addNamedValue("v", 0)
            .build();

    // when executing that statement
    SESSION_RULE.session().execute(insert);

    // then the server will throw an InvalidQueryException which is thrown up to the client.
  }

  @Test(expected = IllegalArgumentException.class)
  public void should_fail_when_mixing_named_and_positional_values() {
    SimpleStatement.builder("SELECT * from test where k = :k and v = :v")
        .addNamedValue("k", KEY)
        .addPositionalValue(0)
        .build();
  }

  @Test(expected = IllegalArgumentException.class)
  public void should_fail_when_mixing_positional_and_named_values() {
    SimpleStatement.builder("SELECT * from test where k = :k and v = :v")
        .addPositionalValue(0)
        .addNamedValue("k", KEY)
        .build();
  }

  @Test
  public void should_use_positional_value_with_case_sensitive_id() {
    SimpleStatement statement =
        SimpleStatement.builder("SELECT count(*) FROM test2 WHERE k=:\"theKey\"")
            .addNamedValue(CqlIdentifier.fromCql("\"theKey\""), 0)
            .build();
    Row row = SESSION_RULE.session().execute(statement).one();
    assertThat(row.getLong(0)).isEqualTo(0);
  }

  @Test
  public void should_use_page_size() {
    Statement<?> st = SimpleStatement.builder("SELECT v FROM test").setPageSize(10).build();
    CompletionStage<AsyncResultSet> future = SESSION_RULE.session().executeAsync(st);
    AsyncResultSet result = CompletableFutures.getUninterruptibly(future);

    // Should have only fetched 10 (page size) rows.
    assertThat(result.remaining()).isEqualTo(10);
  }
}
