/*
 * Copyright (c) 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.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Supplier;
import java.util.stream.*;
import java.util.stream.Gatherer;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.*;

/**
 * @test
 * @summary Testing public API of Gatherer
 * @enablePreview
 * @run junit GathererAPITest
 */

public class GathererAPITest {
    final static Supplier<Void> initializer = () -> (Void)null;
    final static Gatherer.Integrator<Void, Integer, Integer> integrator = (v,e,d) -> d.push(e);
    final static BinaryOperator<Void> combiner = (l,r) -> l;
    final static BiConsumer<Void,Gatherer.Downstream<? super Integer>> finisher = (v,d) -> {};

    final static Supplier<Void> nullInitializer = null;
    final static Gatherer.Integrator<Void, Integer, Integer> nullIntegrator = null;
    final static BinaryOperator<Void> nullCombiner = null;
    final static BiConsumer<Void,Gatherer.Downstream<? super Integer>> nullFinisher = null;

    private final static <T> Gatherer<T,?,T> passthrough() {
        return Gatherer.of(
                () -> (Void)null,
                Gatherer.Integrator.<Void,T,T>ofGreedy((v,e,d) -> d.push(e)),
                (l,r) -> l,
                (v,d) -> {}
        );
    }

    private final static <T,A,R> Gatherer<T,A,R> verifyGathererContract(Gatherer<T,A,R> gatherer) {
        // basics
        assertNotNull(gatherer);

        // components
        assertNotNull(gatherer.initializer());
        assertNotNull(gatherer.integrator());
        assertNotNull(gatherer.combiner());
        assertNotNull(gatherer.finisher());
        assertNotNull(gatherer.andThen(passthrough()));

        return gatherer;
    }

    private final static <T,A,R> Gatherer<T,A,R> verifyGathererStructure(
            Gatherer<T,A,R> gatherer,
            Supplier<A> expectedSupplier,
            Gatherer.Integrator<A,T,R> expectedIntegrator,
            BinaryOperator<A> expectedCombiner,
            BiConsumer<A,Gatherer.Downstream<? super R>> expectedFinisher
    ) {
        // basics
        assertNotNull(gatherer);

        // components
        assertSame(expectedSupplier, gatherer.initializer());
        assertSame(expectedIntegrator, gatherer.integrator());
        assertSame(expectedCombiner, gatherer.combiner());
        assertSame(expectedFinisher, gatherer.finisher());

        return gatherer;
    }

    @Test
    public void testGathererDefaults() {
        final Gatherer.Integrator<Void,Void,Void> expectedIntegrator =
                (a,b,c) -> false;

        class Test implements Gatherer<Void,Void,Void> {
            @Override
            public Integrator<Void, Void, Void> integrator() {
                return expectedIntegrator;
            }
        }

        var t = new Test();
        assertSame(Gatherer.<Void>defaultInitializer(), t.initializer());
        assertSame(expectedIntegrator, t.integrator());
        assertSame(Gatherer.<Void>defaultCombiner(), t.combiner());
        assertSame(Gatherer.<Void,Gatherer.Downstream<? super Void>>defaultFinisher(), t.finisher());
    }

    @Test
    public void testDownstreamDefaults() {
        class Test implements Gatherer.Downstream<Void> {
            @Override public boolean push(Void v) { return false; }
        }

        var t = new Test();
        assertEquals(false, t.isRejecting());
    }

