// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Test fixture for utility.js.
 * @constructor
 * @extends {testing.Test}
 */
function GoogleNowUtilityUnitTest () {
  testing.Test.call(this);
}

GoogleNowUtilityUnitTest.prototype = {
  __proto__: testing.Test.prototype,

  /** @override */
  extraLibraries: [
    'common_test_util.js',
    'utility_test_util.js',
    'utility.js'
  ]
};

TEST_F('GoogleNowUtilityUnitTest', 'SendErrorReport1', function() {
  // Test sending report for an error with a message that can be sent to server.

  // Setup and expectations.
  var testStack = 'Error: TEST ERROR MESSAGE\n    ' +
      'at buildErrorWithMessageForServer ' +
      '(chrome-extension://ext_id/utility.js:29:15)\n' +
      '    at <anonymous>:2:16\n    ' +
      'at Object.InjectedScript._evaluateOn (<anonymous>:580:39)\n    ' +
      'at Object.InjectedScript._evaluateAndWrap (<anonymous>:539:52)\n    ' +
      'at Object.InjectedScript.evaluate (<anonymous>:458:21)';

  var testError = {
    canSendMessageToServer: true,
    stack: testStack,
    name: 'TEST ERROR NAME',
    message: 'TEST ERROR MESSAGE'
  };

  var testIdentityToken = 'test identity token';

  this.makeAndRegisterMockGlobals(['buildServerRequest']);
  this.makeMockLocalFunctions(['sendRequest', 'setRequestHeader']);
  this.makeAndRegisterMockApis(['chrome.identity.getAuthToken']);

  var mockRequest = {
    send: this.mockLocalFunctions.functions().sendRequest,
    setRequestHeader: this.mockLocalFunctions.functions().setRequestHeader
  };

  var expectedRequestObject = {
    message: 'TEST ERROR NAME: TEST ERROR MESSAGE',
    file: '//ext_id/utility.js',
    line: '29',
    trace: 'Error: TEST ERROR MESSAGE\n    ' +
           'at buildErrorWithMessageForServer (chrome-extension://ext_id/util' +
           'ity.js:29:15)\n    ' +
           'at <anonymous>:2:16\n    ' +
           'at Object.InjectedScript._evaluateOn (<anonymous>:580:39)\n    ' +
           'at Object.InjectedScript._evaluateAndWrap (<anonymous>:539:52)\n' +
           '    at Object.InjectedScript.evaluate (<anonymous>:458:21)'
  };

  this.mockGlobals.expects(once()).
      buildServerRequest('POST', 'jserrors', 'application/json').
      will(returnValue(mockRequest));

  var chromeIdentityGetAuthTokenSavedArgs = new SaveMockArguments();
  this.mockApis.expects(once()).
      chrome_identity_getAuthToken(
          chromeIdentityGetAuthTokenSavedArgs.match(
              eqJSON({interactive: false})),
          chromeIdentityGetAuthTokenSavedArgs.match(ANYTHING)).
      will(invokeCallback(
          chromeIdentityGetAuthTokenSavedArgs,
          1,
          testIdentityToken));

  this.mockLocalFunctions.expects(once()).setRequestHeader(
      'Authorization', 'Bearer test identity token');
  this.mockLocalFunctions.expects(once()).sendRequest(
      JSON.stringify(expectedRequestObject));

  // Invoking the tested function.
  sendErrorReport(testError);
});

