

package org.waveprotocol.wave.concurrencycontrol.channel;


import junit.framework.TestCase;

import org.waveprotocol.wave.common.logging.AbstractLogger;
import org.waveprotocol.wave.common.logging.PrintLogger;
import org.waveprotocol.wave.concurrencycontrol.channel.ViewChannel.Listener;
import org.waveprotocol.wave.concurrencycontrol.common.ChannelException;
import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode;
import org.waveprotocol.wave.concurrencycontrol.testing.FakeWaveViewServiceUpdate;
import org.waveprotocol.wave.concurrencycontrol.testing.MockWaveViewService;
import org.waveprotocol.wave.model.id.IdFilter;
import org.waveprotocol.wave.model.id.IdFilters;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * Unit test for ViewChannelImpl.
 *
 * @author zdwang@google.com (David Wang)
 */

public class ViewChannelImplTest extends TestCase {

  /**
   * This is mock class to test that calls back from ViewChannel are as expected.
   */
  private static class MockViewChannelListener implements Listener {
    public enum MethodCall {
      ON_CONNECTED,
      ON_CLOSED,
      ON_EXCEPTION,
      ON_OPEN_FINISHED,
      ON_UPDATE,
      ON_SNAPSHOT
    }

    public class MethodCallContext {
      final MethodCall method;
      final WaveletId waveletId;
      final ObservableWaveletData snapshot;
      final List<TransformedWaveletDelta> deltas;
      final HashedVersion lastCommittedVersion;
      final HashedVersion currentSignedVersion;

      public MethodCallContext(MethodCall method) {
        this.method = method;
        this.waveletId = null;
        this.snapshot = null;
        this.deltas = null;
        this.lastCommittedVersion = null;
        this.currentSignedVersion = null;
      }

      public MethodCallContext(MethodCall method, WaveletId waveletId,
          ObservableWaveletData snapshot,
          HashedVersion lastCommittedVersion,
          HashedVersion currentSignedVersion) {
        this.method = method;
        this.waveletId = waveletId;
        this.snapshot = snapshot;
        this.deltas = null;
        this.lastCommittedVersion = lastCommittedVersion;
        this.currentSignedVersion = currentSignedVersion;
      }

      public MethodCallContext(MethodCall method, WaveletId waveletId,
          List<TransformedWaveletDelta> deltas,
          HashedVersion lastCommittedVersion,
          HashedVersion currentSignedVersion) {
        this.method = method;
        this.waveletId = waveletId;
        this.snapshot = null;
        this.deltas = deltas;
        this.lastCommittedVersion = lastCommittedVersion;
        this.currentSignedVersion = currentSignedVersion;
      }

      public MethodCall method() {
        return method;
      }
    }

    private final ArrayList<MethodCallContext> methodCalls = new ArrayList<MethodCallContext>();

    @Override
    public void onConnected() {
      methodCalls.add(new MethodCallContext(MethodCall.ON_CONNECTED));
    }

    @Override
    public void onClosed() {
      methodCalls.add(new MethodCallContext(MethodCall.ON_CLOSED));
    }

    @Override
    public void onException(ChannelException e) {
      methodCalls.add(new MethodCallContext(MethodCall.ON_EXCEPTION));
    }

    @Override
    public void onOpenFinished() {
      methodCalls.add(new MethodCallContext(MethodCall.ON_OPEN_FINISHED));
    }

    @Override
    public void onSnapshot(WaveletId waveletId, ObservableWaveletData wavelet,
        HashedVersion lastCommittedVersion, HashedVersion currentSignedVersion) {
      methodCalls.add(new MethodCallContext(MethodCall.ON_SNAPSHOT,
          waveletId, wavelet, lastCommittedVersion, currentSignedVersion));
    }

    @Override
    public void onUpdate(WaveletId waveletId, List<TransformedWaveletDelta> deltas,
        HashedVersion lastCommittedVersion, HashedVersion currentSignedVersion) {
      methodCalls.add(new MethodCallContext(MethodCall.ON_UPDATE,
          waveletId, deltas, lastCommittedVersion, currentSignedVersion));
    }

