/*
 * 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.commons.rng.core;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.Arrays;
import org.apache.commons.rng.ArbitrarilyJumpableUniformRandomProvider;
import org.apache.commons.rng.JumpableUniformRandomProvider;
import org.apache.commons.rng.LongJumpableUniformRandomProvider;
import org.apache.commons.rng.RandomProviderState;
import org.apache.commons.rng.RestorableUniformRandomProvider;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.core.source32.IntProvider;
import org.apache.commons.rng.core.source64.LongProvider;

/**
 * Tests which all jumpable generators must pass.
 */
class JumpableProvidersParametricTest {
    /** The default state for the IntProvider. */
    private static final byte[] INT_PROVIDER_STATE;
    /** The default state for the LongProvider. */
    private static final byte[] LONG_PROVIDER_STATE;

    static {
        INT_PROVIDER_STATE = new State32Generator().getState();
        LONG_PROVIDER_STATE = new State64Generator().getState();
    }

    /**
     * Gets the list of Jumpable generators.
     *
     * @return the list
     */
    private static Iterable<JumpableUniformRandomProvider> getJumpableProviders() {
        return ProvidersList.listJumpable();
    }

    /**
     * Gets the list of arbitrarily Jumpable generators.
     *
     * @return the list
     */
    private static Iterable<ArbitrarilyJumpableUniformRandomProvider> getArbitrarilyJumpableProviders() {
        return ProvidersList.listArbitrarilyJumpable();
    }

    /**
     * Gets the function using the {@link LongJumpableUniformRandomProvider#longJump()} method.
     * If the RNG is not long jumpable then this will raise an exception to skip the test.
     *
     * @param generator RNG under test.
     * @return the jump function
     */
    private static TestJumpFunction getLongJumpFunction(JumpableUniformRandomProvider generator) {
        Assumptions.assumeTrue(generator instanceof LongJumpableUniformRandomProvider, "No long jump function");
        final LongJumpableUniformRandomProvider rng2 = (LongJumpableUniformRandomProvider) generator;
        return rng2::longJump;
    }

    /**
     * Test that the random generator returned from the jump is a new instance of the same class.
     */
    @ParameterizedTest
    @MethodSource("getJumpableProviders")
    void testJumpReturnsACopy(JumpableUniformRandomProvider generator) {
        assertJumpReturnsACopy(generator::jump, generator);
    }

    /**
     * Test that the random generator returned from the long jump is a new instance of the same class.
     */
    @ParameterizedTest
    @MethodSource("getJumpableProviders")
    void testLongJumpReturnsACopy(JumpableUniformRandomProvider generator) {
        assertJumpReturnsACopy(getLongJumpFunction(generator), generator);
    }

    /**
     * Test that the random generator returned from the long jump is a new instance of the same class.
     */
    @ParameterizedTest
    @MethodSource("getArbitrarilyJumpableProviders")
    void testArbitraryJumpReturnsACopy(ArbitrarilyJumpableUniformRandomProvider generator) {
        assertJumpReturnsACopy(() -> generator.jump(42), generator);
    }

    /**
     * Test that the random generator returned from the long jump is a new instance of the same class.
     */
    @ParameterizedTest
    @MethodSource("getArbitrarilyJumpableProviders")
    void testArbitraryJumpPowerOfTwoReturnsACopy(ArbitrarilyJumpableUniformRandomProvider generator) {
        assertJumpReturnsACopy(() -> generator.jumpPowerOfTwo(7), generator);
    }

    /**
     * Assert that the random generator returned from the jump function is a new instance of the same class.
     *
     * @param jumpFunction Jump function to test.
     * @param generator RNG under test.
     */
    private static void assertJumpReturnsACopy(TestJumpFunction jumpFunction,
                                               UniformRandomProvider generator) {
        final UniformRandomProvider copy = jumpFunction.jump();
        Assertions.assertNotSame(generator, copy, () -> generator + ": The copy instance should be a different object");
        Assertions.assertEquals(generator.getClass(), copy.getClass(), () -> generator + ": The copy instance should be the same class");
    }

