/*
 * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package vm.runtime.defmeth;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nsk.share.Pair;
import nsk.share.TestFailure;
import nsk.share.test.TestBase;
import vm.runtime.defmeth.shared.DefMethTest;
import vm.runtime.defmeth.shared.DefMethTestFailure;
import vm.runtime.defmeth.shared.MemoryClassLoader;
import vm.runtime.defmeth.shared.annotation.NotApplicableFor;
import vm.runtime.defmeth.shared.builder.TestBuilder;
import vm.runtime.defmeth.shared.executor.TestExecutor;
import vm.runtime.defmeth.shared.data.Clazz;
import vm.runtime.defmeth.shared.data.ConcreteClass;
import vm.runtime.defmeth.shared.data.Interface;
import vm.runtime.defmeth.shared.data.Tester;
import static vm.runtime.defmeth.shared.ExecutionMode.*;

/*
 * Basic scenarios on class redefinition.
 */
public class RedefineTest extends DefMethTest {

    public static void main(String[] args) {
        TestBase.runTest(new RedefineTest(), args);
    }

    @Override
    protected void configure() {
        // There are no testers being generated for reflection-based scenarios,
        // so scenarios on class redefinition don't work
        String mode = factory.getExecutionMode();
        if ( "REFLECTION".equals(mode) || "INVOKE_WITH_ARGS".equals(mode)) {
            throw new TestFailure("RedefineTest isn't applicable to reflection-based execution scenario " +
                    "(REDEFINE & INVOKE_WITH_ARGS).");
        }
    }

    /**
     * Run test {@code b1} w/ redefined {@code classes} from {@code b2}.
     *
     * @param b1
     * @param b2
     * @param classes
     */
    private void redefineAndRun(TestBuilder b1, TestBuilder b2, Clazz... classes) {
        TestExecutor executor = b1.prepare();

        getLog().info("Before");
        List<Pair<Tester,Throwable>> errorsBefore =
                executor.run(); // run b1

        // redefine in b1
        MemoryClassLoader cl = executor.getLoader(); // b1.cl
        Map<String,byte[]> cf = b2.produce(); //
        Map<String,byte[]> forRedef = new HashMap<>();
        for (Clazz clz : classes) {
            String name = clz.name();
            forRedef.put(name, cf.get(name));
        }

        cl.modifyClasses(forRedef, factory.isRetransformClasses());

        getLog().info("After");
        List<Pair<Tester,Throwable>> errorsAfter =
                executor.run();

        if (!errorsBefore.isEmpty()) {
            throw new DefMethTestFailure(errorsBefore);
        }

        if (!errorsAfter.isEmpty()) {
            throw new DefMethTestFailure(errorsAfter);
        }
    }

    /*
     * Before redefinition:
     *   interface I { public int m() { return 1; } }
     *   class C extends I { public int m() { return 2; } }
     *
     * TEST: I i = new C(); i.m() == 2
     * TEST: C c = new C(); c.m() == 2
     *
     * After redefinition:
     *   interface I { public int m() { return 1; } }
     *   class C extends I { public int m() { return 3; } }
     *
     * TEST: I i = new C(); i.m() == 3
     * TEST: C c = new C(); c.m() == 3
     */
    @NotApplicableFor(modes = { REFLECTION, INVOKE_WITH_ARGS }) // reflection-based scenarios rely on checks in bytecode
    public void testRedefineConcreteMethod() {
        TestBuilder before = factory.getBuilder();
        { // Before redefinition
            Interface I = before.intf("I")
                    .defaultMethod("m", "()I").returns(1).build()
                .build();
            ConcreteClass C = before.clazz("C").implement(I)
                    .concreteMethod("m", "()I").returns(2).build()
                .build();

            before.test().callSite(I, C, "m", "()I").returns(2).done()
                  .test().callSite(C, C, "m", "()I").returns(2).done();
        }

        { // After redefinition
            TestBuilder after = factory.getBuilder();

            Interface I = after.intf("I")
                    .defaultMethod("m", "()I").returns(1).build()
                .build();
            ConcreteClass C = after.clazz("C").implement(I)
                    .concreteMethod("m", "()I").returns(3).build()
                .build();

            Tester T1 = after.test().callSite(I, C, "m", "()I").returns(3).build();
            Tester T2 = after.test().callSite(C, C, "m", "()I").returns(3).build();

            redefineAndRun(before, after, C, T1, T2);
        }
    }

    /*
     * Before redefinition:
     *   interface I { public int m() { return 1; } }
     *   class C extends I { public int m() { return 2; } }
     *
     * TEST: I i = new C(); i.m() == 2
     * TEST: C c = new C(); c.m() == 2
     *
     * After redefinition:
     *   interface I { public int m() { return 3; } }
     *   class C extends I { public int m() { return 2; } }
     *
     * TEST: I i = new C(); i.m() == 2
     * TEST: C c = new C(); c.m() == 2
     */
    @NotApplicableFor(modes = { REFLECTION, INVOKE_WITH_ARGS }) // reflection-based scenarios rely on checks in bytecode
    public void testRedefineDefaultMethod() {
        TestBuilder before = factory.getBuilder();
        { // Before redefinition
            Interface I = before.intf("I")
                    .defaultMethod("m", "()I").returns(1).build()
                .build();
            ConcreteClass C = before.clazz("C").implement(I)
                    .concreteMethod("m", "()I").returns(2).build()
                .build();

            before.test().callSite(I, C, "m", "()I").returns(2).done()
                  .test().callSite(C, C, "m", "()I").returns(2).done();
        }

        { // After redefinition
            TestBuilder after = factory.getBuilder();

            Interface I = after.intf("I")
                    .defaultMethod("m", "()I").returns(3).build()
                .build();
            ConcreteClass C = after.clazz("C").implement(I)
                    .concreteMethod("m", "()I").returns(2).build()
                .build();

            redefineAndRun(before, after, C);
        }
    }

    /*
     * Before redefinition:
     *   interface I { public int m() { return 1; } }
     *   class C extends I {}
     *
     * TEST: I i = new C(); i.m() == 1
     * TEST: C c = new C(); c.m() == 1
     *
     * After redefinition:
     *   interface I { public int m() { return 2; } }
     *   class C extends I {}
     *
     * TEST: I i = new C(); i.m() == 2
     * TEST: C c = new C(); c.m() == 2
     */
    @NotApplicableFor(modes = { REFLECTION, INVOKE_WITH_ARGS }) // reflection-based scenarios rely on checks in bytecode
    public void testRedefineDefMethInConcreteClass() {
        TestBuilder before = factory.getBuilder();
        { // Before redefinition
            Interface I = before.intf("I")
                    .defaultMethod("m", "()I").returns(1).build()
                .build();
            ConcreteClass C = before.clazz("C").implement(I).build();

            before.test().callSite(I, C, "m", "()I").returns(1).done()
                  .test().callSite(C, C, "m", "()I").returns(1).done();
        }

        { // After redefinition
            TestBuilder after = factory.getBuilder();

            Interface I = after.intf("I")
                    .defaultMethod("m", "()I").returns(2).build()
                .build();
            ConcreteClass C = after.clazz("C").implement(I).build();

            Tester T1 = after.test().callSite(I, C, "m", "()I").returns(2).build();
            Tester T2 = after.test().callSite(C, C, "m", "()I").returns(2).build();

            redefineAndRun(before, after, I, T1, T2);
        }
    }
}