    public void expectedCall(MethodCall method) {
      assertEquals(method, methodCalls.get(0).method);
      methodCalls.remove(0);
    }

    public void expectedNothing() {
      assertEquals(0, methodCalls.size());
    }

    /**
     * We don't test for container message as it's not important.
     */
    public void expectedCall(MethodCall method, WaveletId waveletId) {
      assertFalse(methodCalls.isEmpty());
      MethodCallContext context = methodCalls.get(0);
      assertEquals(method, context.method);
      assertEquals(waveletId, context.waveletId);

      methodCalls.remove(0);
    }

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

  /**
   * This is mock class to test that calls back from ViewChannel are as expected.
   */
  private static class MockSubmitListener implements SubmitCallback {
    public enum MethodCall {
      ON_SUCCESS,
      ON_FAILURE
    }

    public class MethodCallContext {
      final MethodCall method;
      final int opsApplied;
      final HashedVersion version;
      final String error;

      public MethodCallContext(MethodCall method, int opsApplied,
          HashedVersion version) {
        this(method, null, opsApplied, version);
      }

      public MethodCallContext(MethodCall method, String reason, HashedVersion version) {
        this(method, reason, 0, version);
      }

      public MethodCallContext(MethodCall method, String reason) {
        this(method, reason, 0, HashedVersion.unsigned(0));
      }

      public MethodCallContext(MethodCall method, String reason, int opsApplied,
          HashedVersion version) {
        this.method = method;
        this.error = reason;
        this.version = version;
        this.opsApplied = opsApplied;
      }
    }

    ArrayList<MethodCallContext> methodCalls = new ArrayList<MethodCallContext>();

    @Override
    public void onSuccess(int opsApplied, HashedVersion version,
        ResponseCode responseCode, String errorMessage) {
      methodCalls.add(new MethodCallContext(
          MethodCall.ON_SUCCESS, errorMessage, opsApplied, version));
    }

    @Override
    public void onFailure(String reason) {
      methodCalls.add(new MethodCallContext(MethodCall.ON_FAILURE, reason));

    }

    public void expectedCall(MethodCall method, String error) {
      MethodCallContext context = methodCalls.get(0);
      assertEquals(method, context.method);
      assertEquals(error, context.error);

      methodCalls.remove(0);
    }

    public void expectedCall(MethodCall method, int opsApplied, HashedVersion version) {
      MethodCallContext context = methodCalls.get(0);
      assertEquals(method, context.method);
      assertEquals(opsApplied, context.opsApplied);
      assertEquals(version, context.version);

      methodCalls.remove(0);
    }
  }

  /**
   * Wavelet id to use in the tests.
   */
  private static final WaveletId WAVELET_ID = WaveletId.of("example.com", "waveletId_1");

  /**
   * Channel Id to be used in the tests.
   */
  private static final String CHANNEL_ID = "channelId_1";

  private static final AbstractLogger logger = new PrintLogger();

  //
  // Fields used in most or all tests.
  //

  private ViewChannelImpl channel;
  private MockViewChannelListener viewOpenListener;
  private MockWaveViewService waveViewService;

  @Override
  protected void setUp() {
    WaveId waveId = WaveId.of("example.com", "waveid");
    ViewChannelImpl.setMaxViewChannelsPerWave(Integer.MAX_VALUE);
    waveViewService = new MockWaveViewService();
    viewOpenListener = new MockViewChannelListener();
    channel = new ViewChannelImpl(waveId, waveViewService, logger);
  }

  /**
   * Opens the channel from the client side only.
   */
  private void halfOpen() {
    Map<WaveletId, List<HashedVersion>> knownWavelets = Collections.emptyMap();
    channel.open(viewOpenListener, IdFilters.ALL_IDS, knownWavelets);
  }

  /**
   * Simulates the server responding with the channel id.
   */
  private void respondWithChannelId() {
    waveViewService.lastOpen().callback.onUpdate(
        new FakeWaveViewServiceUpdate().setChannelId(CHANNEL_ID));
  }

