/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.gobblin.writer.test;

import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.annotation.Nonnull;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

import org.apache.gobblin.writer.test.TestingEventBuses.Event;

import lombok.AllArgsConstructor;

/**
 * A wrapper around an EventBus created with {@link TestingEventBuses} that implements various
 * asserts on the incoming messages.
 *
 * <p><b>Important:</b> This class must be instantiated before any messages are sent on the bus or
 * it won't detect them.
 */
public class TestingEventBusAsserter implements Closeable {
  private final BlockingDeque<TestingEventBuses.Event> _events = new LinkedBlockingDeque<>();
  private final EventBus _eventBus;
  private long _defaultTimeoutValue = 1;
  private TimeUnit _defaultTimeoutUnit = TimeUnit.SECONDS;

  @AllArgsConstructor
  public static class StaticMessage implements Function<TestingEventBuses.Event, String> {
    private final String message;
    @Override public String apply(Event input) {
      return this.message;
    }
  }

  public TestingEventBusAsserter(String eventBusId) {
    _eventBus = TestingEventBuses.getEventBus(eventBusId);
    _eventBus.register(this);
  }

  @Subscribe public void processEvent(TestingEventBuses.Event e) {
    _events.offer(e);
  }

  @Override public void close() throws IOException {
    _eventBus.unregister(this);
  }

  public BlockingDeque<TestingEventBuses.Event> getEvents() {
    return _events;
  }

  public void clear() {
    _events.clear();
  }

  /** Sets timeout for all subsequent blocking asserts. */
  public TestingEventBusAsserter withTimeout(long timeout, TimeUnit unit) {
    _defaultTimeoutValue = timeout;
    _defaultTimeoutUnit = unit;
    return this;
  }

  /** Gets the next event from the queue and validates that it satisfies a given predicate. Blocking
   * assert. The event is removed from the internal queue regardless if the predicate has been
   * satisfied.
   * @param predicate the predicate to apply on the next event
   * @param assert error message generator
   * @return the event if the predicate is satisfied
   * @throws AssertionError if the predicate is not satisfied
   */
  public TestingEventBuses.Event assertNext(final Predicate<TestingEventBuses.Event> predicate,
        Function<TestingEventBuses.Event, String> messageGen
        ) throws InterruptedException, TimeoutException {
    TestingEventBuses.Event nextEvent = _events.pollFirst(_defaultTimeoutValue, _defaultTimeoutUnit);
    if (null == nextEvent) {
      throw new TimeoutException();
    }
    if (!predicate.apply(nextEvent)) {
      throw new AssertionError(messageGen.apply(nextEvent));
    }
    return nextEvent;
  }

  /**
   * Variation on {@link #assertNext(Predicate, Function)} with a constant message.
   */
  public TestingEventBuses.Event assertNext(final Predicate<TestingEventBuses.Event> predicate,
        final String message) throws InterruptedException, TimeoutException {
    return assertNext(predicate, new StaticMessage(message));
  }


  /** Similar to {@link #assertNext(Predicate, Function)} but predicate is on the value directly. */
  public <T> TestingEventBuses.Event assertNextValue(final Predicate<T> predicate ,
        Function<TestingEventBuses.Event, String> messageGen)
            throws InterruptedException, TimeoutException {
    return assertNext(new Predicate<TestingEventBuses.Event>() {
      @Override public boolean apply(@Nonnull Event input) {
        return predicate.apply(input.<T>getTypedValue());
      }
    }, messageGen);
  }

  /** Similar to {@link #assertNext(Predicate, String)} but predicate is on the value directly. */
  public <T> TestingEventBuses.Event assertNextValue(final Predicate<T> predicate, String message)
         throws InterruptedException, TimeoutException {
    return assertNext(new Predicate<TestingEventBuses.Event>() {
      @Override public boolean apply(@Nonnull Event input) {
        return predicate.apply(input.<T>getTypedValue());
      }
    }, message);
  }

  public <T> TestingEventBuses.Event assertNextValueEq(final T expected)
         throws InterruptedException, TimeoutException {
    return assertNextValue(Predicates.equalTo(expected),
        new Function<TestingEventBuses.Event, String>() {
          @Override public String apply(@Nonnull Event input) {
            return "Event value mismatch: " + input.getValue() + " != " + expected;
          }
    });
  }

  /**
   * Verify that all next several values are a permutation of the expected collection of event
   * values. This method allows for testing that certain values are produced in some random order.
   * Blocking assert.  */
  public <T> void assertNextValuesEq(final Collection<T> expected)
         throws InterruptedException, TimeoutException {
    final Set<T> remainingExpectedValues = new HashSet<>(expected);
    final Predicate<T> checkInRemainingAndRemove = new Predicate<T>() {
      @Override public boolean apply(@Nonnull T input) {
        if (! remainingExpectedValues.contains(input)) {
          return false;
        }
        remainingExpectedValues.remove(input);
        return true;
      }
    };

    while (remainingExpectedValues.size() > 0) {
      assertNextValue(checkInRemainingAndRemove,
          new Function<TestingEventBuses.Event, String>() {
            @Override public String apply(@Nonnull Event input) {
              return "Event value " + input.getValue() + " not in set " + remainingExpectedValues;
            }
      });
    }
  }

}
