/*
 * 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 static com.datastax.oss.driver.api.mapper.annotations.SchemaHint.TargetElement;
import static com.datastax.oss.driver.internal.core.util.LoggerTest.setupTestLogger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import ch.qos.logback.classic.Level;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException;
import com.datastax.oss.driver.api.mapper.MapperContext;
import com.datastax.oss.driver.api.mapper.annotations.ClusteringColumn;
import com.datastax.oss.driver.api.mapper.annotations.Dao;
import com.datastax.oss.driver.api.mapper.annotations.DaoFactory;
import com.datastax.oss.driver.api.mapper.annotations.DaoKeyspace;
import com.datastax.oss.driver.api.mapper.annotations.Entity;
import com.datastax.oss.driver.api.mapper.annotations.Mapper;
import com.datastax.oss.driver.api.mapper.annotations.PartitionKey;
import com.datastax.oss.driver.api.mapper.annotations.QueryProvider;
import com.datastax.oss.driver.api.mapper.annotations.SchemaHint;
import com.datastax.oss.driver.api.mapper.annotations.Select;
import com.datastax.oss.driver.api.mapper.annotations.Update;
import com.datastax.oss.driver.api.mapper.entity.EntityHelper;
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.categories.ParallelizableTests;
import com.datastax.oss.driver.internal.core.util.LoggerTest;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import org.junit.Before;
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;

@Category(ParallelizableTests.class)
@BackendRequirement(
    type = BackendType.CASSANDRA,
    minInclusive = "3.4",
    description = "Creates a SASI index")
public class SchemaValidationIT extends InventoryITBase {

  private static CcmRule ccm = CcmRule.getInstance();

  private static SessionRule<CqlSession> sessionRule = SessionRule.builder(ccm).build();

  private static InventoryMapper mapper;
  private static InventoryMapper mapperDisabledValidation;

  @ClassRule public static TestRule chain = RuleChain.outerRule(ccm).around(sessionRule);

  @BeforeClass
  public static void setup() {
    CqlSession session = sessionRule.session();
    List<String> statements =
        Arrays.asList(
            "CREATE TABLE product_simple(id uuid PRIMARY KEY, description text, unmapped text)",
            "CREATE TABLE product_simple_missing_p_k(id uuid PRIMARY KEY, description text, unmapped text)",
            "CREATE TABLE product_simple_missing_clustering_column(id uuid PRIMARY KEY, description text, unmapped text)",
            "CREATE TABLE product_pk_and_clustering(id uuid, c_id uuid, PRIMARY KEY (id, c_id))",
            "CREATE TABLE product_wrong_type(id uuid PRIMARY KEY, wrong_type_column text)",
            "CREATE TYPE dimensions_with_incorrect_name(length int, width int, height int)",
            "CREATE TYPE dimensions_with_wrong_type(length int, width int, height text)",
            "CREATE TYPE dimensions_with_incorrect_name_schema_hint_udt(length int, width int, height int)",
            "CREATE TYPE dimensions_with_incorrect_name_schema_hint_table(length int, width int, height int)",
            "CREATE TABLE product_with_incorrect_udt(id uuid PRIMARY KEY, description text, dimensions dimensions_with_incorrect_name)",
            "CREATE TABLE product_with_incorrect_udt_schema_hint_udt(id uuid PRIMARY KEY, description text, dimensions dimensions_with_incorrect_name_schema_hint_udt)",
            "CREATE TABLE product_with_incorrect_udt_schema_hint_table(id uuid PRIMARY KEY, description text, dimensions dimensions_with_incorrect_name_schema_hint_table)",
            "CREATE TABLE product_with_udt_wrong_type(id uuid PRIMARY KEY, description text, dimensions dimensions_with_wrong_type)");

    for (String query : statements) {
      session.execute(
          SimpleStatement.builder(query).setExecutionProfile(sessionRule.slowProfile()).build());
    }
    for (String query : createStatements(ccm)) {
      session.execute(
          SimpleStatement.builder(query).setExecutionProfile(sessionRule.slowProfile()).build());
    }
    mapper =
        new SchemaValidationIT_InventoryMapperBuilder(session)
            .withSchemaValidationEnabled(true)
            .build();
    mapperDisabledValidation =
        new SchemaValidationIT_InventoryMapperBuilder(session)
            .withSchemaValidationEnabled(false)
            .build();
  }

  @Before
  public void clearData() {
    CqlSession session = sessionRule.session();
    session.execute(
        SimpleStatement.builder("TRUNCATE product_simple")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
    session.execute(
        SimpleStatement.builder("TRUNCATE product")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
    session.execute(
        SimpleStatement.builder("TRUNCATE product_with_incorrect_udt")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
    session.execute(
        SimpleStatement.builder("TRUNCATE product_with_incorrect_udt_schema_hint_udt")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
    session.execute(
        SimpleStatement.builder("TRUNCATE product_with_incorrect_udt_schema_hint_table")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
    session.execute(
        SimpleStatement.builder("TRUNCATE product_wrong_type")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
    session.execute(
        SimpleStatement.builder("TRUNCATE product_pk_and_clustering")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
    session.execute(
        SimpleStatement.builder("TRUNCATE product_with_udt_wrong_type")
            .setExecutionProfile(sessionRule.slowProfile())
            .build());
  }

  @Test
  public void should_throw_when_use_not_properly_mapped_entity() {
    assertThatThrownBy(() -> mapper.productSimpleDao(sessionRule.keyspace()))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining(
            String.format(
                "The CQL ks.table: %s.product_simple has missing columns: [description_with_incorrect_name, some_other_not_mapped_field] that are defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.ProductSimple",
                sessionRule.keyspace()));
  }

  @Test
  public void
      should_throw_when_use_not_properly_mapped_entity_when_ks_is_passed_as_null_extracting_ks_from_session() {
    assertThatThrownBy(() -> mapper.productSimpleDao(null))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining(
            String.format(
                "The CQL ks.table: %s.product_simple has missing columns: [description_with_incorrect_name, some_other_not_mapped_field] that are defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.ProductSimple",
                sessionRule.keyspace()));
  }

  @Test
  public void should_log_warn_when_entity_has_no_corresponding_cql_table() {
    LoggerTest.LoggerSetup logger =
        setupTestLogger(
            SchemaValidationIT_ProductCqlTableMissingHelper__MapperGenerated.class, Level.WARN);
    try {
      assertThatThrownBy(() -> mapper.productCqlTableMissingDao(sessionRule.keyspace()))
          .isInstanceOf(InvalidQueryException.class);

      verify(logger.appender, timeout(500).times(1)).doAppend(logger.loggingEventCaptor.capture());
      assertThat(logger.loggingEventCaptor.getValue().getMessage()).isNotNull();
      assertThat(logger.loggingEventCaptor.getValue().getFormattedMessage())
          .contains(
              String.format(
                  "There is no ks.table or UDT: %s.product_cql_table_missing for the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.ProductCqlTableMissing, or metadata is out of date.",
                  sessionRule.keyspace()));

    } finally {
      logger.close();
    }
  }

  @Test
  public void should_throw_general_driver_exception_when_schema_validation_check_is_disabled() {
    assertThatThrownBy(
            () -> mapperDisabledValidation.productDaoValidationDisabled(sessionRule.keyspace()))
        .isInstanceOf(InvalidQueryException.class)
        .hasMessageContaining("Undefined column name description_with_incorrect_name");
  }

  @Test
  public void should_not_throw_on_table_with_properly_mapped_udt_field() {
    assertThatCode(() -> mapper.productDao(sessionRule.keyspace())).doesNotThrowAnyException();
  }

  @Test
  public void should_throw_when_use_not_properly_mapped_entity_with_udt() {
    assertThatThrownBy(() -> mapper.productWithIncorrectUdtDao(sessionRule.keyspace()))
        .isInstanceOf(IllegalArgumentException.class)
        .hasStackTraceContaining(
            String.format(
                "The CQL ks.udt: %s.dimensions_with_incorrect_name has missing columns: [length_not_present] that are defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.DimensionsWithIncorrectName",
                sessionRule.keyspace()));
  }

  @Test
  public void should_throw_when_use_not_properly_mapped_entity_with_udt_with_udt_schema_hint() {
    assertThatThrownBy(() -> mapper.productWithIncorrectUdtSchemaHintUdt(sessionRule.keyspace()))
        .isInstanceOf(IllegalArgumentException.class)
        .hasStackTraceContaining(
            String.format(
                "The CQL ks.udt: %s.dimensions_with_incorrect_name_schema_hint_udt has missing columns: [length_not_present] that are defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.DimensionsWithIncorrectNameSchemaHintUdt",
                sessionRule.keyspace()));
  }

  @Test
  public void
      should_warn_about_missing_table_when_use_not_properly_mapped_entity_with_udt_with_table_schema_hint() {
    LoggerTest.LoggerSetup logger =
        setupTestLogger(
            SchemaValidationIT_DimensionsWithIncorrectNameSchemaHintTableHelper__MapperGenerated
                .class,
            Level.WARN);
    try {
      // when
      mapper.productWithIncorrectUdtSchemaHintTable(sessionRule.keyspace());

      verify(logger.appender, timeout(500).times(1)).doAppend(logger.loggingEventCaptor.capture());
      assertThat(logger.loggingEventCaptor.getValue().getMessage()).isNotNull();
      assertThat(logger.loggingEventCaptor.getValue().getFormattedMessage())
          .contains(
              String.format(
                  "There is no ks.table or UDT: %s.dimensions_with_incorrect_name_schema_hint_table for the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.DimensionsWithIncorrectNameSchemaHintTable, or metadata is out of date.",
                  sessionRule.keyspace()));
    } finally {
      logger.close();
    }
  }

  @Test
  public void should_throw_when_table_is_missing_PKs() {
    assertThatThrownBy(() -> mapper.productSimpleMissingPKDao(sessionRule.keyspace()))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining(
            String.format(
                "The CQL ks.table: %s.product_simple_missing_p_k has missing Primary Key columns: [id_not_present] that are defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.ProductSimpleMissingPK",
                sessionRule.keyspace()));
  }

  @Test
  public void should_throw_when_table_is_missing_clustering_column() {
    assertThatThrownBy(() -> mapper.productSimpleMissingClusteringColumn(sessionRule.keyspace()))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining(
            String.format(
                "The CQL ks.table: %s.product_simple_missing_clustering_column has missing Clustering columns: [not_existing_clustering_column] that are defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.ProductSimpleMissingClusteringColumn",
                sessionRule.keyspace()));
  }

  @Test
  public void should_throw_when_type_defined_in_table_does_not_match_type_from_entity() {
    assertThatThrownBy(() -> mapper.productDaoWrongType(sessionRule.keyspace()))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining(
            String.format(
                "The CQL ks.table: %s.product_wrong_type defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.ProductWrongType declares type mappings that are not supported by the codec registry:\n"
                    + "Field: wrong_type_column, Entity Type: java.lang.Integer, CQL type: TEXT",
                sessionRule.keyspace()));
  }

  @Test
  public void should_throw_when_type_defined_in_udt_does_not_match_type_from_entity() {
    assertThatThrownBy(() -> mapper.productWithUdtWrongTypeDao(sessionRule.keyspace()))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining(
            String.format(
                "The CQL ks.udt: %s.dimensions_with_wrong_type defined in the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.DimensionsWithWrongType declares type mappings that are not supported by the codec registry:\n"
                    + "Field: height, Entity Type: java.lang.Integer, CQL type: TEXT",
                sessionRule.keyspace()));
  }

  @Test
  public void should_not_throw_when_have_correct_pk_and_clustering() {
    assertThatCode(() -> mapper.productPkAndClusteringDao(sessionRule.keyspace()))
        .doesNotThrowAnyException();
  }

  @Test
  public void should_log_warning_when_passing_not_existing_keyspace() {
    LoggerTest.LoggerSetup logger =
        setupTestLogger(SchemaValidationIT_ProductSimpleHelper__MapperGenerated.class, Level.WARN);
    try {
      // when
      assertThatThrownBy(
              () -> mapper.productSimpleDao(CqlIdentifier.fromCql("not_existing_keyspace")))
          .isInstanceOf(InvalidQueryException.class)
          .hasMessageContaining("not_existing_keyspace does not exist");

      // then
      verify(logger.appender, timeout(500).times(1)).doAppend(logger.loggingEventCaptor.capture());
      assertThat(logger.loggingEventCaptor.getValue().getMessage()).isNotNull();
      assertThat(logger.loggingEventCaptor.getValue().getFormattedMessage())
          .contains(
              "Unable to validate table: product_simple for the entity class: com.datastax.oss.driver.mapper.SchemaValidationIT.ProductSimple because the session metadata has no information about the keyspace: not_existing_keyspace.");
    } finally {
      logger.close();
    }
  }

  @Test
  public void should_not_warn_or_throw_when_target_element_is_NONE() {
    LoggerTest.LoggerSetup logger =
        setupTestLogger(
            SchemaValidationIT_DoesNotExistNoValidationHelper__MapperGenerated.class, Level.WARN);

    // when
    mapper.noValidationDao(sessionRule.keyspace());

    // then
    // no exceptions, no logs
    verify(logger.appender, never()).doAppend(any());
  }

  @Mapper
  public interface InventoryMapper {
    @DaoFactory
    ProductSimpleDao productSimpleDao(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductSimpleCqlTableMissingDao productCqlTableMissingDao(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductSimpleDaoValidationDisabledDao productDaoValidationDisabled(
        @DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductDao productDao(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductWithIncorrectUdtDao productWithIncorrectUdtDao(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductWithIncorrectUdtSchemaHintUdtDao productWithIncorrectUdtSchemaHintUdt(
        @DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductWithIncorrectUdtSchemaHintTableDao productWithIncorrectUdtSchemaHintTable(
        @DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductWithUdtWrongTypeDao productWithUdtWrongTypeDao(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductSimpleMissingPKDao productSimpleMissingPKDao(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductSimpleMissingClusteringColumnDao productSimpleMissingClusteringColumn(
        @DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductDaoWrongTypeDao productDaoWrongType(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    ProductPkAndClusteringDao productPkAndClusteringDao(@DaoKeyspace CqlIdentifier keyspace);

    @DaoFactory
    NoValidationDao noValidationDao(@DaoKeyspace CqlIdentifier keyspace);
  }

  @Dao
  public interface ProductWithIncorrectUdtDao {

    @Update(customWhereClause = "id = :id")
    void updateWhereId(ProductWithIncorrectUdt product, UUID id);
  }

  @Dao
  public interface ProductWithIncorrectUdtSchemaHintUdtDao {

    @Update(customWhereClause = "id = :id")
    void updateWhereId(ProductWithIncorrectUdtSchemaHintUdt product, UUID id);
  }

  @Dao
  public interface ProductWithIncorrectUdtSchemaHintTableDao {

    @Update(customWhereClause = "id = :id")
    void updateWhereId(ProductWithIncorrectUdtSchemaHintTable product, UUID id);
  }

  @Dao
  public interface ProductWithUdtWrongTypeDao {

    @Update(customWhereClause = "id = :id")
    void updateWhereId(ProductWithUdtWrongType product, UUID id);
  }

  @Dao
  public interface ProductDao {

    @Update(customWhereClause = "id = :id")
    void updateWhereId(Product product, UUID id);
  }

  @Dao
  public interface ProductSimpleDao {

    @Select
    ProductSimple findById(UUID productId);
  }

  @Dao
  public interface ProductSimpleDaoValidationDisabledDao {

    @Select
    ProductSimple findById(UUID productId);
  }

  @Dao
  public interface ProductSimpleCqlTableMissingDao {

    @Select
    ProductCqlTableMissing findById(UUID productId);
  }

  @Dao
  public interface ProductSimpleMissingPKDao {
    @Select
    ProductSimpleMissingPK findById(UUID productId);
  }

  @Dao
  public interface ProductSimpleMissingClusteringColumnDao {
    @Select
    ProductSimpleMissingClusteringColumn findById(UUID productId);
  }

  @Dao
  public interface ProductDaoWrongTypeDao {

    @Select
    ProductWrongType findById(UUID productId);
  }

  @Dao
  public interface ProductPkAndClusteringDao {

    @Select
    ProductPkAndClustering findById(UUID productId);
  }

  @Dao
  public interface NoValidationDao {
    // Not a real query, we just need to reference the entities
    @QueryProvider(
        providerClass = DummyProvider.class,
        entityHelpers = {DoesNotExistNoValidation.class, ProductCqlTableMissingNoValidation.class})
    void doNothing();
  }

  @SuppressWarnings("unused")
  static class DummyProvider {
    DummyProvider(
        MapperContext context,
        EntityHelper<DoesNotExistNoValidation> helper1,
        EntityHelper<ProductCqlTableMissingNoValidation> helper2) {}

    void doNothing() {}
  }

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

    public ProductCqlTableMissing() {}

    public UUID getId() {
      return id;
    }

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

  @Entity
  public static class ProductSimpleMissingPK {
    @PartitionKey private UUID idNotPresent;

    public ProductSimpleMissingPK() {}

    public UUID getIdNotPresent() {
      return idNotPresent;
    }

    public void setIdNotPresent(UUID idNotPresent) {
      this.idNotPresent = idNotPresent;
    }
  }

  @Entity
  public static class ProductWrongType {
    @PartitionKey private UUID id;
    private Integer wrongTypeColumn;

    public ProductWrongType() {}

    public UUID getId() {
      return id;
    }

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

    public Integer getWrongTypeColumn() {
      return wrongTypeColumn;
    }

    public void setWrongTypeColumn(Integer wrongTypeColumn) {
      this.wrongTypeColumn = wrongTypeColumn;
    }
  }

  @Entity
  public static class ProductSimpleMissingClusteringColumn {
    @PartitionKey private UUID id;
    @ClusteringColumn private Integer notExistingClusteringColumn;

    public ProductSimpleMissingClusteringColumn() {}

    public UUID getId() {
      return id;
    }

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

    public Integer getNotExistingClusteringColumn() {
      return notExistingClusteringColumn;
    }

    public void setNotExistingClusteringColumn(Integer notExistingClusteringColumn) {
      this.notExistingClusteringColumn = notExistingClusteringColumn;
    }
  }

  @Entity
  public static class ProductPkAndClustering {
    @PartitionKey private UUID id;
    @ClusteringColumn private UUID cId;

    public ProductPkAndClustering() {}

    public UUID getId() {
      return id;
    }

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

    public UUID getcId() {
      return cId;
    }

    public void setcId(UUID cId) {
      this.cId = cId;
    }
  }

  @Entity
  public static class ProductSimple {
    @PartitionKey private UUID id;
    private String descriptionWithIncorrectName;
    private Integer someOtherNotMappedField;

    public ProductSimple() {}

    public ProductSimple(
        UUID id, String descriptionWithIncorrectName, Integer someOtherNotMappedField) {
      this.id = id;
      this.descriptionWithIncorrectName = descriptionWithIncorrectName;
      this.someOtherNotMappedField = someOtherNotMappedField;
    }

    public UUID getId() {
      return id;
    }

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

    public String getDescriptionWithIncorrectName() {
      return descriptionWithIncorrectName;
    }

    public void setDescriptionWithIncorrectName(String descriptionWithIncorrectName) {
      this.descriptionWithIncorrectName = descriptionWithIncorrectName;
    }

    public Integer getSomeOtherNotMappedField() {
      return someOtherNotMappedField;
    }

    public void setSomeOtherNotMappedField(Integer someOtherNotMappedField) {
      this.someOtherNotMappedField = someOtherNotMappedField;
    }

    @Override
    public boolean equals(Object o) {

      if (this == o) {
        return true;
      }
      if (!(o instanceof ProductSimple)) {
        return false;
      }
      ProductSimple that = (ProductSimple) o;
      return this.id.equals(that.id)
          && this.someOtherNotMappedField.equals(that.someOtherNotMappedField)
          && this.descriptionWithIncorrectName.equals(that.descriptionWithIncorrectName);
    }

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

    @Override
    public String toString() {
      return "ProductSimple{"
          + "id="
          + id
          + ", descriptionWithIncorrectName='"
          + descriptionWithIncorrectName
          + '\''
          + ", someOtherNotMappedField="
          + someOtherNotMappedField
          + '}';
    }
  }

  @Entity
  public static class ProductWithIncorrectUdt {

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

    public ProductWithIncorrectUdt() {}

    public ProductWithIncorrectUdt(
        UUID id, String description, DimensionsWithIncorrectName 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 DimensionsWithIncorrectName getDimensions() {
      return dimensions;
    }

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

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ProductWithIncorrectUdt)) {
        return false;
      }
      ProductWithIncorrectUdt that = (ProductWithIncorrectUdt) o;
      return this.id.equals(that.id)
          && this.description.equals(that.description)
          && this.dimensions.equals(that.dimensions);
    }

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

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

  @Entity
  public static class ProductWithUdtWrongType {

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

    public ProductWithUdtWrongType() {}

    public ProductWithUdtWrongType(
        UUID id, String description, DimensionsWithWrongType 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 DimensionsWithWrongType getDimensions() {
      return dimensions;
    }

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

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ProductWithUdtWrongType)) {
        return false;
      }
      ProductWithUdtWrongType that = (ProductWithUdtWrongType) o;
      return this.id.equals(that.id)
          && this.description.equals(that.description)
          && this.dimensions.equals(that.dimensions);
    }

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

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

  @Entity
  public static class ProductWithIncorrectUdtSchemaHintUdt {

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

    public ProductWithIncorrectUdtSchemaHintUdt() {}

    public ProductWithIncorrectUdtSchemaHintUdt(
        UUID id, String description, DimensionsWithIncorrectNameSchemaHintUdt 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 DimensionsWithIncorrectNameSchemaHintUdt getDimensions() {
      return dimensions;
    }

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

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ProductWithIncorrectUdtSchemaHintUdt)) {
        return false;
      }
      ProductWithIncorrectUdtSchemaHintUdt that = (ProductWithIncorrectUdtSchemaHintUdt) o;
      return this.id.equals(that.id)
          && this.description.equals(that.description)
          && this.dimensions.equals(that.dimensions);
    }

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

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

  @Entity
  public static class ProductWithIncorrectUdtSchemaHintTable {

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

    public ProductWithIncorrectUdtSchemaHintTable() {}

    public ProductWithIncorrectUdtSchemaHintTable(
        UUID id, String description, DimensionsWithIncorrectNameSchemaHintTable 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 DimensionsWithIncorrectNameSchemaHintTable getDimensions() {
      return dimensions;
    }

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

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ProductWithIncorrectUdtSchemaHintTable)) {
        return false;
      }
      ProductWithIncorrectUdtSchemaHintTable that = (ProductWithIncorrectUdtSchemaHintTable) o;
      return this.id.equals(that.id)
          && this.description.equals(that.description)
          && this.dimensions.equals(that.dimensions);
    }

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

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

  @Entity
  public static class DimensionsWithIncorrectName {

    private int lengthNotPresent;
    private int width;
    private int height;

    public DimensionsWithIncorrectName() {}

    public DimensionsWithIncorrectName(int lengthNotPresent, int width, int height) {
      this.lengthNotPresent = lengthNotPresent;
      this.width = width;
      this.height = height;
    }

    public int getLengthNotPresent() {
      return lengthNotPresent;
    }

    public void setLengthNotPresent(int lengthNotPresent) {
      this.lengthNotPresent = lengthNotPresent;
    }

    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 o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof DimensionsWithIncorrectName)) {
        return false;
      }
      DimensionsWithIncorrectName that = (DimensionsWithIncorrectName) o;
      return this.lengthNotPresent == that.lengthNotPresent
          && this.height == that.height
          && this.width == that.width;
    }

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

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

  @Entity
  public static class DimensionsWithWrongType {

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

    public DimensionsWithWrongType() {}

    public DimensionsWithWrongType(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 o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof DimensionsWithWrongType)) {
        return false;
      }
      DimensionsWithWrongType that = (DimensionsWithWrongType) o;
      return this.length == that.length && this.height == that.height && this.width == that.width;
    }

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

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

  @Entity
  @SchemaHint(targetElement = TargetElement.UDT)
  public static class DimensionsWithIncorrectNameSchemaHintUdt {

    private int lengthNotPresent;
    private int width;
    private int height;

    public DimensionsWithIncorrectNameSchemaHintUdt() {}

    public DimensionsWithIncorrectNameSchemaHintUdt(int lengthNotPresent, int width, int height) {
      this.lengthNotPresent = lengthNotPresent;
      this.width = width;
      this.height = height;
    }

    public int getLengthNotPresent() {
      return lengthNotPresent;
    }

    public void setLengthNotPresent(int lengthNotPresent) {
      this.lengthNotPresent = lengthNotPresent;
    }

    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 o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof DimensionsWithIncorrectNameSchemaHintUdt)) {
        return false;
      }
      DimensionsWithIncorrectNameSchemaHintUdt that = (DimensionsWithIncorrectNameSchemaHintUdt) o;
      return this.lengthNotPresent == that.lengthNotPresent
          && this.height == that.height
          && this.width == that.width;
    }

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

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

  @Entity
  @SchemaHint(targetElement = TargetElement.TABLE)
  public static class DimensionsWithIncorrectNameSchemaHintTable {

    private int lengthNotPresent;
    private int width;
    private int height;

    public DimensionsWithIncorrectNameSchemaHintTable() {}

    public DimensionsWithIncorrectNameSchemaHintTable(int lengthNotPresent, int width, int height) {
      this.lengthNotPresent = lengthNotPresent;
      this.width = width;
      this.height = height;
    }

    public int getLengthNotPresent() {
      return lengthNotPresent;
    }

    public void setLengthNotPresent(int lengthNotPresent) {
      this.lengthNotPresent = lengthNotPresent;
    }

    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 o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof DimensionsWithIncorrectNameSchemaHintTable)) {
        return false;
      }
      DimensionsWithIncorrectNameSchemaHintTable that =
          (DimensionsWithIncorrectNameSchemaHintTable) o;
      return this.lengthNotPresent == that.lengthNotPresent
          && this.height == that.height
          && this.width == that.width;
    }

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

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

  @Entity
  @SchemaHint(targetElement = TargetElement.NONE)
  public static class DoesNotExistNoValidation {
    private int k;

    public int getK() {
      return k;
    }

    public void setK(int k) {
      this.k = k;
    }
  }

  @Entity
  @SchemaHint(targetElement = TargetElement.NONE)
  public static class ProductCqlTableMissingNoValidation extends ProductCqlTableMissing {}
}