TEST_F('GoogleNowUtilityUnitTest', 'SendErrorReport2', function() {
  // Test sending report for an error with a message that should not be sent to
  // server, with an error generated in an anonymous function.

  // Setup and expectations.
  var testStack = 'TypeError: Property \'processPendingDismissals\' of ' +
      'object [object Object] is not a function\n    ' +
      'at chrome-extension://ext_id/background.js:444:11\n    ' +
      'at chrome-extension://ext_id/utility.js:509:7';

  var testError = {
    stack: testStack,
    name: 'TypeError'
  };

  var testIdentityToken = 'test identity token';

  this.makeAndRegisterMockGlobals(['buildServerRequest']);
  this.makeMockLocalFunctions(['sendRequest', 'setRequestHeader']);
  this.makeAndRegisterMockApis(['chrome.identity.getAuthToken']);

  var mockRequest = {
    send: this.mockLocalFunctions.functions().sendRequest,
    setRequestHeader: this.mockLocalFunctions.functions().setRequestHeader
  };

  var expectedRequestObject = {
    message: 'TypeError',
    file: '//ext_id/background.js',
    line: '444',
    trace: '(message removed)\n    ' +
           'at chrome-extension://ext_id/background.js:444:11\n    ' +
           'at chrome-extension://ext_id/utility.js:509:7'
  };

  this.mockGlobals.expects(once()).
      buildServerRequest('POST', 'jserrors', 'application/json').
      will(returnValue(mockRequest));

  var chromeIdentityGetAuthTokenSavedArgs = new SaveMockArguments();
  this.mockApis.expects(once()).
      chrome_identity_getAuthToken(
          chromeIdentityGetAuthTokenSavedArgs.match(
              eqJSON({interactive: false})),
          chromeIdentityGetAuthTokenSavedArgs.match(ANYTHING)).
      will(invokeCallback(
          chromeIdentityGetAuthTokenSavedArgs,
          1,
          testIdentityToken));

  this.mockLocalFunctions.expects(once()).setRequestHeader(
      'Authorization', 'Bearer test identity token');
  this.mockLocalFunctions.expects(once()).sendRequest(
      JSON.stringify(expectedRequestObject));

  // Invoking the tested function.
  sendErrorReport(testError);
});

TEST_F('GoogleNowUtilityUnitTest', 'WrapperCheckInWrappedCallback', function() {
  // Test generating an error when calling wrapper.checkInWrappedCallback from a
  // non-instrumented code.

  // Setup and expectations.
  var testError = {
    testField: 'TEST VALUE'
  };

  this.makeAndRegisterMockGlobals([
    'buildErrorWithMessageForServer',
    'reportError'
  ]);

  this.mockGlobals.expects(once()).
      buildErrorWithMessageForServer('Not in instrumented callback').
      will(returnValue(testError));
  this.mockGlobals.expects(once()).
      reportError(eqJSON(testError));

  // Invoking the tested function.
  wrapper.checkInWrappedCallback();
});

TEST_F('GoogleNowUtilityUnitTest', 'WrapperWrapCallbackEvent', function() {
  // Tests wrapping event handler and that the handler code counts as an
  // instrumented callback.

  // Setup.
  var testError = {
    testField: 'TEST VALUE'
  };

  this.makeAndRegisterMockGlobals([
    'buildErrorWithMessageForServer',
    'reportError'
  ]);
  var onSuspendHandlerContainer = getMockHandlerContainer('runtime.onSuspend');

  this.makeMockLocalFunctions(['callback']);

  // Step 1. Wrap a callback.
  var wrappedCallback =
    wrapper.wrapCallback(this.mockLocalFunctions.functions().callback, true);
  Mock4JS.verifyAllMocks();

  // Step 2. Invoke wrapped callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).
      callback('test string', 239).
      will(callFunction(function() {
        wrapper.checkInWrappedCallback(); // it should succeed
      }));

  // Invoking tested function.
  wrappedCallback('test string', 239);
  Mock4JS.verifyAllMocks();

  // Step 3. Check that after the callback we are again in non-instrumented
  // code.
  // Expectations.
  this.mockGlobals.expects(once()).
      buildErrorWithMessageForServer('Not in instrumented callback').
      will(returnValue(testError));
  this.mockGlobals.expects(once()).
      reportError(eqJSON(testError));

  // Invocation.
  wrapper.checkInWrappedCallback();

  // Step 4. Check that there won't be errors whe the page unloads.
  assertEquals(1, onSuspendHandlerContainer.length);
  onSuspendHandlerContainer[0]();
});

TEST_F('GoogleNowUtilityUnitTest', 'WrapperWrapCallbackPlugin', function() {
  // Tests calling plugin's prologue and epilogue.

  // Setup.
  this.makeMockLocalFunctions([
    'callback',
    'pluginFactory',
    'prologue',
    'epilogue'
  ]);

  // Step 1. Register plugin factory.
  wrapper.registerWrapperPluginFactory(
      this.mockLocalFunctions.functions().pluginFactory);
  Mock4JS.verifyAllMocks();

  // Step 2. Wrap a callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).
      pluginFactory().
      will(returnValue({
        prologue: this.mockLocalFunctions.functions().prologue,
        epilogue: this.mockLocalFunctions.functions().epilogue
      }));

  // Invoking tested function.
  var wrappedCallback =
    wrapper.wrapCallback(this.mockLocalFunctions.functions().callback, true);
  Mock4JS.verifyAllMocks();

  // Step 3. Call the wrapped callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).prologue();
  this.mockLocalFunctions.expects(once()).callback();
  this.mockLocalFunctions.expects(once()).epilogue();

  // Invoking wrapped callback.
  wrappedCallback();
});