  /**
   * Simulates the server responding with the open-finished marker.
   */
  private void respondWithMarker(boolean waveEmpty) {
    waveViewService.lastOpen().callback.onUpdate(
        new FakeWaveViewServiceUpdate().setMarker(waveEmpty));
  }

  /**
   * Simulates the server sending a streaming update.
   *
   * @param waveletId wavelet to update
   */
  private void respondWithEmptyUpdate(WaveletId waveletId) {
    waveViewService.lastOpen().callback.onUpdate(new FakeWaveViewServiceUpdate()
        .setWaveletId(waveletId)
        .setLastCommittedVersion(HashedVersion.unsigned(0))
        .addDelta(new TransformedWaveletDelta(null, HashedVersion.unsigned(0), 0L,
            Arrays.<WaveletOperation> asList())));
  }

  private void respondToSubmit(HashedVersion version, int opsApplied, String error,
      ResponseCode response) {
    waveViewService.lastSubmit().callback.onSuccess(version, opsApplied, error, response);
  }

  private void respondToSubmitWithFailure() {
    waveViewService.lastSubmit().callback.onFailure("WAVE_SERVER_ERROR");
  }

  /**
   * Opens the channel, and simulates the server responding with a channel and
   * a marker.
   */
  private void open() {
    halfOpen();
    respondWithChannelId();
    respondWithMarker(false);
  }

  private void close() {
    channel.close();
  }

  private void terminateOpenRpcWithSuccess() {
    waveViewService.lastOpen().callback.onSuccess(null);
  }

  private void terminateOpenRpcWithError() {
    waveViewService.lastOpen().callback.onSuccess("Server error for testing");
  }

  private void terminateOpenRpcWithFailure(String status) {
    waveViewService.lastOpen().callback.onFailure(status);
  }

  private static WaveletDelta emptyDelta() {
    return new WaveletDelta(null, null, Arrays.<WaveletOperation> asList());
  }

  /**
   * Test that when everything is ok, we can connect, submit, create wavelet and disconnect.
   * This is not supposed to be a thorough test.
   */
  public void testSunnyDayScenario() {
    open();

    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CONNECTED);
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_OPEN_FINISHED);

