/*
 * 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.BoundStatement;
import com.datastax.driver.core.CCMTestsSupport;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.utils.CassandraVersion;
import com.datastax.driver.mapping.Mapper.Option;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.Set;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

@SuppressWarnings("unused")
@CassandraVersion("2.1.0")
public class MapperSaveNullFieldsTest extends CCMTestsSupport {

  Mapper<User> mapper;

  @Override
  public void onTestContextInitialized() {
    execute("CREATE TABLE user (login text primary key, name text, phone text)");
  }

  @BeforeMethod(groups = "short")
  public void setup() {
    mapper = new MappingManager(session()).mapper(User.class);
  }

  /**
   * ensure that queries generated between different sessions consistently generates the same
   * prepared statement query for different permutations of null columns being present and saved.
   *
   * @jira_ticket JAVA-1587
   */
  @Test(groups = "short")
  public void should_order_save_query_prepared_statement_columns_consistently() {
    List<List<String>> allSessionStatements = Lists.newArrayList();
    for (int i = 0; i < 5; i++) {
      Session session = cluster().connect(keyspace);
      Mapper<User> userMapper = new MappingManager(session()).mapper(User.class);

      List<String> statements = Lists.newArrayList();

      // generate a variety of permutations of columns being present
      // both present
      statements.add(getQuery(userMapper, false, false, Option.saveNullFields(false)));
      // neither name or phone present
      statements.add(getQuery(userMapper, true, true, Option.saveNullFields(false)));
      // name not present
      statements.add(getQuery(userMapper, true, false, Option.saveNullFields(false)));
      // phone not present
      statements.add(getQuery(userMapper, false, true, Option.saveNullFields(false)));

      allSessionStatements.add(statements);
      session.close();
    }

    int statementCount = allSessionStatements.iterator().next().size();
    for (int i = 0; i < statementCount; i++) {
      Set<String> uniqueStatements = Sets.newTreeSet();
      for (List<String> statements : allSessionStatements) {
        uniqueStatements.add(statements.get(i));
      }
      assertThat(uniqueStatements)
          .as("Expected only one statement permutation, must not be ordered consistently.")
          .hasSize(1);
    }
  }

  private String getQuery(
      Mapper<User> mapper, boolean nullName, boolean nullPhone, Option... options) {
    String newName = nullName ? null : "new_name";
    String newPhone = nullPhone ? null : "new_phone";
    User newUser = new User("test_login", newName, newPhone);

    return ((BoundStatement) mapper.saveQuery(newUser, options))
        .preparedStatement()
        .getQueryString();
  }

  @CassandraVersion("2.1.0")
  @Test(groups = "short")
  void should_save_null_fields_if_requested() {
    should_save_null_fields(true, Option.saveNullFields(true));

    mapper.setDefaultSaveOptions(Option.saveNullFields(true));
    should_save_null_fields(true);
  }

  @Test(groups = "short")
  void should_save_null_fields_by_default() {
    should_save_null_fields(true);
  }

  @Test(groups = "short")
  void should_ignore_null_fields_if_requested() {
    should_save_null_fields(false, Option.saveNullFields(false));

    mapper.setDefaultSaveOptions(Option.saveNullFields(false));
    should_save_null_fields(false);
  }

  private void should_save_null_fields(boolean saveExpected, Option... options) {
    // Try different combinations of null fields
    should_save_null_fields(true, true, saveExpected, options);
    should_save_null_fields(true, false, saveExpected, options);
    should_save_null_fields(false, true, saveExpected, options);
    should_save_null_fields(false, false, saveExpected, options);
  }

  private void should_save_null_fields(
      boolean nullName, boolean nullPhone, boolean saveExpected, Option... options) {
    // Start with clean data
    session()
        .execute(
            "insert into user(login, name, phone) "
                + "values ('test_login', 'previous_name', 'previous_phone')");

    boolean unsetSupported =
        session().getCluster().getConfiguration().getProtocolOptions().getProtocolVersion().toInt()
            >= 4;

    String newName = nullName ? null : "new_name";
    String newPhone = nullPhone ? null : "new_phone";
    String description = String.format("update with name=%s, phone = %s", newName, newPhone);
    User newUser = new User("test_login", newName, newPhone);

    // Check if null fields are included in generated statement:
    BoundStatement bs = (BoundStatement) mapper.saveQuery(newUser, options);
    String queryString = bs.preparedStatement().getQueryString();
    if (nullName && !saveExpected) {
      if (unsetSupported) {
        assertThat(queryString).as(description).contains("name");
        assertThat(!bs.isSet("name"));
      } else {
        assertThat(queryString).as(description).doesNotContain("name");
      }
    } else {
      assertThat(queryString).as(description).contains("name");
    }

    if (nullPhone && !saveExpected) {
      if (unsetSupported) {
        assertThat(queryString).as(description).contains("phone");
        assertThat(!bs.isSet("phone"));
      } else {
        assertThat(queryString).as(description).doesNotContain("phone");
      }
    } else {
      assertThat(queryString).as(description).contains("phone");
    }

    // Save entity and check the data
    mapper.save(newUser, options);
    User savedUser = mapper.get("test_login");
    String expectedName = nullName ? (saveExpected ? null : "previous_name") : "new_name";
    String expectedPhone = nullPhone ? (saveExpected ? null : "previous_phone") : "new_phone";
    assertThat(savedUser.getName()).as(description).isEqualTo(expectedName);
    assertThat(savedUser.getPhone()).as(description).isEqualTo(expectedPhone);
  }

  @Table(name = "user")
  public static class User {
    @PartitionKey private String login;
    private String name;
    private String phone;

    public User() {}

    public User(String login, String name, String phone) {
      this.login = login;
      this.name = name;
      this.phone = phone;
    }

    public String getLogin() {
      return login;
    }

    public void setLogin(String login) {
      this.login = login;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public String getPhone() {
      return phone;
    }

    public void setPhone(String phone) {
      this.phone = phone;
    }
  }
}
