/*
 * 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.oss.driver.querybuilder;

import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom;
import static com.datastax.oss.driver.assertions.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.cql.PreparedStatement;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException;
import com.datastax.oss.driver.api.core.type.codec.ExtraTypeCodecs;
import com.datastax.oss.driver.api.core.type.codec.TypeCodec;
import com.datastax.oss.driver.api.testinfra.CassandraRequirement;
import com.datastax.oss.driver.api.testinfra.ccm.CcmRule;
import com.datastax.oss.driver.api.testinfra.session.SessionRule;
import com.datastax.oss.driver.api.testinfra.session.SessionUtils;
import com.datastax.oss.driver.categories.ParallelizableTests;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import org.junit.After;
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)
@CassandraRequirement(min = "2.2", description = "JSON support in Cassandra was added in 2.2")
public class JsonInsertIT {
  private static final CcmRule CCM_RULE = CcmRule.getInstance();

  private static final SessionRule<CqlSession> SESSION_RULE =
      SessionRule.builder(CCM_RULE)
          .withConfigLoader(
              SessionUtils.configLoaderBuilder()
                  .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(30))
                  .build())
          .build();

  @ClassRule
  public static final TestRule CHAIN = RuleChain.outerRule(CCM_RULE).around(SESSION_RULE);

  private static final TypeCodec<User> JACKSON_JSON_CODEC = ExtraTypeCodecs.json(User.class);

  @BeforeClass
  public static void setup() {
    SESSION_RULE
        .session()
        .execute("CREATE TABLE json_jackson_row(id int PRIMARY KEY, name text, age int)");
  }

  @After
  public void clearTable() {
    SESSION_RULE.session().execute("TRUNCATE TABLE json_jackson_row");
  }

  @Test
  public void should_insert_string_as_json_using_simple_statement() {
    // given a simple statement
    try (CqlSession session = sessionWithCustomCodec()) {
      String jsonUser = "{ \"id\": 2, \"name\": \"Alice\", \"age\": 3 }";
      Statement stmt = insertInto("json_jackson_row").json(jsonUser).build();

      // when
      session.execute(stmt);

      // then
      String jsonUserResult =
          session
              .execute(selectFrom("json_jackson_row").json().all().build())
              .all()
              .get(0)
              .getString(0);

      assertThat(jsonUserResult).contains("\"id\": 2");
      assertThat(jsonUserResult).contains(" \"name\": \"Alice\"");
      assertThat(jsonUserResult).contains("\"age\": 3");
    }
  }

  @Test
  public void should_insert_json_using_prepare_statement() {
    // given prepare statement
    try (CqlSession session = sessionWithCustomCodec()) {
      User user = new User(2, "bob", 35);
      PreparedStatement pst =
          session.prepare(insertInto("json_jackson_row").json(bindMarker("user")).build());

      // when
      session.execute(pst.bind().set("user", user, User.class));

      // then
      List<Row> rows = session.execute(selectFrom("json_jackson_row").json().all().build()).all();
      assertThat(rows.get(0).get(0, User.class)).isEqualTo(user);
    }
  }

  @Test
  public void should_insert_json_using_simple_statement_with_custom_codec() {
    // given a simple statement
    try (CqlSession session = sessionWithCustomCodec()) {
      User user = new User(1, "alice", 30);
      Statement stmt = insertInto("json_jackson_row").json(user, JACKSON_JSON_CODEC).build();

      // when
      session.execute(stmt);

      // then
      List<Row> rows = session.execute(selectFrom("json_jackson_row").json().all().build()).all();

      assertThat(rows.get(0).get(0, User.class)).isEqualTo(user);
    }
  }

  @Test
  public void should_insert_json_using_simple_statement_with_custom_codec_without_codec_registry() {
    try (CqlSession session = sessionWithoutCustomCodec()) {
      // given
      User user = new User(1, "alice", 30);
      SimpleStatement stmt = insertInto("json_jackson_row").json(user, JACKSON_JSON_CODEC).build();

      // when
      session.execute(stmt);

      // then
      List<Row> rows = session.execute(selectFrom("json_jackson_row").json().all().build()).all();
      assertThat(rows.get(0).get(0, JACKSON_JSON_CODEC)).isEqualTo(user);
    }
  }

  @Test
  public void should_insert_json_using_simple_statement_with_codec_registry() {
    // given a simple statement
    try (CqlSession session = sessionWithCustomCodec()) {
      User user = new User(1, "alice", 30);
      Statement stmt =
          insertInto("json_jackson_row")
              .json(user, session.getContext().getCodecRegistry())
              .build();

      // when
      session.execute(stmt);

      // then
      List<Row> rows = session.execute(selectFrom("json_jackson_row").json().all().build()).all();

      assertThat(rows.get(0).get(0, User.class)).isEqualTo(user);
    }
  }

  @Test
  public void
      should_throw_when_insert_json_using_simple_statement_with_codec_registry_without_custom_codec() {
    assertThatThrownBy(
            () -> {
              try (CqlSession session = sessionWithoutCustomCodec()) {
                insertInto("json_jackson_row")
                    .json(new User(1, "alice", 30), session.getContext().getCodecRegistry())
                    .build();
              }
            })
        .isExactlyInstanceOf(IllegalArgumentException.class)
        .hasMessage(
            String.format(
                "Could not inline JSON literal of type %s. "
                    + "This happens because the provided CodecRegistry does not contain "
                    + "a codec for this type. Try registering your TypeCodec in the registry "
                    + "first, or use json(Object, TypeCodec).",
                User.class.getName()))
        .hasCauseInstanceOf(CodecNotFoundException.class);
  }

  @SuppressWarnings("unchecked")
  private CqlSession sessionWithCustomCodec() {
    return (CqlSession)
        SessionUtils.baseBuilder()
            .addContactEndPoints(CCM_RULE.getContactPoints())
            .withKeyspace(SESSION_RULE.keyspace())
            .addTypeCodecs(JACKSON_JSON_CODEC)
            .build();
  }

  @SuppressWarnings("unchecked")
  private CqlSession sessionWithoutCustomCodec() {
    return (CqlSession)
        SessionUtils.baseBuilder()
            .addContactEndPoints(CCM_RULE.getContactPoints())
            .withKeyspace(SESSION_RULE.keyspace())
            .build();
  }

  @SuppressWarnings("unused")
  public static class User {

    private final int id;

    private final String name;

    private final int age;

    @JsonCreator
    public User(
        @JsonProperty("id") int id,
        @JsonProperty("name") String name,
        @JsonProperty("age") int age) {
      this.id = id;
      this.name = name;
      this.age = age;
    }

    public int getId() {
      return id;
    }

    public String getName() {
      return name;
    }

    public int getAge() {
      return age;
    }

    @Override
    public String toString() {
      return String.format("%s (id %d, age %d)", name, id, age);
    }

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

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