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

import com.datastax.oss.driver.api.core.Version;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.datastax.oss.driver.api.mapper.annotations.ClusteringColumn;
import com.datastax.oss.driver.api.mapper.annotations.Entity;
import com.datastax.oss.driver.api.mapper.annotations.PartitionKey;
import com.datastax.oss.driver.api.testinfra.ccm.BaseCcmRule;
import com.datastax.oss.driver.api.testinfra.requirement.BackendType;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

/** Factors common code for mapper tests that rely on a simple inventory model. */
public abstract class InventoryITBase {

  protected static Product FLAMETHROWER =
      new Product(UUID.randomUUID(), "Flamethrower", new InventoryITBase.Dimensions(30, 10, 8));
  protected static Product MP3_DOWNLOAD = new Product(UUID.randomUUID(), "MP3 download", null);

  protected static String DATE_1 = "2019-06-27";
  protected static String DATE_2 = "2019-06-28";
  protected static String DATE_3 = "2019-01-01";

  protected static ProductSale FLAMETHROWER_SALE_1 =
      new ProductSale(FLAMETHROWER.getId(), DATE_1, 1, Uuids.startOf(1561643130), 500.00, 5);

  protected static ProductSale FLAMETHROWER_SALE_2 =
      new ProductSale(FLAMETHROWER.getId(), DATE_1, 2, Uuids.startOf(1561645130), 500.00, 1);

  protected static ProductSale FLAMETHROWER_SALE_3 =
      new ProductSale(FLAMETHROWER.getId(), DATE_1, 1, Uuids.startOf(1561653130), 500.00, 2);

  protected static ProductSale FLAMETHROWER_SALE_4 =
      new ProductSale(FLAMETHROWER.getId(), DATE_1, 1, Uuids.startOf(1561657504), 702.00, 3);

  protected static ProductSale FLAMETHROWER_SALE_5 =
      new ProductSale(FLAMETHROWER.getId(), DATE_2, 1, Uuids.startOf(1561729530), 500.00, 23);

  protected static ProductSale MP3_DOWNLOAD_SALE_1 =
      new ProductSale(MP3_DOWNLOAD.getId(), DATE_3, 7, Uuids.startOf(915192000), 0.99, 12);

  protected static List<String> createStatements(BaseCcmRule ccmRule) {
    return createStatements(ccmRule, false);
  }

  protected static List<String> createStatements(BaseCcmRule ccmRule, boolean requiresSasiIndex) {
    ImmutableList.Builder<String> builder =
        ImmutableList.<String>builder()
            .add(
                "CREATE TYPE dimensions(length int, width int, height int)",
                "CREATE TABLE product(id uuid PRIMARY KEY, description text, dimensions frozen<dimensions>)",
                "CREATE TYPE dimensions2d(width int, height int)",
                "CREATE TABLE product2d(id uuid PRIMARY KEY, description text, dimensions frozen<dimensions2d>)",
                "CREATE TABLE product_without_id(id uuid, clustering int, description text, "
                    + "PRIMARY KEY((id), clustering))",
                "CREATE TABLE product_sale(id uuid, day text, ts uuid, customer_id int, price "
                    + "double, count int, PRIMARY KEY ((id, day), customer_id, ts))");

    if (requiresSasiIndex && supportsSASI(ccmRule) && !isSasiBroken(ccmRule)) {
      builder.add(
          "CREATE CUSTOM INDEX product_description ON product(description) "
              + "USING 'org.apache.cassandra.index.sasi.SASIIndex' "
              + "WITH OPTIONS = {"
              + "'mode': 'CONTAINS',"
              + "'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer',"
              + "'tokenization_enable_stemming': 'true',"
              + "'tokenization_locale': 'en',"
              + "'tokenization_skip_stop_words': 'true',"
              + "'analyzed': 'true',"
              + "'tokenization_normalize_lowercase': 'true'"
              + "}");
    }

    return builder.build();
  }

  private static final Version MINIMUM_SASI_VERSION =
      Objects.requireNonNull(Version.parse("3.4.0"));
  private static final Version BROKEN_SASI_VERSION = Objects.requireNonNull(Version.parse("6.8.0"));

  protected static boolean isSasiBroken(BaseCcmRule ccmRule) {
    // creating SASI indexes is broken in DSE 6.8.0
    return ccmRule.isDistributionOf(
        BackendType.DSE, (dist, cass) -> dist.compareTo(BROKEN_SASI_VERSION) == 0);
  }

  protected static boolean supportsSASI(BaseCcmRule ccmRule) {
    return ccmRule.getCassandraVersion().compareTo(MINIMUM_SASI_VERSION) >= 0;
  }

  @Entity
  public static class Product {

    @PartitionKey private UUID id;
    private String description;
    private Dimensions dimensions;

    public Product() {}

    public Product(UUID id, String description, Dimensions dimensions) {
      this.id = id;
      this.description = description;
      this.dimensions = dimensions;
    }

    public UUID getId() {
      return id;
    }

    public void setId(UUID id) {
      this.id = id;
    }

