/**
 * Copyright 2014 Netflix, Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package rx.util;

import rx.Notification;
import rx.Observable;
import rx.functions.Func1;
import rx.functions.Func2;

public final class AssertObservable {
    private AssertObservable() {
        throw new IllegalStateException("No instances!");
    }
    /**
     * Asserts that two Observables are equal. If they are not, an {@link AssertionError} is thrown
     * with the given message. If <code>expecteds</code> and <code>actuals</code> are
     * <code>null</code>, they are considered equal.
     * 
     * @param expected
     *            Observable with expected values.
     * @param actual
     *            Observable with actual values
     */
    public static <T> void assertObservableEqualsBlocking(Observable<T> expected, Observable<T> actual) {
        assertObservableEqualsBlocking(null, expected, actual);
    }

    /**
     * Asserts that two Observables are equal. If they are not, an {@link AssertionError} is thrown
     * with the given message. If <code>expected</code> and <code>actual</code> are
     * <code>null</code>, they are considered equal.
     * 
     * @param message
     *            the identifying message for the {@link AssertionError} (<code>null</code> okay)
     * @param expected
     *            Observable with expected values.
     * @param actual
     *            Observable with actual values
     */
    public static <T> void assertObservableEqualsBlocking(String message, Observable<T> expected, Observable<T> actual) {
        assertObservableEquals(expected, actual).toBlocking().lastOrDefault(null);
    }

    /**
     * Asserts that two {@link Observable}s are equal and returns an empty {@link Observable}. If
     * they are not, an {@link Observable} is returned that calls onError with an {@link AssertionError} when subscribed to. If <code>expected</code> and <code>actual</code>
     * are <code>null</code>, they are considered equal.
     * 
     * @param message
     *            the identifying message for the {@link AssertionError} (<code>null</code> okay)
     * @param expected
     *            Observable with expected values.
     * @param actual
     *            Observable with actual values
     */
    public static <T> Observable<Void> assertObservableEquals(Observable<T> expected, Observable<T> actual) {
        return assertObservableEquals(null, expected, actual);
    }

    /**
     * Asserts that two {@link Observable}s are equal and returns an empty {@link Observable}. If
     * they are not, an {@link Observable} is returned that calls onError with an {@link AssertionError} when subscribed to with the given message. If <code>expected</code>
     * and <code>actual</code> are <code>null</code>, they are considered equal.
     * 
     * @param message
     *            the identifying message for the {@link AssertionError} (<code>null</code> okay)
     * @param expected
     *            Observable with expected values.
     * @param actual
     *            Observable with actual values
     */
    public static <T> Observable<Void> assertObservableEquals(final String message, Observable<T> expected, Observable<T> actual) {
        if (actual == null && expected != null) {
            return Observable.error(new AssertionError((message != null ? message + ": " : "") + "Actual was null and expected was not"));
        }
        if (actual != null && expected == null) {
            return Observable.error(new AssertionError((message != null ? message + ": " : "") + "Expected was null and actual was not"));
        }
        if (actual == null && expected == null) {
            return Observable.empty();
        }

        Func2<? super Notification<T>, ? super Notification<T>, Notification<String>> zipFunction = new Func2<Notification<T>, Notification<T>, Notification<String>>() {
            @Override
            public Notification<String> call(Notification<T> expectedNotfication, Notification<T> actualNotification) {
                if (expectedNotfication.equals(actualNotification)) {
                    StringBuilder message = new StringBuilder();
                    message.append(expectedNotfication.getKind());
                    if (expectedNotfication.hasValue())
                        message.append(" ").append(expectedNotfication.getValue());
                    if (expectedNotfication.hasThrowable())
                        message.append(" ").append(expectedNotfication.getThrowable());
                    return Notification.createOnNext("equals " + message.toString());
                }
                else {
                    StringBuilder error = new StringBuilder();
                    error.append("expected:<").append(expectedNotfication.getKind());
                    if (expectedNotfication.hasValue())
                        error.append(" ").append(expectedNotfication.getValue());
                    if (expectedNotfication.hasThrowable())
                        error.append(" ").append(expectedNotfication.getThrowable());
                    error.append("> but was:<").append(actualNotification.getKind());
                    if (actualNotification.hasValue())
                        error.append(" ").append(actualNotification.getValue());
                    if (actualNotification.hasThrowable())
                        error.append(" ").append(actualNotification.getThrowable());
                    error.append(">");

                    return Notification.createOnError(new AssertionError(error.toString()));
                }
            }
        };

        Func2<Notification<String>, Notification<String>, Notification<String>> accumulator = new Func2<Notification<String>, Notification<String>, Notification<String>>() {
            @Override
            public Notification<String> call(Notification<String> a, Notification<String> b) {
                String message = a.isOnError() ? a.getThrowable().getMessage() : a.getValue();
                boolean fail = a.isOnError();

                message += "\n\t" + (b.isOnError() ? b.getThrowable().getMessage() : b.getValue());
                fail |= b.isOnError();

                if (fail)
                    return Notification.createOnError(new AssertionError(message));
                else
                    return Notification.createOnNext(message);
            }
        };

        Observable<Void> outcomeObservable = Observable.zip(expected.materialize(), actual.materialize(), zipFunction).reduce(accumulator).map(new Func1<Notification<String>, Notification<Void>>() {
            @Override
            public Notification<Void> call(Notification<String> outcome) {
                if (outcome.isOnError()) {
                    String fullMessage = (message != null ? message + ": " : "") + "Observables are different\n\t" + outcome.getThrowable().getMessage();
                    return Notification.createOnError(new AssertionError(fullMessage));
                }
                return Notification.createOnCompleted();
            }
        }).dematerialize();
        return outcomeObservable;
    }
}
