/*
 * 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.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.Arrays;
import java.util.SplittableRandom;

import org.apache.commons.rng.core.source64.SplitMix64;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;

/**
 * Tests for {@link BaseProvider}.
 *
 * This class should only contain unit tests that cover code paths not
 * exercised elsewhere.  Those code paths are most probably only taken
 * in case of a wrong implementation (and would therefore fail other
 * tests too).
 */
class BaseProviderTest {
    @Test
    void testStateSizeTooSmall() {
        final DummyGenerator dummy = new DummyGenerator();
        final int size = dummy.getStateSize();
        Assumptions.assumeTrue(size > 0);
        final RandomProviderDefaultState state = new RandomProviderDefaultState(new byte[size - 1]);
        Assertions.assertThrows(IllegalStateException.class, () -> dummy.restoreState(state));
    }

    @Test
    void testStateSizeTooLarge() {
        final DummyGenerator dummy = new DummyGenerator();
        final int size = dummy.getStateSize();
        final RandomProviderDefaultState state = new RandomProviderDefaultState(new byte[size + 1]);
        Assertions.assertThrows(IllegalStateException.class, () -> dummy.restoreState(state));
    }

    @Test
    void testFillStateInt() {
        final int[] state = new int[10];
        final int[] seed = {1, 2, 3};

        for (int i = 0; i < state.length; i++) {
            Assertions.assertEquals(0, state[i]);
        }

        new DummyGenerator().fillState(state, seed);
        for (int i = 0; i < seed.length; i++) {
            Assertions.assertEquals(seed[i], state[i]);
        }
        for (int i = seed.length; i < state.length; i++) {
            Assertions.assertNotEquals(0, state[i]);
        }
    }

    @Test
    void testFillStateLong() {
        final long[] state = new long[10];
        final long[] seed = {1, 2, 3};

        for (int i = 0; i < state.length; i++) {
            Assertions.assertEquals(0, state[i]);
        }

        new DummyGenerator().fillState(state, seed);
        for (int i = 0; i < seed.length; i++) {
            Assertions.assertEquals(seed[i], state[i]);
        }
        for (int i = seed.length; i < state.length; i++) {
            Assertions.assertNotEquals(0, state[i]);
        }
    }

