/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.web.integration;

import com.sun.jersey.core.util.MultivaluedMapImpl;
import org.apache.atlas.AtlasClientV2;
import org.apache.atlas.AtlasServiceException;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.SearchFilter;
import org.apache.atlas.model.TypeCategory;
import org.apache.atlas.model.typedef.AtlasBaseTypeDef;
import org.apache.atlas.model.typedef.AtlasBusinessMetadataDef;
import org.apache.atlas.model.typedef.AtlasClassificationDef;
import org.apache.atlas.model.typedef.AtlasEntityDef;
import org.apache.atlas.model.typedef.AtlasEnumDef;
import org.apache.atlas.model.typedef.AtlasRelationshipDef;
import org.apache.atlas.model.typedef.AtlasStructDef;
import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef;
import org.apache.atlas.model.typedef.AtlasTypeDefHeader;
import org.apache.atlas.model.typedef.AtlasTypesDef;
import org.apache.atlas.type.AtlasTypeUtil;
import org.apache.atlas.utils.AuthenticationUtil;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import static org.apache.atlas.AtlasErrorCode.TYPE_NAME_NOT_FOUND;
import static org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.Cardinality;
import static org.apache.atlas.type.AtlasTypeUtil.createClassTypeDef;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

/**
 * Integration test for types jersey resource.
 */
public class TypedefsJerseyResourceIT extends BaseResourceIT {
    private AtlasTypesDef typeDefinitions;

    private AtlasClientV2 clientV2;

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

        typeDefinitions = createHiveTypesV2();

