/*
 * 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.source32;

import java.util.stream.Stream;
import org.apache.commons.rng.LongJumpableUniformRandomProvider;
import org.apache.commons.rng.SplittableUniformRandomProvider;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.core.RandomAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

/**
 * Test for {@link L32X64Mix}.
 */
class L32X64MixTest extends AbstractLXMTest {

    /**
     * Factory to create a composite LXM generator that is equivalent
     * to the RNG under test.
     */
    private static class Factory implements LXMGeneratorFactory {
        static final Factory INSTANCE = new Factory();

        @Override
        public int lcgSeedSize() {
            return 2;
        }

        @Override
        public int xbgSeedSize() {
            return 2;
        }

        @Override
        public LXMGenerator create(int[] seed) {
            return new LXMGenerator(getMix(),
                                    new LCG32(seed[0], seed[1]),
                                    new XBGXoRoShiRo64(seed[2], seed[3]));
        }
    }

    @Override
    LXMGeneratorFactory getFactory() {
        return Factory.INSTANCE;
    }

    @Override
    LongJumpableUniformRandomProvider create(int[] seed) {
        return new L32X64Mix(seed);
    }

    @Override
    Stream<Arguments> getReferenceData() {
        /*
         * Reference data from JDK 17:
         * java.util.random.RandomGeneratorFactory.of("L32X64MixRandom").create(seed)
         *
         * Full byte[] seed created using SecureRandom.nextBytes. The seed was converted
         * to int[] by filling the long bits sequentially starting at the most
         * significant byte matching the method used by JDK 17. A sign extension bug in
         * byte[] conversion required all byte indices (i % 4 != 0) to be non-negative.
         * See: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8282144
         *
         * Note: Seed order: LCG addition; LCG state; XBG state.
         */
        return Stream.of(
            Arguments.of(
                new int[] {
                    0x5a16253f, 0xd449657e, 0x5b46012d, 0x1d504d64,
                },
                new int[] {
                    0xa6cc8f9b, 0xb9d3f0e3, 0xb8861d42, 0x9f8001a2,
                    0xaf1eea5b, 0x4e3bc947, 0x1c6378b8, 0x54ccc942,
                    0x4629da71, 0x0126a7e7, 0x6038f89c, 0xff72cb5c,
                    0x3853ae58, 0xc86ccf16, 0x02d32147, 0x78afed22,
                    0x432194a4, 0xf79809aa, 0x8c115507, 0x646b0445,
                    0xed63cc9c, 0x4e1d7b55, 0x699cf21d, 0x7d2adf74,
                    0xc2613634, 0x5ac9c96f, 0x1f2e59d2, 0x4c9878fb,
                    0x49c7a137, 0xd277cdf8, 0x369dd190, 0x1edeb2dc,
                    0x10bd09bd, 0x86e5140c, 0xe6bcb04c, 0x67f97775,
                    0x8f979b43, 0x36dfa266, 0x3acf5157, 0xe69aac08,
                }),
            Arguments.of(
                new int[] {
                    0xa3512c5b, 0x970f3100, 0xfc22313b, 0xa71f183a,
                },
                new int[] {
                    0x971dc6d7, 0xd8200b05, 0x0093d467, 0x74f11b96,
                    0x8945aa8f, 0x88a3d635, 0xb1fcb212, 0x66ed8543,
                    0xd5970056, 0x7b8b2d89, 0xd2d3957b, 0xc4ecc777,
                    0xf8e295c3, 0x4ffb4a29, 0x509fc6b0, 0x9c5b1965,
                    0x5008c757, 0x3a19410a, 0xc3992498, 0x0c4a8f22,
                    0x3f6abfd8, 0xe943ac45, 0xb632fcd3, 0x3d1f3201,
                    0xb17bc9f5, 0x902a9b9c, 0x39a76388, 0xd8c856a0,
                    0x7422cf86, 0x3f776592, 0xef70a122, 0x053b628e,
                    0x0dec8c4e, 0x5b0d80c9, 0xf58d19fc, 0x741f1361,
                    0x01b747f7, 0x6b325a32, 0x714ed761, 0xf1facf8e,
                }),
            Arguments.of(
                new int[] {
                    0x17767167, 0x1b6e286c, 0xe84a2f12, 0x637a5034,
                },
                new int[] {
                    0xd69fd4c1, 0x134945f8, 0x34c2fd4e, 0xd152b08f,
                    0xb5a7e2bd, 0xd8afab0d, 0xed15f9d2, 0x6baba06e,
                    0x9a1e8bcd, 0x8b392fbd, 0xb1e1cad9, 0x929f06d9,
                    0xc9467860, 0x3d492690, 0x84fd7d06, 0xb576b0f9,
                    0x860eac4c, 0xd3b7aa5b, 0x3ff4dd13, 0xfea00d01,
                    0x025143a0, 0xf1c5885a, 0x3041a8be, 0xbfbfb908,
                    0x186cd9cc, 0x1771875b, 0x9cc65fe2, 0xc9189702,
                    0x6063b09d, 0x20f0286b, 0x6094b83d, 0x8c3fe8f5,
                    0x2bf720eb, 0xe229e207, 0xe33f93db, 0x443b3849,
                    0x79cc70a8, 0xcbf0de84, 0xe57c6367, 0xe555ee21,
                }));
    }

    @ParameterizedTest
    @MethodSource(value = "getReferenceData")
    void testElementConstructor(int[] seed, int[] expected) {
        final L32X64Mix rng1 = new L32X64Mix(seed);
        final L32X64Mix rng2 = new L32X64Mix(seed[0], seed[1], seed[2], seed[3]);
        RandomAssert.assertNextLongEquals(seed.length * 2, rng1, rng2);
    }

    /**
     * Test split with zero bits from the source. This should be robust to escape the state
     * of all zero bits that will create an invalid state for the xor-based generator (XBG).
     */
    @Test
    void testSplitWithZeroBits() {
        final UniformRandomProvider zeroSource = () -> 0;
        final int[] seed = new int[Factory.INSTANCE.seedSize()];
        // Here we copy the split which sets the LCG increment to odd
        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
        final SplittableUniformRandomProvider rng1 = new L32X64Mix(seed);
        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
        RandomAssert.assertNextIntNotEquals(seed.length * 2, rng1, rng2);

        // Since we know how the zero seed is amended
        int z = 0;
        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
            seed[i] = LXMSupport.lea32(z);
            z += LXMSupport.GOLDEN_RATIO_32;
        }
        final SplittableUniformRandomProvider rng3 = new L32X64Mix(seed);
        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
        RandomAssert.assertNextIntEquals(seed.length * 2, rng3, rng4);
    }
}
