/*
 * Copyright (c) 2012, 2015, 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 org.openjdk.tests.java.util.stream;

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

import org.openjdk.testlib.java.util.stream.OpTestCase;
import org.openjdk.testlib.java.util.stream.StreamTestDataProvider;
import org.openjdk.testlib.java.util.stream.TestData;
import org.openjdk.testlib.java.util.stream.DoubleStreamTestScenario;
import org.openjdk.testlib.java.util.stream.IntStreamTestScenario;
import org.openjdk.testlib.java.util.stream.LongStreamTestScenario;
import org.openjdk.testlib.java.util.stream.LambdaTestHelpers;
import org.openjdk.testlib.java.util.stream.StreamTestScenario;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static org.openjdk.testlib.java.util.stream.LambdaTestHelpers.assertUnique;


@Test
public class InfiniteStreamWithLimitOpTest extends OpTestCase {

    private static final long SKIP_LIMIT_SIZE = 1 << 16;

    @DataProvider(name = "Stream.limit")
    @SuppressWarnings("rawtypes")
    public static Object[][] sliceFunctionsDataProvider() {
        Function<String, String> f = s -> String.format(s, SKIP_LIMIT_SIZE);

        List<Object[]> data = new ArrayList<>();

        data.add(new Object[]{f.apply("Stream.limit(%d)"),
                (UnaryOperator<Stream>) s -> s.limit(SKIP_LIMIT_SIZE)});
        data.add(new Object[]{f.apply("Stream.skip(%1$d).limit(%1$d)"),
                (UnaryOperator<Stream>) s -> s.skip(SKIP_LIMIT_SIZE).limit(SKIP_LIMIT_SIZE)});

        return data.toArray(new Object[0][]);
    }

    @DataProvider(name = "IntStream.limit")
    public static Object[][] intSliceFunctionsDataProvider() {
        Function<String, String> f = s -> String.format(s, SKIP_LIMIT_SIZE);

        List<Object[]> data = new ArrayList<>();

        data.add(new Object[]{f.apply("IntStream.limit(%d)"),
                (UnaryOperator<IntStream>) s -> s.limit(SKIP_LIMIT_SIZE)});
        data.add(new Object[]{f.apply("IntStream.skip(%1$d).limit(%1$d)"),
                (UnaryOperator<IntStream>) s -> s.skip(SKIP_LIMIT_SIZE).limit(SKIP_LIMIT_SIZE)});

        return data.toArray(new Object[0][]);
    }

    @DataProvider(name = "LongStream.limit")
    public static Object[][] longSliceFunctionsDataProvider() {
        Function<String, String> f = s -> String.format(s, SKIP_LIMIT_SIZE);

        List<Object[]> data = new ArrayList<>();

        data.add(new Object[]{f.apply("LongStream.limit(%d)"),
                (UnaryOperator<LongStream>) s -> s.limit(SKIP_LIMIT_SIZE)});
        data.add(new Object[]{f.apply("LongStream.skip(%1$d).limit(%1$d)"),
                (UnaryOperator<LongStream>) s -> s.skip(SKIP_LIMIT_SIZE).limit(SKIP_LIMIT_SIZE)});

        return data.toArray(new Object[0][]);
    }

    @DataProvider(name = "DoubleStream.limit")
    public static Object[][] doubleSliceFunctionsDataProvider() {
        Function<String, String> f = s -> String.format(s, SKIP_LIMIT_SIZE);

        List<Object[]> data = new ArrayList<>();

        data.add(new Object[]{f.apply("DoubleStream.limit(%d)"),
                (UnaryOperator<DoubleStream>) s -> s.limit(SKIP_LIMIT_SIZE)});
        data.add(new Object[]{f.apply("DoubleStream.skip(%1$d).limit(%1$d)"),
                (UnaryOperator<DoubleStream>) s -> s.skip(SKIP_LIMIT_SIZE).limit(SKIP_LIMIT_SIZE)});

        return data.toArray(new Object[0][]);
    }

    private <T> ResultAsserter<Iterable<T>> unorderedAsserter() {
        return (act, exp, ord, par) -> {
            if (par & !ord) {
                // Can only assert that all elements of the actual result
                // are distinct and that the count is the limit size
                // any element within the range [0, Long.MAX_VALUE) may be
                // present
                assertUnique(act);
                long count = 0;
                for (T l : act) {
                    count++;
                }
                assertEquals(count, SKIP_LIMIT_SIZE, "size not equal");
            }
            else {
                LambdaTestHelpers.assertContents(act, exp);
            }
        };
    }

    private TestData.OfRef<Long> refLongs() {
        return refLongRange(0, Long.MAX_VALUE);
    }

    private TestData.OfRef<Long> refLongRange(long l, long u) {
        return TestData.Factory.ofSupplier(
                String.format("[%d, %d)", l, u),
                () -> LongStream.range(l, u).boxed());
    }

    private TestData.OfInt ints() {
        return intRange(0, Integer.MAX_VALUE);
    }

    private TestData.OfInt intRange(int l, int u) {
        return TestData.Factory.ofIntSupplier(
                String.format("[%d, %d)", l, u),
                () -> IntStream.range(l, u));
    }

    private TestData.OfLong longs() {
        return longRange(0, Long.MAX_VALUE);
    }

    private TestData.OfLong longRange(long l, long u) {
        return TestData.Factory.ofLongSupplier(
                String.format("[%d, %d)", l, u),
                () -> LongStream.range(l, u));
    }

    private TestData.OfDouble doubles() {
        return doubleRange(0, 1L << 53);
    }

    private TestData.OfDouble doubleRange(long l, long u) {
        return TestData.Factory.ofDoubleSupplier(
                String.format("[%d, %d)", l, u),
                () -> LongStream.range(l, u).mapToDouble(i -> (double) i));
    }


    // Sized/subsized range

    @Test(dataProvider = "Stream.limit")
    public void testSubsizedWithRange(String description, UnaryOperator<Stream<Long>> fs) {
        // Range is [0, Long.MAX_VALUE), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(refLongs()).
                stream(s -> fs.apply(s)).
                without(StreamTestScenario.CLEAR_SIZED_SCENARIOS).
                exercise();
    }

    @Test(dataProvider = "IntStream.limit")
    public void testIntSubsizedWithRange(String description, UnaryOperator<IntStream> fs) {
        // Range is [0, Integer.MAX_VALUE), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(ints()).
                stream(s -> fs.apply(s)).
                without(IntStreamTestScenario.CLEAR_SIZED_SCENARIOS).
                exercise();
    }

    @Test(dataProvider = "LongStream.limit")
    public void testLongSubsizedWithRange(String description, UnaryOperator<LongStream> fs) {
        // Range is [0, Long.MAX_VALUE), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(longs()).
                stream(s -> fs.apply(s)).
                without(LongStreamTestScenario.CLEAR_SIZED_SCENARIOS).
                exercise();
    }

    @Test(dataProvider = "DoubleStream.limit")
    public void testDoubleSubsizedWithRange(String description, UnaryOperator<DoubleStream> fs) {
        // Range is [0, 2^53), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(doubles()).
                stream(s -> fs.apply(s)).
                without(DoubleStreamTestScenario.CLEAR_SIZED_SCENARIOS).
                exercise();
    }


    // Unordered finite not SIZED/SUBSIZED

    @Test(dataProvider = "Stream.limit")
    public void testUnorderedFinite(String description, UnaryOperator<Stream<Long>> fs) {
        // Range is [0, Long.MAX_VALUE), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(longs()).
                stream(s -> fs.apply(s.filter(i -> true).unordered().boxed())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "IntStream.limit")
    public void testIntUnorderedFinite(String description, UnaryOperator<IntStream> fs) {
        // Range is [0, Integer.MAX_VALUE), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(ints()).
                stream(s -> fs.apply(s.filter(i -> true).unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "LongStream.limit")
    public void testLongUnorderedFinite(String description, UnaryOperator<LongStream> fs) {
        // Range is [0, Long.MAX_VALUE), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(longs()).
                stream(s -> fs.apply(s.filter(i -> true).unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "DoubleStream.limit")
    public void testDoubleUnorderedFinite(String description, UnaryOperator<DoubleStream> fs) {
        // Range is [0, 1L << 53), splits are SUBSIZED
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        // Upper bound ensures values mapped to doubles will be unique
        withData(doubles()).
                stream(s -> fs.apply(s.filter(i -> true).unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }


    // Unordered finite not SUBSIZED

    @SuppressWarnings({"rawtypes", "unchecked"})
    private Spliterator.OfLong proxyNotSubsized(Spliterator.OfLong s) {
        InvocationHandler ih = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                switch (method.getName()) {
                    case "characteristics": {
                        int c = (Integer) method.invoke(s, args);
                        return c & ~Spliterator.SUBSIZED;
                    }
                    case "hasCharacteristics": {
                        int c = (Integer) args[0];
                        boolean b = (Boolean) method.invoke(s, args);
                        return b & ((c & Spliterator.SUBSIZED) == 0);
                    }
                    default:
                        return method.invoke(s, args);
                }
            }
        };

        return (Spliterator.OfLong) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                                                           new Class[]{Spliterator.OfLong.class},
                                                           ih);
    }

    private TestData.OfLong proxiedLongRange(long l, long u) {
        return TestData.Factory.ofLongSupplier(
                String.format("[%d, %d)", l, u),
                () -> StreamSupport.longStream(proxyNotSubsized(LongStream.range(l, u).spliterator()), false));
    }

    @Test(dataProvider = "Stream.limit")
    public void testUnorderedSizedNotSubsizedFinite(String description, UnaryOperator<Stream<Long>> fs) {
        // Range is [0, Long.MAX_VALUE), splits are not SUBSIZED (proxy clears
        // the SUBSIZED characteristic)
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(proxiedLongRange(0, Long.MAX_VALUE)).
                stream(s -> fs.apply(s.unordered().boxed())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "IntStream.limit")
    public void testIntUnorderedSizedNotSubsizedFinite(String description, UnaryOperator<IntStream> fs) {
        // Range is [0, Integer.MAX_VALUE), splits are not SUBSIZED (proxy clears
        // the SUBSIZED characteristic)
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(proxiedLongRange(0, Integer.MAX_VALUE)).
                stream(s -> fs.apply(s.unordered().mapToInt(i -> (int) i))).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "LongStream.limit")
    public void testLongUnorderedSizedNotSubsizedFinite(String description, UnaryOperator<LongStream> fs) {
        // Range is [0, Long.MAX_VALUE), splits are not SUBSIZED (proxy clears
        // the SUBSIZED characteristic)
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(proxiedLongRange(0, Long.MAX_VALUE)).
                stream(s -> fs.apply(s.unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "DoubleStream.limit")
    public void testDoubleUnorderedSizedNotSubsizedFinite(String description, UnaryOperator<DoubleStream> fs) {
        // Range is [0, Double.MAX_VALUE), splits are not SUBSIZED (proxy clears
        // the SUBSIZED characteristic)
        // Such a size will induce out of memory errors for incorrect
        // slice implementations
        withData(proxiedLongRange(0, 1L << 53)).
                stream(s -> fs.apply(s.unordered().mapToDouble(i -> (double) i))).
                resultAsserter(unorderedAsserter()).
                exercise();
    }


    // Unordered generation

    @Test(dataProvider = "Stream.limit")
    public void testUnorderedGenerator(String description, UnaryOperator<Stream<Long>> fs) {
        // Source is spliterator of infinite size
        TestData.OfRef<Long> generator = TestData.Factory.ofSupplier(
                "[1L, 1L, ...]", () -> Stream.generate(() -> 1L));

        withData(generator).
                stream(s -> fs.apply(s.filter(i -> true).unordered())).
                exercise();
    }

    @Test(dataProvider = "IntStream.limit")
    public void testIntUnorderedGenerator(String description, UnaryOperator<IntStream> fs) {
        // Source is spliterator of infinite size
        TestData.OfInt generator = TestData.Factory.ofIntSupplier(
                "[1, 1, ...]", () -> IntStream.generate(() -> 1));

        withData(generator).
                stream(s -> fs.apply(s.filter(i -> true).unordered())).
                exercise();
    }

    @Test(dataProvider = "LongStream.limit")
    public void testLongUnorderedGenerator(String description, UnaryOperator<LongStream> fs) {
        // Source is spliterator of infinite size
        TestData.OfLong generator = TestData.Factory.ofLongSupplier(
                "[1L, 1L, ...]", () -> LongStream.generate(() -> 1));

        withData(generator).
                stream(s -> fs.apply(s.filter(i -> true).unordered())).
                exercise();
    }

    @Test(dataProvider = "DoubleStream.limit")
    public void testDoubleUnorderedGenerator(String description, UnaryOperator<DoubleStream> fs) {
        // Source is spliterator of infinite size
        TestData.OfDouble generator = TestData.Factory.ofDoubleSupplier(
                "[1.0, 1.0, ...]", () -> DoubleStream.generate(() -> 1.0));

        withData(generator).
                stream(s -> fs.apply(s.filter(i -> true).unordered())).
                exercise();
    }


    // Unordered iteration

    @Test(dataProvider = "Stream.limit")
    public void testUnorderedIteration(String description, UnaryOperator<Stream<Long>> fs) {
        // Source is a right-balanced tree of infinite size
        TestData.OfRef<Long> iterator = TestData.Factory.ofSupplier(
                "[1L, 2L, 3L, ...]", () -> Stream.iterate(1L, i -> i + 1L));

        // Ref
        withData(iterator).
                stream(s -> fs.apply(s.unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "IntStream.limit")
    public void testIntUnorderedIteration(String description, UnaryOperator<IntStream> fs) {
        // Source is a right-balanced tree of infinite size
        TestData.OfInt iterator = TestData.Factory.ofIntSupplier(
                "[1, 2, 3, ...]", () -> IntStream.iterate(1, i -> i + 1));

        // Ref
        withData(iterator).
                stream(s -> fs.apply(s.unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "LongStream.limit")
    public void testLongUnorderedIteration(String description, UnaryOperator<LongStream> fs) {
        // Source is a right-balanced tree of infinite size
        TestData.OfLong iterator = TestData.Factory.ofLongSupplier(
                "[1L, 2L, 3L, ...]", () -> LongStream.iterate(1, i -> i + 1));

        // Ref
        withData(iterator).
                stream(s -> fs.apply(s.unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }

    @Test(dataProvider = "DoubleStream.limit")
    public void testDoubleUnorderedIteration(String description, UnaryOperator<DoubleStream> fs) {
        // Source is a right-balanced tree of infinite size
        TestData.OfDouble iterator = TestData.Factory.ofDoubleSupplier(
                "[1.0, 2.0, 3.0, ...]", () -> DoubleStream.iterate(1, i -> i + 1));

        // Ref
        withData(iterator).
                stream(s -> fs.apply(s.unordered())).
                resultAsserter(unorderedAsserter()).
                exercise();
    }
}