TEST_F('GoogleNowUtilityUnitTest', 'WrapperWrapCallbackCatchError', function() {
  // Tests catching and sending errors by a wrapped callback.

  // Setup.
  this.makeAndRegisterMockGlobals([
    'reportError'
  ]);
  this.makeMockLocalFunctions(['callback']);

  // Step 1. Wrap a callback.
  var wrappedCallback =
    wrapper.wrapCallback(this.mockLocalFunctions.functions().callback, true);
  Mock4JS.verifyAllMocks();

  // Step 2. Invoke wrapped callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).
      callback().
      will(callFunction(function() {
        undefined.x = 5;
      }));
  this.mockGlobals.expects(once()).
      reportError(
          eqToString('TypeError: Cannot set property \'x\' of undefined'));

  // Invoking tested function.
  wrappedCallback();
});

TEST_F('GoogleNowUtilityUnitTest',
       'WrapperInstrumentChromeApiFunction',
       function() {
  // Tests wrapper.instrumentChromeApiFunction().

  // Setup.
  this.makeMockLocalFunctions([
    'apiFunction1',
    'apiFunction2',
    'callback1',
    'callback2',
    'pluginFactory',
    'prologue',
    'epilogue'
  ]);
  chrome.testApi = {
      addListener: this.mockLocalFunctions.functions().apiFunction1
  };

  // Step 1. Instrument the listener.
  wrapper.instrumentChromeApiFunction('testApi.addListener', 1);
  Mock4JS.verifyAllMocks();

  // Step 2. Invoke the instrumented API call.
  // Expectations.
  var function1SavedArgs = new SaveMockArguments();
  this.mockLocalFunctions.expects(once()).
      apiFunction1(
          function1SavedArgs.match(eq(239)),
          function1SavedArgs.match(ANYTHING));

  // Invocation.
  instrumented.testApi.addListener(
      239, this.mockLocalFunctions.functions().callback1);
  Mock4JS.verifyAllMocks();

  // Step 3. Invoke the callback that was passed by the instrumented function
  // to the original one.
  // Expectations.
  this.mockLocalFunctions.expects(once()).callback1(237);

  // Invocation.
  function1SavedArgs.arguments[1](237);
  Mock4JS.verifyAllMocks();

  // Step 4. Register plugin factory.
  wrapper.registerWrapperPluginFactory(
      this.mockLocalFunctions.functions().pluginFactory);
  Mock4JS.verifyAllMocks();

  // Step 5. Bind the API to another function.
  chrome.testApi.addListener = this.mockLocalFunctions.functions().apiFunction2;

  // Step 6. Invoke the API with another callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).
      pluginFactory().
      will(returnValue({
        prologue: this.mockLocalFunctions.functions().prologue,
        epilogue: this.mockLocalFunctions.functions().epilogue
      }));
  var function2SavedArgs = new SaveMockArguments();
  this.mockLocalFunctions.expects(once()).
      apiFunction2(
          function2SavedArgs.match(eq(239)),
          function2SavedArgs.match(ANYTHING));

  // Invocation.
  instrumented.testApi.addListener(
      239, this.mockLocalFunctions.functions().callback2);
  Mock4JS.verifyAllMocks();

  // Step 7. Invoke the callback that was passed by the instrumented function
  // to the original one.
  // Expectations.
  this.mockLocalFunctions.expects(once()).prologue();
  this.mockLocalFunctions.expects(once()).callback2(237);
  this.mockLocalFunctions.expects(once()).epilogue();

  // Invocation.
  function2SavedArgs.arguments[1](237);
});

