/*
 * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.bus.client.tests;

import java.util.ArrayList;
import java.util.Collection;

import org.jboss.errai.bus.client.api.Subscription;
import org.jboss.errai.bus.client.api.base.DefaultErrorCallback;
import org.jboss.errai.bus.client.api.base.MessageBuilder;
import org.jboss.errai.bus.client.api.base.NoSubscribersToDeliverTo;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.tests.support.ErrorThrowingRPCService;
import org.jboss.errai.bus.client.tests.support.SimpleRPCService;
import org.jboss.errai.bus.common.AbstractErraiTest;
import org.jboss.errai.common.client.api.ErrorCallback;
import org.jboss.errai.common.client.protocols.MessageParts;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
import com.google.gwt.user.client.Timer;

/**
 * Error handling tests
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public class ErrorHandlingTest extends AbstractErraiTest {

  private static final String LOCAL_ERROR_THROWING_SERVICE = "LocalErrorThrowingService";

  private static class Ref<T> {
    T value;
    public Ref(final T initial) {
      this.value = initial;
    }
  }

  private final Collection<Subscription> subscriptions = new ArrayList<>();

  @Override
  public String getModuleName() {
    return "org.jboss.errai.bus.ErraiBusTests";
  }

  @Override
  protected void gwtSetUp() throws Exception {
    GWT.setUncaughtExceptionHandler(e -> {
      if (e instanceof AssertionError) {
        throw (AssertionError) e;
      }
    });
    super.gwtSetUp();
    subscriptions.add(bus.subscribe(LOCAL_ERROR_THROWING_SERVICE, m -> {
      throw new RuntimeException("Thrown by " + LOCAL_ERROR_THROWING_SERVICE);
    }));
  }

  @Override
  protected void gwtTearDown() throws Exception {
    for (final Subscription sub : subscriptions) {
      sub.remove();
    }
    subscriptions.clear();
    super.gwtTearDown();
  }

  public void testDefaultErrorHandlerCallsGwtUncaughtExceptionHandler() throws Exception {
    runAfterInit(() -> {
      final Throwable t = new Throwable();
      final Ref<Throwable> caughtRef = new Ref<>(null);
      GWT.setUncaughtExceptionHandler(caught -> {
        caughtRef.value = caught;
      });
      MessageBuilder
      .createMessage(DefaultErrorCallback.CLIENT_ERROR_SUBJECT)
      .with(MessageParts.Throwable, t)
      .noErrorHandling()
      .sendNowWith(bus);
      assertSame(t, caughtRef.value);
      finishTest();
    });
  }

  public void testRemoteDefaultErrorHandling() {
    final String subject = "ErrorThrowingService";
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        final Throwable thrown = message.get(Throwable.class, MessageParts.Throwable);
        assertTrue(thrown instanceof RuntimeException);
        assertTrue(thrown.getMessage().contains(subject));
        finishTest();
      }));
      MessageBuilder
        .createMessage(subject)
        .signalling()
        .defaultErrorHandling()
        .sendNowWith(bus);
    });
  }

  public void testRemoteNoErrorHandling() {
    final String subject = "ErrorThrowingService";
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        fail("Should not have received message on " + DefaultErrorCallback.CLIENT_ERROR_SUBJECT);
      }));

      final Timer finishTimer = new Timer() {
        @Override
        public void run() {
          finishTest();
        }
      };
      subscriptions.add(() -> finishTimer.cancel());
      finishTimer.schedule(5000);

      MessageBuilder
        .createMessage(subject)
        .signalling()
        .noErrorHandling()
        .sendNowWith(bus);
    });
  }

  public void testLocalDefaultErrorHandling() {
    final String subject = LOCAL_ERROR_THROWING_SERVICE;
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        final Throwable thrown = message.get(Throwable.class, MessageParts.Throwable);
        assertTrue(thrown instanceof RuntimeException);
        assertTrue(thrown.getMessage().contains(subject));
        finishTest();
      }));
      MessageBuilder
        .createMessage(subject)
        .signalling()
        .defaultErrorHandling()
        .sendNowWith(bus);
    });
  }

  public void testLocalNoErrorHandling() {
    final String subject = LOCAL_ERROR_THROWING_SERVICE;
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        fail("Should not have received message on " + DefaultErrorCallback.CLIENT_ERROR_SUBJECT);
      }));

      final Timer finishTimer = new Timer() {
        @Override
        public void run() {
          finishTest();
        }
      };
      subscriptions.add(() -> finishTimer.cancel());
      finishTimer.schedule(5000);

      MessageBuilder
        .createMessage(subject)
        .signalling()
        .noErrorHandling()
        .sendNowWith(bus);
    });
  }

  public void testLocalCustomAndDefaultErrorHandling() {
    final String subject = "NonExistentService";
    runAfterInit(() -> {
      final Ref<Boolean> ref = new Ref<>(false);
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        final Throwable thrown = message.get(Throwable.class, MessageParts.Throwable);
        assertTrue(thrown instanceof NoSubscribersToDeliverTo);
        assertTrue(ref.value);
        finishTest();
      }));
      MessageBuilder
        .createMessage(subject)
        .signalling()
        .errorsHandledBy((ErrorCallback<Message>) (m, t) -> {
          ref.value = true;
          return true;
        })
        .sendNowWith(bus);
    });
  }

  public void testLocalCustomOnlyErrorHandling() {
    final String subject = "NonExistentService";
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        fail("Should not have received error on subject " + DefaultErrorCallback.CLIENT_ERROR_SUBJECT);
      }));
      MessageBuilder
        .createMessage(subject)
        .signalling()
        .errorsHandledBy((ErrorCallback<Message>) (m, t) -> {
          final Timer finishTimer = new Timer() {
            @Override
            public void run() {
              finishTest();
            }
          };
          subscriptions.add(() -> {
            finishTimer.cancel();
          });
          finishTimer.schedule(5000);
          return false;
        })
        .sendNowWith(bus);
    });
  }

  public void testRemoteRPCDefaultErrorHandling() {
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        final Throwable thrown = message.get(Throwable.class, MessageParts.Throwable);
        assertNotNull(thrown);
        assertNotNull(thrown.getMessage());
        assertTrue(thrown.getMessage().contains(ErrorThrowingRPCService.class.getSimpleName()));
        finishTest();
      }));
      MessageBuilder
        .createCall((final Long time) -> fail("Should not receive response."), ErrorThrowingRPCService.class)
        .currentTime();
    });
  }

  public void testRemoteRPCCustomOnlyErrorHandling() {
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        fail("Should not have received error on subject " + DefaultErrorCallback.CLIENT_ERROR_SUBJECT);
      }));
      MessageBuilder
        .createCall((final Long time) -> fail("Should not receive response."),
                    (m, t) -> {
                      final Timer finishTimer = new Timer() {
                        @Override
                        public void run() {
                          finishTest();
                        }
                      };
                      subscriptions.add(() -> {
                        finishTimer.cancel();
                      });
                      finishTimer.schedule(5000);
                      return false;
                    },
                    ErrorThrowingRPCService.class)
        .currentTime();
    });
  }

  public void testRemoteRPCCustomAndDefaultErrorHandling() {
    runAfterInit(() -> {
      subscriptions.add(bus.subscribe(DefaultErrorCallback.CLIENT_ERROR_SUBJECT, message -> {
        final Throwable thrown = message.get(Throwable.class, MessageParts.Throwable);
        assertNotNull(thrown);
        assertNotNull(thrown.getMessage());
        assertTrue(thrown.getMessage().contains(ErrorThrowingRPCService.class.getSimpleName()));
        finishTest();
      }));
      MessageBuilder
        .createCall((final Long time) -> fail("Should not receive response."),
                    (m, t) -> {
                      assertNotNull(t);
                      assertNotNull(t.getMessage());
                      assertTrue(t.getMessage().contains(ErrorThrowingRPCService.class.getSimpleName()));
                      return true;
                    },
                    ErrorThrowingRPCService.class)
        .currentTime();
    });
  }

  public void testRemoteRPCUncaughtExceptionHandlerInvokedForErrorInClientCallback() {
    runAfterInit(() -> {
      final Timer failTimer = new Timer() {
        @Override
        public void run() {
          fail("Timed-out without UncaughtExceptionHandler being invoked.");
        }
      };
      final UncaughtExceptionHandler handler = t -> {
        if (t instanceof AssertionError) {
          throw (AssertionError) t;
        }
        else {
          finishTest();
        }
      };
      GWT.setUncaughtExceptionHandler(handler);

      subscriptions.add(() -> failTimer.cancel());

      MessageBuilder
        .createCall((final Long time) -> {
          throw new RuntimeException("Client callback exception.");
        }, ErrorThrowingRPCService.class)
        .currentTime();
      failTimer.schedule(5000);
    });
  }

  public void testRemoteRPCUncaughtExceptionHandlerInvokedWhenErrorHandlerSetForErrorInClientCallback() {
    runAfterInit(() -> {
      final Timer failTimer = new Timer() {
        @Override
        public void run() {
          fail("Timed-out without UncaughtExceptionHandler being invoked.");
        }
      };
      final UncaughtExceptionHandler handler = t -> {
        if (t instanceof AssertionError) {
          throw (AssertionError) t;
        }
        else {
          finishTest();
        }
      };
      GWT.setUncaughtExceptionHandler(handler);

      subscriptions.add(() -> failTimer.cancel());

      MessageBuilder
        .createCall((final Long time) -> {
          throw new RuntimeException("Client callback exception.");
        },
        (m, t) -> {
          fail("Should not invoke error handler for error in client callback.");
          return false;
        },
        SimpleRPCService.class)
        .currentTime();
      failTimer.schedule(5000);
    });
  }
}