    @Test
    public void testGathererFactoriesNPE() {
        assertThrows(NullPointerException.class,
                () -> Gatherer.of(nullInitializer, integrator, combiner, finisher));
        assertThrows(NullPointerException.class,
                () -> Gatherer.of(initializer, nullIntegrator, combiner, finisher));
        assertThrows(NullPointerException.class,
                () -> Gatherer.of(initializer, integrator, nullCombiner, finisher));
        assertThrows(NullPointerException.class,
                () -> Gatherer.of(initializer, integrator, combiner, nullFinisher));

        assertThrows(NullPointerException.class,
                () -> Gatherer.of(nullIntegrator));

        assertThrows(NullPointerException.class,
                () -> Gatherer.of(nullIntegrator, finisher));
        assertThrows(NullPointerException.class,
                () -> Gatherer.of(integrator, nullFinisher));

        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(nullInitializer, integrator));
        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(initializer, nullIntegrator));

        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(nullIntegrator));

        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(nullIntegrator, finisher));
        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(integrator, nullFinisher));

        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(nullInitializer, integrator, finisher));
        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(initializer, nullIntegrator, finisher));
        assertThrows(NullPointerException.class,
                () -> Gatherer.ofSequential(initializer, integrator, nullFinisher));
    }

    @Test
    public void testGathererFactoriesAPI() {
        final var defaultInitializer = Gatherer.<Void>defaultInitializer();
        final var defaultCombiner = Gatherer.<Void>defaultCombiner();
        final var defaultFinisher = Gatherer.<Void,Integer>defaultFinisher();

        var g1 = verifyGathererContract(passthrough()); // Quis custodiet ipsos custodes?
        verifyGathererContract(g1.andThen(g1));

        var g2 = verifyGathererContract(Gatherer.of(integrator));
        verifyGathererContract(g2.andThen(g2));
        assertSame(defaultInitializer, g2.initializer());
        assertSame(integrator, g2.integrator());
        assertNotSame(defaultCombiner, g2.combiner());
        assertSame(defaultFinisher, g2.finisher());

        var g3 = verifyGathererContract(Gatherer.of(integrator, finisher));
        verifyGathererContract(g3.andThen(g3));
        assertSame(integrator, g3.integrator());
        assertNotSame(defaultCombiner, g3.combiner());
        assertSame(finisher, g3.finisher());

        var g4 = verifyGathererContract(Gatherer.ofSequential(integrator));
        verifyGathererContract(g4.andThen(g4));
        verifyGathererStructure(g4, defaultInitializer, integrator, defaultCombiner, defaultFinisher);

        var g5 = verifyGathererContract(Gatherer.ofSequential(initializer, integrator));
        verifyGathererContract(g5.andThen(g5));
        verifyGathererStructure(g5, initializer, integrator, defaultCombiner, defaultFinisher);

        var g6 = verifyGathererContract(Gatherer.ofSequential(integrator, finisher));
        verifyGathererContract(g6.andThen(g6));
        verifyGathererStructure(g6, defaultInitializer, integrator, defaultCombiner, finisher);

        var g7 = verifyGathererContract(Gatherer.ofSequential(initializer, integrator, finisher));
        verifyGathererContract(g7.andThen(g7));
        verifyGathererStructure(g7, initializer, integrator, defaultCombiner, finisher);

        var g8 = verifyGathererContract(Gatherer.of(initializer, integrator, combiner, finisher));
        verifyGathererContract(g8.andThen(g8));
        verifyGathererStructure(g8, initializer, integrator, combiner, finisher);
    }

    @Test
    public void testGathererVariance() {

        // Make sure that Gatherers can pass-through type
        Gatherer<Number,?,Number> nums = Gatherer.of((unused, element, downstream) -> downstream.push(element));

        // Make sure that Gatherers can upcast the output type from the input type
        Gatherer<Number,?,Object> upcast = Gatherer.of((unused, element, downstream) -> downstream.push(element));

        // Make sure that Gatherers can consume a supertype of the Stream output
        assertEquals(List.of(1,2,3,4,5), Stream.<Integer>of(1,2,3,4,5).gather(nums).toList());

        Gatherer<Integer,?,Integer> ints = Gatherer.of((unused, element, downstream) -> downstream.push(element));

        // Make sure that Gatherers can be composed where the output is a subtype of the input type of the next
        Gatherer<Integer,?,Number> composition = ints.andThen(nums);

        // Make sure that composition works transitively, typing-wise
        Gatherer<Integer,?,Object> upcastComposition = ints.andThen(nums.andThen(upcast));

        assertEquals(List.of(1,2,3,4,5), Stream.<Integer>of(1,2,3,4,5).gather(composition).toList());
        assertEquals(List.of(1,2,3,4,5), Stream.<Integer>of(1,2,3,4,5).gather(upcastComposition).toList());
    }
}