TEST_F('GoogleNowUtilityUnitTest', 'WrapperOnSuspendListenerFail', function() {
  // Tests that upon unloading event page, we get an error if there are pending
  // required callbacks.

  // Setup.
  var testError = {
    testField: 'TEST VALUE'
  };
  this.makeAndRegisterMockGlobals([
    'buildErrorWithMessageForServer',
    'reportError'
  ]);
  this.makeMockLocalFunctions(['listener', 'callback']);
  var onSuspendHandlerContainer = getMockHandlerContainer('runtime.onSuspend');

  // Step 1. Wrap event listener.
  var wrappedListener =
    wrapper.wrapCallback(this.mockLocalFunctions.functions().listener, true);
  Mock4JS.verifyAllMocks();

  // Step 2. Invoke event listener, which will wrap a required callback.
  // Setup and expectations.
  var wrappedCallback;
  var testFixture = this;
  this.mockLocalFunctions.expects(once()).
      listener().
      will(callFunction(function() {
        wrappedCallback = wrapper.wrapCallback(
            testFixture.mockLocalFunctions.functions().callback);
      }));

  // Invocation.
  wrappedListener();
  Mock4JS.verifyAllMocks();

  // Step 3. Fire runtime.onSuspend event.
  // Expectations.
  this.mockGlobals.expects(once()).
      buildErrorWithMessageForServer(stringContains(
          'ASSERT: Pending callbacks when unloading event page')).
      will(returnValue(testError));
  this.mockGlobals.expects(once()).
      reportError(eqJSON(testError));

  // Invocation.
  assertEquals(1, onSuspendHandlerContainer.length);
  onSuspendHandlerContainer[0]();
});

TEST_F('GoogleNowUtilityUnitTest',
       'WrapperOnSuspendListenerSuccess',
       function() {
  // Tests that upon unloading event page, we don't get an error if there are no
  // pending required callbacks.

  // Setup.
  this.makeMockLocalFunctions(['listener', 'callback']);
  var onSuspendHandlerContainer = getMockHandlerContainer('runtime.onSuspend');

  // Step 1. Wrap event listener.
  var wrappedListener =
    wrapper.wrapCallback(this.mockLocalFunctions.functions().listener, true);
  Mock4JS.verifyAllMocks();

  // Step 2. Invoke event listener, which will wrap a required callback.
  // Setup and expectations.
  var wrappedCallback;
  var testFixture = this;
  this.mockLocalFunctions.expects(once()).
      listener().
      will(callFunction(function() {
        wrappedCallback = wrapper.wrapCallback(
            testFixture.mockLocalFunctions.functions().callback);
      }));

  // Invocation.
  wrappedListener();
  Mock4JS.verifyAllMocks();

  // Step 3. Call wrapped callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).callback();

  // Invocation.
  wrappedCallback();

  // Step 4. Fire runtime.onSuspend event.
  assertEquals(1, onSuspendHandlerContainer.length);
  onSuspendHandlerContainer[0]();
});

var taskNameA = 'TASK A';
var taskNameB = 'TASK B';
var taskNameC = 'TASK C';

function areTasksConflicting(newTaskName, scheduledTaskName) {
  // Task B is conflicting with Task A. This means that if Task B is added when
  // Task A is running, Task B will be ignored (but not vice versa). No other
  // pair is conflicting.
  return newTaskName == taskNameB && scheduledTaskName == taskNameA;
}

function setUpTaskManagerTest(fixture) {
  fixture.makeAndRegisterMockApis([
    'wrapper.checkInWrappedCallback',
    'wrapper.registerWrapperPluginFactory',
    'wrapper.debugGetStateString'
  ]);
  fixture.makeMockLocalFunctions(['task1', 'task2', 'task3']);
  fixture.makeAndRegisterMockGlobals(['reportError']);

  fixture.mockApis.stubs().wrapper_checkInWrappedCallback();
  fixture.mockApis.stubs().wrapper_debugGetStateString().
      will(returnValue('testWrapperDebugState'));

  var registerWrapperPluginFactorySavedArgs = new SaveMockArguments();
  fixture.mockApis.expects(once()).wrapper_registerWrapperPluginFactory(
      registerWrapperPluginFactorySavedArgs.match(ANYTHING));
  var tasks = buildTaskManager(areTasksConflicting);
  Mock4JS.verifyAllMocks();

  return {
    tasks: tasks,
    pluginFactory: registerWrapperPluginFactorySavedArgs.arguments[0]
  };
}

