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

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

import com.datastax.driver.core.CCMTestsSupport;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.utils.CassandraVersion;
import com.datastax.driver.mapping.annotations.Accessor;
import com.datastax.driver.mapping.annotations.Column;
import com.datastax.driver.mapping.annotations.Field;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Query;
import com.datastax.driver.mapping.annotations.Table;
import com.datastax.driver.mapping.annotations.UDT;
import java.util.concurrent.atomic.AtomicInteger;
import org.testng.annotations.Test;

@CassandraVersion(value = "2.1.0")
public class MappingConfigurationNamingStrategyTest extends CCMTestsSupport {

  private static AtomicInteger counter = new AtomicInteger(0);

  @Override
  public void onTestContextInitialized() {
    execute(
        "CREATE TABLE lowerlisp (\"primary-key\" int primary key, \"my-value\" int)",
        "CREATE TABLE uppersnake (\"PRIMARY_KEY\" int primary key, \"MY_VALUE\" int)",
        "CREATE TYPE \"ADDRESS\" (\"ZIP_CODE\" int, \"CITY_AND_STATE\" text)",
        "CREATE TABLE user (\"NAME\" text primary key, \"ADDRESS\" frozen<\"ADDRESS\">)");
  }

  @Table(name = "lowerlisp")
  static class UpperSnake {

    @PartitionKey int PRIMARY_KEY;

    int MY_VALUE;

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      UpperSnake that = (UpperSnake) o;

      return PRIMARY_KEY == that.PRIMARY_KEY && MY_VALUE == that.MY_VALUE;
    }

