/*
 * Copyright 2019, 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.testing.server;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import com.google.common.truth.extensions.proto.ProtoSubject;
import com.google.common.truth.extensions.proto.ProtoTruth;
import com.google.protobuf.Empty;
import com.google.protobuf.Message;
import io.spine.base.SerializableMessage;
import io.spine.core.Signal;
import io.spine.protobuf.AnyPacker;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.List;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.size;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.simpleFact;
import static com.google.common.truth.extensions.proto.ProtoTruth.protos;
import static io.spine.testing.server.EmittedMessageSubject.FactKey.ACTUAL;
import static io.spine.testing.server.EmittedMessageSubject.FactKey.MESSAGE_COUNT;
import static io.spine.testing.server.EmittedMessageSubject.FactKey.REQUESTED_INDEX;

/**
 * Abstract base for subjects checking messages, such as events or commands, generated by
 * a Bounded Context under the test.
 *
 * @param <S>
 *         the self-type for return type covariance
 * @param <T>
 *         the type of the outer objects of the checked messages,
 *         such as {@link io.spine.core.Command Command} or {@link io.spine.core.Event Event}
 * @param <M>
 *         the type of emitted messages, such as {@link io.spine.base.CommandMessage CommandMessage}
 *         or {@link io.spine.base.EventMessage EventMessage}.
 */
public abstract class EmittedMessageSubject<S extends EmittedMessageSubject<S, T, M>,
                                            T extends Signal,
                                            M extends SerializableMessage>
        extends Subject {

    private final @Nullable Iterable<T> actual;

    protected EmittedMessageSubject(FailureMetadata metadata, @Nullable Iterable<T> actual) {
        super(metadata, actual);
        this.actual = actual;
    }

    /** Fails if the subject does not have the given size. */
    public final void hasSize(int expectedSize) {
        checkArgument(expectedSize >= 0, "expectedSize(%s) must be >= 0", expectedSize);
        if (actual == null) {
            failWithoutActual(simpleFact("message list should not be equal to null"));
        } else {
            int actualSize = size(actual);
            check("size()").that(actualSize).isEqualTo(expectedSize);
        }
    }

    /** Fails if the subject is not empty. */
    public final void isEmpty() {
        check("isEmpty()").that(messages())
                          .isEmpty();
    }

    /** Fails if the subject is empty. */
    public final void isNotEmpty() {
        check("isNotEmpty()").that(messages())
                             .isNotEmpty();
    }

    /**
     * Obtains the subject for the message at the given index.
     *
     * <p>Fails if the index is out of the range of the generated message sequence.
     */
    public final ProtoSubject message(int index) {
        if (messages() == null) {
            failWithActual(fact(ACTUAL.value, null));
            return ignoreCheck().about(protos())
                                .that(Empty.getDefaultInstance());
        }
        int size = size(messages());
        if (index >= size(messages())) {
            failWithActual(
                    fact(MESSAGE_COUNT.value, size),
                    fact(REQUESTED_INDEX.value, index)
            );
            return ignoreCheck().about(protos())
                                .that(Empty.getDefaultInstance());
        }
        T outerObject = Iterables.get(messages(), index);
        Message unpacked = AnyPacker.unpack(outerObject.getMessage());
        return ProtoTruth.assertThat(unpacked);
    }

    /**
     * Provides factory for creating the same type of subject on a subset of messages.
     */
    protected abstract Subject.Factory<S, Iterable<T>> factory();

    /**
     * Obtains the subject over outer objects that contain messages of the passed class.
     */
    public final S withType(Class<? extends M> messageClass) {
        Iterable<T> actual = messages();
        if (actual == null) {
            failWithActual(fact(ACTUAL.value, null));
            return ignoreCheck().about(factory())
                                .that(ImmutableList.of());
        } else {
            List<T> filtered =
                    Streams.stream(actual)
                           .filter(m -> {
                               @SuppressWarnings({"unchecked", "RedundantSuppression"})
                               /* avoid `unchecked` warning when calling raw instance
                               of `Signal` when filtering. This warning is given only
                               when compiling. Hence, the second suppression. */
                               boolean match = m.is(messageClass);
                               return match;
                           })
                           .collect(toImmutableList());
            return check("withType(%s)", messageClass.getSimpleName())
                    .about(factory())
                    .that(filtered);
        }
    }

    private @Nullable Iterable<T> messages() {
        return actual;
    }

    /**
     * Obtains the list of messages under assertion.
     *
     * @return an immutable copy of the {@code actual} messages
     */
    public List<T> actual() {
        checkNotNull(actual);
        return ImmutableList.copyOf(actual);
    }

    /**
     * Key strings for Truth facts.
     */
    @VisibleForTesting
    enum FactKey {

        MESSAGE_COUNT("the count of the generated messages is"),
        REQUESTED_INDEX("but the requested index was"),
        ACTUAL("actual");

        private final String value;

        FactKey(String value) {
            this.value = value;
        }

        String value() {
            return value;
        }
    }
}

@@ -6822,16 +6822,25 @@
  public 
+Immutable
 List%3CT%3E 

Make `actual` value explicitly immutable