TEST_F('GoogleNowUtilityUnitTest', 'TaskManager2Sequential', function() {
  // Tests that 2 tasks get successfully executed consecutively, even if the
  // second one conflicts with the first.

  // Setup.
  var test = setUpTaskManagerTest(this);

  // Step 1. Add 1st task that doesn't create pending callbacks.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1();
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Add 2nd task.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task2();
  // Invocation.
  test.tasks.add(taskNameB, this.mockLocalFunctions.functions().task2);
});

TEST_F('GoogleNowUtilityUnitTest', 'TaskManagerConflicting', function() {
  // Tests that adding a task while a conflicting task is being executed causes
  // the second one to be ignored.

  // Setup.
  var test = setUpTaskManagerTest(this);
  var task1PluginInstance;

  // Step 1. Add 1st task that creates a pending callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1().
      will(callFunction(function() {
        task1PluginInstance = test.pluginFactory();
      }));
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Add 2nd task. Since it conflicts with currently running task1
  // (see areTasksConflicting), it should be ignored.
  test.tasks.add(taskNameB, this.mockLocalFunctions.functions().task2);
  Mock4JS.verifyAllMocks();

  // Step 3. Enter the callback of task1.
  task1PluginInstance.prologue();
  Mock4JS.verifyAllMocks();

  // Step 4. Leave the callback of task1.
  task1PluginInstance.epilogue();
});

TEST_F('GoogleNowUtilityUnitTest', 'TaskManagerNestedTaskEnqueue', function() {
  // Tests that adding a task while a non-conflicting task is being executed
  // causes the second one to be executed after the first one completes.

  // Setup.
  var test = setUpTaskManagerTest(this);
  var task1PluginInstance;

  // Step 1. Add 1st task that creates a pending callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1().
      will(callFunction(function() {
        task1PluginInstance = test.pluginFactory();
      }));
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Add 2nd task. Since it doesn't conflict with currently running
  // task1 (see areTasksConflicting), it should not be ignored.
  test.tasks.add(taskNameC, this.mockLocalFunctions.functions().task2);
  Mock4JS.verifyAllMocks();

  // Step 3. Enter the callback of task1.
  task1PluginInstance.prologue();
  Mock4JS.verifyAllMocks();

  // Step 4. Leave the callback of task1.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task2();
  // Invocation.
  task1PluginInstance.epilogue();
});

TEST_F('GoogleNowUtilityUnitTest', 'TaskManagerBranching', function() {
  // Tests that task manager correctly detects completion of tasks that create
  // branching chains of callbacks (in this test, task1 creates pending
  // callbacks 1 and 2, and callback 1 creates pending callback 3).

  // Setup.
  var test = setUpTaskManagerTest(this);
  var task1PluginInstance1, task1PluginInstance2, task1PluginInstance3;

  // Step 1. Add 1st task that creates a 2 pending callbacks.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1().
      will(callFunction(function() {
        task1PluginInstance1 = test.pluginFactory();
        task1PluginInstance2 = test.pluginFactory();
      }));
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Add 2nd task, which is not conflicting (see areTasksConflicting)
  // with task1.
  test.tasks.add(taskNameC, this.mockLocalFunctions.functions().task2);
  Mock4JS.verifyAllMocks();

  // Step 3. Enter callback 1, create pending callback 3, exit callback 1.
  // Enter/exit callback 2. Enter callback 3.
  task1PluginInstance1.prologue();
  task1PluginInstance3 = test.pluginFactory();
  task1PluginInstance1.epilogue();
  task1PluginInstance2.prologue();
  task1PluginInstance2.epilogue();
  task1PluginInstance3.prologue();
  Mock4JS.verifyAllMocks();

  // Step 4. Leave 3rd callback of task1. Now task1 is complete, and task2
  // should start.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task2();
  // Invocation.
  task1PluginInstance3.epilogue();
});

