// Copyright 2012 The Closure Library Authors. All Rights Reserved.
// Use of this source code is governed by the Apache License, Version 2.0.

goog.provide('goog.result.SimpleResultTest');
goog.setTestOnly('goog.result.SimpleResultTest');

goog.require('goog.Timer');
goog.require('goog.Promise');
goog.require('goog.Thenable');
goog.require('goog.result');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.recordFunction');
goog.require('goog.testing.jsunit');

var result, mockClock, resultCallback;
var resultCallback1;
var resultCallback2;

function setUpPage() {
  mockClock = new goog.testing.MockClock();
  mockClock.install();
}

function setUp() {
  mockClock.reset();
  resultCallback = new goog.testing.recordFunction();
  resultCallback1 = new goog.testing.recordFunction();
  resultCallback2 = new goog.testing.recordFunction();
  result = new goog.result.SimpleResult();
}

function tearDown() {
  resultCallback = resultCallback1 = resultCallback2 = result = null;
}

function tearDownPage() {
  mockClock.uninstall();
  goog.dispose(mockClock);
}

function testHandlersCalledOnSuccess() {
  result.wait(resultCallback1);
  result.wait(resultCallback2);

  assertEquals(goog.result.Result.State.PENDING, result.getState());
  assertEquals(0, resultCallback1.getCallCount());
  assertEquals(0, resultCallback2.getCallCount());

  result.setValue(2);

  assertEquals(goog.result.Result.State.SUCCESS, result.getState());
  assertEquals(2, result.getValue());
  assertEquals(1, resultCallback1.getCallCount());
  assertEquals(1, resultCallback2.getCallCount());

  var res1 = resultCallback1.popLastCall().getArgument(0);
  assertObjectEquals(result, res1);

  var res2 = resultCallback2.popLastCall().getArgument(0);
  assertObjectEquals(result, res2);
}

function testCustomHandlerScope() {
  result.wait(resultCallback1);
  var scope = {};
  result.wait(resultCallback2, scope);

  result.setValue(2);

  assertEquals(1, resultCallback1.getCallCount());
  assertEquals(1, resultCallback2.getCallCount());

  var this1 = resultCallback1.popLastCall().getThis();
  assertObjectEquals(goog.global, this1);

  var this2 = resultCallback2.popLastCall().getThis();
  assertObjectEquals(scope, this2);
}

function testHandlersCalledOnError() {
  result.wait(resultCallback1);
  result.wait(resultCallback2);
  assertEquals(goog.result.Result.State.PENDING, result.getState());

  var error = "Network Error";
  result.setError(error);

  assertEquals(goog.result.Result.State.ERROR, result.getState());
  assertEquals(error, result.getError());
  assertEquals(1, resultCallback1.getCallCount());
  assertEquals(1, resultCallback2.getCallCount());

  var res1 = resultCallback1.popLastCall().getArgument(0);
  assertObjectEquals(result, res1);
  var res2 = resultCallback2.popLastCall().getArgument(0);
  assertObjectEquals(result, res2);
}

function testAttachingHandlerOnSuccessfulResult() {
  result.setValue(2);
  assertEquals(goog.result.Result.State.SUCCESS, result.getState());
  assertEquals(2, result.getValue());
  // resultCallback should be called immediately on a resolved Result
  assertEquals(0, resultCallback.getCallCount());

  result.wait(resultCallback);

  assertEquals(1, resultCallback.getCallCount());
  var res = resultCallback.popLastCall().getArgument(0);
  assertEquals(result, res);
}

function testAttachingHandlerOnErrorResult() {
  var error = { code: -1, errorString: "Invalid JSON" };
  result.setError(error);
  assertEquals(goog.result.Result.State.ERROR, result.getState());
  assertEquals(error, result.getError());
  // resultCallback should be called immediately on a resolved Result
  assertEquals(0, resultCallback.getCallCount());

  result.wait(resultCallback);

  assertEquals(1, resultCallback.getCallCount());
  var res = resultCallback.popLastCall().getArgument(0);
  assertEquals(result, res);
}

function testExceptionThrownOnMultipleSuccessfulResolutionAttempts() {
  result.setValue(1);
  assertEquals(goog.result.Result.State.SUCCESS, result.getState());
  assertEquals(1, result.getValue());

  // Try to set the value again
  var e = assertThrows(goog.bind(result.setValue, result, 3));
  assertTrue(e instanceof goog.result.SimpleResult.StateError);
}

function testExceptionThrownOnMultipleErrorResolutionAttempts() {
  assertEquals(goog.result.Result.State.PENDING, result.getState());

  result.setError(5);

  assertEquals(goog.result.Result.State.ERROR, result.getState());
  assertEquals(5, result.getError());
  // Try to set error again
  var e = assertThrows(goog.bind(result.setError, result, 4));
  assertTrue(e instanceof goog.result.SimpleResult.StateError);
}

function testExceptionThrownOnSuccessThenErrorResolutionAttempt() {
  assertEquals(goog.result.Result.State.PENDING, result.getState());

  result.setValue(1);

  assertEquals(goog.result.Result.State.SUCCESS, result.getState());
  assertEquals(1, result.getValue());

  // Try to set error after setting value
  var e = assertThrows(goog.bind(result.setError, result, 3));
  assertTrue(e instanceof goog.result.SimpleResult.StateError);
}

function testExceptionThrownOnErrorThenSuccessResolutionAttempt() {
  assertEquals(goog.result.Result.State.PENDING, result.getState());

  var error = "fail";
  result.setError(error);

  assertEquals(goog.result.Result.State.ERROR, result.getState());
  assertEquals(error, result.getError());
  // Try to set value after setting error
  var e = assertThrows(goog.bind(result.setValue, result, 1));
  assertTrue(e instanceof goog.result.SimpleResult.StateError);
}