    /**
     * Test the checkIndex method. This is not used by any RNG implementations
     * as it has been superseded by the equivalent of JDK 9 Objects.checkFromIndexSize.
     */
    @Test
    void testCheckIndex() {
        final BaseProvider rng = new BaseProvider() {
            @Override
            public void nextBytes(byte[] bytes) { /* noop */ }
            @Override
            public void nextBytes(byte[] bytes, int start, int len) { /* noop */ }
            @Override
            public int nextInt() {
                return 0;
            }
            @Override
            public long nextLong() {
                return 0;
            }
            @Override
            public boolean nextBoolean() {
                return false;
            }
            @Override
            public float nextFloat() {
                return 0;
            }
            @Override
            public double nextDouble() {
                return 0;
            }
        };
        // Arguments are (min, max, index)
        // index must be in [min, max]
        rng.checkIndex(-10, 5, 0);
        rng.checkIndex(-10, 5, -10);
        rng.checkIndex(-10, 5, 5);
        rng.checkIndex(Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE);
        rng.checkIndex(Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, -11));
        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, 6));
        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, Integer.MIN_VALUE));
        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, Integer.MAX_VALUE));
    }

    /**
     * Test a seed can be extended to a required size by filling with a SplitMix64 generator.
     */
    @ParameterizedTest
    @ValueSource(ints = {0, 1, 2, 4, 5, 6, 7, 8, 9})
    void testExpandSeedLong(int length) {
        // The seed does not matter.
        // Create random seeds that are smaller or larger than length.
        final SplittableRandom rng = new SplittableRandom();
        for (final long[] seed : new long[][] {
            {},
            rng.longs(1).toArray(),
            rng.longs(2).toArray(),
            rng.longs(3).toArray(),
            rng.longs(4).toArray(),
            rng.longs(5).toArray(),
            rng.longs(6).toArray(),
            rng.longs(7).toArray(),
            rng.longs(8).toArray(),
            rng.longs(9).toArray(),
        }) {
            Assertions.assertArrayEquals(expandSeed(length, seed),
                                         BaseProvider.extendSeed(seed, length));
        }
    }

    /**
     * Expand the seed to the minimum specified length using a {@link SplitMix64} generator
     * seeded with {@code seed[0]}, or zero if the seed length is zero.
     *
     * @param length the length
     * @param seed the seed
     * @return the seed
     */
    private static long[] expandSeed(int length, long... seed) {
        if (seed.length < length) {
            final long[] s = Arrays.copyOf(seed, length);
            final SplitMix64 rng = new SplitMix64(s[0]);
            for (int i = seed.length; i < length; i++) {
                s[i] = rng.nextLong();
            }
            return s;
        }
        return seed;
    }

    /**
     * Test a seed can be extended to a required size by filling with a SplitMix64-style
     * generator using MurmurHash3's 32-bit mix function.
     *
     * <p>There is no reference RNG for this output. The test uses fixed output computed
     * from the reference c++ function in smhasher.
     */
    @ParameterizedTest
    @ValueSource(ints = {0, 1, 2, 4, 5, 6, 7, 8, 9})
    void testExpandSeedInt(int length) {
        // Reference output from the c++ function fmix32(uint32_t) in smhasher.
        // https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp
        final int seedA = 0x012de1ba;
        final int[] valuesA = {
            0x2f66c8b6, 0x256c0269, 0x054ef409, 0x402425ba, 0x78ebf590, 0x76bea1db,
            0x8bf5dcbe, 0x104ecdd4, 0x43cfc87e, 0xa33c7643, 0x4d210f56, 0xfa12093d,
        };
        // Values from a seed of zero
        final int[] values0 = {
            0x92ca2f0e, 0x3cd6e3f3, 0x1b147dcc, 0x4c081dbf, 0x487981ab, 0xdb408c9d,
            0x78bc1b8f, 0xd83072e5, 0x65cbdd54, 0x1f4b8cef, 0x91783bb0, 0x0231739b,
        };

        // Create a random seed that is larger than the maximum length;
        // start with the initial value
        final int[] data = new SplittableRandom().ints(10).toArray();
        data[0] = seedA;

        for (int i = 0; i <= 9; i++) {
            final int seedLength = i;
            // Truncate the random seed
            final int[] seed = Arrays.copyOf(data, seedLength);
            // Create the expected output length.
            // If too short it should be extended with values from the reference output
            final int[] expected = Arrays.copyOf(seed, Math.max(seedLength, length));
            if (expected.length == 0) {
                // Edge case for zero length
                Assertions.assertArrayEquals(new int[0],
                                             BaseProvider.extendSeed(seed, length));
                continue;
            }
            // Extend the truncated seed using the reference output.
            // This may be seeded with zero or the non-zero initial value.
            final int[] source = expected[0] == 0 ? values0 : valuesA;
            System.arraycopy(source, 0, expected, seedLength, expected.length - seedLength);
            Assertions.assertArrayEquals(expected,
                                         BaseProvider.extendSeed(seed, length),
                                         () -> String.format("%d -> %d", seedLength, length));
        }
    }

    /**
     * Dummy class for checking the behaviorof the IntProvider. Tests:
     * <ul>
     *  <li>an incomplete implementation</li>
     *  <li>{@code fillState} methods with "protected" access</li>
     * </ul>
     */
    static class DummyGenerator extends org.apache.commons.rng.core.source32.IntProvider {
        /** {@inheritDoc} */
        @Override
        public int next() {
            return 4; // https://www.xkcd.com/221/
        }

        /**
         * Gets the state size. This captures the state size of the IntProvider.
         *
         * @return the state size
         */
        int getStateSize() {
            return getStateInternal().length;
        }

        // Missing overrides of "setStateInternal" and "getStateInternal".

        /** {@inheritDoc} */
        @Override
        public void fillState(int[] state, int[] seed) {
            super.fillState(state, seed);
        }

        /** {@inheritDoc} */
        @Override
        public void fillState(long[] state, long[] seed) {
            super.fillState(state, seed);
        }
    }
}
