/*
 * Copyright (c) 2015, 2023, 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.
 */

import java.lang.ref.Cleaner;
import java.lang.ref.Reference;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.Objects;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;

import jdk.internal.ref.PhantomCleanable;
import jdk.internal.ref.CleanerFactory;

import jdk.test.whitebox.WhiteBox;

import jdk.test.lib.Utils;

import org.testng.Assert;
import org.testng.TestNG;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/*
 * @test
 * @library /lib/testlibrary /test/lib
 * @build jdk.test.whitebox.WhiteBox
 *        jdk.test.lib.Utils
 *        jdk.test.lib.Asserts
 *        jdk.test.lib.JDKToolFinder
 *        jdk.test.lib.JDKToolLauncher
 *        jdk.test.lib.Platform
 *        jdk.test.lib.process.*
 * @modules java.base/jdk.internal.misc
 *          java.base/jdk.internal.ref
 *          java.management
 * @compile CleanerTest.java
 * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
 * @run testng/othervm
 *      -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:.
 *      -verbose:gc CleanerTest
 */

@Test
public class CleanerTest {
    // A common CleaningService used by the test for notifications
    static final Cleaner COMMON = CleanerFactory.cleaner();

    // Access to WhiteBox utilities
    static final WhiteBox whitebox = WhiteBox.getWhiteBox();

    @DataProvider(name = "cleanerSuppliers")
    public Object[][] factories() {
        Supplier<Cleaner> supplier1 = () -> Cleaner.create();
        Supplier<Cleaner> supplier2 = () -> Cleaner.create(Thread.ofVirtual().factory());
        return new Object[][] { { supplier1 }, { supplier2 } };
    }

    /**
     * Test that sequences of the various actions on a Reference
     * and on the Cleanable instance have the desired result.
     * The test cases are generated for each of phantom, weak and soft
     * references.
     * The sequence of actions includes all permutations to an initial
     * list of actions including clearing the ref and resulting garbage
     * collection actions on the reference and explicitly performing
     * the cleaning action.
     */
    @Test(dataProvider = "cleanerSuppliers")
    @SuppressWarnings("unchecked")
    public void testCleanableActions(Supplier<Cleaner> supplier) {
        Cleaner cleaner = supplier.get();

        // Individually
        generateCases(cleaner, c -> c.clearRef());
        generateCases(cleaner, c -> c.doClean());

        // Pairs
        generateCases(cleaner, c -> c.doClean(), c -> c.clearRef());

        CleanableCase s = setupPhantom(COMMON, cleaner);
        cleaner = null;
        checkCleaned(s.getSemaphore(), true, "Cleaner was cleaned");
    }

    /**
     * Test the jdk.internal.misc APIs with sequences of the various actions
     * on a Reference and on the Cleanable instance have the desired result.
     * The test cases are generated for each of phantom, weak and soft
     * references.
     * The sequence of actions includes all permutations to an initial
     * list of actions including clearing the ref and resulting garbage
     * collection actions on the reference, explicitly performing
     * the cleanup and explicitly clearing the cleaning action.
     */
    @Test(dataProvider = "cleanerSuppliers")
    @SuppressWarnings("unchecked")
    public void testRefSubtypes(Supplier<Cleaner> supplier) {
        Cleaner cleaner = supplier.get();

        // Individually
        generateCasesInternal(cleaner, c -> c.clearRef());
        generateCasesInternal(cleaner, c -> c.doClean());
        generateCasesInternal(cleaner, c -> c.doClear());

        // Pairs
        generateCasesInternal(cleaner,
                c -> c.doClear(), c -> c.doClean());

        // Triplets
        generateCasesInternal(cleaner,
                c -> c.doClear(), c -> c.doClean(), c -> c.clearRef());

        generateExceptionCasesInternal(cleaner);

        CleanableCase s = setupPhantom(COMMON, cleaner);
        cleaner = null;
        checkCleaned(s.getSemaphore(), true, "Cleaner was cleaned");
    }

    /**
     * Generate tests using the runnables for each of phantom, weak,
     * and soft references.
     * @param cleaner  the cleaner
     * @param runnables the sequence of actions on the test case
     */
    @SuppressWarnings("unchecked")
    void generateCases(Cleaner cleaner, Consumer<CleanableCase>... runnables) {
        generateCases(() -> setupPhantom(cleaner, null), runnables.length, runnables);
    }