TEST_F('GoogleNowUtilityUnitTest', 'TaskManagerSuspendError', function() {
  // Tests that task manager's onSuspend method reports an error if there are
  // pending tasks.

  // Setup.
  var test = setUpTaskManagerTest(this);
  var onSuspendHandlerContainer = getMockHandlerContainer('runtime.onSuspend');

  // Step 1. Add a task that creates a pending callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1().
      will(callFunction(function() {
        test.pluginFactory();
      }));
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Invoke onSuspend event of the task manager.
  // Setup and expectations. The 2 callbacks in onSuspendHandlerContainer are
  // from the wrapper and the task manager.
  assertEquals(2, onSuspendHandlerContainer.length);
  this.mockGlobals.expects(once()).reportError(eqToString(
      'Error: ASSERT: Incomplete task when unloading event page,' +
      ' queue = [{"name":"TASK A"}], testWrapperDebugState'));
  // Invocation.
  onSuspendHandlerContainer[1]();
});

TEST_F('GoogleNowUtilityUnitTest', 'TaskManagerSuspendSuccess', function() {
  // Tests that task manager's onSuspend method does not report an error if all
  // tasks completed.

  // Setup.
  var test = setUpTaskManagerTest(this);
  var onSuspendHandlerContainer = getMockHandlerContainer('runtime.onSuspend');
  var task1PluginInstance;

  // Step 1. Add a task that creates a pending callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1().
      will(callFunction(function() {
        task1PluginInstance = test.pluginFactory();
      }));
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Invoke task's callback and the onSuspend event of the task manager.
  // The 2 callbacks in onSuspendHandlerContainer are from the wrapper and the
  // task manager.
  task1PluginInstance.prologue();
  task1PluginInstance.epilogue();
  onSuspendHandlerContainer[1]();
});

