package com.github.fridujo.rabbitmq.mock.integration.alpakka;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import com.github.fridujo.rabbitmq.mock.compatibility.MockConnectionFactoryFactory;

import akka.Done;
import akka.NotUsed;
import akka.actor.ActorSystem;
import akka.japi.Pair;
import akka.stream.ActorMaterializer;
import akka.stream.KillSwitches;
import akka.stream.Materializer;
import akka.stream.UniqueKillSwitch;
import akka.stream.alpakka.amqp.AmqpConnectionFactoryConnectionProvider;
import akka.stream.alpakka.amqp.AmqpConnectionProvider;
import akka.stream.alpakka.amqp.AmqpReplyToSinkSettings;
import akka.stream.alpakka.amqp.AmqpWriteSettings;
import akka.stream.alpakka.amqp.NamedQueueSourceSettings;
import akka.stream.alpakka.amqp.QueueDeclaration;
import akka.stream.alpakka.amqp.ReadResult;
import akka.stream.alpakka.amqp.WriteMessage;
import akka.stream.alpakka.amqp.javadsl.AmqpRpcFlow;
import akka.stream.alpakka.amqp.javadsl.AmqpSink;
import akka.stream.alpakka.amqp.javadsl.AmqpSource;
import akka.stream.alpakka.amqp.javadsl.CommittableReadResult;
import akka.stream.javadsl.Flow;
import akka.stream.javadsl.Keep;
import akka.stream.javadsl.Sink;
import akka.stream.javadsl.Source;
import akka.stream.testkit.TestSubscriber;
import akka.stream.testkit.javadsl.StreamTestKit;
import akka.stream.testkit.javadsl.TestSink;
import akka.testkit.javadsl.TestKit;
import akka.util.ByteString;
import scala.collection.JavaConverters;
import scala.concurrent.duration.Duration;

/**
 * Shamefully copied from the <a href="https://github.com/akka/alpakka">Alpakka project</a> (under Apache 2.0 license).
 */
class AmqpConnectorsTest {

    private static ActorSystem system;
    private static Materializer materializer;

    private AmqpConnectionProvider connectionProvider = AmqpConnectionFactoryConnectionProvider.create(MockConnectionFactoryFactory.build());

    @BeforeAll
    public static void setUp() {
        system = ActorSystem.create();
        materializer = ActorMaterializer.create(system);
    }

    @AfterAll
    public static void tearDown() {
        TestKit.shutdownActorSystem(system);
    }

    @AfterEach
    public void checkForStageLeaks() {
        StreamTestKit.assertAllStagesStopped(materializer);
    }

    @Test
    public void publishAndConsumeRpcWithoutAutoAck() throws Exception {

        final String queueName = "amqp-conn-it-spec-rpc-queue-" + System.currentTimeMillis();
        final QueueDeclaration queueDeclaration = QueueDeclaration.create(queueName);

        final List<String> input = Arrays.asList("one", "two", "three", "four", "five");

        final Flow<WriteMessage, CommittableReadResult, CompletionStage<String>> ampqRpcFlow =
            AmqpRpcFlow.committableFlow(
                AmqpWriteSettings.create(connectionProvider)
                    .withRoutingKey(queueName)
                    .withDeclaration(queueDeclaration),
                10,
                1);
        Pair<CompletionStage<String>, TestSubscriber.Probe<ReadResult>> result =
            Source.from(input)
                .map(ByteString::fromString)
                .map(bytes -> WriteMessage.create(bytes))
                .viaMat(ampqRpcFlow, Keep.right())
                .mapAsync(1, cm -> cm.ack().thenApply(unused -> cm.message()))
                .toMat(TestSink.probe(system), Keep.both())
                .run(materializer);

        result.first().toCompletableFuture().get(5, TimeUnit.SECONDS);

        Sink<WriteMessage, CompletionStage<Done>> amqpSink =
            AmqpSink.createReplyTo(AmqpReplyToSinkSettings.create(connectionProvider));

        final Source<ReadResult, NotUsed> amqpSource =
            AmqpSource.atMostOnceSource(
                NamedQueueSourceSettings.create(connectionProvider, queueName)
                    .withDeclaration(queueDeclaration),
                1);

        UniqueKillSwitch sourceToSink =
            amqpSource
                .viaMat(KillSwitches.single(), Keep.right())
                .map(b -> WriteMessage.create(b.bytes()).withProperties(b.properties()))
                .to(amqpSink)
                .run(materializer);

        List<ReadResult> probeResult =
            JavaConverters.seqAsJavaListConverter(
                result.second().toStrict(Duration.create(5, TimeUnit.SECONDS)))
                .asJava();
        assertEquals(
            probeResult.stream().map(s -> s.bytes().utf8String()).collect(Collectors.toList()), input);
        sourceToSink.shutdown();
    }

    @Test
    public void keepConnectionOpenIfDownstreamClosesAndThereArePendingAcks() throws Exception {
        final String queueName = "amqp-conn-it-spec-simple-queue-" + System.currentTimeMillis();
        final QueueDeclaration queueDeclaration = QueueDeclaration.create(queueName);

        final Sink<ByteString, CompletionStage<Done>> amqpSink =
            AmqpSink.createSimple(
                AmqpWriteSettings.create(connectionProvider)
                    .withRoutingKey(queueName)
                    .withDeclaration(queueDeclaration));

        final Integer bufferSize = 10;
        final Source<CommittableReadResult, NotUsed> amqpSource =
            AmqpSource.committableSource(
                NamedQueueSourceSettings.create(connectionProvider, queueName)
                    .withDeclaration(queueDeclaration),
                bufferSize);

        final List<String> input = Arrays.asList("one", "two", "three", "four", "five");
        Source.from(input)
            .map(ByteString::fromString)
            .runWith(amqpSink, materializer)
            .toCompletableFuture()
            .get(3, TimeUnit.SECONDS);

        final CompletionStage<List<CommittableReadResult>> result =
            amqpSource.take(input.size()).runWith(Sink.seq(), materializer);

        List<CommittableReadResult> committableMessages =
            result.toCompletableFuture().get(3, TimeUnit.SECONDS);

        assertEquals(input.size(), committableMessages.size());
        committableMessages.forEach(
            cm -> {
                try {
                    cm.ack(false).toCompletableFuture().get(3, TimeUnit.SECONDS);
                } catch (Exception e) {
                    fail(e.getMessage());
                }
            });
    }
}