    @Override
    public int hashCode() {
      int result = PRIMARY_KEY;
      result = 31 * result + MY_VALUE;
      return result;
    }
  }

  @Test(groups = "short")
  public void should_use_naming_strategy_fields() {
    // given a configuration with a java naming convention of upper snake case (i.e. HELLO_WORLD)
    // and a
    // cassandra naming convention of lower lisp case (i.e. hello-world) with and access strategy of
    // fields
    MappingConfiguration conf =
        MappingConfiguration.builder()
            .withPropertyMapper(
                new DefaultPropertyMapper()
                    .setNamingStrategy(
                        new DefaultNamingStrategy(
                            NamingConventions.UPPER_SNAKE_CASE, NamingConventions.LOWER_LISP_CASE))
                    .setPropertyAccessStrategy(PropertyAccessStrategy.FIELDS))
            .build();

    MappingManager mappingManager = new MappingManager(session(), conf);
    // when creating a mapper
    // should succeed since fields match the upper snake case strategy and they are appropriately
    // converted
    // to lower lisp format (PRIMARY_KEY -> primary-key, MY_VALUE -> my-value)
    Mapper<UpperSnake> mapper = mappingManager.mapper(UpperSnake.class);

    // should be able to insert and retrieve data
    UpperSnake in = new UpperSnake();
    in.PRIMARY_KEY = counter.incrementAndGet();
    in.MY_VALUE = counter.incrementAndGet();

    mapper.save(in);

    UpperSnake out = mapper.get(in.PRIMARY_KEY);
    assertThat(out).isEqualTo(in);
  }

  @Table(name = "lowerlisp")
  static class NamingStrategyOverrideField {

    // Override what would be mapped to key to primary-key
    @PartitionKey
    @Column(name = "primary-key")
    int key;

    int MY_VALUE;

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      NamingStrategyOverrideField that = (NamingStrategyOverrideField) o;

      return key == that.key && MY_VALUE == that.MY_VALUE;
    }

    @Override
    public int hashCode() {
      int result = key;
      result = 31 * result + MY_VALUE;
      return result;
    }
  }

  @Test(groups = "short")
  public void should_override_naming_strategy_with_column_annotation_name_fields() {
    // given a configuration with a java naming convention of upper snake case (i.e. HELLO_WORLD)
    // and a
    // cassandra naming convention of lower lisp case (i.e. hello-world) with an access strategy of
    // fields
    MappingConfiguration conf =
        MappingConfiguration.builder()
            .withPropertyMapper(
                new DefaultPropertyMapper()
                    .setNamingStrategy(
                        new DefaultNamingStrategy(
                            NamingConventions.UPPER_SNAKE_CASE, NamingConventions.LOWER_LISP_CASE))
                    .setPropertyAccessStrategy(PropertyAccessStrategy.FIELDS))
            .build();

    // when creating a mapper
    // should succeed since fields match the upper snake case strategy and they are appropriately
    // converted
    // to lower lisp format (MY_VALUE -> my-value) and since there is a @Column-annotated name
    // override (key field
    // has override to 'primary-key'
    MappingManager mappingManager = new MappingManager(session(), conf);
    Mapper<NamingStrategyOverrideField> mapper =
        mappingManager.mapper(NamingStrategyOverrideField.class);

    // should be able to insert and retrieve data
    NamingStrategyOverrideField in = new NamingStrategyOverrideField();
    in.key = counter.incrementAndGet();
    in.MY_VALUE = counter.incrementAndGet();

    mapper.save(in);

    NamingStrategyOverrideField out = mapper.get(in.key);
    assertThat(out).isEqualTo(in);
  }

  @Table(name = "uppersnake")
  @SuppressWarnings({"unused", "WeakerAccess"})
  static class LowerSnake {

    private int _primaryKey;
    private int _myValue;

    @PartitionKey
    public int getprimary_key() {
      return _primaryKey;
    }

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

    public int getmy_value() {
      return _myValue;
    }

    public void setmy_value(int v) {
      this._myValue = v;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      LowerSnake that = (LowerSnake) o;

      return _primaryKey == that._primaryKey && _myValue == that._myValue;
    }

    @Override
    public int hashCode() {
      int result = _primaryKey;
      result = 31 * result + _myValue;
      return result;
    }
  }

  @Test(groups = "short")
  public void should_use_naming_strategy_getters_and_setters() {
    // given a configuration with a java naming convention of lower snake case (i.e. hello_world)
    // and a
    // cassandra naming convention of upper snake case (i.e. HELLO_WORLD) with an access strategy of
    // getters and setters
    MappingConfiguration conf =
        MappingConfiguration.builder()
            .withPropertyMapper(
                new DefaultPropertyMapper()
                    .setNamingStrategy(
                        new DefaultNamingStrategy(
                            NamingConventions.LOWER_SNAKE_CASE, NamingConventions.UPPER_SNAKE_CASE))
                    .setPropertyAccessStrategy(PropertyAccessStrategy.GETTERS_AND_SETTERS))
            .build();

    // when creating a mapper
    // should succeed since getters match the lower snake case strategy and they are appropriately
    // converted
    // to upper snake case format (getprimary_key -> PRIMARY_KEY, getmy_value -> MY_VALUE)
    MappingManager mappingManager = new MappingManager(session(), conf);
    Mapper<LowerSnake> mapper = mappingManager.mapper(LowerSnake.class);

    // should be able to insert and retrieve data
    LowerSnake in = new LowerSnake();
    in.setprimary_key(counter.incrementAndGet());
    in.setmy_value(counter.incrementAndGet());

    mapper.save(in);

    LowerSnake out = mapper.get(in.getprimary_key());
    assertThat(out).isEqualTo(in);
  }

  @Table(name = "uppersnake")
  @SuppressWarnings({"unused", "WeakerAccess"})
  static class NamingStrategyOverrideGetter {

    private int _primaryKey;
    private int _myValue;

    // Override column name from what would be mapped to KEY to PRIMARY_KEY
    @PartitionKey
    @Column(name = "PRIMARY_KEY", caseSensitive = true)
    public int getKey() {
      return _primaryKey;
    }

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

    public int getmy_value() {
      return _myValue;
    }

    public void setmy_value(int v) {
      this._myValue = v;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      NamingStrategyOverrideGetter that = (NamingStrategyOverrideGetter) o;

      return _primaryKey == that._primaryKey && _myValue == that._myValue;
    }

    @Override
    public int hashCode() {
      int result = _primaryKey;
      result = 31 * result + _myValue;
      return result;
    }
  }

  @Test(groups = "short")
  public void should_override_naming_strategy_with_column_annotation_name_getters() {
    // given a configuration with a java naming convention of lower snake case (i.e. hello_world)
    // and a
    // cassandra naming convention of upper snake case (i.e. HELLO_WORLD) with an access strategy
    // for
    // getters and setters
    MappingConfiguration conf =
        MappingConfiguration.builder()
            .withPropertyMapper(
                new DefaultPropertyMapper()
                    .setNamingStrategy(
                        new DefaultNamingStrategy(
                            NamingConventions.LOWER_SNAKE_CASE, NamingConventions.UPPER_SNAKE_CASE))
                    .setPropertyAccessStrategy(PropertyAccessStrategy.GETTERS_AND_SETTERS))
            .build();

    // when creating a mapper
    // should succeed since getters match the lower snake case strategy and they are appropriately
    // converted
    // to upper snake case format (getmy_value -> MY_VALUE) and @Column-annotated name override on
    // getKey is
    // PRIMARY_KEY.
    MappingManager mappingManager = new MappingManager(session(), conf);
    Mapper<NamingStrategyOverrideGetter> mapper =
        mappingManager.mapper(NamingStrategyOverrideGetter.class);

    // should be able to insert and retrieve data
    NamingStrategyOverrideGetter in = new NamingStrategyOverrideGetter();
    in.setKey(counter.incrementAndGet());
    in.setmy_value(counter.incrementAndGet());

    mapper.save(in);

    NamingStrategyOverrideGetter out = mapper.get(in.getKey());
    assertThat(out).isEqualTo(in);
  }

  @Accessor
  interface NamingStrategyAccessor {
    @Query("SELECT * from uppersnake where \"PRIMARY_KEY\" = ?")
    Result<NamingStrategyOverrideGetter> getValue(int key);
  }

  @Test(groups = "short")
  public void should_apply_naming_strategy_when_mapping_from_accessor_result() {
    // given a configuration with a java naming convention of lower snake case (i.e. hello_world)
    // and a
    // cassandra naming convention of upper snake case (i.e. HELLO_WORLD) with an access strategy
    // for
    // getters and setters
    MappingConfiguration conf =
        MappingConfiguration.builder()
            .withPropertyMapper(
                new DefaultPropertyMapper()
                    .setNamingStrategy(
                        new DefaultNamingStrategy(
                            NamingConventions.LOWER_SNAKE_CASE, NamingConventions.UPPER_SNAKE_CASE))
                    .setPropertyAccessStrategy(PropertyAccessStrategy.GETTERS_AND_SETTERS))
            .build();

    MappingManager mappingManager = new MappingManager(session(), conf);
    Mapper<NamingStrategyOverrideGetter> mapper =
        mappingManager.mapper(NamingStrategyOverrideGetter.class);
    NamingStrategyAccessor acc = mappingManager.createAccessor(NamingStrategyAccessor.class);

    NamingStrategyOverrideGetter in = new NamingStrategyOverrideGetter();
    in.setKey(counter.incrementAndGet());
    in.setmy_value(counter.incrementAndGet());

    mapper.save(in);

    Result<NamingStrategyOverrideGetter> out = acc.getValue(in.getKey());

    assertThat(out.one()).isEqualTo(in);
  }

  @UDT(name = "ADDRESS", caseSensitiveType = true)
  static class Address {
    int zip_code;

    @Field(name = "CITY_AND_STATE", caseSensitive = true)
    String cityAndState;

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      Address address = (Address) o;

      return zip_code == address.zip_code
          && (cityAndState != null
              ? cityAndState.equals(address.cityAndState)
              : address.cityAndState == null);
    }

    @Override
    public int hashCode() {
      int result = zip_code;
      result = 31 * result + (cityAndState != null ? cityAndState.hashCode() : 0);
      return result;
    }
  }

  @Table(name = "user")
  static class User {
    @PartitionKey String name;

    Address address;

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      User user = (User) o;

      return (name != null ? name.equals(user.name) : user.name == null)
          && (address != null ? address.equals(user.address) : user.address == null);
    }

    @Override
    public int hashCode() {
      int result = name != null ? name.hashCode() : 0;
      result = 31 * result + (address != null ? address.hashCode() : 0);
      return result;
    }
  }

  @Test(groups = "short")
  public void should_apply_naming_strategy_to_udts() {
    // given a configuration with a java naming convention of lower snake case (i.e. hello_world)
    // and a
    // cassandra naming convention of upper snake case (i.e. HELLO_WORLD) with an access strategy
    // for
    // fields
    MappingConfiguration conf =
        MappingConfiguration.builder()
            .withPropertyMapper(
                new DefaultPropertyMapper()
                    .setNamingStrategy(
                        new DefaultNamingStrategy(
                            NamingConventions.LOWER_SNAKE_CASE, NamingConventions.UPPER_SNAKE_CASE))
                    .setPropertyAccessStrategy(PropertyAccessStrategy.FIELDS))
            .build();

    MappingManager mappingManager = new MappingManager(session(), conf);

    // when creating a mapper
    // should succeed since fields match the lower snake case strategy and they are appropriately
    // converted for
    // both the table (User) and the underlying UDT (Address)
    Mapper<User> mapper = mappingManager.mapper(User.class);

    // insert user w/ address
    Address address = new Address();
    address.zip_code = 90210;
    address.cityAndState = "Beverly Hills, CA";

    User user = new User();
    user.name = "John Doe";
    user.address = address;

    mapper.save(user);

    // retrieve user w/ address and ensure it matches the input.
    User out = mapper.get(user.name);
    assertThat(out).isEqualTo(user);

    // should be able to use udtCodec to get a codec back that is capable of converting a retrieved
    // udt column value into an Address object and it should match the input.
    TypeCodec<Address> addressCodec = mappingManager.udtCodec(Address.class);
    Row row = session().execute("select \"ADDRESS\" from user where \"NAME\" = 'John Doe'").one();
    Address outAddress = row.get(0, addressCodec);
    assertThat(outAddress).isEqualTo(address);
  }
}
