/**
 * 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;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.exception.TypeExistsException;
import org.apache.atlas.typesystem.types.utils.TypesUtil;
import org.apache.commons.lang3.RandomStringUtils;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import scala.actors.threadpool.Arrays;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

import static org.apache.atlas.typesystem.types.utils.TypesUtil.createClassTypeDef;
import static org.apache.atlas.typesystem.types.utils.TypesUtil.createOptionalAttrDef;
import static org.apache.atlas.typesystem.types.utils.TypesUtil.createRequiredAttrDef;
import static org.apache.atlas.typesystem.types.utils.TypesUtil.createStructTypeDef;
import static org.apache.atlas.typesystem.types.utils.TypesUtil.createTraitTypeDef;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

public class TypeSystemTest extends BaseTest {

    public static final long TEST_DATE_IN_LONG = 1418265358440L;
    public static final String TEST_DATE_STRING = "2014-12-11T02:35:58.440Z";

    @BeforeClass
    public void setUp() throws Exception {
        super.setup();
    }

    @AfterMethod
    public void tearDown() throws Exception {
        getTypeSystem().reset();
    }

    @Test
    public void testGetTypeNames() throws Exception {
        getTypeSystem().defineEnumType("enum_test", new EnumValue("0", 0), new EnumValue("1", 1), new EnumValue("2", 2),
                new EnumValue("3", 3));
        assertTrue(getTypeSystem().getTypeNames().contains("enum_test"));
    }

    @Test
    public void testGetTypeDescription() throws Exception {
        String typeName = "enum_type";
        String description = "_description";
        String typeDescription = typeName + description;
        getTypeSystem().defineEnumType(typeName, typeDescription, new EnumValue("0", 0), new EnumValue("1", 1), new EnumValue("2", 2),
                new EnumValue("3", 3));
        assertTrue(getTypeSystem().getTypeNames().contains(typeName));
        IDataType type = getTypeSystem().getDataType(EnumType.class, typeName);
        Assert.assertNotNull(type);
        Assert.assertEquals(type.getDescription(), typeDescription);

        typeName = "trait_type";
        typeDescription = typeName + description;
        HierarchicalTypeDefinition<TraitType> trait = TypesUtil
            .createTraitTypeDef(typeName, typeDescription, ImmutableSet.<String>of(),
                TypesUtil.createRequiredAttrDef("type", DataTypes.STRING_TYPE));
        getTypeSystem().defineTraitType(trait);
        assertTrue(getTypeSystem().getTypeNames().contains(typeName));
        type = getTypeSystem().getDataType(TraitType.class, typeName);
        Assert.assertNotNull(type);
        Assert.assertEquals(type.getDescription(), typeDescription);

        typeName = "class_type";
        typeDescription = typeName + description;
        HierarchicalTypeDefinition<ClassType> classType = TypesUtil
            .createClassTypeDef(typeName, typeDescription, ImmutableSet.<String>of(),
                TypesUtil.createRequiredAttrDef("type", DataTypes.STRING_TYPE));
        getTypeSystem().defineClassType(classType);
        assertTrue(getTypeSystem().getTypeNames().contains(typeName));
        type = getTypeSystem().getDataType(ClassType.class, typeName);
        Assert.assertNotNull(type);
        Assert.assertEquals(type.getDescription(), typeDescription);

        typeName = "struct_type";
        typeDescription = typeName + description;
        getTypeSystem().defineStructType(typeName, typeDescription, true, createRequiredAttrDef("a", DataTypes.INT_TYPE));
        assertTrue(getTypeSystem().getTypeNames().contains(typeName));
        type = getTypeSystem().getDataType(StructType.class, typeName);
        Assert.assertNotNull(type);
        Assert.assertEquals(type.getDescription(), typeDescription);
        
    }

    @Test
    public void testIsRegistered() throws Exception {
        getTypeSystem().defineEnumType("enum_test", new EnumValue("0", 0), new EnumValue("1", 1), new EnumValue("2", 2),
                new EnumValue("3", 3));
        assertTrue(getTypeSystem().isRegistered("enum_test"));
    }

    @Test
    public void testGetTraitsNames() throws Exception {
        HierarchicalTypeDefinition<TraitType> classificationTraitDefinition = TypesUtil
                .createTraitTypeDef("Classification", ImmutableSet.<String>of(),
                        TypesUtil.createRequiredAttrDef("tag", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<TraitType> piiTrait =
                TypesUtil.createTraitTypeDef("PII", ImmutableSet.<String>of());
        HierarchicalTypeDefinition<TraitType> phiTrait =
                TypesUtil.createTraitTypeDef("PHI", ImmutableSet.<String>of());
        HierarchicalTypeDefinition<TraitType> pciTrait =
                TypesUtil.createTraitTypeDef("PCI", ImmutableSet.<String>of());
        HierarchicalTypeDefinition<TraitType> soxTrait =
                TypesUtil.createTraitTypeDef("SOX", ImmutableSet.<String>of());
        HierarchicalTypeDefinition<TraitType> secTrait =
                TypesUtil.createTraitTypeDef("SEC", ImmutableSet.<String>of());
        HierarchicalTypeDefinition<TraitType> financeTrait =
                TypesUtil.createTraitTypeDef("Finance", ImmutableSet.<String>of());

        getTypeSystem().defineTypes(ImmutableList.<EnumTypeDefinition>of(),
                ImmutableList.<StructTypeDefinition>of(),
                ImmutableList.of(classificationTraitDefinition, piiTrait, phiTrait, pciTrait, soxTrait, secTrait,
                        financeTrait), ImmutableList.<HierarchicalTypeDefinition<ClassType>>of());

        final ImmutableList<String> traitsNames = getTypeSystem().getTypeNamesByCategory(DataTypes.TypeCategory.TRAIT);
        Assert.assertEquals(traitsNames.size(), 7);
        List traits = Arrays.asList(new String[]{"Classification", "PII", "PHI", "PCI", "SOX", "SEC", "Finance",});

        Assert.assertFalse(Collections.disjoint(traitsNames, traits));
    }

    private String random() {
        return RandomStringUtils.random(10);
    }

    @Test
    public void testUTFNames() throws Exception {
        TypeSystem ts = getTypeSystem();

        String enumType = random();
        EnumTypeDefinition orgLevelEnum =
                new EnumTypeDefinition(enumType, new EnumValue(random(), 1), new EnumValue(random(), 2));

        String structName = random();
        String attrType = random();
        StructTypeDefinition structType =
                createStructTypeDef(structName, createRequiredAttrDef(attrType, DataTypes.STRING_TYPE));

        String className = random();
        HierarchicalTypeDefinition<ClassType> classType = createClassTypeDef(className, ImmutableSet.<String>of(),
                createRequiredAttrDef(attrType, DataTypes.STRING_TYPE));

        String traitName = random();
        HierarchicalTypeDefinition<TraitType> traitType = createTraitTypeDef(traitName, ImmutableSet.<String>of(),
                createRequiredAttrDef(attrType, DataTypes.INT_TYPE));

        ts.defineTypes(ImmutableList.of(orgLevelEnum), ImmutableList.of(structType),
                ImmutableList.of(traitType), ImmutableList.of(classType));
    }

    @Test
    public void testTypeCategory() throws AtlasException {
        TypeSystem ts = getTypeSystem();
        ts.reset();

        StructTypeDefinition struct_A = createStructTypeDef("struct_A", createRequiredAttrDef("s_A", DataTypes.STRING_TYPE));
        StructTypeDefinition struct_B = createStructTypeDef("struct_B", createRequiredAttrDef("s_B", DataTypes.STRING_TYPE));

        HierarchicalTypeDefinition<TraitType> trait_A = createTraitTypeDef("trait_A", null,
                createRequiredAttrDef("t_A", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<TraitType> trait_B = createTraitTypeDef("trait_B", ImmutableSet.of("trait_A"),
                createRequiredAttrDef("t_B", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<TraitType> trait_C = createTraitTypeDef("trait_C", ImmutableSet.of("trait_A"),
                createRequiredAttrDef("t_C", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<TraitType> trait_D = createTraitTypeDef("trait_D", ImmutableSet.of("trait_B", "trait_C"),
                createRequiredAttrDef("t_D", DataTypes.STRING_TYPE));

        HierarchicalTypeDefinition<ClassType> class_A = createClassTypeDef("class_A", null,
                createRequiredAttrDef("c_A", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> class_B = createClassTypeDef("class_B", ImmutableSet.of("class_A"),
                createRequiredAttrDef("c_B", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> class_C = createClassTypeDef("class_C", ImmutableSet.of("class_B"),
                createRequiredAttrDef("c_C", DataTypes.STRING_TYPE));

        ts.defineTypes(ImmutableList.<EnumTypeDefinition>of(), ImmutableList.of(struct_A, struct_B),
                ImmutableList.of(trait_A, trait_B, trait_C, trait_D),
                ImmutableList.of(class_A, class_B, class_C));

        final ImmutableList<String> structNames = ts.getTypeNamesByCategory(DataTypes.TypeCategory.STRUCT);
        final ImmutableList<String> traitNames = ts.getTypeNamesByCategory(DataTypes.TypeCategory.TRAIT);
        final ImmutableList<String> classNames = ts.getTypeNamesByCategory(DataTypes.TypeCategory.CLASS);

        Assert.assertEquals(structNames.size(), 2);
        Assert.assertEquals(traitNames.size(), 4);
        Assert.assertEquals(classNames.size(), 3);
    }

    @Test
    public void testTypeNamesAreNotDuplicated() throws Exception {
        TypeSystem typeSystem = getTypeSystem();
        ImmutableList<String> traitNames = typeSystem.getTypeNamesByCategory(DataTypes.TypeCategory.TRAIT);
        int numTraits = traitNames.size();

        HashMap<String, IDataType> typesAdded = new HashMap<>();
        String traitName = "dup_type_test" + random();
        TraitType traitType = new TraitType(typeSystem, traitName, null, null, 0);
        typesAdded.put(traitName, traitType);
        typeSystem.commitTypes(typesAdded);

        traitNames = typeSystem.getTypeNamesByCategory(DataTypes.TypeCategory.TRAIT);
        Assert.assertEquals(traitNames.size(), numTraits+1);

        // add again with another trait this time
        traitName = "dup_type_test" + random();
        TraitType traitTypeNew = new TraitType(typeSystem, traitName, null, null, 0);
        typesAdded.put(traitName, traitTypeNew);

        typeSystem.commitTypes(typesAdded);
        traitNames = typeSystem.getTypeNamesByCategory(DataTypes.TypeCategory.TRAIT);
        Assert.assertEquals(traitNames.size(), numTraits+2);
    }

    @Test
    public void testHierarchy() throws Exception {
        HierarchicalTypeDefinition<ClassType> testObjectDef = TypesUtil.createClassTypeDef("TestObject", ImmutableSet.<String>of(),
            createOptionalAttrDef("name", DataTypes.STRING_TYPE),
            createOptionalAttrDef("description", DataTypes.STRING_TYPE),
            createOptionalAttrDef("topAttribute", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> testDataSetDef = TypesUtil.createClassTypeDef("TestDataSet", ImmutableSet.of("TestObject"));
        HierarchicalTypeDefinition<ClassType> testColumnDef = TypesUtil.createClassTypeDef("TestColumn", ImmutableSet.of("TestObject"),
            createRequiredAttrDef("name", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> testRelationalDataSetDef = 
            TypesUtil.createClassTypeDef("TestRelationalDataSet", ImmutableSet.of("TestDataSet"),
                new AttributeDefinition("columns", DataTypes.arrayTypeName("TestColumn"),
                    Multiplicity.OPTIONAL, true, null));
        HierarchicalTypeDefinition<ClassType> testTableDef = TypesUtil.createClassTypeDef("TestTable", ImmutableSet.of("TestRelationalDataSet"),
            createOptionalAttrDef("schema", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> testDataFileDef = TypesUtil.createClassTypeDef("TestDataFile", ImmutableSet.of("TestRelationalDataSet"),
            createOptionalAttrDef("urlString", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> testDocumentDef = TypesUtil.createClassTypeDef("TestDocument", ImmutableSet.of("TestDataSet"),
            createOptionalAttrDef("urlString", DataTypes.STRING_TYPE),
            createOptionalAttrDef("encoding", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> testAnnotationDef =TypesUtil.createClassTypeDef("TestAnnotation",  ImmutableSet.<String>of(),
            createOptionalAttrDef("inheritedAttribute", DataTypes.STRING_TYPE));
        HierarchicalTypeDefinition<ClassType> myNewAnnotationDef = TypesUtil.createClassTypeDef("MyNewAnnotation", ImmutableSet.of("TestAnnotation"),
            createRequiredAttrDef("myNewAnnotationAttribute", DataTypes.STRING_TYPE));
        getTypeSystem().defineTypes(ImmutableList.<EnumTypeDefinition>of(), ImmutableList.<StructTypeDefinition>of(),
            ImmutableList.<HierarchicalTypeDefinition<TraitType>>of(),
            ImmutableList.of(testObjectDef, testDataSetDef, testColumnDef, testRelationalDataSetDef, testTableDef, testDataFileDef, testDocumentDef, testAnnotationDef, myNewAnnotationDef));

        // Verify that field mappings for MyNewAnnotation contains the attribute inherited from the TestAnnotation superclass.
        // Prior to fix for ATLAS-573, the inherited attribute was missing.
        ClassType dataType = getTypeSystem().getDataType(ClassType.class, "MyNewAnnotation");
        Assert.assertTrue(dataType.fieldMapping.fields.containsKey("inheritedAttribute"));
    }

    @Test
    public void testRedefineExistingType() throws Exception {
        TypeSystem typeSystem = getTypeSystem();
        HierarchicalTypeDefinition<TraitType> trait = TypesUtil
                .createTraitTypeDef(random(), "description", ImmutableSet.<String>of(),
                        TypesUtil.createRequiredAttrDef("type", DataTypes.STRING_TYPE));
        typeSystem.defineTraitType(trait);

        try {
            typeSystem.defineTraitType(trait);
            fail("Expected TypeExistsException");
        } catch(TypeExistsException e) {
            //expected
        }
    }

    @Test
    public void testDuplicateNewTypenames() throws Exception {
        TypeSystem typeSystem = getTypeSystem();
        HierarchicalTypeDefinition<TraitType> trait1 = TypesUtil
                .createTraitTypeDef(random(), "description", ImmutableSet.<String>of(),
                        TypesUtil.createRequiredAttrDef("type", DataTypes.STRING_TYPE));
        // create another trait with the same name
        HierarchicalTypeDefinition<TraitType> trait2 = TypesUtil
                .createTraitTypeDef(trait1.typeName, "description", ImmutableSet.<String>of(),
                        TypesUtil.createRequiredAttrDef("type", DataTypes.STRING_TYPE));

        try {
            typeSystem.defineTypes(ImmutableList.<EnumTypeDefinition>of(), ImmutableList.<StructTypeDefinition>of(),
                    ImmutableList.of(trait1, trait2), ImmutableList.<HierarchicalTypeDefinition<ClassType>>of());
        } catch(AtlasException e) {
            fail("Exception unexpected");
        }
    }

    @Test(expectedExceptions = ValueConversionException.class)
    public void testConvertInvalidDate() throws Exception {
       DataTypes.DATE_TYPE.convert("", Multiplicity.OPTIONAL);
    }

    @Test()
    public void testConvertValidDate() throws Exception {
        Date date = DataTypes.DATE_TYPE.convert(TEST_DATE_STRING, Multiplicity.OPTIONAL);
        Assert.assertEquals(date, new Date(TEST_DATE_IN_LONG));


        StringBuilder buf = new StringBuilder();
        DataTypes.DATE_TYPE.output(new Date(TEST_DATE_IN_LONG), buf, "", new HashSet<Date>());
        Assert.assertEquals(buf.toString(), TEST_DATE_STRING);
    }
}