        if (!AuthenticationUtil.isKerberosAuthenticationEnabled()) {
            clientV2 = new AtlasClientV2(atlasUrls, new String[] {"admin", "admin"});
        } else {
            clientV2 = new AtlasClientV2(atlasUrls);
        }
    }

    @AfterClass
    public void tearDown() throws Exception {
        emptyTypeDefs(typeDefinitions);
    }

    @Test
    public void testCreate() throws Exception {
        createType(typeDefinitions);

        // validate if all types created successfully
        for (AtlasEnumDef enumDef : typeDefinitions.getEnumDefs()) {
            checkIfTypeExists(enumDef.getName());
        }

        for (AtlasStructDef structDef : typeDefinitions.getStructDefs()) {
            checkIfTypeExists(structDef.getName());
        }

        for (AtlasClassificationDef classificationDef : typeDefinitions.getClassificationDefs()) {
            checkIfTypeExists(classificationDef.getName());
        }

        for (AtlasEntityDef entityDef : typeDefinitions.getEntityDefs()) {
            checkIfTypeExists(entityDef.getName());
        }

        for (AtlasRelationshipDef relationshipDef : typeDefinitions.getRelationshipDefs()) {
            checkIfTypeExists(relationshipDef.getName());
        }

        for (AtlasBusinessMetadataDef businessMetadataDef : typeDefinitions.getBusinessMetadataDefs()) {
            checkIfTypeExists(businessMetadataDef.getName());
        }
    }

    @Test
    public void testGetHeaders() throws Exception {
        MultivaluedMap<String, String> filterParams = new MultivaluedMapImpl();

        filterParams.add(SearchFilter.PARAM_TYPE, "ENTITY");

        List<AtlasTypeDefHeader> headers = clientV2.getAllTypeDefHeaders(new SearchFilter(filterParams));

        assertNotNull(headers);
    }

    @Test(dependsOnMethods = "testGetDefinition")
    public void testDeleteAtlasTypeByName() throws Exception {
        String  typeName   = "table";
        boolean typeExists = atlasClientV2.typeWithNameExists(typeName);

        if (typeExists) {
            clientV2.deleteTypeByName(typeName);

            boolean afterDelete = atlasClientV2.typeWithNameExists(typeName);

            assertFalse(afterDelete);
        }
    }

    @Test
    public void testDuplicateCreate() throws Exception {
        AtlasEntityDef type     = createClassTypeDef(randomString(), Collections.emptySet(), AtlasTypeUtil.createUniqueRequiredAttrDef("name", "string"));
        AtlasTypesDef  typesDef = new AtlasTypesDef();

        typesDef.getEntityDefs().add(type);

        AtlasTypesDef created = clientV2.createAtlasTypeDefs(typesDef);

        assertNotNull(created);

        try {
            created = clientV2.createAtlasTypeDefs(typesDef);
            fail("Expected 409");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.CONFLICT.getStatusCode());
        }
    }

    @Test
    public void testUpdate() throws Exception {
        String         entityType     = randomString();
        AtlasEntityDef typeDefinition = createClassTypeDef(entityType, Collections.emptySet(), AtlasTypeUtil.createUniqueRequiredAttrDef("name", "string"));
        AtlasTypesDef  atlasTypesDef  = new AtlasTypesDef();

        atlasTypesDef.getEntityDefs().add(typeDefinition);

        AtlasTypesDef createdTypeDefs = clientV2.createAtlasTypeDefs(atlasTypesDef);

        assertNotNull(createdTypeDefs);
        assertEquals(createdTypeDefs.getEntityDefs().size(), atlasTypesDef.getEntityDefs().size());

        //Add attribute description
        typeDefinition = createClassTypeDef(typeDefinition.getName(), Collections.emptySet(),
                AtlasTypeUtil.createUniqueRequiredAttrDef("name", "string"),
                AtlasTypeUtil.createOptionalAttrDef("description", "string"));

        emptyTypeDefs(atlasTypesDef);

        atlasTypesDef.getEntityDefs().add(typeDefinition);

        AtlasTypesDef updatedTypeDefs = clientV2.updateAtlasTypeDefs(atlasTypesDef);

        assertNotNull(updatedTypeDefs);
        assertEquals(updatedTypeDefs.getEntityDefs().size(), atlasTypesDef.getEntityDefs().size());
        assertEquals(updatedTypeDefs.getEntityDefs().get(0).getName(), atlasTypesDef.getEntityDefs().get(0).getName());

        MultivaluedMap<String, String> filterParams = new MultivaluedMapImpl();

        filterParams.add(SearchFilter.PARAM_TYPE, "ENTITY");

        AtlasTypesDef allTypeDefs = clientV2.getAllTypeDefs(new SearchFilter(filterParams));

        assertNotNull(allTypeDefs);

        boolean entityDefFound = false;

        for (AtlasEntityDef atlasEntityDef : allTypeDefs.getEntityDefs()) {
            if (atlasEntityDef.getName().equals(typeDefinition.getName())) {
                assertEquals(atlasEntityDef.getAttributeDefs().size(), 2);

                entityDefFound = true;
                break;
            }
        }

        assertTrue(entityDefFound, "Required entityDef not found.");
    }

    @Test(dependsOnMethods = "testCreate")
    public void testGetDefinition() {
        if (CollectionUtils.isNotEmpty(typeDefinitions.getEnumDefs())) {
            for (AtlasEnumDef atlasEnumDef : typeDefinitions.getEnumDefs()) {
                verifyByNameAndGUID(atlasEnumDef);
            }
        }

        if (CollectionUtils.isNotEmpty(typeDefinitions.getStructDefs())) {
            for (AtlasStructDef structDef : typeDefinitions.getStructDefs()) {
                verifyByNameAndGUID(structDef);
            }
        }

        if (CollectionUtils.isNotEmpty(typeDefinitions.getClassificationDefs())) {
            for (AtlasClassificationDef classificationDef : typeDefinitions.getClassificationDefs()) {
                verifyByNameAndGUID(classificationDef);
            }
        }

        if (CollectionUtils.isNotEmpty(typeDefinitions.getEntityDefs())) {
            for (AtlasEntityDef entityDef : typeDefinitions.getEntityDefs()) {
                verifyByNameAndGUID(entityDef);
            }
        }

        if (CollectionUtils.isNotEmpty(typeDefinitions.getRelationshipDefs())) {
            for (AtlasRelationshipDef relationshipDef : typeDefinitions.getRelationshipDefs()) {
                verifyByNameAndGUID(relationshipDef);
            }
        }

        if (CollectionUtils.isNotEmpty(typeDefinitions.getBusinessMetadataDefs())) {
            for (AtlasBusinessMetadataDef businessMetadataDef : typeDefinitions.getBusinessMetadataDefs()) {
                verifyByNameAndGUID(businessMetadataDef);
            }
        }
    }

    @Test
    public void testInvalidGets() {
        try {
            AtlasEnumDef byName = clientV2.getEnumDefByName("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasEnumDef byGuid = clientV2.getEnumDefByGuid("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasStructDef byName = clientV2.getStructDefByName("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasStructDef byGuid = clientV2.getStructDefByGuid("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasClassificationDef byName = clientV2.getClassificationDefByName("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(),
                    "Should've returned a 404");
        }

        try {
            AtlasClassificationDef byGuid = clientV2.getClassificationDefByGuid("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasEntityDef byName = clientV2.getEntityDefByName("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasEntityDef byGuid = clientV2.getEntityDefByGuid("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasRelationshipDef byName = clientV2.getRelationshipDefByName("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasRelationshipDef byGuid = clientV2.getRelationshipDefByGuid("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasBusinessMetadataDef byName = clientV2.getBusinessMetadataDefByName("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }

        try {
            AtlasBusinessMetadataDef byGuid = clientV2.getBusinessMetadataDefGuid("blah");
            fail("Get for invalid name should have reported a failure");
        } catch (AtlasServiceException e) {
            assertEquals(e.getStatus().getStatusCode(), Response.Status.NOT_FOUND.getStatusCode(), "Should've returned a 404");
        }
    }

    @Test
    public void testListTypesByFilter() throws Exception {
        AtlasAttributeDef attr       = AtlasTypeUtil.createOptionalAttrDef("attr", "string");
        AtlasAttributeDef attr1      = AtlasTypeUtil.createOptionalAttrDef("attr1", "string");
        AtlasEntityDef    classDefA  = AtlasTypeUtil.createClassTypeDef("A" + randomString(), Collections.emptySet(), attr);
        AtlasEntityDef    classDefA1 = AtlasTypeUtil.createClassTypeDef("A1" + randomString(), Collections.singleton(classDefA.getName()));
        AtlasEntityDef    classDefB  = AtlasTypeUtil.createClassTypeDef("B" + randomString(), Collections.emptySet(), attr1);
        AtlasEntityDef    classDefC  = AtlasTypeUtil.createClassTypeDef("C" + randomString(), new HashSet<>(Arrays.asList(classDefB.getName(), classDefA.getName())));

        AtlasTypesDef atlasTypesDef = new AtlasTypesDef();
        atlasTypesDef.getEntityDefs().add(classDefA);
        atlasTypesDef.getEntityDefs().add(classDefA1);
        atlasTypesDef.getEntityDefs().add(classDefB);
        atlasTypesDef.getEntityDefs().add(classDefC);

        AtlasTypesDef created = clientV2.createAtlasTypeDefs(atlasTypesDef);

        assertNotNull(created);
        assertEquals(created.getEntityDefs().size(), atlasTypesDef.getEntityDefs().size());

        MultivaluedMap<String, String> searchParams = new MultivaluedMapImpl();

        searchParams.add(SearchFilter.PARAM_TYPE, "CLASS");
        searchParams.add(SearchFilter.PARAM_SUPERTYPE, classDefA.getName());

        SearchFilter  searchFilter = new SearchFilter(searchParams);
        AtlasTypesDef searchDefs   = clientV2.getAllTypeDefs(searchFilter);

        assertNotNull(searchDefs);
        assertEquals(searchDefs.getEntityDefs().size(), 2);

        searchParams.add(SearchFilter.PARAM_NOT_SUPERTYPE, classDefB.getName());

        searchFilter = new SearchFilter(searchParams);
        searchDefs   = clientV2.getAllTypeDefs(searchFilter);

        assertNotNull(searchDefs);
        assertEquals(searchDefs.getEntityDefs().size(), 1);
    }

    private AtlasTypesDef createHiveTypesV2() {
        AtlasTypesDef atlasTypesDef = new AtlasTypesDef();

        AtlasEntityDef databaseTypeDefinition = createClassTypeDef("database", Collections.emptySet(),
                        AtlasTypeUtil.createUniqueRequiredAttrDef("name", "string"),
                        AtlasTypeUtil.createRequiredAttrDef("description", "string"));

        atlasTypesDef.getEntityDefs().add(databaseTypeDefinition);

        AtlasEntityDef tableTypeDefinition =
                createClassTypeDef("table", Collections.emptySet(),
                        AtlasTypeUtil.createUniqueRequiredAttrDef("name", "string"),
                        AtlasTypeUtil.createRequiredAttrDef("description", "string"),
                        AtlasTypeUtil.createOptionalAttrDef("columnNames", AtlasBaseTypeDef.getArrayTypeName("string")),
                        AtlasTypeUtil.createOptionalAttrDef("created", "date"),
                        AtlasTypeUtil.createOptionalAttrDef("parameters", AtlasBaseTypeDef.getMapTypeName("string", "string")),
                        AtlasTypeUtil.createRequiredAttrDef("type", "string"),
                        new AtlasAttributeDef("database", "database", false, Cardinality.SINGLE, 1, 1, true, true, false, Collections.emptyList()));

        atlasTypesDef.getEntityDefs().add(tableTypeDefinition);

        AtlasClassificationDef fetlTypeDefinition = AtlasTypeUtil.createTraitTypeDef("fetl", Collections.emptySet(),
                        AtlasTypeUtil.createRequiredAttrDef("level", "int"));

        atlasTypesDef.getClassificationDefs().add(fetlTypeDefinition);

        return atlasTypesDef;
    }

    private void verifyByNameAndGUID(AtlasBaseTypeDef typeDef) {
        try {
            AtlasBaseTypeDef byName = null;

            if (typeDef.getCategory() == TypeCategory.ENUM) {
                byName = clientV2.getEnumDefByName(typeDef.getName());
            } else if (typeDef.getCategory() == TypeCategory.ENTITY) {
                byName = clientV2.getEntityDefByName(typeDef.getName());
            } else if (typeDef.getCategory() == TypeCategory.CLASSIFICATION) {
                byName = clientV2.getClassificationDefByName(typeDef.getName());
            } else if (typeDef.getCategory() == TypeCategory.STRUCT) {
                byName = clientV2.getStructDefByName(typeDef.getName());
            } else if (typeDef.getCategory() == TypeCategory.RELATIONSHIP) {
                byName = clientV2.getRelationshipDefByName(typeDef.getName());
            } else if (typeDef.getCategory() == TypeCategory.BUSINESS_METADATA) {
                byName = clientV2.getBusinessMetadataDefByName(typeDef.getName());
            }

            assertNotNull(byName);
        } catch (AtlasServiceException e) {
            fail("Get byName should've succeeded", e);
        }

        if (StringUtils.isNotBlank(typeDef.getGuid())) {
            try {
                AtlasBaseTypeDef byGuid = null;

                if (typeDef.getCategory() == TypeCategory.ENUM) {
                    byGuid = clientV2.getEnumDefByGuid(typeDef.getGuid());
                } else if (typeDef.getCategory() == TypeCategory.ENTITY) {
                    byGuid = clientV2.getEntityDefByGuid(typeDef.getGuid());
                } else if (typeDef.getCategory() == TypeCategory.CLASSIFICATION) {
                    byGuid = clientV2.getClassificationDefByGuid(typeDef.getGuid());
                } else if (typeDef.getCategory() == TypeCategory.STRUCT) {
                    byGuid = clientV2.getStructDefByGuid(typeDef.getGuid());
                } else if (typeDef.getCategory() == TypeCategory.RELATIONSHIP) {
                    byGuid = clientV2.getRelationshipDefByGuid(typeDef.getGuid());
                } else if (typeDef.getCategory() == TypeCategory.BUSINESS_METADATA) {
                    byGuid = clientV2.getBusinessMetadataDefGuid(typeDef.getGuid());
                }

                assertNotNull(byGuid);
            } catch (AtlasServiceException e) {
                fail("Get byGuid should've succeeded", e);
            }
        }
    }

    private void emptyTypeDefs(AtlasTypesDef def) {
        def.getEnumDefs().clear();
        def.getStructDefs().clear();
        def.getClassificationDefs().clear();
        def.getEntityDefs().clear();
        def.getRelationshipDefs().clear();
        def.getBusinessMetadataDefs().clear();
    }

    private void checkIfTypeExists(String typeName) throws Exception {
        int retryCount = 0;
        int maxRetries = 3;
        int sleepTime  = 5000;

        while (true) {
            try {
                boolean typeExists = atlasClientV2.typeWithNameExists(typeName);

                if (!typeExists) {
                    throw new AtlasBaseException(TYPE_NAME_NOT_FOUND, typeName);
                } else {
                    break;
                }
            } catch (AtlasBaseException e) {
                Thread.sleep(sleepTime);

                if (++retryCount == maxRetries) {
                    throw e;
                }
            }
        }
    }
}
