/**
 * 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 org.apache.atlas.typesystem.types.cache;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.types.ClassType;
import org.apache.atlas.typesystem.types.DataTypes;
import org.apache.atlas.typesystem.types.DataTypes.TypeCategory;
import org.apache.atlas.typesystem.types.EnumType;
import org.apache.atlas.typesystem.types.EnumValue;
import org.apache.atlas.typesystem.types.IDataType;
import org.apache.atlas.typesystem.types.StructType;
import org.apache.atlas.typesystem.types.TraitType;
import org.apache.atlas.typesystem.types.TypeSystem;
import org.apache.atlas.typesystem.types.utils.TypesUtil;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

/**
 * Tests functional behavior of {@link DefaultTypeCache}
 */
@SuppressWarnings("rawtypes")
public class DefaultTypeCacheTest {

    private String CLASSTYPE_CUSTOMER = "Customer";
    private String STRUCTTYPE_ADDRESS = "Address";
    private String TRAITTYPE_PRIVILEGED = "Privileged";
    private String ENUMTYPE_SHIPPING = "Shipping";

    private String UNKNOWN_TYPE = "UndefinedType";

    private ClassType customerType;
    private StructType addressType;
    private TraitType privilegedTrait;
    private EnumType shippingEnum;

    private DefaultTypeCache cache;

    @BeforeClass
    public void onetimeSetup() throws Exception {

        // init TypeSystem
        TypeSystem ts = TypeSystem.getInstance().reset();

        // Customer ClassType
        customerType = ts.defineClassType(TypesUtil
            .createClassTypeDef(CLASSTYPE_CUSTOMER, ImmutableSet.<String>of(),
                TypesUtil.createRequiredAttrDef("name", DataTypes.STRING_TYPE),
                TypesUtil.createRequiredAttrDef("id", DataTypes.LONG_TYPE)));

        // Address StructType
        addressType = ts.defineStructType(STRUCTTYPE_ADDRESS, true,
                TypesUtil.createRequiredAttrDef("first line", DataTypes.STRING_TYPE),
                TypesUtil.createOptionalAttrDef("second line", DataTypes.STRING_TYPE),
                TypesUtil.createRequiredAttrDef("city", DataTypes.STRING_TYPE),
                TypesUtil.createRequiredAttrDef("pincode", DataTypes.INT_TYPE));

        // Privileged TraitType
        privilegedTrait = ts.defineTraitType(TypesUtil
                .createTraitTypeDef(TRAITTYPE_PRIVILEGED, ImmutableSet.<String>of(),
                        TypesUtil.createRequiredAttrDef("category", DataTypes.INT_TYPE)));

        // Shipping EnumType
        shippingEnum = ts.defineEnumType(TypesUtil.createEnumTypeDef(ENUMTYPE_SHIPPING,
            new EnumValue("Domestic", 1), new EnumValue("International", 2)));
    }

    @BeforeMethod
    public void eachTestSetup() throws Exception {

        cache = new DefaultTypeCache();

        cache.put(customerType);
        cache.put(addressType);
        cache.put(privilegedTrait);
        cache.put(shippingEnum);
    }

    @Test
    public void testCacheGetType() throws Exception {

        IDataType custType = cache.get(CLASSTYPE_CUSTOMER);
        verifyType(custType, CLASSTYPE_CUSTOMER, ClassType.class);

        IDataType addrType = cache.get(STRUCTTYPE_ADDRESS);
        verifyType(addrType, STRUCTTYPE_ADDRESS, StructType.class);

        IDataType privTrait = cache.get(TRAITTYPE_PRIVILEGED);
        verifyType(privTrait, TRAITTYPE_PRIVILEGED, TraitType.class);

        IDataType shippingEnum = cache.get(ENUMTYPE_SHIPPING);
        verifyType(shippingEnum, ENUMTYPE_SHIPPING, EnumType.class);

        assertNull(cache.get(UNKNOWN_TYPE));
    }