    /**
     * Test that the random generator state of the copy instance returned from the jump
     * matches the input state.
     */
    @ParameterizedTest
    @MethodSource("getJumpableProviders")
    void testJumpCopyMatchesPreJumpState(JumpableUniformRandomProvider generator) {
        assertCopyMatchesPreJumpState(generator::jump, generator);
    }

    /**
     * Test that the random generator state of the copy instance returned from the long jump
     * matches the input state.
     */
    @ParameterizedTest
    @MethodSource("getJumpableProviders")
    void testLongJumpCopyMatchesPreJumpState(JumpableUniformRandomProvider generator) {
        assertCopyMatchesPreJumpState(getLongJumpFunction(generator), generator);
    }

    /**
     * Test that the random generator state of the copy instance returned from the arbitrary jump
     * matches the input state.
     */
    @ParameterizedTest
    @MethodSource("getArbitrarilyJumpableProviders")
    void testArbitraryJumpCopyMatchesPreJumpState(ArbitrarilyJumpableUniformRandomProvider generator) {
        assertCopyMatchesPreJumpState(() -> generator.jump(42), generator);
    }

    /**
     * Test that the random generator state of the copy instance returned from the arbitrary jump
     * matches the input state.
     */
    @ParameterizedTest
    @MethodSource("getArbitrarilyJumpableProviders")
    void testArbitraryJumpPowerOfTwoCopyMatchesPreJumpState(ArbitrarilyJumpableUniformRandomProvider generator) {
        assertCopyMatchesPreJumpState(() -> generator.jumpPowerOfTwo(7), generator);
    }

    /**
     * Assert that the random generator state of the copy instance returned from the jump
     * function matches the input state.
     *
     * <p>The generator must be a {@link RestorableUniformRandomProvider} and return an
     * instance of {@link RandomProviderDefaultState}.</p>
     *
     * <p>The input generator is sampled using methods in the
     * {@link UniformRandomProvider} interface, the state is saved and a jump is
     * performed. The states from the pre-jump generator and the returned copy instance
     * must match.</p>
     *
     * <p>This test targets any cached state of the default implementation of a generator
     * in {@link IntProvider} and {@link LongProvider} such as the state cached for the
     * nextBoolean() and nextInt() functions.</p>
     *
     * @param jumpFunction Jump function to test.
     * @param generator RNG under test.
     */
    private static void assertCopyMatchesPreJumpState(TestJumpFunction jumpFunction,
                                                      UniformRandomProvider generator) {
        Assumptions.assumeTrue(generator instanceof RestorableUniformRandomProvider, () -> generator + ": Not a restorable RNG");

        for (int repeats = 0; repeats < 2; repeats++) {
            // Exercise the generator.
            // This calls nextInt() once so the default implementation of LongProvider
            // should have cached a state for nextInt() in one of the two repeats.
            // Calls nextBoolean() to ensure a cached state in one of the two repeats.
            generator.nextInt();
            generator.nextBoolean();

            final RandomProviderState preJumpState = ((RestorableUniformRandomProvider) generator).saveState();
            Assumptions.assumeTrue(preJumpState instanceof RandomProviderDefaultState, () -> generator + ": Not a recognized state");

            final UniformRandomProvider copy = jumpFunction.jump();

            final RandomProviderState copyState = ((RestorableUniformRandomProvider) copy).saveState();
            final RandomProviderDefaultState expected = (RandomProviderDefaultState) preJumpState;
            final RandomProviderDefaultState actual = (RandomProviderDefaultState) copyState;
            Assertions.assertArrayEquals(expected.getState(), actual.getState(),
                () -> generator + ": The copy instance state should match the state of the original");
        }
    }

    /**
     * Test that a jump resets the state of the default implementation of a generator in
     * {@link IntProvider} and {@link LongProvider}.
     */
    @ParameterizedTest
    @MethodSource("getJumpableProviders")
    void testJumpResetsDefaultState(JumpableUniformRandomProvider generator) {
        assertJumpResetsDefaultState(generator::jump, generator);
    }

    /**
     * Test that a long jump resets the state of the default implementation of a generator in
     * {@link IntProvider} and {@link LongProvider}.
     */
    @ParameterizedTest
    @MethodSource("getJumpableProviders")
    void testLongJumpResetsDefaultState(JumpableUniformRandomProvider generator) {
        assertJumpResetsDefaultState(getLongJumpFunction(generator), generator);
    }

