/*
 * Copyright 2013, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jf.dexlib2.analysis;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import junit.framework.Assert;
import org.jf.dexlib2.Opcodes;
import org.jf.dexlib2.iface.ClassDef;
import org.jf.dexlib2.immutable.ImmutableDexFile;
import org.junit.Test;

import java.io.IOException;

public class CommonSuperclassTest {
    // object tree:
    // object
    //   one
    //     onetwo
    //       onetwothree
    //     onethree
    // five (undefined class)
    //   fivetwo
    //     fivetwothree
    //   fivethree

    private final ClassPath oldClassPath;
    private final ClassPath newClassPath;


    public CommonSuperclassTest() throws IOException {
        ImmutableSet<ClassDef> classes = ImmutableSet.of(
                TestUtils.makeClassDef("Ljava/lang/Object;", null),
                TestUtils.makeClassDef("Ltest/one;", "Ljava/lang/Object;"),
                TestUtils.makeClassDef("Ltest/two;", "Ljava/lang/Object;"),
                TestUtils.makeClassDef("Ltest/onetwo;", "Ltest/one;"),
                TestUtils.makeClassDef("Ltest/onetwothree;", "Ltest/onetwo;"),
                TestUtils.makeClassDef("Ltest/onethree;", "Ltest/one;"),
                TestUtils.makeClassDef("Ltest/fivetwo;", "Ltest/five;"),
                TestUtils.makeClassDef("Ltest/fivetwothree;", "Ltest/fivetwo;"),
                TestUtils.makeClassDef("Ltest/fivethree;", "Ltest/five;"),
                TestUtils.makeInterfaceDef("Ljava/lang/Cloneable;"),
                TestUtils.makeInterfaceDef("Ljava/io/Serializable;"),

                // basic class and interface
                TestUtils.makeClassDef("Liface/classiface1;", "Ljava/lang/Object;", "Liface/iface1;"),
                TestUtils.makeInterfaceDef("Liface/iface1;"),

                // a more complex interface tree
                TestUtils.makeInterfaceDef("Liface/base1;"),
                // implements undefined interface
                TestUtils.makeInterfaceDef("Liface/sub1;", "Liface/base1;", "Liface/base2;"),
                // this implements sub1, so that its interfaces can't be fully resolved either
                TestUtils.makeInterfaceDef("Liface/sub2;", "Liface/base1;", "Liface/sub1;"),
                TestUtils.makeInterfaceDef("Liface/sub3;", "Liface/base1;"),
                TestUtils.makeInterfaceDef("Liface/sub4;", "Liface/base1;", "Liface/sub3;"),
                TestUtils.makeClassDef("Liface/classsub1;", "Ljava/lang/Object;", "Liface/sub1;"),
                TestUtils.makeClassDef("Liface/classsub2;", "Ljava/lang/Object;", "Liface/sub2;"),
                TestUtils.makeClassDef("Liface/classsub3;", "Ljava/lang/Object;", "Liface/sub3;",
                        "Liface/base;"),
                TestUtils.makeClassDef("Liface/classsub4;", "Ljava/lang/Object;", "Liface/sub3;",
                        "Liface/sub4;"),
                TestUtils.makeClassDef("Liface/classsubsub4;", "Liface/classsub4;"),
                TestUtils.makeClassDef("Liface/classsub1234;", "Ljava/lang/Object;", "Liface/sub1;",
                        "Liface/sub2;", "Liface/sub3;", "Liface/sub4;"));

        oldClassPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), classes)));
        newClassPath = new ClassPath(Lists.newArrayList(new DexClassProvider(
                new ImmutableDexFile(Opcodes.forArtVersion(72), classes))), true, 72);
    }

    public void superclassTest(ClassPath classPath, String commonSuperclass,
                               String type1, String type2) {
        TypeProto commonSuperclassProto = classPath.getClass(commonSuperclass);
        TypeProto type1Proto = classPath.getClass(type1);
        TypeProto type2Proto = classPath.getClass(type2);

        Assert.assertSame(commonSuperclassProto, type1Proto.getCommonSuperclass(type2Proto));
        Assert.assertSame(commonSuperclassProto, type2Proto.getCommonSuperclass(type1Proto));
    }

    public void superclassTest(String commonSuperclass, String type1, String type2) {
        superclassTest(oldClassPath, commonSuperclass, type1, type2);
        superclassTest(newClassPath, commonSuperclass, type1, type2);
    }

    @Test
    public void testGetCommonSuperclass() throws IOException {
        String object = "Ljava/lang/Object;";
        String unknown = "Ujava/lang/Object;";
        String one = "Ltest/one;";
        String two = "Ltest/two;";
        String onetwo = "Ltest/onetwo;";
        String onetwothree = "Ltest/onetwothree;";
        String onethree = "Ltest/onethree;";
        String five = "Ltest/five;";
        String fivetwo = "Ltest/fivetwo;";
        String fivetwothree = "Ltest/fivetwothree;";
        String fivethree = "Ltest/fivethree;";

        // same object        
        superclassTest(object, object, object);
        superclassTest(unknown, unknown, unknown);
        superclassTest(one, one, one);
        superclassTest(onetwo, onetwo, onetwo);
        superclassTest(onetwothree, onetwothree, onetwothree);
        superclassTest(onethree, onethree, onethree);
        superclassTest(five, five, five);
        superclassTest(fivetwo, fivetwo, fivetwo);
        superclassTest(fivetwothree, fivetwothree, fivetwothree);
        superclassTest(fivethree, fivethree, fivethree);
        
        // same value, but different object
        Assert.assertEquals(
                onetwo,
                oldClassPath.getClass(onetwo).getCommonSuperclass(new ClassProto(oldClassPath, onetwo)).getType());

        Assert.assertEquals(
                onetwo,
                newClassPath.getClass(onetwo).getCommonSuperclass(new ClassProto(newClassPath, onetwo)).getType());

        // other object is superclass
        superclassTest(object, object, one);

        // other object is superclass two levels up
        superclassTest(object, object, onetwo);

        // unknown and non-object class
        superclassTest(unknown, one, unknown);

        // unknown and object class
        superclassTest(object, object, unknown);

        // siblings
        superclassTest(one, onetwo, onethree);

        // nephew
        superclassTest(one, onethree, onetwothree);

        // unrelated
        superclassTest(object, one, two);

        // undefined superclass and object
        superclassTest(object, fivetwo, object);

        // undefined class and unrelated type
        superclassTest(unknown, one, five);

        // undefined superclass and unrelated type
        superclassTest(unknown, one, fivetwo);

        // undefined ancestor and unrelated type
        superclassTest(unknown, one, fivetwothree);

        // undefined class and direct subclass
        superclassTest(five, five, fivetwo);

        // undefined class and descendent
        superclassTest(five, five, fivetwothree);

        // undefined superclass and direct subclass
        superclassTest(fivetwo, fivetwo, fivetwothree);

        // siblings with undefined superclass
        superclassTest(five, fivetwo, fivethree);

        // undefined superclass and nephew
        superclassTest(five, fivethree, fivetwothree);
    }

    @Test
    public void testGetCommonSuperclass_interfaces() {
        String classiface1 = "Liface/classiface1;";
        String iface1 = "Liface/iface1;";
        String base1 = "Liface/base1;";
        String base2 = "Liface/base2;";
        String sub1 = "Liface/sub1;";
        String sub2 = "Liface/sub2;";
        String sub3 = "Liface/sub3;";
        String sub4 = "Liface/sub4;";
        String classsub1 = "Liface/classsub1;";
        String classsub2 = "Liface/classsub2;";
        String classsub3 = "Liface/classsub3;";
        String classsub4 = "Liface/classsub4;";
        String classsubsub4 = "Liface/classsubsub4;";
        String classsub1234 = "Liface/classsub1234;";
        String object = "Ljava/lang/Object;";
        String unknown = "Ujava/lang/Object;";

        superclassTest(iface1, classiface1, iface1);

        superclassTest(base1, base1, base1);
        superclassTest(base1, base1, sub1);
        superclassTest(base1, base1, classsub1);
        superclassTest(base1, base1, sub2);
        superclassTest(base1, base1, classsub2);
        superclassTest(base1, base1, sub3);
        superclassTest(base1, base1, classsub3);
        superclassTest(base1, base1, sub4);
        superclassTest(base1, base1, classsub4);
        superclassTest(base1, base1, classsubsub4);
        superclassTest(base1, base1, classsub1234);

        superclassTest(object, sub3, iface1);
        superclassTest(unknown, sub2, iface1);
        superclassTest(unknown, sub1, iface1);

        superclassTest(base2, base2, sub1);
        superclassTest(base2, base2, classsub1);
        superclassTest(base2, base2, sub2);
        superclassTest(base2, base2, classsub2);
        superclassTest(base2, base2, classsub1234);

        superclassTest(unknown, iface1, classsub1234);

        superclassTest(sub1, sub1, classsub1);

        superclassTest(sub2, sub2, classsub2);
        superclassTest(sub1, sub1, classsub2);

        superclassTest(sub3, sub3, classsub3);

        superclassTest(sub4, sub4, classsub4);
        superclassTest(sub3, sub3, classsub4);
        superclassTest(object, sub2, classsub4);
        superclassTest(object, sub1, classsub4);

        superclassTest(sub1, sub2, sub1);

        superclassTest(sub1, sub1, classsub1234);
        superclassTest(sub2, sub2, classsub1234);
        superclassTest(sub3, sub3, classsub1234);
        superclassTest(sub4, sub4, classsub1234);

        superclassTest(unknown, sub3, classsub1);
        superclassTest(unknown, sub4, classsub1);
        superclassTest(unknown, sub3, classsub2);
        superclassTest(unknown, sub4, classsub2);

        superclassTest(unknown, sub4, base2);
        superclassTest(unknown, classsub4, base2);
    }

    @Test
    public void testGetCommonSuperclass_arrays() throws IOException {
        String object = "Ljava/lang/Object;";
        String one = "Ltest/one;";
        String unknown = "Ujava/lang/Object;";

        String cloneable = "Ljava/lang/Cloneable;";
        String serializable = "Ljava/io/Serializable;";

        String object1 = "[Ljava/lang/Object;";
        String one1 = "[Ltest/one;";
        String one2 = "[[Ltest/one;";
        String two1 = "[Ltest/two;";
        String onetwo1 = "[Ltest/onetwo;";
        String onetwo2 = "[[Ltest/onetwo;";
        String onethree1 = "[Ltest/onethree;";
        String onethree2 = "[[Ltest/onethree;";
        String five = "Ltest/five;";
        String five1 = "[Ltest/five;";
        String unknown1 = "[Ujava/lang/Object;";

        String int1 = "[I";
        String int2 = "[[I";
        String float1 = "[F";

        superclassTest(one1, one1, one1);
        superclassTest(object1, object1, one1);
        superclassTest(one1, onetwo1, onethree1);
        superclassTest(one1, one1, onethree1);
        superclassTest(object1, one1, two1);

        superclassTest(one2, one2, one2);
        superclassTest(one2, one2, onetwo2);
        superclassTest(one2, onetwo2, onethree2);
        superclassTest(object1, one1, one2);
        superclassTest(object1, two1, one2);

        superclassTest(unknown1, five1, one1);
        superclassTest(object1, five1, one2);

        superclassTest(unknown1, one1, unknown1);

        superclassTest(object, one1, one);
        superclassTest(object, object1, one);
        superclassTest(object, onetwo1, one);
        superclassTest(object, five1, one);
        superclassTest(object, one2, one);

        superclassTest(object, one1, unknown);
        superclassTest(object, unknown1, unknown);

        superclassTest(cloneable, one1, cloneable);
        superclassTest(serializable, one1, serializable);

        superclassTest(object, one1, five);

        superclassTest(int1, int1, int1);
        superclassTest(object, int1, float1);
        superclassTest(object, int1, int2);
    }
}