    @SuppressWarnings("unchecked")
    void generateCasesInternal(Cleaner cleaner, Consumer<CleanableCase>... runnables) {
        generateCases(() -> setupPhantomSubclass(cleaner, null),
                runnables.length, runnables);
    }

    @SuppressWarnings("unchecked")
    void generateExceptionCasesInternal(Cleaner cleaner) {
        generateCases(() -> setupPhantomSubclassException(cleaner, null),
                1, c -> c.clearRef());
    }

    /**
     * Generate all permutations of the sequence of runnables
     * and test each one.
     * The permutations are generated using Heap, B.R. (1963) Permutations by Interchanges.
     * @param generator the supplier of a CleanableCase
     * @param n the first index to interchange
     * @param runnables the sequence of actions
     */
    @SuppressWarnings("unchecked")
    void generateCases(Supplier<CleanableCase> generator, int n,
                       Consumer<CleanableCase> ... runnables) {
        if (n == 1) {
            CleanableCase test = generator.get();
            try {
                verifyGetRef(test);

                // Apply the sequence of actions on the Ref
                for (Consumer<CleanableCase> c : runnables) {
                    c.accept(test);
                }
                verify(test);
            } catch (Exception e) {
                Assert.fail(test.toString(), e);
            }
        } else {
            for (int i = 0; i < n - 1; i += 1) {
                generateCases(generator, n - 1, runnables);
                Consumer<CleanableCase> t = runnables[n - 1];
                int ndx = ((n & 1) == 0) ? i : 0;
                runnables[n - 1] = runnables[ndx];
                runnables[ndx] = t;
            }
            generateCases(generator, n - 1, runnables);
        }
    }

    /**
     * Verify the test case.
     * Any actions directly on the Reference or Cleanable have been executed.
     * The CleanableCase under test is given a chance to do the cleanup
     * by forcing a GC.
     * The result is compared with the expected result computed
     * from the sequence of operations on the Cleanable.
     * The Cleanable itself should have been cleanedup.
     *
     * @param test A CleanableCase containing the references
     */
    void verify(CleanableCase test) {
        System.out.println(test);
        int r = test.expectedResult();

        CleanableCase cc = setupPhantom(COMMON, test.getCleanable());
        test.clearCleanable();        // release this hard reference

        checkCleaned(test.getSemaphore(),
                r == CleanableCase.EV_CLEAN,
                "Cleanable was cleaned");
        checkCleaned(cc.getSemaphore(), true,
                "The reference to the Cleanable was freed");
    }

    /**
     * Verify that the reference.get works (or not) as expected.
     * It handles the cases where UnsupportedOperationException is expected.
     *
     * @param test the CleanableCase
     */
    void verifyGetRef(CleanableCase test) {
        Reference<?> r = (Reference) test.getCleanable();
        try {
            Object o = r.get();
            Reference<?> expectedRef = test.getRef();
            Assert.assertEquals(expectedRef.get(), o,
                    "Object reference incorrect");
            if (r.getClass().getName().endsWith("CleanableRef")) {
                Assert.fail("should not be able to get referent");
            }
        } catch (UnsupportedOperationException uoe) {
            if (r.getClass().getName().endsWith("CleanableRef")) {
                // Expected exception
            } else {
                Assert.fail("Unexpected exception from subclassed cleanable: " +
                        uoe.getMessage() + ", class: " + r.getClass());
            }
        }
    }

    /**
     * Test that releasing the reference to the Cleaner service allows it to be
     * be freed.
     */
    @Test(dataProvider = "cleanerSuppliers")
    public void testCleanerTermination(Supplier<Cleaner> supplier) {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        Cleaner service = supplier.get();

        PhantomReference<Object> ref = new PhantomReference<>(service, queue);
        System.gc();
        // Clear the Reference to the cleaning service and force a gc.
        service = null;
        System.gc();
        try {
            Reference<?> r = queue.remove(1000L);
            Assert.assertNotNull(r, "queue.remove timeout,");
            Assert.assertEquals(r, ref, "Wrong Reference dequeued");
        } catch (InterruptedException ie) {
            System.out.printf("queue.remove Interrupted%n");
        }
    }

