/**
 * 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 org.apache.atlas.MetadataException;
import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.ITypedInstance;
import org.apache.atlas.typesystem.ITypedStruct;
import org.apache.atlas.typesystem.Struct;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

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.createTraitTypeDef;

/**
 * Unit tests for type inheritance.
 */
public class TypeInheritanceTest extends BaseTest {

    @BeforeMethod
    public void setup() throws Exception {
        TypeSystem.getInstance().reset();
        super.setup();
    }

    /*
     * Type Hierarchy is:
     *   A(a)
     *   B(b) extends A
     */
    @Test
    public void testSimpleInheritance() throws MetadataException {
        HierarchicalTypeDefinition A = createClassTypeDef("A", null,
                createRequiredAttrDef("a", DataTypes.INT_TYPE));

        HierarchicalTypeDefinition B = createClassTypeDef("B", ImmutableList.of("A"),
                createOptionalAttrDef("b", DataTypes.BOOLEAN_TYPE));

        defineClasses(A, B);

        ClassType BType = getTypeSystem().getDataType(ClassType.class, "B");

        Struct s1 = new Struct("B");
        s1.set("b", true);
        s1.set("a", 1);

        ITypedInstance ts = BType.convert(s1, Multiplicity.REQUIRED);
        Assert.assertEquals(ts.toString(), "{\n" +
                "\tid : (type: B, id: <unassigned>)\n" +
                "\tb : \ttrue\n" +
                "\ta : \t1\n" +
                "}");
    }

    /*
     * Type Hierarchy is:
     *   A(a, b)
     *   B(b) extends A
     */
    @Test
    public void testSimpleInheritanceWithOverrides() throws MetadataException {
        HierarchicalTypeDefinition A = createClassTypeDef("A", null,
                createRequiredAttrDef("a", DataTypes.INT_TYPE),
                createRequiredAttrDef("b", DataTypes.BOOLEAN_TYPE));

        HierarchicalTypeDefinition B = createClassTypeDef("B", ImmutableList.of("A"),
                createOptionalAttrDef("b", DataTypes.BOOLEAN_TYPE));

        defineClasses(A, B);

        ClassType BType = getTypeSystem().getDataType(ClassType.class, "B");

        Struct s1 = new Struct("B");
        s1.set("b", true);
        s1.set("a", 1);
        s1.set("A.B.b", false);

        ITypedInstance ts = BType.convert(s1, Multiplicity.REQUIRED);
        Assert.assertEquals(ts.toString(), "{\n" +
                "\tid : (type: B, id: <unassigned>)\n" +
                "\tb : \ttrue\n" +
                "\ta : \t1\n" +
                "\tA.B.b : \tfalse\n" +
                "}");
    }

    /*
     * Type Hierarchy is:
     *   A(a)
     *   B(b) extends A
     *   C(c) extends B
     *   D(d) extends C
     */
    @Test
    public void testMultiLevelInheritance() throws MetadataException {
        HierarchicalTypeDefinition A = createClassTypeDef("A", null,
                createRequiredAttrDef("a", DataTypes.INT_TYPE));

        HierarchicalTypeDefinition B = createClassTypeDef("B", ImmutableList.of("A"),
                createOptionalAttrDef("b", DataTypes.BOOLEAN_TYPE));

        HierarchicalTypeDefinition C = createClassTypeDef("C", ImmutableList.of("B"),
                createOptionalAttrDef("c", DataTypes.BYTE_TYPE));

        HierarchicalTypeDefinition D = createClassTypeDef("D", ImmutableList.of("C"),
                createOptionalAttrDef("d", DataTypes.SHORT_TYPE));

        defineClasses(A, B, C, D);

        ClassType DType = getTypeSystem().getDataType(ClassType.class, "D");

        Struct s1 = new Struct("D");
        s1.set("d", 1);
        s1.set("c", 1);
        s1.set("b", true);
        s1.set("a", 1);

        ITypedInstance ts = DType.convert(s1, Multiplicity.REQUIRED);
        Assert.assertEquals(ts.toString(), "{\n" +
                "\tid : (type: D, id: <unassigned>)\n" +
                "\td : \t1\n" +
                "\tc : \t1\n" +
                "\tb : \ttrue\n" +
                "\ta : \t1\n" +
                "}");
    }