function testSuccessfulAsyncResolution() {
  result.wait(resultCallback);
  assertEquals(goog.result.Result.State.PENDING, result.getState());

  goog.Timer.callOnce(function() {
    result.setValue(1);
  });
  mockClock.tick();

  assertEquals(1, resultCallback.getCallCount());

  var res = resultCallback.popLastCall().getArgument(0);
  assertEquals(goog.result.Result.State.SUCCESS, res.getState());
  assertEquals(1, res.getValue());
}

function testErrorAsyncResolution() {
  result.wait(resultCallback);
  assertEquals(goog.result.Result.State.PENDING, result.getState());

  var error = 'Network failure';
  goog.Timer.callOnce(function() {
    result.setError(error);
  });
  mockClock.tick();

  assertEquals(1, resultCallback.getCallCount());
  var res = resultCallback.popLastCall().getArgument(0);
  assertEquals(goog.result.Result.State.ERROR, res.getState());
  assertEquals(error, res.getError());
}

function testCancelStateAndReturn() {
  assertFalse(result.isCanceled());
  var canceled = result.cancel();
  assertTrue(result.isCanceled());
  assertEquals(goog.result.Result.State.ERROR, result.getState());
  assertTrue(result.getError() instanceof goog.result.Result.CancelError);
  assertTrue(canceled);
}

function testErrorHandlersFireOnCancel() {
  result.wait(resultCallback);
  result.cancel();

  assertEquals(1, resultCallback.getCallCount());
  var lastCall = resultCallback.popLastCall();
  var res = lastCall.getArgument(0);
  assertEquals(goog.result.Result.State.ERROR, res.getState());
  assertTrue(res.getError() instanceof goog.result.Result.CancelError);
}

function testCancelAfterSetValue() {
  // cancel after setValue/setError => no-op
  result.wait(resultCallback);
  result.setValue(1);

  assertEquals(goog.result.Result.State.SUCCESS, result.getState());
  assertEquals(1, result.getValue());
  assertEquals(1, resultCallback.getCallCount());

  result.cancel();

  assertEquals(goog.result.Result.State.SUCCESS, result.getState());
  assertEquals(1, result.getValue());
  assertEquals(1, resultCallback.getCallCount());
}

function testSetValueAfterCancel() {
  // setValue/setError after cancel => no-op
  result.wait(resultCallback);

  result.cancel();
  assertTrue(result.isCanceled());
  assertTrue(result.getError() instanceof goog.result.Result.CancelError);

  result.setValue(1);
  assertTrue(result.isCanceled());
  assertTrue(result.getError() instanceof goog.result.Result.CancelError);

  result.setError(3);
  assertTrue(result.isCanceled());
  assertTrue(result.getError() instanceof goog.result.Result.CancelError);
}

function testFromResolvedPromise() {
  var promise = goog.Promise.resolve('resolved');
  result = goog.result.SimpleResult.fromPromise(promise);
  assertEquals(goog.result.Result.State.PENDING, result.getState());
  mockClock.tick();
  assertEquals(goog.result.Result.State.SUCCESS, result.getState());
  assertEquals('resolved', result.getValue());
  assertEquals(undefined, result.getError());
}

function testFromRejectedPromise() {
  var promise = goog.Promise.reject('rejected');
  result = goog.result.SimpleResult.fromPromise(promise);
  assertEquals(goog.result.Result.State.PENDING, result.getState());
  mockClock.tick();
  assertEquals(goog.result.Result.State.ERROR, result.getState());
  assertEquals(undefined, result.getValue());
  assertEquals('rejected', result.getError());
}

function testThen() {
  var value1, value2;
  result.then(function(val1) {
    return value1 = val1;
  }).then(function(val2) {
    value2 = val2;
  });
  result.setValue('done');
  assertUndefined(value1);
  assertUndefined(value2);
  mockClock.tick();
  assertEquals('done', value1);
  assertEquals('done', value2);
}

function testThen_reject() {
  var value, reason;
  result.then(
    function(v) { value = v; },
    function(r) { reason = r; });
  result.setError(new Error('oops'));
  assertUndefined(value);
  mockClock.tick();
  assertUndefined(value);
  assertEquals('oops', reason.message);
}

function testPromiseAll() {
  var promise = goog.Promise.resolve('promise');
  goog.Promise.all([result, promise]).then(function(values) {
    assertEquals(2, values.length);
    assertEquals('result', values[0]);
    assertEquals('promise', values[1]);
  });
  result.setValue('result');
  mockClock.tick();
}

function testResolvingPromiseBlocksResult() {
  var value;
  goog.Promise.resolve('promise').then(function(value) {
    result.setValue(value);
  });
  result.wait(function(r) {
    value = r.getValue();
  });
  assertUndefined(value);
  mockClock.tick();
  assertEquals('promise', value);
}

function testRejectingPromiseBlocksResult() {
  var err;
  goog.Promise.reject(new Error('oops')).then(
    undefined /* opt_onResolved */,
    function(reason) {
      result.setError(reason);
    });
  result.wait(function(r) {
    err = r.getError();
  });
  assertUndefined(err);
  mockClock.tick();
  assertEquals('oops', err.message);
}

function testPromiseFromCanceledResult() {
  var reason;
  result.cancel();
  result.then(
    undefined /* opt_onResolved */,
    function(r) {
      reason = r;
    });
  mockClock.tick();
  assertTrue(reason instanceof goog.Promise.CancellationError);
}

function testThenableInterface() {
  assertTrue(goog.Thenable.isImplementedBy(result));
}