    /**
     * Check a semaphore having been released by cleanup handler.
     * Force a number of GC cycles to give the GC a chance to process
     * the Reference and for the cleanup action to be run.
     * Use a larger number of cycles to wait for an expected cleaning to occur.
     *
     * @param semaphore a Semaphore
     * @param expectCleaned true if cleaning the function should have been run, otherwise not run
     * @param msg a message describing the cleaning function expected to be run or not run
     */
    static void checkCleaned(Semaphore semaphore, boolean expectCleaned, String msg) {
        long max_cycles = expectCleaned ? 10 : 3;
        long cycle = 0;
        for (; cycle < max_cycles; cycle++) {
            // Force GC
            whitebox.fullGC();

            try {
                if (semaphore.tryAcquire(Utils.adjustTimeout(200L), TimeUnit.MILLISECONDS)) {
                    System.out.printf(" Cleanable cleaned in cycle: %d%n", cycle);
                    if (!expectCleaned)
                        Assert.fail("Should not have been run: " +  msg);
                    return;
                }
            } catch (InterruptedException ie) {
                // retry in outer loop
            }
        }
        // Object has not been cleaned
        if (expectCleaned)
            Assert.fail("Should have been run: " + msg);
    }

    /**
     * Create a CleanableCase for a PhantomReference.
     * @param cleaner the cleaner to use
     * @param obj an object or null to create a new Object
     * @return a new CleanableCase preset with the object, cleanup, and semaphore
     */
    static CleanableCase setupPhantom(Cleaner cleaner, Object obj) {
        if (obj == null) {
            obj = new Object();
        }
        Semaphore s1 = new Semaphore(0);
        Cleaner.Cleanable c1 = cleaner.register(obj, () -> s1.release());

        return new CleanableCase(new PhantomReference<>(obj, null), c1, s1);
    }

    /**
     * Create a CleanableCase for a PhantomReference.
     * @param cleaner the cleaner to use
     * @param obj an object or null to create a new Object
     * @return a new CleanableCase preset with the object, cleanup, and semaphore
     */
    static CleanableCase setupPhantomSubclass(Cleaner cleaner, Object obj) {
        if (obj == null) {
            obj = new Object();
        }
        Semaphore s1 = new Semaphore(0);

        Cleaner.Cleanable c1 = new PhantomCleanable<Object>(obj, cleaner) {
            protected void performCleanup() {
                s1.release();
            }
        };

        return new CleanableCase(new PhantomReference<>(obj, null), c1, s1);
    }

    /**
     * Create a CleanableCase for a PhantomReference.
     * @param cleaner the cleaner to use
     * @param obj an object or null to create a new Object
     * @return a new CleanableCase preset with the object, cleanup, and semaphore
     */
    static CleanableCase setupPhantomSubclassException(Cleaner cleaner, Object obj) {
        if (obj == null) {
            obj = new Object();
        }
        Semaphore s1 = new Semaphore(0);

        Cleaner.Cleanable c1 = new PhantomCleanable<Object>(obj, cleaner) {
            protected void performCleanup() {
                s1.release();
                throw new RuntimeException("Exception thrown to cleaner thread");
            }
        };

        return new CleanableCase(new PhantomReference<>(obj, null), c1, s1, true);
    }

    /**
     * CleanableCase encapsulates the objects used for a test.
     * The reference to the object is not held directly,
     * but in a Reference object that can be cleared.
     * The semaphore is used to count whether the cleanup occurred.
     * It can be awaited on to determine that the cleanup has occurred.
     * It can be checked for non-zero to determine if it was
     * invoked or if it was invoked twice (a bug).
     */
    static class CleanableCase {

        private volatile Reference<?> ref;
        private volatile Cleaner.Cleanable cleanup;
        private final Semaphore semaphore;
        private final boolean throwsEx;
        private final int[] events;   // Sequence of calls to clean, clear, etc.
        private volatile int eventNdx;

        public static int EV_UNKNOWN = 0;
        public static int EV_CLEAR = 1;
        public static int EV_CLEAN = 2;
        public static int EV_UNREF = 3;
        public static int EV_CLEAR_CLEANUP = 4;


        CleanableCase(Reference<Object> ref, Cleaner.Cleanable cleanup,
                      Semaphore semaphore) {
            this.ref = ref;
            this.cleanup = cleanup;
            this.semaphore = semaphore;
            this.throwsEx = false;
            this.events = new int[4];
            this.eventNdx = 0;
        }
        CleanableCase(Reference<Object> ref, Cleaner.Cleanable cleanup,
                      Semaphore semaphore,
                      boolean throwsEx) {
            this.ref = ref;
            this.cleanup = cleanup;
            this.semaphore = semaphore;
            this.throwsEx = throwsEx;
            this.events = new int[4];
            this.eventNdx = 0;
        }

