/*
 * 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.internal.core.metadata.schema.parsing;

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

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.type.DataTypes;
import com.datastax.oss.driver.api.core.type.ListType;
import com.datastax.oss.driver.api.core.type.MapType;
import com.datastax.oss.driver.api.core.type.SetType;
import com.datastax.oss.driver.api.core.type.TupleType;
import com.datastax.oss.driver.api.core.type.UserDefinedType;
import com.datastax.oss.driver.internal.core.adminrequest.AdminRow;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import java.util.Map;
import org.junit.Test;

public class UserDefinedTypeListParserTest extends SchemaParserTestBase {

  private static final AdminRow PERSON_ROW_2_2 =
      mockTypeRow(
          "ks",
          "person",
          ImmutableList.of("first_name", "last_name", "address"),
          ImmutableList.of(
              "org.apache.cassandra.db.marshal.UTF8Type",
              "org.apache.cassandra.db.marshal.UTF8Type",
              "org.apache.cassandra.db.marshal.UserType("
                  + "ks,61646472657373," // address
                  + "737472656574:org.apache.cassandra.db.marshal.UTF8Type," // street
                  + "7a6970636f6465:org.apache.cassandra.db.marshal.Int32Type)")); // zipcode

  private static final AdminRow PERSON_ROW_3_0 =
      mockTypeRow(
          "ks",
          "person",
          ImmutableList.of("first_name", "last_name", "address"),
          ImmutableList.of("text", "text", "address"));

  private static final AdminRow ADDRESS_ROW_3_0 =
      mockTypeRow(
          "ks", "address", ImmutableList.of("street", "zipcode"), ImmutableList.of("text", "int"));

  @Test
  public void should_parse_modern_table() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    Map<CqlIdentifier, UserDefinedType> types =
        parser.parse(KEYSPACE_ID, PERSON_ROW_3_0, ADDRESS_ROW_3_0);

    assertThat(types).hasSize(2);
    UserDefinedType personType = types.get(CqlIdentifier.fromInternal("person"));
    UserDefinedType addressType = types.get(CqlIdentifier.fromInternal("address"));

    assertThat(personType.getKeyspace().asInternal()).isEqualTo("ks");
    assertThat(personType.getName().asInternal()).isEqualTo("person");
    assertThat(personType.getFieldNames())
        .containsExactly(
            CqlIdentifier.fromInternal("first_name"),
            CqlIdentifier.fromInternal("last_name"),
            CqlIdentifier.fromInternal("address"));
    assertThat(personType.getFieldTypes().get(0)).isEqualTo(DataTypes.TEXT);
    assertThat(personType.getFieldTypes().get(1)).isEqualTo(DataTypes.TEXT);
    assertThat(personType.getFieldTypes().get(2)).isSameAs(addressType);
  }

  @Test
  public void should_parse_legacy_table() {
    UserDefinedTypeParser parser =
        new UserDefinedTypeParser(new DataTypeClassNameParser(), context);
    // no need to add a column for the address type, because in 2.2 UDTs are always fully redefined
    // in column and field types (instead of referencing an existing type)
    Map<CqlIdentifier, UserDefinedType> types = parser.parse(KEYSPACE_ID, PERSON_ROW_2_2);

    assertThat(types).hasSize(1);
    UserDefinedType personType = types.get(CqlIdentifier.fromInternal("person"));

    assertThat(personType.getKeyspace().asInternal()).isEqualTo("ks");
    assertThat(personType.getName().asInternal()).isEqualTo("person");
    assertThat(personType.getFieldNames())
        .containsExactly(
            CqlIdentifier.fromInternal("first_name"),
            CqlIdentifier.fromInternal("last_name"),
            CqlIdentifier.fromInternal("address"));
    assertThat(personType.getFieldTypes().get(0)).isEqualTo(DataTypes.TEXT);
    assertThat(personType.getFieldTypes().get(1)).isEqualTo(DataTypes.TEXT);
    UserDefinedType addressType = ((UserDefinedType) personType.getFieldTypes().get(2));
    assertThat(addressType.getKeyspace().asInternal()).isEqualTo("ks");
    assertThat(addressType.getName().asInternal()).isEqualTo("address");
    assertThat(addressType.getFieldNames())
        .containsExactly(
            CqlIdentifier.fromInternal("street"), CqlIdentifier.fromInternal("zipcode"));
  }

  @Test
  public void should_parse_empty_list() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    assertThat(parser.parse(KEYSPACE_ID /* no types*/)).isEmpty();
  }

  @Test
  public void should_parse_singleton_list() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    Map<CqlIdentifier, UserDefinedType> types =
        parser.parse(
            KEYSPACE_ID, mockTypeRow("ks", "t", ImmutableList.of("i"), ImmutableList.of("int")));

    assertThat(types).hasSize(1);
    UserDefinedType type = types.get(CqlIdentifier.fromInternal("t"));
    assertThat(type.getKeyspace().asInternal()).isEqualTo("ks");
    assertThat(type.getName().asInternal()).isEqualTo("t");
    assertThat(type.getFieldNames()).containsExactly(CqlIdentifier.fromInternal("i"));
    assertThat(type.getFieldTypes()).containsExactly(DataTypes.INT);
  }

  @Test
  public void should_resolve_list_dependency() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    Map<CqlIdentifier, UserDefinedType> types =
        parser.parse(
            KEYSPACE_ID,
            mockTypeRow(
                "ks", "a", ImmutableList.of("bs"), ImmutableList.of("frozen<list<frozen<b>>>")),
            mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int")));

    assertThat(types).hasSize(2);
    UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a"));
    UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b"));
    assertThat(((ListType) aType.getFieldTypes().get(0)).getElementType()).isEqualTo(bType);
  }

  @Test
  public void should_resolve_set_dependency() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    Map<CqlIdentifier, UserDefinedType> types =
        parser.parse(
            KEYSPACE_ID,
            mockTypeRow(
                "ks", "a", ImmutableList.of("bs"), ImmutableList.of("frozen<set<frozen<b>>>")),
            mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int")));

    assertThat(types).hasSize(2);
    UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a"));
    UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b"));
    assertThat(((SetType) aType.getFieldTypes().get(0)).getElementType()).isEqualTo(bType);
  }

  @Test
  public void should_resolve_map_dependency() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    Map<CqlIdentifier, UserDefinedType> types =
        parser.parse(
            KEYSPACE_ID,
            mockTypeRow(
                "ks",
                "a1",
                ImmutableList.of("bs"),
                ImmutableList.of("frozen<map<int, frozen<b>>>")),
            mockTypeRow(
                "ks",
                "a2",
                ImmutableList.of("bs"),
                ImmutableList.of("frozen<map<frozen<b>, int>>")),
            mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int")));

    assertThat(types).hasSize(3);
    UserDefinedType a1Type = types.get(CqlIdentifier.fromInternal("a1"));
    UserDefinedType a2Type = types.get(CqlIdentifier.fromInternal("a2"));
    UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b"));
    assertThat(((MapType) a1Type.getFieldTypes().get(0)).getValueType()).isEqualTo(bType);
    assertThat(((MapType) a2Type.getFieldTypes().get(0)).getKeyType()).isEqualTo(bType);
  }

  @Test
  public void should_resolve_tuple_dependency() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    Map<CqlIdentifier, UserDefinedType> types =
        parser.parse(
            KEYSPACE_ID,
            mockTypeRow(
                "ks",
                "a",
                ImmutableList.of("b"),
                ImmutableList.of("frozen<tuple<int, frozen<b>>>")),
            mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int")));

    assertThat(types).hasSize(2);
    UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a"));
    UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b"));
    assertThat(((TupleType) aType.getFieldTypes().get(0)).getComponentTypes().get(1))
        .isEqualTo(bType);
  }

  @Test
  public void should_resolve_nested_dependency() {
    UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context);
    Map<CqlIdentifier, UserDefinedType> types =
        parser.parse(
            KEYSPACE_ID,
            mockTypeRow(
                "ks",
                "a",
                ImmutableList.of("bs"),
                ImmutableList.of("frozen<tuple<int, frozen<list<frozen<b>>>>>")),
            mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int")));

    assertThat(types).hasSize(2);
    UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a"));
    UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b"));
    TupleType tupleType = (TupleType) aType.getFieldTypes().get(0);
    ListType listType = (ListType) tupleType.getComponentTypes().get(1);
    assertThat(listType.getElementType()).isEqualTo(bType);
  }
}