    @Test
    public void testCacheGetTypeByCategory() throws Exception {

        IDataType custType = cache.get(TypeCategory.CLASS, CLASSTYPE_CUSTOMER);
        verifyType(custType, CLASSTYPE_CUSTOMER, ClassType.class);

        IDataType addrType = cache.get(TypeCategory.STRUCT, STRUCTTYPE_ADDRESS);
        verifyType(addrType, STRUCTTYPE_ADDRESS, StructType.class);

        IDataType privTrait = cache.get(TypeCategory.TRAIT, TRAITTYPE_PRIVILEGED);
        verifyType(privTrait, TRAITTYPE_PRIVILEGED, TraitType.class);

        IDataType shippingEnum = cache.get(TypeCategory.ENUM, ENUMTYPE_SHIPPING);
        verifyType(shippingEnum, ENUMTYPE_SHIPPING, EnumType.class);

        assertNull(cache.get(UNKNOWN_TYPE));
    }

    private void verifyType(IDataType actualType, String expectedName, Class<? extends IDataType> typeClass) {

        assertNotNull(actualType, "The " + expectedName + " type not in cache");
        assertTrue(typeClass.isInstance(actualType));
        assertEquals(actualType.getName(), expectedName, "The type name does not match");
    }

    @Test
    public void testCacheHasType() throws Exception {

        assertTrue(cache.has(CLASSTYPE_CUSTOMER));
        assertTrue(cache.has(STRUCTTYPE_ADDRESS));
        assertTrue(cache.has(TRAITTYPE_PRIVILEGED));
        assertTrue(cache.has(ENUMTYPE_SHIPPING));

        assertFalse(cache.has(UNKNOWN_TYPE));
    }

    @Test
    public void testCacheHasTypeByCategory() throws Exception {

        assertTrue(cache.has(TypeCategory.CLASS, CLASSTYPE_CUSTOMER));
        assertTrue(cache.has(TypeCategory.STRUCT, STRUCTTYPE_ADDRESS));
        assertTrue(cache.has(TypeCategory.TRAIT, TRAITTYPE_PRIVILEGED));
        assertTrue(cache.has(TypeCategory.ENUM, ENUMTYPE_SHIPPING));

        assertFalse(cache.has(UNKNOWN_TYPE));
    }

    @Test
    public void testCacheGetAllTypeNames() throws Exception {

        List<String> allTypeNames = new ArrayList<>(cache.getAllTypeNames());
        Collections.sort(allTypeNames);

        final int EXPECTED_TYPE_COUNT = 4;
        assertEquals(allTypeNames.size(), EXPECTED_TYPE_COUNT, "Total number of types does not match.");

        assertEquals(STRUCTTYPE_ADDRESS, allTypeNames.get(0));
        assertEquals(CLASSTYPE_CUSTOMER, allTypeNames.get(1));
        assertEquals(TRAITTYPE_PRIVILEGED, allTypeNames.get(2));
        assertEquals(ENUMTYPE_SHIPPING, allTypeNames.get(3));
    }