    public String getDescription() {
      return description;
    }

    public void setDescription(String description) {
      this.description = description;
    }

    public Dimensions getDimensions() {
      return dimensions;
    }

    public void setDimensions(Dimensions dimensions) {
      this.dimensions = dimensions;
    }

    @Override
    public boolean equals(Object other) {
      if (other == this) {
        return true;
      } else if (other instanceof Product) {
        Product that = (Product) other;
        return Objects.equals(id, that.id)
            && Objects.equals(description, that.description)
            && Objects.equals(dimensions, that.dimensions);
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      return Objects.hash(id, description, dimensions);
    }

    @Override
    public String toString() {
      return "Product{"
          + "id="
          + id
          + ", description='"
          + description
          + '\''
          + ", dimensions="
          + dimensions
          + '}';
    }
  }

  @Entity
  public static class ProductWithoutId {
    private String description;

    public ProductWithoutId() {}

    public ProductWithoutId(String description) {
      this.description = description;
    }

    public String getDescription() {
      return description;
    }

    public void setDescription(String description) {
      this.description = description;
    }

    @Override
    public boolean equals(Object other) {
      if (other == this) {
        return true;
      } else if (other instanceof ProductWithoutId) {
        ProductWithoutId that = (ProductWithoutId) other;
        return Objects.equals(description, that.description);
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      return Objects.hash(description);
    }

    @Override
    public String toString() {
      return "ProductWithoutId{" + "description='" + description + '\'' + '}';
    }
  }

  @Entity
  public static class Dimensions {

    private int length;
    private int width;
    private int height;

    public Dimensions() {}

    public Dimensions(int length, int width, int height) {
      this.length = length;
      this.width = width;
      this.height = height;
    }

    public int getLength() {
      return length;
    }

    public void setLength(int length) {
      this.length = length;
    }

    public int getWidth() {
      return width;
    }

    public void setWidth(int width) {
      this.width = width;
    }

    public int getHeight() {
      return height;
    }

    public void setHeight(int height) {
      this.height = height;
    }

    @Override
    public boolean equals(Object other) {
      if (this == other) {
        return true;
      } else if (other instanceof Dimensions) {
        Dimensions that = (Dimensions) other;
        return this.length == that.length && this.width == that.width && this.height == that.height;
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      return Objects.hash(length, width, height);
    }

    @Override
    public String toString() {
      return "Dimensions{" + "length=" + length + ", width=" + width + ", height=" + height + '}';
    }
  }

  @Entity
  public static class OnlyPK {
    @PartitionKey private UUID id;

    public OnlyPK() {}

    public OnlyPK(UUID id) {
      this.id = id;
    }

    public UUID getId() {
      return id;
    }

    public void setId(UUID id) {
      this.id = id;
    }

    @Override
    public boolean equals(Object other) {
      if (this == other) {
        return true;
      } else if (other instanceof OnlyPK) {
        OnlyPK that = (OnlyPK) other;
        return Objects.equals(this.id, that.id);
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      return Objects.hash(id);
    }

    @Override
    public String toString() {
      return "OnlyPK{" + "id=" + id + '}';
    }
  }

  @Entity
  public static class ProductSale {
    @PartitionKey private UUID id;

    @PartitionKey(1)
    private String day;

    @ClusteringColumn private int customerId;

    @ClusteringColumn(1)
    private UUID ts;

    private double price;

    private int count;

    public ProductSale() {}

    public ProductSale(UUID id, String day, int customerId, UUID ts, double price, int count) {
      this.id = id;
      this.day = day;
      this.customerId = customerId;
      this.ts = ts;
      this.price = price;
      this.count = count;
    }

    public UUID getId() {
      return id;
    }

    public void setId(UUID id) {
      this.id = id;
    }

    public String getDay() {
      return day;
    }

    public void setDay(String day) {
      this.day = day;
    }

    public UUID getTs() {
      return ts;
    }

    public void setTs(UUID ts) {
      this.ts = ts;
    }

    public int getCustomerId() {
      return customerId;
    }

    public void setCustomerId(int customerId) {
      this.customerId = customerId;
    }

    public double getPrice() {
      return price;
    }

    public void setPrice(double price) {
      this.price = price;
    }

    public int getCount() {
      return count;
    }

    public void setCount(int count) {
      this.count = count;
    }

    @Override
    public boolean equals(Object other) {
      if (this == other) {
        return true;
      } else if (other instanceof ProductSale) {
        ProductSale that = (ProductSale) other;
        return Double.compare(this.price, that.price) == 0
            && this.count == that.count
            && this.id.equals(that.id)
            && this.day.equals(that.day)
            && this.ts.equals(that.ts)
            && this.customerId == that.customerId;
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      return Objects.hash(id, day, ts, customerId, price, count);
    }

    @Override
    public String toString() {
      return "ProductSale{"
          + "id="
          + id
          + ", day='"
          + day
          + '\''
          + ", customerId="
          + customerId
          + ", ts="
          + ts
          + ", price="
          + price
          + ", count="
          + count
          + '}';
    }
  }
}
