/*
 * Copyright 2020, TeamDev. All rights reserved.
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.spine.client;

import com.google.common.collect.ImmutableList;
import com.google.protobuf.Message;
import io.spine.base.CommandMessage;
import io.spine.base.Error;
import io.spine.base.EventMessage;
import io.spine.core.Command;
import io.spine.server.BoundedContextBuilder;
import io.spine.test.client.ClientTestContext;
import io.spine.test.client.command.LogInUser;
import io.spine.test.client.command.UnsupportedCommand;
import io.spine.test.client.event.UserAccountCreated;
import io.spine.test.client.event.UserLoggedIn;
import io.spine.test.client.rejection.Rejections.UserAlreadyLoggedIn;
import io.spine.testing.core.given.GivenUserId;
import io.spine.testing.logging.MuteLogging;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

@MuteLogging
@DisplayName("`CommandRequest` should")
class CommandRequestTest extends AbstractClientTest {

    /** The object under the test. */
    private CommandRequest commandRequest;

    /** Registers which event consumers were called. */
    private final ConsumerCallCounter counter = new ConsumerCallCounter();

    @BeforeEach
    void createCommandRequest() {
        counter.clear();
        CommandMessage cmd = LogInUser
                .newBuilder()
                .setUser(GivenUserId.generated())
                .build();
        commandRequest = client().asGuest()
                                 .command(cmd);
    }

    @AfterEach
    void clearCounter() {
        counter.clear();
    }

    @Override
    protected ImmutableList<BoundedContextBuilder> contexts() {
        return ImmutableList.of(ClientTestContext.builder());
    }

    @Test
    @DisplayName("deliver an event to a consumer")
    void eventConsumer() {
        commandRequest.observe(UserLoggedIn.class, counter::add)
                      .observe(UserAccountCreated.class, counter::add)
                      .post();
        assertThat(counter.containsAll(UserLoggedIn.class, UserAccountCreated.class))
                .isTrue();
    }

    @Test
    @DisplayName("deliver an event and its context to a consumer")
    void eventAndContextConsumer() {
        commandRequest.observe(UserLoggedIn.class, (e, c) -> counter.add(e))
                      .observe(UserAccountCreated.class, (e, c) -> counter.add(e))
                      .post();
        assertThat(counter.containsAll(UserLoggedIn.class, UserAccountCreated.class))
                .isTrue();
    }

    @Test
    @DisplayName("deliver a rejection to its consumers")
    void rejections() {
        // Post the command so that the user is logged in. We are not interested in events here.
        commandRequest.post();
        // Now post the command again, expecting the rejection.
        commandRequest.observe(UserAlreadyLoggedIn.class, counter::add)
                      .post();

        assertThat(counter.contains(UserAlreadyLoggedIn.class))
                .isTrue();
    }

    @Nested
    @DisplayName("Allow setting custom streaming error handler")
    class CustomStreamingErrorHandler {

        @Test
        @DisplayName("rejecting `null`")
        void rejectingNull() {
            assertThrows(NullPointerException.class, () -> commandRequest.onStreamingError(null));
        }
    }

    @Nested
    @DisplayName("Allow setting custom consumer error handler")
    class CustomConsumerErrorHandler {

        private boolean handlerInvoked;
        private @Nullable Throwable passedThrowable;

        @BeforeEach
        void setup() {
            handlerInvoked = false;
            passedThrowable = null;
        }

        @Test
        @DisplayName("rejecting `null`")
        void rejectingNull() {
            assertThrows(NullPointerException.class, () -> commandRequest.onConsumingError(null));
        }

        @Test
        @DisplayName("invoking the handler when a consumer fails")
        void invocation() {
            ConsumerErrorHandler<EventMessage> handler = (c, th) -> {
                handlerInvoked = true;
                passedThrowable = th;
            };
            RuntimeException exception = new RuntimeException("Consumer-generated error.");

            commandRequest.onConsumingError(handler)
                          .observe(UserLoggedIn.class, e -> { throw exception; })
                          .post();

            assertThat(handlerInvoked)
                    .isTrue();
            assertThat(passedThrowable)
                    .isEqualTo(exception);
        }
    }

    @Nested
    @DisplayName("Allow setting custom posting error handler")
    class CustomPostingErrorHandler {

        private @Nullable Message postedMessage;
        private @Nullable Error returnedError;

        @BeforeEach
        void setup() {
            postedMessage = null;
            returnedError = null;
        }

        @Test
        @DisplayName("rejecting `null`")
        void rejectingNull() {
            assertThrows(NullPointerException.class, () -> commandRequest.onPostingError(null));
        }

        @Test
        @DisplayName("invoking handler when an invalid command posted")
        void invocation() {
            PostingErrorHandler handler = (message, error) -> {
                postedMessage = message;
                returnedError = error;
            };

            CommandRequest request =
                    client().asGuest()
                            .command(UnsupportedCommand
                                             .newBuilder()
                                             .setUser(GivenUserId.generated())
                                             .build())
                            .onPostingError(handler);
            request.post();

            assertThat(returnedError)
                    .isNotNull();
            assertThat(postedMessage)
                    .isInstanceOf(Command.class);
        }
    }
}

@@ -1080,16 +1080,76 @@
 leList;%0A
+import com.google.common.truth.extensions.proto.ProtoTruth;%0A
 import c
@@ -1298,32 +1298,68 @@
 e.core.Command;%0A
+import io.spine.protobuf.AnyPacker;%0A
 import io.spine.
@@ -6697,32 +6697,62 @@
     
-CommandRequest request =
+UnsupportedCommand commandMessage = UnsupportedCommand
 %0A   
@@ -6768,32 +6768,27 @@
         
-client().asGuest
+.newBuilder
 ()%0A     
@@ -6806,63 +6806,42 @@
     
-        .command(UnsupportedCommand%0A                   
+.setUser(GivenUserId.generated())%0A
     
@@ -6856,35 +6856,25 @@
         
-      .newB
+.b
 uild
-er
 ()
+;
 %0A       
@@ -6882,87 +6882,72 @@
     
-                                 .setUser(GivenUserId.generated())%0A            
+CommandRequest request =%0A                    client().asGuest()%0A
     
@@ -6970,29 +6970,39 @@
         
-     .build()
+.command(commandMessage
 )%0A      
@@ -7238,16 +7238,329 @@
 class);%0A
+            Command expected = Command%0A                    .newBuilder()%0A                    .setMessage(AnyPacker.pack(commandMessage))%0A                    .build();%0A            ProtoTruth.assertThat(postedMessage)%0A                      .comparingExpectedFieldsOnly()%0A                      .isEqualTo(expected);%0A
         

Verify the content of posted message