package com.typesafe.netty;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.local.LocalChannel;
import org.reactivestreams.Publisher;
import org.reactivestreams.tck.PublisherVerification;
import org.reactivestreams.tck.TestEnvironment;
import org.testng.annotations.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

public class HandlerPublisherVerificationTest extends PublisherVerification<Long> {

    private final int batchSize;
    // The number of elements to publish initially, before the subscriber is received
    private final int publishInitial;
    // Whether we should use scheduled publishing (with a small delay)
    private final boolean scheduled;

    private ScheduledExecutorService executor;
    private DefaultEventLoopGroup eventLoop;

    // For debugging, change the data provider to simple, and adjust the parameters below
    @Factory(dataProvider = "noScheduled")
    public HandlerPublisherVerificationTest(int batchSize, int publishInitial, boolean scheduled) {
        super(new TestEnvironment(200));
        this.batchSize = batchSize;
        this.publishInitial = publishInitial;
        this.scheduled = scheduled;
    }

    @DataProvider
    public static Object[][] simple() {
        boolean scheduled = false;
        int batchSize = 2;
        int publishInitial = 0;
        return new Object[][] {
                new Object[] {batchSize, publishInitial, scheduled}
        };
    }

    @DataProvider
    public static Object[][] full() {
        List<Object[]> data = new ArrayList<>();
        for (Boolean scheduled : Arrays.asList(false, true)) {
            for (int batchSize : Arrays.asList(1, 3)) {
                for (int publishInitial : Arrays.asList(0, 3)) {
                    data.add(new Object[]{batchSize, publishInitial, scheduled});
                }
            }
        }
        return data.toArray(new Object[][]{});
    }

    @DataProvider
    public static Object[][] noScheduled() {
        List<Object[]> data = new ArrayList<>();
        for (int batchSize : Arrays.asList(1, 3)) {
            for (int publishInitial : Arrays.asList(0, 3)) {
                data.add(new Object[]{batchSize, publishInitial, false});
            }
        }
        return data.toArray(new Object[][]{});
    }

    // I tried making this before/after class, but encountered a strange error where after 32 publishers were created,
    // the following tests complained about the executor being shut down when I registered the channel. Though, it
    // doesn't happen if you create 32 publishers in a single test.
    @BeforeMethod
    public void startEventLoop() {
        eventLoop = new DefaultEventLoopGroup();
    }

    @AfterMethod
    public void stopEventLoop() {
        eventLoop.shutdownGracefully();
        eventLoop = null;
    }

    @BeforeClass
    public void startExecutor() {
        if (scheduled) {
            executor = Executors.newSingleThreadScheduledExecutor();
        }
    }

    @AfterClass
    public void stopExecutor() {
        if (scheduled) {
            executor.shutdown();
        }
    }

    @Override
    public Publisher<Long> createPublisher(final long elements) {
        final BatchedProducer out;
        if (scheduled) {
            out = new ScheduledBatchedProducer(elements, batchSize, publishInitial, executor, 5);
        } else {
            out = new BatchedProducer(elements, batchSize, publishInitial);
        }

        final ClosedLoopChannel channel = new ClosedLoopChannel();
        channel.config().setAutoRead(false);
        ChannelFuture registered = eventLoop.register(channel);

        final HandlerPublisher<Long> publisher = new HandlerPublisher<>(registered.channel().eventLoop(), Long.class);

        registered.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                channel.pipeline().addLast("out", out);
                channel.pipeline().addLast("publisher", publisher);

                for (long i = 0; i < publishInitial && i < elements; i++) {
                    channel.pipeline().fireChannelRead(i);
                }
                if (elements <= publishInitial) {
                    channel.pipeline().fireChannelInactive();
                }
            }
        });

        return publisher;
    }

    @Override
    public Publisher<Long> createFailedPublisher() {
        LocalChannel channel = new LocalChannel();
        eventLoop.register(channel);
        HandlerPublisher<Long> publisher = new HandlerPublisher<>(channel.eventLoop(), Long.class);
        channel.pipeline().addLast("publisher", publisher);
        channel.pipeline().fireExceptionCaught(new RuntimeException("failed"));

        return publisher;
    }

    @Override
    public void stochastic_spec103_mustSignalOnMethodsSequentially() throws Throwable {
        try {
            super.stochastic_spec103_mustSignalOnMethodsSequentially();
        } catch (Throwable t) {
            // CI is failing here, but maven doesn't tell us which parameters failed
            System.out.println("Stochastic test failed with parameters batchSize=" + batchSize +
                    " publishInitial=" + publishInitial + " scheduled=" + scheduled);
            throw t;
        }
    }
}