/*
 * 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.oss.driver.internal.core.type.codec.registry;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;

import com.datastax.oss.driver.api.core.ProtocolVersion;
import com.datastax.oss.driver.api.core.data.TupleValue;
import com.datastax.oss.driver.api.core.data.UdtValue;
import com.datastax.oss.driver.api.core.type.DataType;
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.codec.CodecNotFoundException;
import com.datastax.oss.driver.api.core.type.codec.TypeCodec;
import com.datastax.oss.driver.api.core.type.codec.TypeCodecs;
import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
import com.datastax.oss.driver.api.core.type.reflect.GenericType;
import com.datastax.oss.driver.internal.core.data.DefaultTupleValue;
import com.datastax.oss.driver.internal.core.data.DefaultUdtValue;
import com.datastax.oss.driver.internal.core.type.codec.CqlIntToStringCodec;
import com.datastax.oss.driver.internal.core.type.codec.IntCodec;
import com.datastax.oss.driver.internal.core.type.codec.ListCodec;
import com.datastax.oss.driver.internal.core.type.codec.registry.CachingCodecRegistryTest.TestCachingCodecRegistry.MockCache;
import com.datastax.oss.driver.shaded.guava.common.collect.Lists;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.nio.ByteBuffer;
import java.time.Period;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@RunWith(DataProviderRunner.class)
public class CachingCodecRegistryTest {

  @Mock private MockCache mockCache;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
  }

  @Test
  @UseDataProvider(
      value = "primitiveCodecs",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_find_primitive_codecs_for_types(TypeCodec<?> codec) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    DataType cqlType = codec.getCqlType();
    GenericType<?> javaType = codec.getJavaType();
    assertThat(registry.codecFor(cqlType, javaType)).isSameAs(codec);
    assertThat(registry.codecFor(cqlType)).isSameAs(codec);
    assertThat(javaType.__getToken().getType()).isInstanceOf(Class.class);
    Class<?> javaClass = (Class<?>) javaType.__getToken().getType();
    assertThat(registry.codecFor(cqlType, javaClass)).isSameAs(codec);
    // Primitive mappings never hit the cache
    verifyZeroInteractions(mockCache);
  }

  @Test
  @UseDataProvider(
      value = "primitiveCodecsWithValues",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_find_primitive_codecs_for_value(Object value, TypeCodec<?> codec) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    assertThat(registry.codecFor(value)).isEqualTo(codec);
    verifyZeroInteractions(mockCache);
  }

  @Test
  @UseDataProvider(
      value = "primitiveCodecsWithCqlTypesAndValues",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_find_primitive_codecs_for_cql_type_and_value(
      DataType cqlType, Object value, TypeCodec<?> codec) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    assertThat(registry.codecFor(cqlType, value)).isEqualTo(codec);
    verifyZeroInteractions(mockCache);
  }

  @Test
  public void should_find_user_codec_for_built_in_java_type() {
    // int and String are built-in types, but int <-> String is not a built-in mapping
    CqlIntToStringCodec intToStringCodec1 = new CqlIntToStringCodec();
    // register a second codec to also check that the first one is preferred
    CqlIntToStringCodec intToStringCodec2 = new CqlIntToStringCodec();
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    registry.register(intToStringCodec1, intToStringCodec2);
    verify(mockCache).lookup(DataTypes.INT, GenericType.STRING, false);

    // When the mapping is not ambiguous, the user type should be returned
    assertThat(registry.codecFor(DataTypes.INT, GenericType.STRING)).isSameAs(intToStringCodec1);
    assertThat(registry.codecFor(DataTypes.INT, String.class)).isSameAs(intToStringCodec1);
    assertThat(registry.codecFor(DataTypes.INT, "123")).isSameAs(intToStringCodec1);

    // When there is an ambiguity with a built-in codec, the built-in codec should have priority
    assertThat(registry.codecFor(DataTypes.INT)).isSameAs(TypeCodecs.INT);
    assertThat(registry.codecFor("123")).isSameAs(TypeCodecs.TEXT);

    verifyZeroInteractions(mockCache);
  }

  @Test
  public void should_find_user_codec_for_custom_java_type() {
    TextToPeriodCodec textToPeriodCodec1 = new TextToPeriodCodec();
    TextToPeriodCodec textToPeriodCodec2 = new TextToPeriodCodec();
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    registry.register(textToPeriodCodec1, textToPeriodCodec2);
    verify(mockCache).lookup(DataTypes.TEXT, GenericType.of(Period.class), false);

    assertThat(registry.codecFor(DataTypes.TEXT, GenericType.of(Period.class)))
        .isSameAs(textToPeriodCodec1);
    assertThat(registry.codecFor(DataTypes.TEXT, Period.class)).isSameAs(textToPeriodCodec1);
    assertThat(registry.codecFor(DataTypes.TEXT, Period.ofDays(1))).isSameAs(textToPeriodCodec1);
    // Now even the search by Java value only is not ambiguous
    assertThat(registry.codecFor(Period.ofDays(1))).isSameAs(textToPeriodCodec1);

    // The search by CQL type only still returns the built-in codec
    assertThat(registry.codecFor(DataTypes.TEXT)).isSameAs(TypeCodecs.TEXT);

    verifyZeroInteractions(mockCache);
  }

  @Test
  @UseDataProvider(
      value = "collectionsWithCqlAndJavaTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_collection_codec_for_cql_and_java_types(
      DataType cqlType, GenericType<?> javaType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(cqlType, javaType);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(javaType)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, javaType, false);
  }

  @Test
  @UseDataProvider(
      value = "collectionsWithCqlAndJavaTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_collection_codec_for_cql_type(
      DataType cqlType, GenericType<?> javaType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(cqlType);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(javaType)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, null, false);
  }

  @Test
  @UseDataProvider(
      value = "collectionsWithCqlAndJavaTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_collection_codec_for_cql_type_and_java_value(
      DataType cqlType, GenericType<?> javaType, GenericType<?> javaTypeLookup, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(cqlType, value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(javaType)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, javaTypeLookup, true);
  }

  @Test
  @UseDataProvider(
      value = "collectionsWithCqlAndJavaTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_collection_codec_for_java_value(
      DataType cqlType, GenericType<?> javaType, GenericType<?> javaTypeLookup, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(javaType)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, javaTypeLookup, true);
  }

  @Test
  @UseDataProvider(
      value = "emptyCollectionsWithCqlAndJavaTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_collection_codec_for_empty_java_value(
      DataType cqlType,
      GenericType<?> javaType,
      DataType cqlTypeLookup,
      GenericType<?> javaTypeLookup,
      Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<Object> codec = registry.codecFor(value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isFalse();
    assertThat(codec.accepts(javaType)).isFalse();
    assertThat(codec.accepts(value)).isTrue();

    // Note that empty collections without CQL type are a corner case, in that the registry returns
    // a codec that does not accept cqlType, nor the value's declared Java type.
    // The only requirement is that it can encode the value, which holds true:
    codec.encode(value, ProtocolVersion.DEFAULT);

    inOrder.verify(mockCache).lookup(cqlTypeLookup, javaTypeLookup, true);
  }

  @Test
  @UseDataProvider(
      value = "emptyCollectionsWithCqlAndJavaTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_collection_codec_for_cql_type_and_empty_java_value(
      DataType cqlType, GenericType<?> javaType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<Object> codec = registry.codecFor(cqlType, value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(javaType)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    // Verify that the codec can encode the value
    codec.encode(value, ProtocolVersion.DEFAULT);
    inOrder.verify(mockCache).lookup(cqlType, javaType, true);
  }

  @Test
  @UseDataProvider(
      value = "collectionsWithNullElements",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_throw_for_collection_containing_null_element(Object value, String expected) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    assertThatThrownBy(() -> registry.codecFor(value))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage(expected);
  }

  @Test
  @UseDataProvider(
      value = "tuplesWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_tuple_codec_for_cql_and_java_types(DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<TupleValue> codec = registry.codecFor(cqlType, GenericType.TUPLE_VALUE);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue();
    assertThat(codec.accepts(TupleValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, GenericType.TUPLE_VALUE, false);
    // field codecs are only looked up when fields are accessed, so no cache hit for list<int> now
  }

  @Test
  @UseDataProvider(
      value = "tuplesWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_tuple_codec_for_cql_type(DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<TupleValue> codec = registry.codecFor(cqlType);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue();
    assertThat(codec.accepts(TupleValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, null, false);
  }

  @Test
  @UseDataProvider(
      value = "tuplesWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_tuple_codec_for_cql_type_and_java_value(
      DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(cqlType, value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue();
    assertThat(codec.accepts(TupleValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, GenericType.of(DefaultTupleValue.class), true);
    inOrder.verifyNoMoreInteractions();
  }

  @Test
  @UseDataProvider(
      value = "tuplesWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_tuple_codec_for_java_value(DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue();
    assertThat(codec.accepts(TupleValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, GenericType.of(DefaultTupleValue.class), true);
    inOrder.verifyNoMoreInteractions();
  }

  @Test
  @UseDataProvider(
      value = "udtsWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_udt_codec_for_cql_and_java_types(DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<UdtValue> codec = registry.codecFor(cqlType, GenericType.UDT_VALUE);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue();
    assertThat(codec.accepts(UdtValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, GenericType.UDT_VALUE, false);
    // field codecs are only looked up when fields are accessed, so no cache hit for list<int> now

  }

  @Test
  @UseDataProvider(
      value = "udtsWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_udt_codec_for_cql_type(DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<UdtValue> codec = registry.codecFor(cqlType);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue();
    assertThat(codec.accepts(UdtValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, null, false);
  }

  @Test
  @UseDataProvider(
      value = "udtsWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_udt_codec_for_cql_type_and_java_value(DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(cqlType, value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue();
    assertThat(codec.accepts(UdtValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, GenericType.of(DefaultUdtValue.class), true);
    inOrder.verifyNoMoreInteractions();
  }

  @Test
  @UseDataProvider(
      value = "udtsWithCqlTypes",
      location = CachingCodecRegistryTestDataProviders.class)
  public void should_create_udt_codec_for_java_value(DataType cqlType, Object value) {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    InOrder inOrder = inOrder(mockCache);
    TypeCodec<?> codec = registry.codecFor(value);
    assertThat(codec).isNotNull();
    assertThat(codec.accepts(cqlType)).isTrue();
    assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue();
    assertThat(codec.accepts(UdtValue.class)).isTrue();
    assertThat(codec.accepts(value)).isTrue();
    inOrder.verify(mockCache).lookup(cqlType, GenericType.of(DefaultUdtValue.class), true);
    inOrder.verifyNoMoreInteractions();
  }

  @Test
  public void should_not_find_codec_if_java_type_unknown() {
    try {
      CodecRegistry.DEFAULT.codecFor(StringBuilder.class);
      fail("Should not have found a codec for ANY <-> StringBuilder");
    } catch (CodecNotFoundException e) {
      // expected
    }
    try {
      CodecRegistry.DEFAULT.codecFor(DataTypes.TEXT, StringBuilder.class);
      fail("Should not have found a codec for varchar <-> StringBuilder");
    } catch (CodecNotFoundException e) {
      // expected
    }
    try {
      CodecRegistry.DEFAULT.codecFor(new StringBuilder());
      fail("Should not have found a codec for ANY <-> StringBuilder");
    } catch (CodecNotFoundException e) {
      // expected
    }
  }

  @Test
  public void should_not_allow_covariance_for_lookups_by_java_type() {

    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    registry.register(new ACodec());
    InOrder inOrder = inOrder(mockCache);

    // covariance not allowed

    assertThatThrownBy(() -> registry.codecFor(B.class))
        .isInstanceOf(CodecNotFoundException.class)
        .hasMessage("Codec not found for requested operation: [null <-> %s]", B.class.getName());
    // because of invariance, the custom A codec doesn't match so we try the cache
    inOrder.verify(mockCache).lookup(null, GenericType.of(B.class), false);
    inOrder.verifyNoMoreInteractions();

    assertThatThrownBy(() -> registry.codecFor(GenericType.listOf(B.class)))
        .isInstanceOf(CodecNotFoundException.class);
    inOrder.verify(mockCache).lookup(null, GenericType.listOf(B.class), false);
    inOrder.verify(mockCache).lookup(null, GenericType.of(B.class), false);
    inOrder.verifyNoMoreInteractions();
  }

  @Test
  public void should_allow_covariance_for_lookups_by_cql_type_and_value() {

    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    registry.register(new ACodec());
    InOrder inOrder = inOrder(mockCache);
    inOrder.verify(mockCache).lookup(DataTypes.INT, GenericType.of(A.class), false);

    // covariance allowed

    assertThat(registry.codecFor(DataTypes.INT, new B())).isInstanceOf(ACodec.class);
    // no cache hit since we find the custom codec directly
    inOrder.verifyNoMoreInteractions();

    // note: in Java, type parameters are always invariant, so List<B> is not a subtype of List<A>;
    // but in practice, a codec for List<A> is capable of encoding a List<B>, so we allow it (even
    // if in driver 3.x that was forbidden).
    List<B> list = Lists.newArrayList(new B());
    ListType cqlType = DataTypes.listOf(DataTypes.INT);
    TypeCodec<List<B>> actual = registry.codecFor(cqlType, list);
    assertThat(actual).isInstanceOf(ListCodec.class);
    assertThat(actual.getJavaType()).isEqualTo(GenericType.listOf(A.class));
    assertThat(actual.accepts(list)).isTrue();
    // accepts(GenericType) remains invariant, so it returns false for List<B>
    assertThat(actual.accepts(GenericType.listOf(B.class))).isFalse();
    inOrder.verify(mockCache).lookup(cqlType, GenericType.listOf(B.class), true);
    inOrder.verifyNoMoreInteractions();
  }

  @Test
  public void should_allow_covariance_for_lookups_by_value() {

    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    registry.register(new ACodec());
    InOrder inOrder = inOrder(mockCache);
    inOrder.verify(mockCache).lookup(DataTypes.INT, GenericType.of(A.class), false);

    // covariance allowed

    assertThat(registry.codecFor(new B())).isInstanceOf(ACodec.class);
    // no cache hit since we find the custom codec directly
    inOrder.verifyNoMoreInteractions();

    // note: in Java, type parameters are always invariant, so List<B> is not a subtype of List<A>;
    // but in practice, a codec for List<A> is capable of encoding a List<B>, so we allow it (even
    // if in driver 3.x that was forbidden).
    List<B> list = Lists.newArrayList(new B());
    TypeCodec<List<B>> actual = registry.codecFor(list);
    assertThat(actual).isInstanceOf(ListCodec.class);
    assertThat(actual.getJavaType()).isEqualTo(GenericType.listOf(A.class));
    assertThat(actual.accepts(list)).isTrue();
    // accepts(GenericType) remains invariant, so it returns false for List<B>
    assertThat(actual.accepts(GenericType.listOf(B.class))).isFalse();
    inOrder.verify(mockCache).lookup(null, GenericType.listOf(B.class), true);
    inOrder.verifyNoMoreInteractions();
  }

  @Test
  public void should_register_user_codec_at_runtime() {
    CqlIntToStringCodec intToStringCodec = new CqlIntToStringCodec();
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    registry.register(intToStringCodec);
    // register checks the cache for collisions
    verify(mockCache).lookup(DataTypes.INT, GenericType.STRING, false);

    // When the mapping is not ambiguous, the user type should be returned
    assertThat(registry.codecFor(DataTypes.INT, GenericType.STRING)).isSameAs(intToStringCodec);
    assertThat(registry.codecFor(DataTypes.INT, String.class)).isSameAs(intToStringCodec);
    assertThat(registry.codecFor(DataTypes.INT, "123")).isSameAs(intToStringCodec);

    // When there is an ambiguity with a built-in codec, the built-in codec should have priority
    assertThat(registry.codecFor(DataTypes.INT)).isSameAs(TypeCodecs.INT);
    assertThat(registry.codecFor("123")).isSameAs(TypeCodecs.TEXT);

    verifyZeroInteractions(mockCache);
  }

  @Test
  public void should_ignore_user_codec_if_collides_with_builtin_codec() {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);

    IntCodec userIntCodec = new IntCodec();
    registry.register(userIntCodec);

    assertThat(registry.codecFor(DataTypes.INT, Integer.class)).isNotSameAs(userIntCodec);
  }

  @Test
  public void should_ignore_user_codec_if_collides_with_other_user_codec() {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);
    CqlIntToStringCodec intToStringCodec1 = new CqlIntToStringCodec();
    CqlIntToStringCodec intToStringCodec2 = new CqlIntToStringCodec();

    registry.register(intToStringCodec1, intToStringCodec2);

    assertThat(registry.codecFor(DataTypes.INT, GenericType.STRING)).isSameAs(intToStringCodec1);
  }

  @Test
  public void should_ignore_user_codec_if_collides_with_generated_codec() {
    TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache);

    TypeCodec<List<Integer>> userListOfIntCodec = TypeCodecs.listOf(TypeCodecs.INT);
    registry.register(userListOfIntCodec);

    assertThat(
            registry.codecFor(DataTypes.listOf(DataTypes.INT), GenericType.listOf(Integer.class)))
        .isNotSameAs(userListOfIntCodec);
  }

  // Our intent is not to test Guava cache, so we don't need an actual cache here.
  // The only thing we want to check in our tests is if getCachedCodec was called.
  public static class TestCachingCodecRegistry extends CachingCodecRegistry {
    private final MockCache cache;

    TestCachingCodecRegistry(MockCache cache) {
      super("test", CodecRegistryConstants.PRIMITIVE_CODECS);
      this.cache = cache;
    }

    @Override
    protected TypeCodec<?> getCachedCodec(
        @Nullable DataType cqlType, @Nullable GenericType<?> javaType, boolean isJavaCovariant) {
      cache.lookup(cqlType, javaType, isJavaCovariant);
      return createCodec(cqlType, javaType, isJavaCovariant);
    }

    public interface MockCache {
      void lookup(
          @Nullable DataType cqlType, @Nullable GenericType<?> javaType, boolean isJavaCovariant);
    }
  }

  public static class TextToPeriodCodec implements TypeCodec<Period> {
    @NonNull
    @Override
    public GenericType<Period> getJavaType() {
      return GenericType.of(Period.class);
    }

    @NonNull
    @Override
    public DataType getCqlType() {
      return DataTypes.TEXT;
    }

    @Override
    public ByteBuffer encode(Period value, @NonNull ProtocolVersion protocolVersion) {
      throw new UnsupportedOperationException("not implemented for this test");
    }

    @Override
    public Period decode(ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) {
      throw new UnsupportedOperationException("not implemented for this test");
    }

    @NonNull
    @Override
    public String format(Period value) {
      throw new UnsupportedOperationException("not implemented for this test");
    }

    @Override
    public Period parse(String value) {
      throw new UnsupportedOperationException("not implemented for this test");
    }
  }

  private static class A {}

  private static class B extends A {}

  private static class ACodec implements TypeCodec<A> {

    @NonNull
    @Override
    public GenericType<A> getJavaType() {
      return GenericType.of(A.class);
    }

    @NonNull
    @Override
    public DataType getCqlType() {
      return DataTypes.INT;
    }

    @Override
    public ByteBuffer encode(A value, @NonNull ProtocolVersion protocolVersion) {
      throw new UnsupportedOperationException("irrelevant");
    }

    @Override
    public A decode(ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) {
      throw new UnsupportedOperationException("irrelevant");
    }

    @NonNull
    @Override
    public String format(A value) {
      throw new UnsupportedOperationException("irrelevant");
    }

    @Override
    public A parse(String value) {
      throw new UnsupportedOperationException("irrelevant");
    }
  }
}
