/*
 * 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.flume.channel;

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.flume.ChannelException;
import org.apache.flume.Event;
import org.apache.flume.event.EventBuilder;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;

import com.google.common.base.Preconditions;

public abstract class AbstractBasicChannelSemanticsTest {

  protected static List<Event> events;

  static {
    Event[] array = new Event[7];
    for (int i = 0; i < array.length; ++i) {
      array[i] = EventBuilder.withBody(("test event " + i).getBytes());
    }
    events = Collections.unmodifiableList(Arrays.asList(array));
  }

  protected ExecutorService executor = null;
  protected TestChannel channel = null;

  protected static class TestChannel extends BasicChannelSemantics {

    private Queue<Event> queue = new ArrayDeque<Event>();

    public enum Mode {
      NORMAL,
      THROW_ERROR,
      THROW_RUNTIME,
      THROW_CHANNEL,
      SLEEP
    }

    private Mode mode = Mode.NORMAL;
    private boolean lastTransactionCommitted = false;
    private boolean lastTransactionRolledBack = false;
    private boolean lastTransactionClosed = false;

    public Mode getMode() {
      return mode;
    }

    public void setMode(Mode mode) {
      this.mode = mode;
    }

    public boolean wasLastTransactionCommitted() {
      return lastTransactionCommitted;
    }

    public boolean wasLastTransactionRolledBack() {
      return lastTransactionRolledBack;
    }

    public boolean wasLastTransactionClosed() {
      return lastTransactionClosed;
    }

    @Override
    protected BasicTransactionSemantics createTransaction() {
      return new TestTransaction();
    }

    protected class TestTransaction extends BasicTransactionSemantics {

      protected void doMode() throws InterruptedException {
        switch (mode) {
          case THROW_ERROR:
            throw new TestError();
          case THROW_RUNTIME:
            throw new TestRuntimeException();
          case THROW_CHANNEL:
            throw new ChannelException("test");
          case SLEEP:
            Thread.sleep(300000);
            break;
        }
      }

      @Override
      protected void doBegin() throws InterruptedException {
        doMode();
      }

      @Override
      protected void doPut(Event event) throws InterruptedException {
        doMode();
        synchronized (queue) {
          queue.add(event);
        }
      }

      @Override
      protected Event doTake() throws InterruptedException {
        doMode();
        synchronized (queue) {
          return queue.poll();
        }
      }

      @Override
      protected void doCommit() throws InterruptedException {
        doMode();
        lastTransactionCommitted = true;
      }

      @Override
      protected void doRollback() throws InterruptedException {
        lastTransactionRolledBack = true;
        doMode();
      }

      @Override
      protected void doClose() {
        lastTransactionClosed = true;
        Preconditions.checkState(mode != TestChannel.Mode.SLEEP,
            "doClose() can't throw InterruptedException, so why SLEEP?");
        try {
          doMode();
        } catch (InterruptedException e) {
          Assert.fail();
        }
      }
    }
  }

  protected static class TestError extends Error {
    static final long serialVersionUID = -1;
  }

  protected static class TestRuntimeException extends RuntimeException {
    static final long serialVersionUID = -1;
  }

  protected void testException(Class<? extends Throwable> exceptionClass,
      Runnable test) {
    try {
      test.run();
      Assert.fail();
    } catch (Throwable e) {
      if (exceptionClass == InterruptedException.class
          && e instanceof ChannelException
          && e.getCause() instanceof InterruptedException) {
        Assert.assertTrue(Thread.interrupted());
      } else if (!exceptionClass.isInstance(e)) {
        throw new AssertionError(e);
      }
    }
  }

  protected void testIllegalArgument(Runnable test) {
    testException(IllegalArgumentException.class, test);
  }

  protected void testIllegalState(Runnable test) {
    testException(IllegalStateException.class, test);
  }

  protected void testWrongThread(final Runnable test) throws Exception {
    executor.submit(new Runnable() {
        @Override
        public void run() {
          testIllegalState(test);
        }
      }).get();
  }

  protected void testMode(TestChannel.Mode mode, Runnable test) {
    TestChannel.Mode oldMode = channel.getMode();
    try {
      channel.setMode(mode);
      test.run();
    } finally {
      channel.setMode(oldMode);
    }
  }

  protected void testException(TestChannel.Mode mode,
      final Class<? extends Throwable> exceptionClass, final Runnable test) {
    testMode(mode, new Runnable() {
        @Override
        public void run() {
          testException(exceptionClass, test);
        }
      });
  }

  protected void testError(Runnable test) {
    testException(TestChannel.Mode.THROW_ERROR, TestError.class, test);
  }

  protected void testRuntimeException(Runnable test) {
    testException(TestChannel.Mode.THROW_RUNTIME, TestRuntimeException.class,
        test);
  }

  protected void testChannelException(Runnable test) {
    testException(TestChannel.Mode.THROW_CHANNEL, ChannelException.class, test);
  }

  protected void testInterrupt(final Runnable test) {
    testMode(TestChannel.Mode.SLEEP, new Runnable() {
        @Override
        public void run() {
          testException(InterruptedException.class, new Runnable() {
              @Override
              public void run() {
                interruptTest(test);
              }
            });
        }
      });
  }

  protected void interruptTest(final Runnable test) {
    final Thread mainThread = Thread.currentThread();
    Future<?> future = executor.submit(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(500);
          } catch (InterruptedException e) {
          }
          mainThread.interrupt();
        }
      });
    test.run();
    try {
      future.get();
    } catch (Exception e) {
      throw new AssertionError(e);
    }
  }

  protected void testExceptions(Runnable test) throws Exception {
    testWrongThread(test);
    testBasicExceptions(test);
    testInterrupt(test);
  }

  protected void testBasicExceptions(Runnable test) throws Exception {
    testError(test);
    testRuntimeException(test);
    testChannelException(test);
  }

  @Before
  public void before() {
    Preconditions.checkState(channel == null, "test cleanup failed!");
    Preconditions.checkState(executor == null, "test cleanup failed!");
    channel = new TestChannel();
    executor = Executors.newCachedThreadPool();
  }

  @After
  public void after() {
    channel = null;
    executor.shutdown();
    executor = null;
  }
}