    /**
     * Test that an arbitrary jump resets the state of the default implementation of a generator in
     * {@link IntProvider} and {@link LongProvider}.
     */
    @ParameterizedTest
    @MethodSource("getArbitrarilyJumpableProviders")
    void testArbitraryJumpResetsDefaultState(ArbitrarilyJumpableUniformRandomProvider generator) {
        assertJumpResetsDefaultState(() -> generator.jump(42), generator);
    }

    /**
     * Test that an arbitrary jump resets the state of the default implementation of a generator in
     * {@link IntProvider} and {@link LongProvider}.
     */
    @ParameterizedTest
    @MethodSource("getArbitrarilyJumpableProviders")
    void testArbitraryJumpPowerOfTwoResetsDefaultState(ArbitrarilyJumpableUniformRandomProvider generator) {
        assertJumpResetsDefaultState(() -> generator.jumpPowerOfTwo(7), generator);
    }

    /**
     * Assert the jump resets the specified number of bytes of the state. The bytes are
     * checked from the end of the saved state.
     *
     * <p>This is intended to check the default state of the base implementation of
     * {@link IntProvider} and {@link LongProvider} is reset.</p>
     *
     * @param jumpFunction Jump function to test.
     * @param generator RNG under test.
     */
    private static void assertJumpResetsDefaultState(TestJumpFunction jumpFunction,
                                                     UniformRandomProvider generator) {
        byte[] expected;
        if (generator instanceof IntProvider) {
            expected = INT_PROVIDER_STATE;
        } else if (generator instanceof LongProvider) {
            expected = LONG_PROVIDER_STATE;
        } else {
            throw new AssertionError("Unsupported RNG: " + generator);
        }
        final int stateSize = expected.length;
        for (int repeats = 0; repeats < 2; repeats++) {
            // Exercise the generator.
            // This calls nextInt() once so the default implementation of LongProvider
            // should have cached a state for nextInt() in one of the two repeats.
            // Calls nextBoolean() to ensure a cached state in one of the two repeats.
            generator.nextInt();
            generator.nextBoolean();

            jumpFunction.jump();

            // An Int/LongProvider so must be a RestorableUniformRandomProvider
            final RandomProviderState postJumpState = ((RestorableUniformRandomProvider) generator).saveState();
            final byte[] actual = ((RandomProviderDefaultState) postJumpState).getState();

            Assumptions.assumeTrue(actual.length >= stateSize, () -> generator + ": Implementation has removed default state");

            // The implementation requires that any sub-class state is prepended to the
            // state thus the default state is at the end.
            final byte[] defaultState = Arrays.copyOfRange(actual, actual.length - stateSize, actual.length);
            Assertions.assertArrayEquals(expected, defaultState,
                () -> generator + ": The jump should reset the default state to zero");
        }
    }

    /**
     * Dummy class for checking the state size of the IntProvider.
     */
    static class State32Generator extends IntProvider {
        /** {@inheritDoc} */
        @Override
        public int next() {
            return 0;
        }

        /**
         * Gets the default state. This captures the initial state of the IntProvider
         * including the values of the cache variables.
         *
         * @return the state
         */
        byte[] getState() {
            return getStateInternal();
        }
    }

    /**
     * Dummy class for checking the state size of the LongProvider.
     */
    static class State64Generator extends LongProvider {
        /** {@inheritDoc} */
        @Override
        public long next() {
            return 0;
        }

        /**
         * Gets the default state. This captures the initial state of the LongProvider
         * including the values of the cache variables.
         *
         * @return the state size
         */
        byte[] getState() {
            return getStateInternal();
        }
    }

    /**
     * Specify the jump operation to test.
     *
     * <p>This allows testing {@link JumpableUniformRandomProvider} or
     * {@link LongJumpableUniformRandomProvider}.</p>
     */
    interface TestJumpFunction {
        /**
         * Perform the jump and return a pre-jump copy.
         *
         * @return the pre-jump copy.
         */
        UniformRandomProvider jump();
    }
}