    private Collection<String> getTypeNamesByCategory(final TypeCategory category)
            throws AtlasException {
        return cache.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
            put(TypeCache.TYPE_FILTER.CATEGORY, category.name());
        }});
    }

    @Test
    public void testCacheGetTypeNamesByCategory() throws Exception {
        List<String> classTypes = new ArrayList(getTypeNamesByCategory(TypeCategory.CLASS));
        final int EXPECTED_CLASSTYPE_COUNT = 1;
        assertEquals(classTypes.size(), EXPECTED_CLASSTYPE_COUNT);
        assertEquals(CLASSTYPE_CUSTOMER, classTypes.get(0));

        List<String> structTypes = new ArrayList(getTypeNamesByCategory(TypeCategory.STRUCT));
        final int EXPECTED_STRUCTTYPE_COUNT = 1;
        assertEquals(structTypes.size(), EXPECTED_STRUCTTYPE_COUNT);
        assertEquals(STRUCTTYPE_ADDRESS, structTypes.get(0));

        List<String> traitTypes = new ArrayList(getTypeNamesByCategory(TypeCategory.TRAIT));
        final int EXPECTED_TRAITTYPE_COUNT = 1;
        assertEquals(traitTypes.size(), EXPECTED_TRAITTYPE_COUNT);
        assertEquals(TRAITTYPE_PRIVILEGED, traitTypes.get(0));

        List<String> enumTypes = new ArrayList(getTypeNamesByCategory(TypeCategory.ENUM));
        final int EXPECTED_ENUMTYPE_COUNT = 1;
        assertEquals(enumTypes.size(), EXPECTED_ENUMTYPE_COUNT);
        assertEquals(ENUMTYPE_SHIPPING, enumTypes.get(0));
    }

    @Test
    public void testCacheBulkInsert() throws Exception {

        List<IDataType> allTypes = new ArrayList<>();
        allTypes.add(customerType);
        allTypes.add(addressType);
        allTypes.add(privilegedTrait);
        allTypes.add(shippingEnum);

        // create a new cache instead of using the one setup for every method call
        cache = new DefaultTypeCache();
        cache.putAll(allTypes);

        IDataType custType = cache.get(CLASSTYPE_CUSTOMER);
        verifyType(custType, CLASSTYPE_CUSTOMER, ClassType.class);

        IDataType addrType = cache.get(STRUCTTYPE_ADDRESS);
        verifyType(addrType, STRUCTTYPE_ADDRESS, StructType.class);

        IDataType privTrait = cache.get(TRAITTYPE_PRIVILEGED);
        verifyType(privTrait, TRAITTYPE_PRIVILEGED, TraitType.class);

        IDataType shippingEnum = cache.get(ENUMTYPE_SHIPPING);
        verifyType(shippingEnum, ENUMTYPE_SHIPPING, EnumType.class);
    }

    @Test
    public void testCacheRemove() throws Exception {
        cache.remove(CLASSTYPE_CUSTOMER);
        assertNull(cache.get(CLASSTYPE_CUSTOMER));
        assertFalse(cache.has(CLASSTYPE_CUSTOMER));
        assertTrue(getTypeNamesByCategory(TypeCategory.CLASS).isEmpty());

        final int EXPECTED_TYPE_COUNT = 3;
        assertEquals(cache.getAllTypeNames().size(), EXPECTED_TYPE_COUNT);
    }

    @Test
    public void testCacheRemoveByCategory() throws Exception {

        cache.remove(TypeCategory.CLASS, CLASSTYPE_CUSTOMER);
        assertNull(cache.get(CLASSTYPE_CUSTOMER));
        assertFalse(cache.has(CLASSTYPE_CUSTOMER));
        assertTrue(getTypeNamesByCategory(TypeCategory.CLASS).isEmpty());

        final int EXPECTED_TYPE_COUNT = 3;
        assertEquals(cache.getAllTypeNames().size(), EXPECTED_TYPE_COUNT);
    }

    @Test
    public void testCacheClear() throws Exception {

        cache.clear();

        assertNull(cache.get(CLASSTYPE_CUSTOMER));
        assertFalse(cache.has(CLASSTYPE_CUSTOMER));

        assertNull(cache.get(STRUCTTYPE_ADDRESS));
        assertFalse(cache.has(STRUCTTYPE_ADDRESS));

        assertNull(cache.get(TRAITTYPE_PRIVILEGED));
        assertFalse(cache.has(TRAITTYPE_PRIVILEGED));

        assertNull(cache.get(ENUMTYPE_SHIPPING));
        assertFalse(cache.has(ENUMTYPE_SHIPPING));

        assertTrue(getTypeNamesByCategory(TypeCategory.CLASS).isEmpty());
        assertTrue(getTypeNamesByCategory(TypeCategory.STRUCT).isEmpty());
        assertTrue(getTypeNamesByCategory(TypeCategory.TRAIT).isEmpty());
        assertTrue(getTypeNamesByCategory(TypeCategory.ENUM).isEmpty());

        assertTrue(cache.getAllTypeNames().isEmpty());
    }

    @Test(expectedExceptions = AtlasException.class)
    public void testPutTypeWithNullType() throws Exception {

        cache.put(null);
        fail("Null type should be not allowed in 'put'");
    }

    @Test(expectedExceptions = AtlasException.class)
    public void testPutTypeWithInvalidType() throws Exception {

        cache.put(DataTypes.BOOLEAN_TYPE);
        fail("type should only be an instance of ClassType | EnumType | StructType | TraitType in 'put'");
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testGetTypeWithNullCategory() throws Exception {

        cache.get(null, CLASSTYPE_CUSTOMER);
        fail("Null TypeCategory should be not allowed in 'get'");
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testGetTypeWithInvalidCategory() throws Exception {

        cache.get(TypeCategory.PRIMITIVE, DataTypes.BOOLEAN_TYPE.getName());
        fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'get'");
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testCacheHasTypeWithNullCategory() throws Exception {

        cache.has(null, CLASSTYPE_CUSTOMER);
        fail("Null TypeCategory should be not allowed in 'has'");
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testCacheHasTypeWithInvalidCategory() throws Exception {

        cache.has(TypeCategory.PRIMITIVE, DataTypes.BOOLEAN_TYPE.getName());
        fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'has'");
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testCacheGetTypeNamesByInvalidCategory() throws Exception {
        getTypeNamesByCategory(TypeCategory.PRIMITIVE);
        fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'getNames'");
    }

    @Test(expectedExceptions = AtlasException.class)
    public void testCacheBulkInsertWithNullType() throws Exception {

        List<IDataType> allTypes = new ArrayList<>();
        allTypes.add(null);

        // create a new cache instead of using the one setup for every method call
        cache = new DefaultTypeCache();
        cache.putAll(allTypes);

        fail("Null type should be not allowed in 'putAll'");
    }

    @Test(expectedExceptions = AtlasException.class)
    public void testCacheBulkInsertWithInvalidType() throws Exception {

        List<IDataType> allTypes = new ArrayList<>();
        allTypes.add(DataTypes.BOOLEAN_TYPE);

        // create a new cache instead of using the one setup for every method call
        cache = new DefaultTypeCache();
        cache.putAll(allTypes);

        fail("type should only one of ClassType | EnumType | StructType | TraitType in 'putAll'");
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testCacheRemoveByNullCategory() throws Exception {

        cache.remove(null, CLASSTYPE_CUSTOMER);
        fail("Null type should be not allowed in 'remove'");
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testCacheRemoveByInvalidCategory() throws Exception {

        cache.remove(TypeCategory.PRIMITIVE, DataTypes.BOOLEAN_TYPE.getName());
        fail("TypeCategory should only be one of TypeCategory.CLASS | ENUM | STRUCT | TRAIT in 'remove'");
    }

    @Test
    public void testGetTypesByFilter() throws Exception {
        // init TypeSystem
        TypeSystem ts = TypeSystem.getInstance().reset();

        ts.defineClassType(TypesUtil.createClassTypeDef("A", ImmutableSet.<String>of()));
        ts.defineClassType(TypesUtil.createClassTypeDef("A1", ImmutableSet.of("A")));

        ts.defineClassType(TypesUtil.createClassTypeDef("B", ImmutableSet.<String>of()));

        ts.defineClassType(TypesUtil.createClassTypeDef("C", ImmutableSet.of("B", "A")));

        //supertype ~ A
        ImmutableList<String> results = ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
                    put(TypeCache.TYPE_FILTER.SUPERTYPE, "A");
                }});
        assertTrue(results.containsAll(Arrays.asList("A1", "C")), "Results: " + results);

        //!supertype doesn't return the type itself
        results = ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
            put(TypeCache.TYPE_FILTER.NOT_SUPERTYPE, "A");
        }});
        assertTrue(results.containsAll(Arrays.asList("B")), "Results: " + results);

        //supertype ~ A && supertype !~ B
        results = ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
            put(TypeCache.TYPE_FILTER.SUPERTYPE, "A");
            put(TypeCache.TYPE_FILTER.NOT_SUPERTYPE, "B");
        }});
        assertTrue(results.containsAll(Arrays.asList("A1")), "Results: " + results);

        //none of category trait
        results = ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
            put(TypeCache.TYPE_FILTER.CATEGORY, TypeCategory.TRAIT.name());
            put(TypeCache.TYPE_FILTER.SUPERTYPE, "A");
        }});
        assertTrue(results.isEmpty(), "Results: " + results);

        //no filter returns all types
        results = ts.getTypeNames(null);
        assertTrue(results.containsAll(Arrays.asList("A", "A1", "B", "C")), "Results: " + results);

        results = ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>());
        assertTrue(results.containsAll(Arrays.asList("A", "A1", "B", "C")), "Results: " + results);

        //invalid category
        try {
            ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
                put(TypeCache.TYPE_FILTER.CATEGORY, "A");
            }});
            fail("Expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            //expected
        }

        //invalid supertype
        results = ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
            put(TypeCache.TYPE_FILTER.SUPERTYPE, "X");
        }});
        assertTrue(results.isEmpty(), "Expected empty result for non-existent type 'X'. Found: " + results);

        //invalid supertype
        results = ts.getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
            put(TypeCache.TYPE_FILTER.NOT_SUPERTYPE, "X");
        }});
        assertTrue(results.containsAll(Arrays.asList("A", "A1", "B", "C")), "Results: " + results);
    }
}