    /*
     * Type Hierarchy is:
     *   A(a,b,c,d)
     *   B(b) extends A
     *   C(c) extends A
     *   D(d) extends B,C
     *
     * - There are a total of 11 fields in an instance of D
     * - an attribute that is hidden by a SubType can referenced by prefixing it with the
     * complete Path.
     *   For e.g. the 'b' attribute in A (that is a superType for B) is hidden the 'b' attribute
     *   in B.
     *   So it is availabel by the name 'A.B.D.b'
     *
     * - Another way to set attributes is to cast. Casting a 'D' instance of 'B' makes the 'A.B.D
     * .b' attribute
     *   available as 'A.B.b'. Casting one more time to an 'A' makes the 'A.B.b' attribute
     *   available as 'b'.
     */
    @Test
    public void testDiamondInheritance() throws MetadataException {
        HierarchicalTypeDefinition A = createTraitTypeDef("A", null,
                createRequiredAttrDef("a", DataTypes.INT_TYPE),
                createOptionalAttrDef("b", DataTypes.BOOLEAN_TYPE),
                createOptionalAttrDef("c", DataTypes.BYTE_TYPE),
                createOptionalAttrDef("d", DataTypes.SHORT_TYPE));
        HierarchicalTypeDefinition B = createTraitTypeDef("B", ImmutableList.of("A"),
                createOptionalAttrDef("b", DataTypes.BOOLEAN_TYPE));
        HierarchicalTypeDefinition C = createTraitTypeDef("C", ImmutableList.of("A"),
                createOptionalAttrDef("c", DataTypes.BYTE_TYPE));
        HierarchicalTypeDefinition D = createTraitTypeDef("D", ImmutableList.of("B", "C"),
                createOptionalAttrDef("d", DataTypes.SHORT_TYPE));

        defineTraits(A, B, C, D);

        TraitType DType = getTypeSystem().getDataType(TraitType.class, "D");

        Struct s1 = new Struct("D");
        s1.set("d", 1);
        s1.set("c", 1);
        s1.set("b", true);
        s1.set("a", 1);
        s1.set("A.B.D.b", true);
        s1.set("A.B.D.c", 2);
        s1.set("A.B.D.d", 2);

        s1.set("A.C.D.a", 3);
        s1.set("A.C.D.b", false);
        s1.set("A.C.D.c", 3);
        s1.set("A.C.D.d", 3);


        ITypedStruct ts = DType.convert(s1, Multiplicity.REQUIRED);
        Assert.assertEquals(ts.toString(), "{\n" +
                "\td : \t1\n" +
                "\tb : \ttrue\n" +
                "\tc : \t1\n" +
                "\ta : \t1\n" +
                "\tA.B.D.b : \ttrue\n" +
                "\tA.B.D.c : \t2\n" +
                "\tA.B.D.d : \t2\n" +
                "\tA.C.D.a : \t3\n" +
                "\tA.C.D.b : \tfalse\n" +
                "\tA.C.D.c : \t3\n" +
                "\tA.C.D.d : \t3\n" +
                "}");

        /*
         * cast to B and set the 'b' attribute on A.
         */
        TraitType BType = getTypeSystem().getDataType(TraitType.class, "B");
        IStruct s2 = DType.castAs(ts, "B");
        s2.set("A.B.b", false);

        Assert.assertEquals(ts.toString(), "{\n" +
                "\td : \t1\n" +
                "\tb : \ttrue\n" +
                "\tc : \t1\n" +
                "\ta : \t1\n" +
                "\tA.B.D.b : \tfalse\n" +
                "\tA.B.D.c : \t2\n" +
                "\tA.B.D.d : \t2\n" +
                "\tA.C.D.a : \t3\n" +
                "\tA.C.D.b : \tfalse\n" +
                "\tA.C.D.c : \t3\n" +
                "\tA.C.D.d : \t3\n" +
                "}");

        /*
         * cast again to A and set the 'b' attribute on A.
         */
        IStruct s3 = BType.castAs(s2, "A");
        s3.set("b", true);
        Assert.assertEquals(ts.toString(), "{\n" +
                "\td : \t1\n" +
                "\tb : \ttrue\n" +
                "\tc : \t1\n" +
                "\ta : \t1\n" +
                "\tA.B.D.b : \ttrue\n" +
                "\tA.B.D.c : \t2\n" +
                "\tA.B.D.d : \t2\n" +
                "\tA.C.D.a : \t3\n" +
                "\tA.C.D.b : \tfalse\n" +
                "\tA.C.D.c : \t3\n" +
                "\tA.C.D.d : \t3\n" +
                "}");
    }
}