        public Reference<?> getRef() {
            return ref;
        }

        public void clearRef() {
            addEvent(EV_UNREF);
            ref.clear();
        }

        public Cleaner.Cleanable getCleanable() {
            return cleanup;
        }

        public void doClean() {
            try {
                addEvent(EV_CLEAN);
                cleanup.clean();
            } catch (RuntimeException ex) {
                if (!throwsEx) {
                    // unless it is known this case throws an exception, rethrow
                    throw ex;
                }
            }
        }

        public void doClear() {
            addEvent(EV_CLEAR);
            ((Reference)cleanup).clear();
        }

        public void clearCleanable() {
            addEvent(EV_CLEAR_CLEANUP);
            cleanup = null;
        }

        public Semaphore getSemaphore() {
            return semaphore;
        }

        public boolean isCleaned() {
            return semaphore.availablePermits() != 0;
        }

        private synchronized void addEvent(int e) {
            events[eventNdx++] = e;
        }

        /**
         * Computed the expected result from the sequence of events.
         * If EV_CLEAR appears before anything else, it is cleared.
         * If EV_CLEAN appears before EV_UNREF, then it is cleaned.
         * Anything else is Unknown.
         * @return EV_CLEAR if the cleanup should occur;
         *         EV_CLEAN if the cleanup should occur;
         *         EV_UNKNOWN if it is unknown.
         */
        public synchronized int expectedResult() {
            // Test if EV_CLEAR appears before anything else
            int clearNdx = indexOfEvent(EV_CLEAR);
            int cleanNdx = indexOfEvent(EV_CLEAN);
            int unrefNdx = indexOfEvent(EV_UNREF);
            if (clearNdx < cleanNdx) {
                return EV_CLEAR;
            }
            if (cleanNdx < clearNdx || cleanNdx < unrefNdx) {
                return EV_CLEAN;
            }
            if (unrefNdx < eventNdx) {
                return EV_CLEAN;
            }

            return EV_UNKNOWN;
        }

        private synchronized  int indexOfEvent(int e) {
            for (int i = 0; i < eventNdx; i++) {
                if (events[i] == e) {
                    return i;
                }
            }
            return eventNdx;
        }

        private static final String[] names =
                {"UNKNOWN", "EV_CLEAR", "EV_CLEAN", "EV_UNREF", "EV_CLEAR_CLEANUP"};

        public String eventName(int event) {
            return names[event];
        }

        public synchronized String eventsString() {
            StringBuilder sb = new StringBuilder();
            sb.append('[');
            for (int i = 0; i < eventNdx; i++) {
                if (i > 0) {
                    sb.append(", ");
                }
                sb.append(eventName(events[i]));
            }
            sb.append(']');
            sb.append(", throwEx: ");
            sb.append(throwsEx);
            return sb.toString();
        }

        public String toString() {
            return String.format("Case: %s, expect: %s, events: %s",
                    getRef().getClass().getName(),
                    eventName(expectedResult()), eventsString());
        }
    }

    /**
     * Verify that casting a Cleanup to a Reference is not allowed to
     * get the referent or clear the reference.
     */
    @Test(dataProvider = "cleanerSuppliers")
    @SuppressWarnings("rawtypes")
    public void testReferentNotAvailable(Supplier<Cleaner> supplier) {
        Cleaner cleaner = supplier.get();
        Semaphore s1 = new Semaphore(0);

        Object obj = new String("a new string");
        Cleaner.Cleanable c = cleaner.register(obj, () -> s1.release());
        Reference r = (Reference) c;
        try {
            Object o = r.get();
            System.out.printf("r: %s%n", Objects.toString(o));
            Assert.fail("should not be able to get the referent from Cleanable");
        } catch (UnsupportedOperationException uoe) {
            // expected
        }

        try {
            r.clear();
            Assert.fail("should not be able to clear the referent from Cleanable");
        } catch (UnsupportedOperationException uoe) {
            // expected
        }

        obj = null;
        checkCleaned(s1, true, "reference was cleaned");
        cleaner = null;
    }

    /**
     * Test the Cleaner from the CleanerFactory.
     */
    @Test
    public void testCleanerFactory() {
        Cleaner cleaner = CleanerFactory.cleaner();

        Object obj = new Object();
        CleanableCase s = setupPhantom(cleaner, obj);
        obj = null;
        checkCleaned(s.getSemaphore(), true,
                "Object cleaned using internal CleanerFactory.cleaner()");
    }
}