    // pretend an update with update wavelet. Note we don't add any data in the wavelet
    // because it's not really relevant for the test.
    respondWithEmptyUpdate(WAVELET_ID);
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_UPDATE, WAVELET_ID);

    // Submit a delta and check that we have the right channel id remembered.
    MockSubmitListener submitListener = new MockSubmitListener();
    channel.submitDelta(WAVELET_ID, emptyDelta(), submitListener);
    assertEquals(1, waveViewService.submits.size());
    assertEquals(CHANNEL_ID, waveViewService.lastSubmit().channelId);

    // Return a success message on the submitted delta
    byte[] hash = new byte[] {1, 2, 3, 4};
    respondToSubmit(HashedVersion.of(2, hash), 1, null, ResponseCode.OK);
    submitListener.expectedCall(MockSubmitListener.MethodCall.ON_SUCCESS, 1,
        HashedVersion.of(2, hash));

    // Check disconnect
    channel.close();
    assertEquals(1, waveViewService.closes.size());
    waveViewService.lastClose().callback.onSuccess();
  }

  /**
   * Tests that {@link ViewChannelImpl#open(Listener, IdFilter, Map)}
   * synchronously calls the ViewOpen rpc on its wave service.
   */
  public void testOpenIssuesViewOpenRpc() {
    open();
    assertEquals(1, waveViewService.opens.size());
  }

  /**
   * Tests that a channel fails if it does not receive a channel id in the first
   * message.
   */
  public void testInitialUpdateWithoutAChannelIdFails() {
    halfOpen();

    // Receive an update with no channel id.
    waveViewService.lastOpen().callback.onUpdate(new FakeWaveViewServiceUpdate());
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_EXCEPTION);
  }

  /**
   * Tests that a channel fails if it receives a message before it is opened.
   */
  public void testMessageBeforeOpenThrowsException() {
    try {
      channel.onUpdate(new FakeWaveViewServiceUpdate());
      fail("Should not be able to receive update without open call");
    } catch (IllegalStateException expected) {
      // Expected.
    }
  }

  public void testSuccessBeforeOpenThrowsException() {
    try {
      channel.onSuccess("for testing");
      fail("Should not be able to receive onSuccess without open call");
    } catch (IllegalStateException expected) {
      // Expected.
    }
  }

  public void testFailureBeforeOpenThrowsException() {
    try {
      channel.onFailure("for testing");
      fail("Should not be able to receive onFailure without open call");
    } catch (IllegalStateException expected) {
      // Expected.
    }
  }

  /**
   * Tests a channel fails if it receives success before channel id.
   */
  public void testSuccessBeforeChannelIdFails() {
    halfOpen();
    channel.onSuccess("for testing");
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_EXCEPTION);
  }

  /**
   * Tests a channel closes if it receives failure before channel id.
   */
  public void testFailureBeforeChannelIdClosesChannel() {
    halfOpen();
    channel.onFailure("for testing");
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED);
  }

  /**
   * Tests that an update with the end-marker triggers the open-finished
   * callback.
   */
  public void testMarkerTriggersOpenFinished() {
    halfOpen();
    respondWithChannelId();
    viewOpenListener.clear();
    respondWithMarker(false);
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_OPEN_FINISHED);
  }

  /**
   * Tests that an update with the end-marker triggers the open-finished
   * callback.
   */
  public void testChannelIdTriggersConnectCallback() {
    halfOpen();
    respondWithChannelId();
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CONNECTED);
  }

  /**
   * Tests that updates that arrive before the end-marker are passed on as
   * updates, and that when the end-marker eventually arrives, open-finished
   * is triggered then.
   */
  public void testUpdatesBeforeOpenFinishedStillTriggersOpenFinished() {
    halfOpen();
    respondWithChannelId();

    viewOpenListener.clear();
    respondWithEmptyUpdate(WAVELET_ID);
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_UPDATE, WAVELET_ID);

    respondWithMarker(false);
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_OPEN_FINISHED);
  }

  /**
   * Tests that closing the channel after a full open issues a ViewClose rpc.
   */
  public void testCloseAfterChannelIdCallsCloseRpc() {
    open();
    close();
    assertEquals(1, waveViewService.closes.size());
    waveViewService.lastClose().callback.onSuccess();
  }

  /**
   * Tests that closing the channel before the server has responded with a
   * channel id does not issue a ViewClose rpc.
   */
  public void testCloseWithoutChannelIdDoesNotCallCloseRpc() {
    halfOpen();
    close();
    assertEquals(0, waveViewService.closes.size());
  }

  /**
   * Tests that closing the channel before the server has responded with a
   * channel id causes a ViewClose rpc to be sent as soon as a channel id
   * arrives later.
   */
  public void testCloseWithoutChannelIdCallsCloseRpcIfChannelIdArrives() {
    halfOpen();
    close();
    assertEquals(0, waveViewService.closes.size());
    respondWithChannelId();
    // Have got channel id, so we should now have issued a close.
    assertEquals(1, waveViewService.closes.size());
    waveViewService.lastClose().callback.onSuccess();
  }

  /**
   * Tests that closing the channel prevents updates that arrive later from
   * being passed to the channel listener.
   */
  public void testCloseMasksFutureUpdatesFromOpenListener() {
    open();
    close();
    viewOpenListener.clear();
    respondWithEmptyUpdate(WAVELET_ID);
    viewOpenListener.expectedNothing();
  }

  public void testCloseTriggersCloseCallback() {
    open();
    viewOpenListener.clear();
    close();
    // The underlying service is expected to have the following behaviour.
    terminateOpenRpcWithSuccess();
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED);
  }

  public void testOpenRpcTerminationAfterUpdatesAndCloseTriggersCloseCallback() {
    open();
    respondWithEmptyUpdate(WAVELET_ID);
    viewOpenListener.clear();
    close();

    // The service should cause the open rpc to terminate successfully.
    terminateOpenRpcWithSuccess();
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED);
  }

  public void testOpenRpcTerminationWithoutCloseTriggersFailureAndClose() {
    open();
    viewOpenListener.clear();
    terminateOpenRpcWithSuccess();
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED);
  }

  public void testOpenRpcOnFailureAndThenTerminationWithoutCloseTriggersOneFailureAndClose() {
    open();
    viewOpenListener.clear();
    terminateOpenRpcWithFailure("WAVE_SERVER_ERROR");
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED);
    terminateOpenRpcWithError();
    viewOpenListener.expectedNothing();
  }

  public void testMultipleClose() {
    open();
    viewOpenListener.clear();
    close();
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED);
    close();
    viewOpenListener.expectedNothing();
  }

  public void testCloseWithoutOpen() {
    close();
    viewOpenListener.expectedNothing();
  }

  public void testOpenAfterOpenThrowsException() {
    open();
    try {
      channel.open(null, null, null);
      fail("Should not be able to open again after open is called.");
    } catch (RuntimeException ex) {
      // Expected error.
    }
  }

  public void testSubmitDeltaWithErrorMessage() {
    open();

    // submit a delta
    MockSubmitListener submitListener = new MockSubmitListener();
    channel.submitDelta(WAVELET_ID, emptyDelta(), submitListener);

    // Success with error the submit delta.
    String errorMessage = "Bad things happened on the server";
    respondToSubmit(HashedVersion.of(2, new byte[] {1, 2, 3, 4}), 1, errorMessage, ResponseCode.OK);
    submitListener.expectedCall(MockSubmitListener.MethodCall.ON_SUCCESS, errorMessage);
  }

  /**
   * Tests that a failure of a ViewSubmit rpc causes the channel to call the
   * failure callback registered on delta submission.
   */
  public void testDeltaSubmissionFailureCallsSubmissionFailureCallback() {
    open();

    // Submit a delta.
    MockSubmitListener listener = new MockSubmitListener();
    channel.submitDelta(WAVELET_ID, emptyDelta(), listener);

    // Fail the submit delta, and expect that the submit listened got the error.
    respondToSubmitWithFailure();
    listener.expectedCall(MockSubmitListener.MethodCall.ON_FAILURE, "WAVE_SERVER_ERROR");
  }

  public void testSubmitDeltaOnClosedChannelThrowsIllegalStateException() {
    open();
    close();

    // submit a delta should fail
    try {
      channel.submitDelta(WAVELET_ID, emptyDelta(), new MockSubmitListener());
      fail("Should not be able to submit on a closed channel");
    } catch (IllegalStateException ex) {
      // expect exception
    }
  }

  public void testOpenAfterCloseThrowsIllegalStateException() {
    open();
    close();

    // Opening the channel again should fail.
    try {
      halfOpen();
      fail("Should not be able to open a closed channel");
    } catch (IllegalStateException ex) {
      // expect exception
    }
  }

  public void testFailedOpenCallsListenerFailure() {
    halfOpen();
    // fail the open
    terminateOpenRpcWithFailure("WAVE_SERVER_ERROR");
    viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED);
  }

  public void testCannotCreateTooManyChannels() {
    ViewChannelImpl.setMaxViewChannelsPerWave(4);
    WaveId waveId = WaveId.of("example.com", "toomanywaveid");
    for  (int i = 0; i < 4; i++) {
      channel = new ViewChannelImpl(waveId, waveViewService, logger);
    }
    try {
      channel = new ViewChannelImpl(waveId, waveViewService, logger);
      fail("Should not be allowed to create any more view channels");
    } catch (IllegalStateException ex) {
      // expected
    }
  }

  public void testClosingOneChannelMakesRoomForAnother() {
    WaveId waveId = WaveId.of("example.com", "makeroomwaveid");
    ViewChannelImpl.setMaxViewChannelsPerWave(4);
    for (int i = 0; i < 4; i++) {
      channel = new ViewChannelImpl(waveId, waveViewService, logger);
    }
    channel.close(); // Close the last channel, making room for another.
    channel = new ViewChannelImpl(waveId, waveViewService, logger);
  }
}
