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.dse.driver.api.core.data.time;
  
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assertions.assertThatThrownBy;
  
  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.PreparedStatement;
  import com.datastax.oss.driver.api.core.cql.Row;
  import com.datastax.oss.driver.api.core.data.TupleValue;
  import com.datastax.oss.driver.api.core.data.UdtValue;
  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.requirement.BackendRequirement;
  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.google.common.collect.Sets;
  import java.time.Duration;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import org.junit.ClassRule;
  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})
  @BackendRequirement(type = BackendType.DSE, minInclusive = "5.1")
  public class DateRangeIT {
  
    private static CcmRule ccmRule = CcmRule.getInstance();
  
    private static SessionRule<CqlSession> sessionRule =
        SessionRule.builder(ccmRule)
            .withConfigLoader(
                SessionUtils.configLoaderBuilder()
                    .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(30))
                    .build())
            .build();
  
    @ClassRule public static TestRule chain = RuleChain.outerRule(ccmRule).around(sessionRule);
  
    @Rule public TestName testName = new TestName();
  
    /**
     * Validates that data can be retrieved by primary key where its primary key is a 'DateRangeType'
     * column, and that the data returned properly parses into the expected {@link DateRange}.
     */
    @Test
    public void should_use_date_range_as_primary_key() throws Exception {
      CqlSession session = sessionRule.session();
      String tableName = testName.getMethodName();
  
      session.execute(
          String.format("CREATE TABLE %s (k 'DateRangeType' PRIMARY KEY, v int)", tableName));
      session.execute(
          String.format("INSERT INTO %s (k, v) VALUES ('[2010-12-03 TO 2010-12-04]', 1)", tableName));
      session.execute(
          String.format(
              "INSERT INTO %s (k, v) VALUES ('[2015-12-03T10:15:30.001Z TO 2016-01-01T00:05:11.967Z]', 2)",
              tableName));
  
      List<Row> rows = session.execute("SELECT * FROM " + tableName).all();
  
      assertThat(rows).hasSize(2);
      assertThat(rows.get(0).get("k", DateRange.class))
          .isEqualTo(DateRange.parse("[2010-12-03 TO 2010-12-04]"));
      assertThat(rows.get(1).get("k", DateRange.class))
          .isEqualTo(DateRange.parse("[2015-12-03T10:15:30.001Z TO 2016-01-01T00:05:11.967Z]"));
  
      rows =
          session
              .execute(
                  String.format(
                      "SELECT * FROM %s WHERE k = '[2015-12-03T10:15:30.001Z TO 2016-01-01T00:05:11.967]'",
                      tableName))
              .all();
      assertThat(rows.size()).isEqualTo(1);
      assertThat(rows.get(0).getInt("v")).isEqualTo(2);
    }
  
    /**
     * Validates that a 'DateRangeType' column can take a variety of {@link DateRange} inputs:
     *
     * <ol>
     *   <li>Upper bound unbounded
     *   <li>Lower bound unbounded
     *   <li>Unbounded
     *   <li>Bounded
     *   <li>null
     *   <li>unset
     * </ol>
     */
    @Test
    public void should_store_date_range() throws Exception {
      CqlSession session = sessionRule.session();
      String tableName = testName.getMethodName();
  
      session.execute(
          String.format("CREATE TABLE %s (k int PRIMARY KEY, v 'DateRangeType')", tableName));
      session.execute(
          String.format(
              "INSERT INTO %s (k, v) VALUES (1, '[2000-01-01T10:15:30.301Z TO *]')", tableName));
      session.execute(
          String.format("INSERT INTO %s (k, v) VALUES (2, '[2000-02 TO 2000-03]')", tableName));
      session.execute(String.format("INSERT INTO %s (k, v) VALUES (3, '[* TO 2020]')", tableName));
      session.execute(String.format("INSERT INTO %s (k, v) VALUES (4, null)", tableName));
      session.execute(String.format("INSERT INTO %s (k) VALUES (5)", tableName));
      session.execute(String.format("INSERT INTO %s (k, v) VALUES (6, '*')", tableName));
  
      List<Row> rows = session.execute("SELECT * FROM " + tableName).all();
  
      assertThat(rows)
          .extracting(input -> input.get("v", DateRange.class))
          .containsOnly(
              DateRange.parse("[2000-01-01T10:15:30.301Z TO *]"),
              DateRange.parse("[2000-02 TO 2000-03]"),
              DateRange.parse("[* TO 2020]"),
              null,
              DateRange.parse("*"));
    }
  
    /**
     * Validates that if a provided {@link DateRange} for a 'DateRangeType' column has the bounds
     * reversed (lower bound is later than upper bound), then an {@link InvalidQueryException} is
     * thrown.
     */
    @Test
    public void should_disallow_invalid_order() throws Exception {
      CqlSession session = sessionRule.session();
      String tableName = testName.getMethodName();
  
      session.execute(
          String.format("CREATE TABLE %s (k int PRIMARY KEY, v 'DateRangeType')", tableName));
  
      assertThatThrownBy(
              () ->
                  session.execute(
                      String.format(
                          "INSERT INTO %s (k, v) "
                              + "VALUES (1, '[2020-01-01T10:15:30.009Z TO 2010-01-01T00:05:11.031Z]')",
                          tableName)))
          .isInstanceOf(InvalidQueryException.class)
          .hasMessageContaining("Wrong order: 2020-01-01T10:15:30.009Z TO 2010-01-01T00:05:11.031Z")
          .hasMessageContaining(
              "Could not parse date range: [2020-01-01T10:15:30.009Z TO 2010-01-01T00:05:11.031Z]");
    }
  
    /** Validates that {@link DateRange} can be used in UDT and Tuple types. */
    @Test
    public void should_allow_date_range_in_udt_and_tuple() throws Exception {
      CqlSession session = sessionRule.session();
      String tableName = testName.getMethodName();
  
      session.execute("CREATE TYPE IF NOT EXISTS test_udt (i int, range 'DateRangeType')");
      session.execute(
          String.format(
              "CREATE TABLE %s (k int PRIMARY KEY, u test_udt, uf frozen<test_udt>, "
                  + "t tuple<'DateRangeType', int>, tf frozen<tuple<'DateRangeType', int>>)",
              tableName));
      session.execute(
          String.format(
              "INSERT INTO %s (k, u, uf, t, tf) VALUES ("
                  + "1, "
                  + "{i: 10, range: '[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]'}, "
                  + "{i: 20, range: '[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]'}, "
                  + "('[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]', 30), "
                  + "('[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]', 40))",
              tableName));
  
      DateRange expected = DateRange.parse("[2000-01-01T10:15:30.003Z TO 2020-01-01T10:15:30.001Z]");
  
      List<Row> rows = session.execute("SELECT * FROM " + tableName).all();
      assertThat(rows).hasSize(1);
  
      UdtValue u = rows.get(0).get("u", UdtValue.class);
      DateRange dateRange = u.get("range", DateRange.class);
      assertThat(dateRange).isEqualTo(expected);
      assertThat(u.getInt("i")).isEqualTo(10);
  
      u = rows.get(0).get("uf", UdtValue.class);
      dateRange = u.get("range", DateRange.class);
      assertThat(dateRange).isEqualTo(expected);
      assertThat(u.getInt("i")).isEqualTo(20);
  
      TupleValue t = rows.get(0).get("t", TupleValue.class);
      dateRange = t.get(0, DateRange.class);
      assertThat(dateRange).isEqualTo(expected);
      assertThat(t.getInt(1)).isEqualTo(30);
  
      t = rows.get(0).get("tf", TupleValue.class);
      dateRange = t.get(0, DateRange.class);
      assertThat(dateRange).isEqualTo(expected);
      assertThat(t.getInt(1)).isEqualTo(40);
    }
  
    /** Validates that {@link DateRange} can be used in Collection types (Map, Set, List). */
    @Test
    public void should_allow_date_range_in_collections() throws Exception {
      CqlSession session = sessionRule.session();
      String tableName = testName.getMethodName();
  
      session.execute(
          String.format(
              "CREATE TABLE %s (k int PRIMARY KEY, l list<'DateRangeType'>, s set<'DateRangeType'>, "
                  + "dr2i map<'DateRangeType', int>, i2dr map<int, 'DateRangeType'>)",
              tableName));
      session.execute(
          String.format(
              "INSERT INTO %s (k, l, s, i2dr, dr2i) VALUES ("
                  + "1, "
                  // l
                  + "['[2000-01-01T10:15:30.001Z TO 2020]', '[2010-01-01T10:15:30.001Z TO 2020]',"
                  + " '2001-01-02'], "
                  // s
                  + "{'[2000-01-01T10:15:30.001Z TO 2020]', '[2000-01-01T10:15:30.001Z TO 2020]', "
                  + "'[2010-01-01T10:15:30.001Z TO 2020]'}, "
                  // i2dr
                  + "{1: '[2000-01-01T10:15:30.001Z TO 2020]', "
                  + "2: '[2010-01-01T10:15:30.001Z TO 2020]'}, "
                  // dr2i
                  + "{'[2000-01-01T10:15:30.001Z TO 2020]': 1, "
                  + "'[2010-01-01T10:15:30.001Z TO 2020]': 2})",
              tableName));
  
      List<Row> rows = session.execute("SELECT * FROM " + tableName).all();
      assertThat(rows.size()).isEqualTo(1);
  
      List<DateRange> drList = rows.get(0).getList("l", DateRange.class);
      assertThat(drList.size()).isEqualTo(3);
      assertThat(drList.get(0)).isEqualTo(DateRange.parse("[2000-01-01T10:15:30.001Z TO 2020]"));
      assertThat(drList.get(1)).isEqualTo(DateRange.parse("[2010-01-01T10:15:30.001Z TO 2020]"));
      assertThat(drList.get(2)).isEqualTo(DateRange.parse("2001-01-02"));
  
      Set<DateRange> drSet = rows.get(0).getSet("s", DateRange.class);
      assertThat(drSet.size()).isEqualTo(2);
      assertThat(drSet)
          .isEqualTo(
              Sets.newHashSet(
                  DateRange.parse("[2000-01-01T10:15:30.001Z TO 2020]"),
                  DateRange.parse("[2010-01-01T10:15:30.001Z TO 2020]")));
  
      Map<DateRange, Integer> dr2i = rows.get(0).getMap("dr2i", DateRange.class, Integer.class);
      assertThat(dr2i.size()).isEqualTo(2);
      assertThat((int) dr2i.get(DateRange.parse("[2000-01-01T10:15:30.001Z TO 2020]"))).isEqualTo(1);
      assertThat((int) dr2i.get(DateRange.parse("[2010-01-01T10:15:30.001Z TO 2020]"))).isEqualTo(2);
  
      Map<Integer, DateRange> i2dr = rows.get(0).getMap("i2dr", Integer.class, DateRange.class);
      assertThat(i2dr.size()).isEqualTo(2);
      assertThat(i2dr.get(1)).isEqualTo(DateRange.parse("[2000-01-01T10:15:30.001Z TO 2020]"));
      assertThat(i2dr.get(2)).isEqualTo(DateRange.parse("[2010-01-01T10:15:30.001Z TO 2020]"));
    }
  
    /**
     * Validates that a 'DateRangeType' column can take a {@link DateRange} inputs as a prepared
     * statement parameter.
     */
    @Test
    public void should_bind_date_range_in_prepared_statement() throws Exception {
      CqlSession session = sessionRule.session();
      String tableName = testName.getMethodName();
  
      session.execute(
          String.format("CREATE TABLE %s (k int PRIMARY KEY, v 'DateRangeType')", tableName));
      PreparedStatement statement =
          session.prepare(String.format("INSERT INTO %s (k,v) VALUES(?,?)", tableName));
  
      DateRange expected = DateRange.parse("[2007-12-03 TO 2007-12]");
      session.execute(statement.bind(1, expected));
      List<Row> rows = session.execute("SELECT * FROM " + tableName).all();
      assertThat(rows.size()).isEqualTo(1);
      DateRange actual = rows.get(0).get("v", DateRange.class);
      assertThat(actual).isEqualTo(expected);
      assertThat(actual.getLowerBound().getPrecision()).isEqualTo(DateRangePrecision.DAY);
      assertThat(actual.getUpperBound())
          .hasValueSatisfying(
              upperBound ->
                  assertThat(upperBound.getPrecision()).isEqualTo(DateRangePrecision.MONTH));
      assertThat(actual.toString()).isEqualTo("[2007-12-03 TO 2007-12]");
  
      expected = DateRange.parse("[* TO *]");
      session.execute(statement.bind(1, expected));
      rows = session.execute("SELECT * FROM " + tableName).all();
      assertThat(rows.size()).isEqualTo(1);
      actual = rows.get(0).get("v", DateRange.class);
      assertThat(actual).isEqualTo(expected);
      assertThat(actual.getLowerBound().isUnbounded()).isTrue();
      assertThat(actual.isSingleBounded()).isFalse();
      assertThat(actual.getUpperBound())
          .hasValueSatisfying(upperBound -> assertThat(upperBound.isUnbounded()).isTrue());
      assertThat(actual.toString()).isEqualTo("[* TO *]");
  
      expected = DateRange.parse("*");
      session.execute(statement.bind(1, expected));
      rows = session.execute("SELECT * FROM " + tableName).all();
      assertThat(rows.size()).isEqualTo(1);
      actual = rows.get(0).get("v", DateRange.class);
      assertThat(actual).isEqualTo(expected);
      assertThat(actual.getLowerBound().isUnbounded()).isTrue();
      assertThat(actual.isSingleBounded()).isTrue();
      assertThat(actual.toString()).isEqualTo("*");
    }
  
    /**
     * Validates that 'DateRangeType' columns are retrievable using <code>SELECT JSON</code> queries
     * and that their value representations match their input.
     */
    @Test
    public void should_select_date_range_using_json() throws Exception {
      CqlSession session = sessionRule.session();
      String tableName = testName.getMethodName();
  
      session.execute(
          String.format("CREATE TABLE %s (k int PRIMARY KEY, v 'DateRangeType')", tableName));
      PreparedStatement statement =
          session.prepare(String.format("INSERT INTO %s (k,v) VALUES(?,?)", tableName));
  
      DateRange expected = DateRange.parse("[2007-12-03 TO 2007-12]");
      session.execute(statement.bind(1, expected));
      List<Row> rows = session.execute("SELECT JSON * FROM " + tableName).all();
      assertThat(rows.get(0).getString(0))
          .isEqualTo("{\"k\": 1, \"v\": \"[2007-12-03 TO 2007-12]\"}");
  
      expected = DateRange.parse("[* TO *]");
      session.execute(statement.bind(1, expected));
      rows = session.execute("SELECT JSON * FROM " + tableName).all();
      assertThat(rows.get(0).getString(0)).isEqualTo("{\"k\": 1, \"v\": \"[* TO *]\"}");
  
      expected = DateRange.parse("*");
      session.execute(statement.bind(1, expected));
      rows = session.execute("SELECT JSON * FROM " + tableName).all();
      assertThat(rows.get(0).getString(0)).isEqualTo("{\"k\": 1, \"v\": \"*\"}");
    }
  }
  