TEST_F('GoogleNowUtilityUnitTest', 'TaskManager3Tasks', function() {
  // Tests that 3 tasks can be executed too. In particular, that if the second
  // task is a single-step task which execution was originally blocked by task1,
  // unblocking it causes immediate synchronous execution of both tasks 2 and 3.

  // Setup.
  var test = setUpTaskManagerTest(this);
  var task1PluginInstance;

  // Step 1. Add 1st task that creates a pending callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1().
      will(callFunction(function() {
        task1PluginInstance = test.pluginFactory();
      }));
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Add 2nd and 3rd tasks, both non-conflicting (see
  // areTasksConflicting) with task1.
  test.tasks.add(taskNameC, this.mockLocalFunctions.functions().task2);
  test.tasks.add(taskNameC, this.mockLocalFunctions.functions().task3);
  Mock4JS.verifyAllMocks();

  // Step 3. Enter the callback of task1.
  task1PluginInstance.prologue();
  Mock4JS.verifyAllMocks();

  // Step 4. Leave the callback of task1.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task2();
  this.mockLocalFunctions.expects(once()).task3();
  // Invocation.
  task1PluginInstance.epilogue();
});

TEST_F('GoogleNowUtilityUnitTest', 'TaskManagerNestedNonTask', function() {
  // Tests callbacks requested while a task is running, but not from a callback
  // belonging to a task, are not counted as a part of the task.

  // Setup.
  var test = setUpTaskManagerTest(this);
  var task1PluginInstance;

  // Step 1. Add 1st task that creates a pending callback.
  // Expectations.
  this.mockLocalFunctions.expects(once()).task1().
      will(callFunction(function() {
        task1PluginInstance = test.pluginFactory();
      }));
  // Invocation.
  test.tasks.add(taskNameA, this.mockLocalFunctions.functions().task1);
  Mock4JS.verifyAllMocks();

  // Step 2. Create a pending callback from code that is not a part of the task.
  test.pluginFactory();
  Mock4JS.verifyAllMocks();

  // Step 3. Enter the callback of task1. After this, task1 should be
  // finished despite the pending non-task callback.
  task1PluginInstance.prologue();
  task1PluginInstance.epilogue();
  Mock4JS.verifyAllMocks();

  // Step 4. Check that task1 is finished by submitting task2, which should
  // be executed immediately.
  this.mockLocalFunctions.expects(once()).task2();
  test.tasks.add(taskNameC, this.mockLocalFunctions.functions().task2);
});

var testAttemptAlarmName = 'attempt-scheduler-testAttempts';
var testAttemptStorageKey = 'current-delay-testAttempts';
var testInitialDelaySeconds = 239;
var testMaximumDelaySeconds = 2239;
// Value to be returned by mocked Math.random(). We want the value returned by
// Math.random() to be predictable to be able to check results against expected
// values. A fixed seed would be okay, but fixed seeding isn't possible in JS at
// the moment.
var testRandomValue = 0.31415926;

/**
 * Creates a default storage current delay object.
 */
function createTestAttemptStorageEntryRequest() {
  var storageObject = {};
  storageObject[testAttemptStorageKey] = undefined;
  return storageObject;
}

/**
 * Creates a test storage object that attempt manager uses to store current
 * delay.
 */
function createTestAttemptStorageEntry(delaySeconds) {
  var storageObject = {};
  storageObject[testAttemptStorageKey] = delaySeconds;
  return storageObject;
}

function setupAttemptManagerTest(fixture) {
  Math.random = function() { return testRandomValue; }

  fixture.makeMockLocalFunctions([
    'attempt',
    'isRunningCallback'
  ]);
  fixture.makeAndRegisterMockApis([
    'chrome.alarms.clear',
    'chrome.alarms.create',
    'chrome.storage.local.remove',
    'chrome.storage.local.set',
    'fillFromChromeLocalStorage',
    'instrumented.alarms.get'
  ]);

  var testAttempts = buildAttemptManager(
      'testAttempts',
      fixture.mockLocalFunctions.functions().attempt,
      testInitialDelaySeconds,
      testMaximumDelaySeconds);
  Mock4JS.verifyAllMocks();

  return {
    attempts: testAttempts
  };
}

TEST_F('GoogleNowUtilityUnitTest', 'AttemptManagerStartStop', function() {
  // Tests starting and stopping an attempt manager.

  // Setup.
  var test = setupAttemptManagerTest(this);

  // Step 1. Check that attempt manager is not running.
  // Expectations.
  var alarmsGetSavedArgs = new SaveMockArguments();
  this.mockApis.expects(once()).
      instrumented_alarms_get(
          alarmsGetSavedArgs.match(eq(testAttemptAlarmName)),
          alarmsGetSavedArgs.match(ANYTHING)).
      will(invokeCallback(alarmsGetSavedArgs, 1, undefined));
  this.mockLocalFunctions.expects(once()).isRunningCallback(false);
  // Invocation.
  test.attempts.isRunning(
      this.mockLocalFunctions.functions().isRunningCallback);
  Mock4JS.verifyAllMocks();

  // Step 2. Start attempt manager with no parameters.
  // Expectations.
  var expectedRetryDelaySeconds =
      testInitialDelaySeconds * (1 + testRandomValue * 0.2);
  this.mockApis.expects(once()).chrome_alarms_create(
    testAttemptAlarmName,
    eqJSON({
      delayInMinutes: expectedRetryDelaySeconds / 60,
      periodInMinutes: testMaximumDelaySeconds / 60
    }));
  this.mockApis.expects(once()).chrome_storage_local_set(
      eqJSON(createTestAttemptStorageEntry(expectedRetryDelaySeconds)));
  // Invocation.
  test.attempts.start();
  Mock4JS.verifyAllMocks();

  // Step 3. Check that attempt manager is running.
  // Expectations.
  alarmsGetSavedArgs = new SaveMockArguments();
  this.mockApis.expects(once()).
      instrumented_alarms_get(
          alarmsGetSavedArgs.match(eq(testAttemptAlarmName)),
          alarmsGetSavedArgs.match(ANYTHING)).
      will(invokeCallback(alarmsGetSavedArgs, 1, {testField: 'TEST VALUE'}));
  this.mockLocalFunctions.expects(once()).isRunningCallback(true);
  // Invocation.
  test.attempts.isRunning(
      this.mockLocalFunctions.functions().isRunningCallback);
  Mock4JS.verifyAllMocks();

  // Step 4. Stop task manager.
  // Expectations.
  this.mockApis.expects(once()).chrome_alarms_clear(testAttemptAlarmName);
  this.mockApis.expects(once()).chrome_storage_local_remove(
      testAttemptStorageKey);
  // Invocation.
  test.attempts.stop();
});

TEST_F(
    'GoogleNowUtilityUnitTest',
    'AttemptManagerStartWithDelayParam',
    function() {
  // Tests starting an attempt manager with a delay parameter.

  // Setup.
  var test = setupAttemptManagerTest(this);
  var testFirstDelaySeconds = 1039;

  // Starting attempt manager with a parameter specifying first delay.
  // Expectations.
  this.mockApis.expects(once()).chrome_alarms_create(
      testAttemptAlarmName,
      eqJSON({
        delayInMinutes: testFirstDelaySeconds / 60,
        periodInMinutes: testMaximumDelaySeconds / 60
      }));
  this.mockApis.expects(once()).chrome_storage_local_remove(
    testAttemptStorageKey);
  // Invocation.
  test.attempts.start(testFirstDelaySeconds);
});

TEST_F('GoogleNowUtilityUnitTest', 'AttemptManagerExponGrowth', function() {
  // Tests that retry time grows exponentially. We don't need to check the case
  // of growing more than once, since the object doesn't have state, and the
  // test checks all its inputs and outputs of the tested code.

  // Setup.
  var test = setupAttemptManagerTest(this);
  var testStoredRetryDelay = 433;

  // Call scheduleRetry, which schedules a retry.
  // Current retry time is less than 1/2 of the maximum delay.
  // Expectations.
  var expectedRetryDelaySeconds =
      testStoredRetryDelay * 2 * (1 + testRandomValue * 0.2);
  expectChromeLocalStorageGet(
      this,
      createTestAttemptStorageEntryRequest(),
      createTestAttemptStorageEntry(testStoredRetryDelay),
      true);
  this.mockApis.expects(once()).chrome_alarms_create(
      testAttemptAlarmName,
      eqJSON({
        delayInMinutes: expectedRetryDelaySeconds / 60,
        periodInMinutes: testMaximumDelaySeconds / 60}));
  this.mockApis.expects(once()).chrome_storage_local_set(
      eqJSON(createTestAttemptStorageEntry(expectedRetryDelaySeconds)));
  // Invocation.
  var thenCalled = false;
  var catchCalled = false;
  test.attempts.scheduleRetry().then(function(request) {
    thenCalled = true;
  }).catch(function(request) {
    catchCalled = true;
  });
  assertTrue(thenCalled);
  assertFalse(catchCalled);
});

TEST_F('GoogleNowUtilityUnitTest', 'AttemptManagerGrowthLimit', function() {
  // Tests that retry time stops growing at the maximum value.

  // Setup.
  var test = setupAttemptManagerTest(this);
  var testStoredRetryDelay = 1500;

  // Call scheduleRetry, which schedules a retry.
  // Current retry time is greater than 1/2 of the maximum delay.
  // Expectations.
  var expectedRetryDelaySeconds = testMaximumDelaySeconds;
  expectChromeLocalStorageGet(
      this,
      createTestAttemptStorageEntryRequest(),
      createTestAttemptStorageEntry(testStoredRetryDelay),
      true);
  this.mockApis.expects(once()).chrome_alarms_create(
      testAttemptAlarmName,
      eqJSON({
        delayInMinutes: expectedRetryDelaySeconds / 60,
        periodInMinutes: testMaximumDelaySeconds / 60
      }));
  this.mockApis.expects(once()).chrome_storage_local_set(
      eqJSON(createTestAttemptStorageEntry(expectedRetryDelaySeconds)));
  // Invocation.
  var thenCalled = false;
  var catchCalled = false;
  test.attempts.scheduleRetry().then(function(request) {
    thenCalled = true;
  }).catch(function(request) {
    catchCalled = true;
  });
  assertTrue(thenCalled);
  assertFalse(catchCalled);
});

TEST_F('GoogleNowUtilityUnitTest', 'AttemptManagerAlarm', function() {
  // Tests that firing the alarm invokes the attempt.

  // Setup.
  var test = setupAttemptManagerTest(this);
  var onAlarmHandlerContainer = getMockHandlerContainer('alarms.onAlarm');
  assertEquals(1, onAlarmHandlerContainer.length);

  // Fire the alarm and check that this invokes the attempt callback.
  // Expectations.
  var alarmsGetSavedArgs = new SaveMockArguments();
  this.mockApis.expects(once()).
      instrumented_alarms_get(
          alarmsGetSavedArgs.match(eq(testAttemptAlarmName)),
          alarmsGetSavedArgs.match(ANYTHING)).
      will(invokeCallback(alarmsGetSavedArgs, 1, {testField: 'TEST VALUE'}));
  this.mockLocalFunctions.expects(once()).attempt();
  // Invocation.
  onAlarmHandlerContainer[0]({name: testAttemptAlarmName});
});