/*
 * Copyright DataStax, Inc.
 *
 * Licensed 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 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.*;
import org.testng.annotations.Test;

import java.util.concurrent.atomic.AtomicInteger;

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

@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);
    }
}
